mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-18 22:36: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:
@@ -22,6 +22,8 @@ interface OverviewTabProps {
|
||||
trend: SecurityRiskTrendPoint[];
|
||||
/** Actionable Critical/High findings with KEV/EPSS for the exploit-intel charts. */
|
||||
exploitIntel: ExploitIntelFinding[];
|
||||
/** True when the exploit-intel set hit its row cap (highest-risk shown, not all). */
|
||||
exploitTruncated: boolean;
|
||||
onNavigate: (tab: SecurityTab) => void;
|
||||
onInspect: (scanId: number) => void;
|
||||
/** Admin on a node with a ready scanner; enables the node-scan launcher. */
|
||||
@@ -124,7 +126,7 @@ function ReviewQueueCard({
|
||||
);
|
||||
}
|
||||
|
||||
export function OverviewTab({ overview, loadError, trend, exploitIntel, onNavigate, onInspect, canScan, onScanComplete, isPaid }: OverviewTabProps) {
|
||||
export function OverviewTab({ overview, loadError, trend, exploitIntel, exploitTruncated, onNavigate, onInspect, canScan, onScanComplete, isPaid }: OverviewTabProps) {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
if (loadError === 'unsupported') {
|
||||
@@ -218,7 +220,7 @@ export function OverviewTab({ overview, loadError, trend, exploitIntel, onNaviga
|
||||
card never stretches to a taller exploit table (which left dead space
|
||||
under the chart). The exploit-risk table owns its own card chrome. */}
|
||||
<div className="grid items-start gap-4 lg:grid-cols-2">
|
||||
<TopExploitRiskList items={exploitIntel} onInspect={onInspect} />
|
||||
<TopExploitRiskList items={exploitIntel} truncated={exploitTruncated} onInspect={onInspect} />
|
||||
<ChartCard title="Severity × exploitability">
|
||||
<CvssEpssQuadrantChart items={exploitIntel} />
|
||||
</ChartCard>
|
||||
|
||||
@@ -132,6 +132,22 @@ describe('TopExploitRiskList', () => {
|
||||
expect(onInspect).toHaveBeenCalledWith(11);
|
||||
});
|
||||
|
||||
it('colors the severity dot by the finding severity (a Medium KEV is not shown as High)', () => {
|
||||
const { container } = render(
|
||||
<TopExploitRiskList items={[finding({ vulnerability_id: 'CVE-MED', severity: 'MEDIUM', kev: true, cvss_score: 5 })]} onInspect={vi.fn()} />,
|
||||
);
|
||||
const dot = container.querySelector('li[role="button"] span[aria-hidden]') as HTMLElement;
|
||||
expect(dot.style.background).toContain('sev-medium');
|
||||
});
|
||||
|
||||
it('discloses truncation only when the result was capped', () => {
|
||||
const item = finding({ vulnerability_id: 'CVE-A', cvss_score: 8 });
|
||||
const capped = render(<TopExploitRiskList items={[item]} truncated onInspect={vi.fn()} />);
|
||||
expect(capped.container.textContent).toContain('more exist than can be listed');
|
||||
const full = render(<TopExploitRiskList items={[item]} onInspect={vi.fn()} />);
|
||||
expect(full.container.textContent).not.toContain('more exist than can be listed');
|
||||
});
|
||||
|
||||
it('paginates beyond the page size and advances and rewinds pages', () => {
|
||||
const items = Array.from({ length: 9 }, (_, i) =>
|
||||
finding({ vulnerability_id: `CVE-${i}`, cvss_score: 9 - i * 0.1, epss_score: 0.5, scan_id: i }),
|
||||
|
||||
@@ -162,14 +162,29 @@ const EXPLOIT_PAGE_SIZE = 8;
|
||||
// horizontally instead; desktop is untouched by the `max-md:` prefix.
|
||||
const EXPLOIT_GRID = 'grid-cols-[10px_minmax(0,1.4fr)_minmax(0,1fr)_56px_52px] max-md:min-w-[480px]';
|
||||
|
||||
// Per-severity dot color. A KEV finding can now be any severity, so the dot must
|
||||
// reflect the row's real severity rather than collapsing everything non-Critical
|
||||
// to the High color. UNKNOWN gets the neutral subtitle tone (matching SeverityChip),
|
||||
// not the low color, so an UNKNOWN-severity finding is not understated as low risk.
|
||||
const SEV_DOT_VAR: Record<string, string> = {
|
||||
CRITICAL: 'var(--sev-critical)',
|
||||
HIGH: 'var(--sev-high)',
|
||||
MEDIUM: 'var(--sev-medium)',
|
||||
LOW: 'var(--sev-low)',
|
||||
UNKNOWN: 'var(--stat-subtitle)',
|
||||
};
|
||||
|
||||
/** Ranked, paginated table of the highest exploit-risk actionable findings; a row opens the scan.
|
||||
* Renders its own card chrome (header + pagination + column headers) so the Overview reads as a
|
||||
* table, mirroring the dashboard Stack-health table. */
|
||||
export function TopExploitRiskList({
|
||||
items,
|
||||
truncated = false,
|
||||
onInspect,
|
||||
}: {
|
||||
items: ExploitIntelFinding[];
|
||||
/** The backend capped the result; the highest-risk findings are shown, not all. */
|
||||
truncated?: boolean;
|
||||
onInspect: (scanId: number) => void;
|
||||
}) {
|
||||
const [page, setPage] = useState(0);
|
||||
@@ -231,7 +246,7 @@ export function TopExploitRiskList({
|
||||
>
|
||||
<span
|
||||
className="h-[7px] w-[7px] shrink-0 justify-self-center rounded-full"
|
||||
style={{ background: f.severity === 'CRITICAL' ? 'var(--sev-critical)' : 'var(--sev-high)' }}
|
||||
style={{ background: SEV_DOT_VAR[f.severity] ?? 'var(--stat-subtitle)' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
@@ -254,6 +269,11 @@ export function TopExploitRiskList({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{truncated && (
|
||||
<p className="border-t border-border/40 px-4 py-2 text-[10px] leading-snug text-stat-subtitle">
|
||||
Showing the highest-risk findings; more exist than can be listed here.
|
||||
</p>
|
||||
)}
|
||||
{!anyIntel && (
|
||||
<p className="border-t border-border/40 px-4 py-2 text-[10px] leading-snug text-stat-subtitle">
|
||||
Ranked by severity. Enable exploit intelligence and re-scan to rank by known-exploited and EPSS.
|
||||
|
||||
Reference in New Issue
Block a user