mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-20 23:32:19 +00:00
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user