- Infoblox WAPI integration (read-only) for resolving IPs to FQDNs - backend/helpers/infobloxApi.js — Basic auth, host/PTR/IPv6 lookups - backend/routes/infoblox.js — /api/infoblox endpoints - Globe icon on Reporting page hostName/dns columns for one-click DNS lookup - Pending: WAPI credentials with API access permissions - Scan Posture page for Access Ops platform/version tracking - backend/routes/scanPosture.js - frontend/src/components/pages/ScanPosturePage.js - Page visibility and nav drawer entries - Ivanti findings OS field enrichment - Migration to add os_name/os_class/os_version columns - Backfill script for existing findings - ivantiFindings route updates to persist OS data on sync
169 lines
6.9 KiB
JavaScript
169 lines
6.9 KiB
JavaScript
// Infoblox WAPI helpers (read-only)
|
|
// Provides DNS host record lookups via the Infoblox WAPI proxy.
|
|
// Auth: HTTP Basic (username/password).
|
|
// Base URL: https://api-proxy-ease.charterlab.com/wapi/v2.13.8
|
|
//
|
|
// This is a lightweight, read-only integration — no create/update/delete.
|
|
// Used to resolve IPs to their authoritative FQDN when Ivanti data is wrong.
|
|
//
|
|
// Quirks:
|
|
// - Infoblox returns 401 when a record:host query matches no records (not an
|
|
// empty array). This is treated as "not found" rather than an auth error.
|
|
// - Many hosts are IPv6-only in Infoblox. If IPv4 host lookup fails, the
|
|
// caller should try lookupByIpv6 or lookupPtrByIp as fallbacks.
|
|
// - PTR records exist for IPs that don't have a host record. Use
|
|
// lookupPtrByIp to resolve those.
|
|
|
|
const https = require('https');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Configuration
|
|
// ---------------------------------------------------------------------------
|
|
const INFOBLOX_HOST = process.env.INFOBLOX_HOST || 'api-proxy-ease.charterlab.com';
|
|
const INFOBLOX_VERSION = process.env.INFOBLOX_VERSION || '2.13.8';
|
|
const INFOBLOX_USER = process.env.INFOBLOX_USER || '';
|
|
const INFOBLOX_PASS = process.env.INFOBLOX_PASS || '';
|
|
const INFOBLOX_SKIP_TLS = process.env.INFOBLOX_SKIP_TLS === 'true';
|
|
|
|
const requiredVars = ['INFOBLOX_USER', 'INFOBLOX_PASS'];
|
|
const missingVars = requiredVars.filter((v) => !process.env[v]);
|
|
if (missingVars.length > 0) {
|
|
console.warn(`[infoblox-api] WARNING: Missing env vars: ${missingVars.join(', ')}. Infoblox lookups will fail.`);
|
|
}
|
|
|
|
const isConfigured = missingVars.length === 0;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Generic WAPI GET request with Basic auth
|
|
// ---------------------------------------------------------------------------
|
|
function infobloxGet(path, queryParams = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const qs = new URLSearchParams(queryParams).toString();
|
|
const fullPath = `/wapi/v${INFOBLOX_VERSION}/${path}${qs ? '?' + qs : ''}`;
|
|
|
|
const auth = Buffer.from(`${INFOBLOX_USER}:${INFOBLOX_PASS}`).toString('base64');
|
|
|
|
const options = {
|
|
hostname: INFOBLOX_HOST,
|
|
port: 443,
|
|
path: fullPath,
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': `Basic ${auth}`,
|
|
'Accept': 'application/json',
|
|
},
|
|
rejectUnauthorized: !INFOBLOX_SKIP_TLS,
|
|
timeout: 15000,
|
|
};
|
|
|
|
const req = https.request(options, (res) => {
|
|
const chunks = [];
|
|
res.on('data', (chunk) => chunks.push(chunk));
|
|
res.on('end', () => {
|
|
const body = Buffer.concat(chunks).toString('utf8');
|
|
let parsed;
|
|
try { parsed = JSON.parse(body); } catch (_) { parsed = body; }
|
|
resolve({ status: res.statusCode, ok: res.statusCode >= 200 && res.statusCode < 300, body: parsed });
|
|
});
|
|
});
|
|
|
|
req.on('timeout', () => { req.destroy(); reject(new Error('Infoblox request timed out')); });
|
|
req.on('error', (err) => reject(err));
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// lookupByIp — search host records that have a given IPv4 address.
|
|
// NOTE: Infoblox returns 401 when no host record exists for the IP.
|
|
// This is treated as "not found" (returns empty records), not auth error.
|
|
// ---------------------------------------------------------------------------
|
|
async function lookupByIp(ipAddress) {
|
|
const result = await infobloxGet('record:host', { ipv4addr: ipAddress });
|
|
// 401 from Infoblox = no matching host record (quirk of the API)
|
|
if (result.status === 401) {
|
|
return { ok: true, records: [] };
|
|
}
|
|
if (!result.ok) {
|
|
return { ok: false, status: result.status, error: result.body };
|
|
}
|
|
const records = Array.isArray(result.body) ? result.body : [];
|
|
return { ok: true, records };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// lookupByIpv6 — search host records by IPv6 address
|
|
// ---------------------------------------------------------------------------
|
|
async function lookupByIpv6(ipv6Address) {
|
|
const result = await infobloxGet('record:host', { ipv6addr: ipv6Address });
|
|
if (result.status === 401) {
|
|
return { ok: true, records: [] };
|
|
}
|
|
if (!result.ok) {
|
|
return { ok: false, status: result.status, error: result.body };
|
|
}
|
|
const records = Array.isArray(result.body) ? result.body : [];
|
|
return { ok: true, records };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// lookupByName — search host records by FQDN (exact match)
|
|
// ---------------------------------------------------------------------------
|
|
async function lookupByName(fqdn) {
|
|
const result = await infobloxGet('record:host', { name: fqdn });
|
|
if (result.status === 401) {
|
|
return { ok: true, records: [] };
|
|
}
|
|
if (!result.ok) {
|
|
return { ok: false, status: result.status, error: result.body };
|
|
}
|
|
const records = Array.isArray(result.body) ? result.body : [];
|
|
return { ok: true, records };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// lookupPtrByIp — search PTR records for an IPv4 address.
|
|
// Many IPs only have a PTR record (no host record). The PTR gives the FQDN.
|
|
// ---------------------------------------------------------------------------
|
|
async function lookupPtrByIp(ipAddress) {
|
|
const result = await infobloxGet('record:ptr', { ipv4addr: ipAddress });
|
|
if (result.status === 401) {
|
|
return { ok: true, records: [] };
|
|
}
|
|
if (!result.ok) {
|
|
return { ok: false, status: result.status, error: result.body };
|
|
}
|
|
const records = Array.isArray(result.body) ? result.body : [];
|
|
return { ok: true, records };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// lookupAnyByIp — tries host record first, then PTR as fallback.
|
|
// Returns the best available FQDN for a given IPv4 address.
|
|
// ---------------------------------------------------------------------------
|
|
async function lookupAnyByIp(ipAddress) {
|
|
// Try host record first
|
|
const hostResult = await lookupByIp(ipAddress);
|
|
if (hostResult.ok && hostResult.records.length > 0) {
|
|
return { ok: true, source: 'host', records: hostResult.records };
|
|
}
|
|
|
|
// Fall back to PTR record
|
|
const ptrResult = await lookupPtrByIp(ipAddress);
|
|
if (ptrResult.ok && ptrResult.records.length > 0) {
|
|
return { ok: true, source: 'ptr', records: ptrResult.records };
|
|
}
|
|
|
|
return { ok: true, source: null, records: [] };
|
|
}
|
|
|
|
module.exports = {
|
|
isConfigured,
|
|
missingVars,
|
|
lookupByIp,
|
|
lookupByIpv6,
|
|
lookupByName,
|
|
lookupPtrByIp,
|
|
lookupAnyByIp,
|
|
};
|