|
|
|
|
@@ -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());
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|