Files
cve-dashboard/backend/helpers/infobloxApi.js
Jordan Ramos 416bfd2e28 Add Infoblox DNS lookup, Scan Posture page, and Ivanti OS fields
- 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
2026-08-21 10:33:46 -06:00

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,
};