fix: rank exploit-risk findings before the cap and disclose truncation (#1482)

The Security overview's top exploit-risk list is built from a query capped at
2000 rows. The query had no ORDER BY, so when a node had more findings than the
cap the rows kept were arbitrary: the list could rank and display a subset that
omitted higher-risk findings, and the frontend discarded the truncated flag the
endpoint already returned, so nothing told the operator the list was partial.

- The query now orders by known-exploited, then EPSS, then CVSS before the cap,
  so the rows that survive truncation are the highest-risk ones, matching the
  client-side ranking the list applies.
- SecurityView keeps the truncated flag and threads it through to the list,
  which now shows a short "more exist than can be listed here" note when the set
  was capped.

Also fixes a presentation regression: the list colored every non-Critical
severity dot with the High color, so a Medium or Low known-exploited finding
(now surfaced alongside Critical/High) showed as High. The dot now maps to the
finding's actual severity.
This commit is contained in:
Anso
2026-06-26 21:12:06 -04:00
committed by GitHub
parent 7c12081645
commit 7c9c640625
6 changed files with 92 additions and 8 deletions
+13 -5
View File
@@ -76,6 +76,9 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
const [summariesError, setSummariesError] = useState(false);
const [trend, setTrend] = useState<SecurityRiskTrendPoint[]>([]);
const [exploitIntel, setExploitIntel] = useState<ExploitIntelFinding[]>([]);
// True when the exploit-intel query hit its row cap: the list shows the
// highest-risk findings but not every one, so the UI discloses it.
const [exploitTruncated, setExploitTruncated] = useState(false);
const [isReplica, setIsReplica] = useState(false);
// Bumped after a node-wide scan completes to refetch the active node's posture.
const [reloadToken, setReloadToken] = useState(0);
@@ -117,10 +120,13 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
.catch(() => []);
// Exploit-intel powers two overview charts; isolate it like the trend so a
// failure (or an older node without the endpoint) degrades to empty panels.
const exploitIntelPromise: Promise<ExploitIntelFinding[]> = apiFetch('/security/overview/exploit-intel')
.then((r) => (r.ok ? r.json() : { items: [] }))
.then((b) => (b && Array.isArray(b.items) ? b.items : []))
.catch(() => []);
const exploitIntelPromise: Promise<{ items: ExploitIntelFinding[]; truncated: boolean }> = apiFetch('/security/overview/exploit-intel')
.then((r) => (r.ok ? r.json() : { items: [], truncated: false }))
.then((b) => ({
items: b && Array.isArray(b.items) ? b.items : [],
truncated: b?.truncated === true,
}))
.catch(() => ({ items: [], truncated: false }));
try {
const [overviewRes, summariesRes] = await Promise.all([
apiFetch('/security/overview'),
@@ -164,7 +170,8 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
const [trendData, intelData] = await Promise.all([trendPromise, exploitIntelPromise]);
if (!cancelled) {
setTrend(trendData);
setExploitIntel(intelData);
setExploitIntel(intelData.items);
setExploitTruncated(intelData.truncated);
}
})();
return () => { cancelled = true; };
@@ -246,6 +253,7 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
loadError={overviewLoadError}
trend={trend}
exploitIntel={exploitIntel}
exploitTruncated={exploitTruncated}
onNavigate={onTabChange}
onInspect={onInspect}
canScan={canScan}