feat(reporting): add CVEs column from vulnerabilities.vulnInfoList
- Backend extracts cves[] array from f.vulnerabilities.vulnInfoList[].cve - Frontend shows up to 2 CVE badges (purple) with "+N more" overflow tooltip - Filter is multi-value aware: selecting a CVE matches any finding containing it - FilterDropdown expands multi-value arrays into individual checkbox options - Sort by CVE count (number of associated CVEs) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,9 @@ function extractFinding(f) {
|
|||||||
// BU ownership: assetCustomAttributes['1550_host_1'] is an array like ["NTS-AEO-STEAM"]
|
// BU ownership: assetCustomAttributes['1550_host_1'] is an array like ["NTS-AEO-STEAM"]
|
||||||
const buOwnership = f.assetCustomAttributes?.['1550_host_1']?.[0] || '';
|
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 {
|
return {
|
||||||
id: String(f.id),
|
id: String(f.id),
|
||||||
title: f.title || '',
|
title: f.title || '',
|
||||||
@@ -139,7 +142,8 @@ function extractFinding(f) {
|
|||||||
slaStatus: f.slaStatus || '',
|
slaStatus: f.slaStatus || '',
|
||||||
dueDate,
|
dueDate,
|
||||||
lastFoundOn: f.lastFoundOn || '',
|
lastFoundOn: f.lastFoundOn || '',
|
||||||
buOwnership
|
buOwnership,
|
||||||
|
cves
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,21 +9,23 @@ const STORAGE_KEY = 'steam_findings_columns_v1';
|
|||||||
// Column definitions — source of truth for labels, sort behaviour, rendering
|
// Column definitions — source of truth for labels, sort behaviour, rendering
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const COLUMN_DEFS = {
|
const COLUMN_DEFS = {
|
||||||
severity: { label: 'Severity', sortable: true, filterable: true },
|
severity: { label: 'Severity', sortable: true, filterable: true },
|
||||||
title: { label: 'Title', sortable: true, filterable: true },
|
title: { label: 'Title', sortable: true, filterable: true },
|
||||||
hostName: { label: 'Host', sortable: true, filterable: true },
|
cves: { label: 'CVEs', sortable: true, filterable: true, multiValue: true },
|
||||||
ipAddress: { label: 'IP Address', sortable: true, filterable: true },
|
hostName: { label: 'Host', sortable: true, filterable: true },
|
||||||
dns: { label: 'DNS', sortable: true, filterable: true },
|
ipAddress: { label: 'IP Address', sortable: true, filterable: true },
|
||||||
dueDate: { label: 'Due Date', sortable: true, filterable: true },
|
dns: { label: 'DNS', sortable: true, filterable: true },
|
||||||
slaStatus: { label: 'SLA', sortable: true, filterable: true },
|
dueDate: { label: 'Due Date', sortable: true, filterable: true },
|
||||||
buOwnership: { label: 'BU', sortable: true, filterable: true },
|
slaStatus: { label: 'SLA', sortable: true, filterable: true },
|
||||||
lastFoundOn: { label: 'Last Found', sortable: true, filterable: true },
|
buOwnership: { label: 'BU', sortable: true, filterable: true },
|
||||||
note: { label: 'Notes', sortable: false, filterable: false },
|
lastFoundOn: { label: 'Last Found', sortable: true, filterable: true },
|
||||||
|
note: { label: 'Notes', sortable: false, filterable: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_COLUMN_ORDER = [
|
const DEFAULT_COLUMN_ORDER = [
|
||||||
{ key: 'severity', visible: true },
|
{ key: 'severity', visible: true },
|
||||||
{ key: 'title', visible: true },
|
{ key: 'title', visible: true },
|
||||||
|
{ key: 'cves', visible: true },
|
||||||
{ key: 'hostName', visible: true },
|
{ key: 'hostName', visible: true },
|
||||||
{ key: 'ipAddress', visible: true },
|
{ key: 'ipAddress', visible: true },
|
||||||
{ key: 'dns', visible: true },
|
{ key: 'dns', visible: true },
|
||||||
@@ -68,6 +70,7 @@ function getVal(finding, key) {
|
|||||||
case 'dns': return finding.dns ?? '';
|
case 'dns': return finding.dns ?? '';
|
||||||
case 'dueDate': return finding.dueDate ?? '';
|
case 'dueDate': return finding.dueDate ?? '';
|
||||||
case 'slaStatus': return finding.slaStatus ?? '';
|
case 'slaStatus': return finding.slaStatus ?? '';
|
||||||
|
case 'cves': return (finding.cves || []).length; // sort by CVE count
|
||||||
case 'buOwnership': return finding.buOwnership ?? '';
|
case 'buOwnership': return finding.buOwnership ?? '';
|
||||||
case 'lastFoundOn': return finding.lastFoundOn ?? '';
|
case 'lastFoundOn': return finding.lastFoundOn ?? '';
|
||||||
case 'note': return finding.note ?? '';
|
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) {
|
function getFilterVal(finding, key) {
|
||||||
if (key === 'severity') return finding.vrrGroup || '';
|
if (key === 'severity') return finding.vrrGroup || '';
|
||||||
|
if (key === 'cves') return (finding.cves || []).join(','); // not used directly; see multiValue logic
|
||||||
return String(getVal(finding, key) ?? '');
|
return String(getVal(finding, key) ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,12 +331,18 @@ function FilterDropdown({ anchorEl, colKey, findings, activeFilter, onFilterChan
|
|||||||
return () => document.removeEventListener('keydown', handler);
|
return () => document.removeEventListener('keydown', handler);
|
||||||
}, [onClose]);
|
}, [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 allValues = useMemo(() => {
|
||||||
|
const def = COLUMN_DEFS[colKey];
|
||||||
const vals = new Set();
|
const vals = new Set();
|
||||||
findings.forEach((f) => {
|
findings.forEach((f) => {
|
||||||
const v = getFilterVal(f, colKey).trim();
|
if (def?.multiValue) {
|
||||||
if (v) vals.add(v);
|
(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 }));
|
return [...vals].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||||
}, [findings, colKey]);
|
}, [findings, colKey]);
|
||||||
@@ -458,6 +468,28 @@ function TableCell({ colKey, finding }) {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
case 'cves': {
|
||||||
|
const cves = finding.cves || [];
|
||||||
|
if (cves.length === 0) return <td style={{ padding: '0.45rem 0.75rem', color: '#475569' }}>—</td>;
|
||||||
|
const shown = cves.slice(0, 2);
|
||||||
|
const rest = cves.length - shown.length;
|
||||||
|
return (
|
||||||
|
<td style={{ padding: '0.45rem 0.75rem', minWidth: '160px', maxWidth: '240px' }}>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.2rem' }}>
|
||||||
|
{shown.map((cve) => (
|
||||||
|
<span key={cve} style={{ padding: '0.1rem 0.35rem', borderRadius: '0.2rem', background: 'rgba(139,92,246,0.1)', border: '1px solid rgba(139,92,246,0.3)', color: '#A78BFA', fontFamily: 'monospace', fontSize: '0.65rem', fontWeight: '600', whiteSpace: 'nowrap' }}>
|
||||||
|
{cve}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{rest > 0 && (
|
||||||
|
<span title={cves.slice(2).join('\n')} style={{ padding: '0.1rem 0.35rem', borderRadius: '0.2rem', background: 'rgba(100,116,139,0.12)', border: '1px solid rgba(100,116,139,0.25)', color: '#64748B', fontFamily: 'monospace', fontSize: '0.65rem', cursor: 'help', whiteSpace: 'nowrap' }}>
|
||||||
|
+{rest} more
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
case 'hostName':
|
case 'hostName':
|
||||||
case 'ipAddress':
|
case 'ipAddress':
|
||||||
return (
|
return (
|
||||||
@@ -606,6 +638,11 @@ export default function ReportingPage() {
|
|||||||
return findings.filter((f) =>
|
return findings.filter((f) =>
|
||||||
active.every(([key, vals]) => {
|
active.every(([key, vals]) => {
|
||||||
if (!vals || vals.size === 0) return false;
|
if (!vals || vals.size === 0) return false;
|
||||||
|
const def = COLUMN_DEFS[key];
|
||||||
|
if (def?.multiValue) {
|
||||||
|
// Row matches if ANY of its values is in the selected set
|
||||||
|
return (f[key] || []).some((v) => vals.has(String(v).trim()));
|
||||||
|
}
|
||||||
return vals.has(getFilterVal(f, key).trim());
|
return vals.has(getFilterVal(f, key).trim());
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user