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:
@@ -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 }) {
|
||||
</span>
|
||||
</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 'ipAddress':
|
||||
return (
|
||||
@@ -606,6 +638,11 @@ export default function ReportingPage() {
|
||||
return findings.filter((f) =>
|
||||
active.every(([key, vals]) => {
|
||||
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());
|
||||
})
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user