Add IPv6 fallback display for findings without IPv4
Findings with no IPv4 address now display Qualys IPv6 or Primary IPv6 as fallback in the IP column, with a badge indicator: - 'Q' (amber) = Qualys IPv6 from hostAdditionalDetails - 'v6' (indigo) = Primary IPv6 from assetCustomAttributes Priority: IPv4 > Qualys IPv6 > Primary IPv6 Backend changes: - extractFinding now captures qualysIpv6 and primaryIpv6 - New extractQualysIpv6 helper parses hostAdditionalDetails - upsertFindingsBatch stores both fields - API response includes qualysIpv6 and primaryIpv6 - Migration adds qualys_ipv6 and primary_ipv6 columns The Qualys IPv6 is preferred over Primary IPv6 because it resolves in CARD (confirmed via testing with PMADEV-1).
This commit is contained in:
32
backend/migrations/add_ivanti_findings_ipv6_columns.js
Normal file
32
backend/migrations/add_ivanti_findings_ipv6_columns.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Migration: Add qualys_ipv6 and primary_ipv6 columns to ivanti_findings
|
||||||
|
// These capture IPv6 addresses for findings that have no IPv4.
|
||||||
|
// Qualys IPv6 comes from hostAdditionalDetails; Primary IPv6 from assetCustomAttributes.
|
||||||
|
|
||||||
|
const pool = require('../db');
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log('Adding IPv6 columns to ivanti_findings...');
|
||||||
|
try {
|
||||||
|
await pool.query(`
|
||||||
|
ALTER TABLE ivanti_findings
|
||||||
|
ADD COLUMN IF NOT EXISTS qualys_ipv6 TEXT DEFAULT NULL
|
||||||
|
`);
|
||||||
|
console.log('✓ qualys_ipv6 column added (or already exists)');
|
||||||
|
|
||||||
|
await pool.query(`
|
||||||
|
ALTER TABLE ivanti_findings
|
||||||
|
ADD COLUMN IF NOT EXISTS primary_ipv6 TEXT DEFAULT NULL
|
||||||
|
`);
|
||||||
|
console.log('✓ primary_ipv6 column added (or already exists)');
|
||||||
|
|
||||||
|
console.log('Migration complete.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Migration failed:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
@@ -30,6 +30,7 @@ const POSTGRES_MIGRATIONS = [
|
|||||||
'add_queue_remediation_notes_table.js',
|
'add_queue_remediation_notes_table.js',
|
||||||
'add_remediate_workflow_type.js',
|
'add_remediate_workflow_type.js',
|
||||||
'add_notifications_table.js',
|
'add_notifications_table.js',
|
||||||
|
'add_ivanti_findings_ipv6_columns.js',
|
||||||
];
|
];
|
||||||
|
|
||||||
async function runAll() {
|
async function runAll() {
|
||||||
|
|||||||
@@ -89,6 +89,22 @@ const CLOSED_COUNT_FILTERS = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Extract Qualys IPv6 address from hostAdditionalDetails
|
||||||
|
// Looks for "IPv6 Address" (string) or "IPv6 Addresses" (array) fields
|
||||||
|
// in the scanner-specific details from Qualys.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function extractQualysIpv6(f) {
|
||||||
|
const details = f.hostAdditionalDetails || [];
|
||||||
|
for (const entry of details) {
|
||||||
|
if (entry['IPv6 Address']) return entry['IPv6 Address'];
|
||||||
|
if (Array.isArray(entry['IPv6 Addresses']) && entry['IPv6 Addresses'].length > 0) {
|
||||||
|
return entry['IPv6 Addresses'][0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Extract only the fields we need from a raw finding object
|
// Extract only the fields we need from a raw finding object
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -149,7 +165,10 @@ function extractFinding(f) {
|
|||||||
lastFoundOn: f.lastFoundOn || '',
|
lastFoundOn: f.lastFoundOn || '',
|
||||||
buOwnership,
|
buOwnership,
|
||||||
cves,
|
cves,
|
||||||
workflow
|
workflow,
|
||||||
|
// IPv6 fallbacks for findings with no IPv4
|
||||||
|
qualysIpv6: extractQualysIpv6(f),
|
||||||
|
primaryIpv6: f.assetCustomAttributes?.['1550_host_6']?.[0] || '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +205,7 @@ async function upsertFindingsBatch(findings, state) {
|
|||||||
const placeholders = [];
|
const placeholders = [];
|
||||||
|
|
||||||
batch.forEach((f, idx) => {
|
batch.forEach((f, idx) => {
|
||||||
const offset = idx * 18;
|
const offset = idx * 20;
|
||||||
values.push(
|
values.push(
|
||||||
f.id,
|
f.id,
|
||||||
f.hostId,
|
f.hostId,
|
||||||
@@ -205,13 +224,15 @@ async function upsertFindingsBatch(findings, state) {
|
|||||||
f.workflow ? f.workflow.id : null,
|
f.workflow ? f.workflow.id : null,
|
||||||
f.workflow ? f.workflow.state : null,
|
f.workflow ? f.workflow.state : null,
|
||||||
f.workflow ? f.workflow.type : null,
|
f.workflow ? f.workflow.type : null,
|
||||||
state
|
state,
|
||||||
|
f.qualysIpv6 || null,
|
||||||
|
f.primaryIpv6 || null
|
||||||
);
|
);
|
||||||
placeholders.push(
|
placeholders.push(
|
||||||
`($${offset+1}, $${offset+2}, $${offset+3}, $${offset+4}, $${offset+5}, ` +
|
`($${offset+1}, $${offset+2}, $${offset+3}, $${offset+4}, $${offset+5}, ` +
|
||||||
`$${offset+6}, $${offset+7}, $${offset+8}, $${offset+9}, $${offset+10}, ` +
|
`$${offset+6}, $${offset+7}, $${offset+8}, $${offset+9}, $${offset+10}, ` +
|
||||||
`$${offset+11}, $${offset+12}, $${offset+13}, $${offset+14}, $${offset+15}, ` +
|
`$${offset+11}, $${offset+12}, $${offset+13}, $${offset+14}, $${offset+15}, ` +
|
||||||
`$${offset+16}, $${offset+17}, $${offset+18})`
|
`$${offset+16}, $${offset+17}, $${offset+18}, $${offset+19}, $${offset+20})`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -220,7 +241,8 @@ async function upsertFindingsBatch(findings, state) {
|
|||||||
id, host_id, title, severity, vrr_group,
|
id, host_id, title, severity, vrr_group,
|
||||||
host_name, ip_address, dns, status, sla_status,
|
host_name, ip_address, dns, status, sla_status,
|
||||||
due_date, last_found_on, bu_ownership, cves,
|
due_date, last_found_on, bu_ownership, cves,
|
||||||
workflow_id, workflow_state, workflow_type, state
|
workflow_id, workflow_state, workflow_type, state,
|
||||||
|
qualys_ipv6, primary_ipv6
|
||||||
)
|
)
|
||||||
VALUES ${placeholders.join(', ')}
|
VALUES ${placeholders.join(', ')}
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
@@ -241,6 +263,8 @@ async function upsertFindingsBatch(findings, state) {
|
|||||||
workflow_state = EXCLUDED.workflow_state,
|
workflow_state = EXCLUDED.workflow_state,
|
||||||
workflow_type = EXCLUDED.workflow_type,
|
workflow_type = EXCLUDED.workflow_type,
|
||||||
state = EXCLUDED.state,
|
state = EXCLUDED.state,
|
||||||
|
qualys_ipv6 = EXCLUDED.qualys_ipv6,
|
||||||
|
primary_ipv6 = EXCLUDED.primary_ipv6,
|
||||||
synced_at = NOW()
|
synced_at = NOW()
|
||||||
`, values);
|
`, values);
|
||||||
}
|
}
|
||||||
@@ -1052,6 +1076,8 @@ function createIvantiFindingsRouter(db, requireAuth) {
|
|||||||
cves: row.cves || [],
|
cves: row.cves || [],
|
||||||
workflow: row.workflow_id ? { id: row.workflow_id, state: row.workflow_state, type: row.workflow_type } : null,
|
workflow: row.workflow_id ? { id: row.workflow_id, state: row.workflow_state, type: row.workflow_type } : null,
|
||||||
note: row.note || '',
|
note: row.note || '',
|
||||||
|
qualysIpv6: row.qualys_ipv6 || null,
|
||||||
|
primaryIpv6: row.primary_ipv6 || null,
|
||||||
overrides: {
|
overrides: {
|
||||||
...(row.override_host_name ? { hostName: row.override_host_name } : {}),
|
...(row.override_host_name ? { hostName: row.override_host_name } : {}),
|
||||||
...(row.override_dns ? { dns: row.override_dns } : {})
|
...(row.override_dns ? { dns: row.override_dns } : {})
|
||||||
@@ -1108,6 +1134,8 @@ function createIvantiFindingsRouter(db, requireAuth) {
|
|||||||
cves: row.cves || [],
|
cves: row.cves || [],
|
||||||
workflow: row.workflow_id ? { id: row.workflow_id, state: row.workflow_state, type: row.workflow_type } : null,
|
workflow: row.workflow_id ? { id: row.workflow_id, state: row.workflow_state, type: row.workflow_type } : null,
|
||||||
note: row.note || '',
|
note: row.note || '',
|
||||||
|
qualysIpv6: row.qualys_ipv6 || null,
|
||||||
|
primaryIpv6: row.primary_ipv6 || null,
|
||||||
overrides: {
|
overrides: {
|
||||||
...(row.override_host_name ? { hostName: row.override_host_name } : {}),
|
...(row.override_host_name ? { hostName: row.override_host_name } : {}),
|
||||||
...(row.override_dns ? { dns: row.override_dns } : {})
|
...(row.override_dns ? { dns: row.override_dns } : {})
|
||||||
|
|||||||
@@ -1261,16 +1261,31 @@ function TableCell({ colKey, finding, canWrite, onCveMouseEnter, onCveMouseLeave
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'ipAddress':
|
case 'ipAddress': {
|
||||||
|
// Display priority: IPv4 > Qualys IPv6 > Primary IPv6
|
||||||
|
const displayIp = finding.ipAddress || finding.qualysIpv6 || finding.primaryIpv6 || '';
|
||||||
|
const ipSource = finding.ipAddress ? null : finding.qualysIpv6 ? 'Q' : finding.primaryIpv6 ? 'v6' : null;
|
||||||
|
const hoverIp = displayIp || null;
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
style={{ padding: '0.45rem 0.75rem', whiteSpace: 'nowrap', color: '#94A3B8', fontFamily: 'monospace', fontSize: '0.72rem', cursor: finding.ipAddress ? 'help' : 'default' }}
|
style={{ padding: '0.45rem 0.75rem', whiteSpace: 'nowrap', color: '#94A3B8', fontFamily: 'monospace', fontSize: '0.72rem', cursor: hoverIp ? 'help' : 'default' }}
|
||||||
onMouseEnter={onIpMouseEnter && finding.ipAddress ? (e) => onIpMouseEnter(finding.ipAddress, e, finding.hostId) : undefined}
|
onMouseEnter={onIpMouseEnter && hoverIp ? (e) => onIpMouseEnter(hoverIp, e, finding.hostId) : undefined}
|
||||||
onMouseLeave={onIpMouseLeave || undefined}
|
onMouseLeave={onIpMouseLeave || undefined}
|
||||||
|
title={ipSource === 'Q' ? 'Qualys IPv6 (no IPv4 available)' : ipSource === 'v6' ? 'Primary IPv6 (no IPv4 available)' : undefined}
|
||||||
>
|
>
|
||||||
{finding.ipAddress || '—'}
|
{displayIp ? (
|
||||||
|
<>
|
||||||
|
<span style={{ maxWidth: '140px', overflow: 'hidden', textOverflow: 'ellipsis', display: 'inline-block', verticalAlign: 'middle' }}>{displayIp}</span>
|
||||||
|
{ipSource && (
|
||||||
|
<span style={{ marginLeft: '0.3rem', fontSize: '0.55rem', padding: '0.08rem 0.25rem', borderRadius: '0.2rem', background: ipSource === 'Q' ? 'rgba(245, 158, 11, 0.15)' : 'rgba(99, 102, 241, 0.15)', border: ipSource === 'Q' ? '1px solid rgba(245, 158, 11, 0.4)' : '1px solid rgba(99, 102, 241, 0.4)', color: ipSource === 'Q' ? '#FBBF24' : '#A5B4FC', fontWeight: '700', verticalAlign: 'middle' }}>
|
||||||
|
{ipSource}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : '—'}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
case 'dns':
|
case 'dns':
|
||||||
return (
|
return (
|
||||||
<OverrideCell
|
<OverrideCell
|
||||||
|
|||||||
Reference in New Issue
Block a user