diff --git a/backend/routes/ivantiFindings.js b/backend/routes/ivantiFindings.js index 5fa7e94..2f41ce2 100644 --- a/backend/routes/ivantiFindings.js +++ b/backend/routes/ivantiFindings.js @@ -127,6 +127,9 @@ function extractFinding(f) { // BU ownership: assetCustomAttributes['1550_host_1'] is an array like ["NTS-AEO-STEAM"] const buOwnership = f.assetCustomAttributes?.['1550_host_1']?.[0] || ''; + // CVE list: vulnerabilities.vulnInfoList[].cve + const cves = (f.vulnerabilities?.vulnInfoList || []).map(v => v.cve).filter(Boolean); + return { id: String(f.id), title: f.title || '', @@ -139,7 +142,8 @@ function extractFinding(f) { slaStatus: f.slaStatus || '', dueDate, lastFoundOn: f.lastFoundOn || '', - buOwnership + buOwnership, + cves }; } diff --git a/frontend/src/components/pages/ReportingPage.js b/frontend/src/components/pages/ReportingPage.js index 43712cb..fd41be7 100644 --- a/frontend/src/components/pages/ReportingPage.js +++ b/frontend/src/components/pages/ReportingPage.js @@ -9,21 +9,23 @@ const STORAGE_KEY = 'steam_findings_columns_v1'; // Column definitions — source of truth for labels, sort behaviour, rendering // --------------------------------------------------------------------------- const COLUMN_DEFS = { - severity: { label: 'Severity', sortable: true, filterable: true }, - title: { label: 'Title', sortable: true, filterable: true }, - hostName: { label: 'Host', sortable: true, filterable: true }, - ipAddress: { label: 'IP Address', sortable: true, filterable: true }, - dns: { label: 'DNS', sortable: true, filterable: true }, - dueDate: { label: 'Due Date', sortable: true, filterable: true }, - slaStatus: { label: 'SLA', sortable: true, filterable: true }, - buOwnership: { label: 'BU', sortable: true, filterable: true }, - lastFoundOn: { label: 'Last Found', sortable: true, filterable: true }, - note: { label: 'Notes', sortable: false, filterable: false }, + severity: { label: 'Severity', sortable: true, filterable: true }, + title: { label: 'Title', sortable: true, filterable: true }, + cves: { label: 'CVEs', sortable: true, filterable: true, multiValue: true }, + hostName: { label: 'Host', sortable: true, filterable: true }, + ipAddress: { label: 'IP Address', sortable: true, filterable: true }, + dns: { label: 'DNS', sortable: true, filterable: true }, + dueDate: { label: 'Due Date', sortable: true, filterable: true }, + slaStatus: { label: 'SLA', sortable: true, filterable: true }, + buOwnership: { label: 'BU', sortable: true, filterable: true }, + lastFoundOn: { label: 'Last Found', sortable: true, filterable: true }, + note: { label: 'Notes', sortable: false, filterable: false }, }; const DEFAULT_COLUMN_ORDER = [ { key: 'severity', visible: true }, { key: 'title', visible: true }, + { key: 'cves', visible: true }, { key: 'hostName', visible: true }, { key: 'ipAddress', visible: true }, { key: 'dns', visible: true }, @@ -68,6 +70,7 @@ function getVal(finding, key) { case 'dns': return finding.dns ?? ''; case 'dueDate': return finding.dueDate ?? ''; case 'slaStatus': return finding.slaStatus ?? ''; + case 'cves': return (finding.cves || []).length; // sort by CVE count case 'buOwnership': return finding.buOwnership ?? ''; case 'lastFoundOn': return finding.lastFoundOn ?? ''; case 'note': return finding.note ?? ''; @@ -76,10 +79,11 @@ function getVal(finding, key) { } // --------------------------------------------------------------------------- -// Filter accessor — severity filters by vrrGroup label, not numeric value +// Filter accessor — severity → vrrGroup label; cves handled as multi-value // --------------------------------------------------------------------------- function getFilterVal(finding, key) { if (key === 'severity') return finding.vrrGroup || ''; + if (key === 'cves') return (finding.cves || []).join(','); // not used directly; see multiValue logic return String(getVal(finding, key) ?? ''); } @@ -327,12 +331,18 @@ function FilterDropdown({ anchorEl, colKey, findings, activeFilter, onFilterChan return () => document.removeEventListener('keydown', handler); }, [onClose]); - // Unique values from the full (unfiltered) findings list + // Unique values from the full (unfiltered) findings list. + // Multi-value columns (e.g. cves) expand their array so each item is a separate option. const allValues = useMemo(() => { + const def = COLUMN_DEFS[colKey]; const vals = new Set(); findings.forEach((f) => { - const v = getFilterVal(f, colKey).trim(); - if (v) vals.add(v); + if (def?.multiValue) { + (f[colKey] || []).forEach((v) => { if (String(v).trim()) vals.add(String(v).trim()); }); + } else { + const v = getFilterVal(f, colKey).trim(); + if (v) vals.add(v); + } }); return [...vals].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); }, [findings, colKey]); @@ -458,6 +468,28 @@ function TableCell({ colKey, finding }) { ); + case 'cves': { + const cves = finding.cves || []; + if (cves.length === 0) return