mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-26 02:06:49 +00:00
feat(security): prioritization-led Overview charts (posture + exploit intel) (#1427)
* feat(security): rework Overview charts around posture and exploit intel Replace the three severity-variation charts (severity donut, top exposed images, findings by type) with prioritization views, keeping the risk trend for context: - Action posture: bars of fixable / known-exploited / needs-review / accepted / not-affected with a known-exploited headline, derived from the existing overview facts (no new fetch). - Top exploit-risk findings: ranked actionable Critical/High by KEV, then EPSS, then CVSS, each row opening its scan. - Severity x exploitability: a CVSS-by-EPSS quadrant that separates scary-but-not-exploitable from act-first. The two intel panels are fed by a new bounded GET /security/overview/exploit-intel (latest-scan Critical/High, suppression-filtered, KEV/EPSS joined at read time) and degrade to clear empty states until exploit intel is fetched and images are rescanned. * fix(security): drop the redundant SECURITY kicker from the desktop masthead The desktop nav strip already names the page, so the masthead's "SECURITY" label above the posture word was redundant. Make PageMasthead's kicker optional (pages that pass one render unchanged) and omit it on the Security masthead. The mobile masthead keeps its kicker, since on the phone layout it is the page identity and there is no nav strip. * docs(security): describe the prioritization-led Overview charts Update the Security overview docs for the reworked chart set (risk trend, action posture, top exploit-risk, and the severity-by-exploitability quadrant) and note the exploit-risk charts populate once exploit intelligence is enabled. * fix(security): move the scanner-detections note into a masthead info icon Replace the standing "scanner detections show vulnerable components..." caption below the masthead (desktop and mobile) with an info affordance next to the scanned-images count in the masthead subtitle. Declutters the overview while keeping the disclaimer one hover away. * fix(security): apply "assume it's automatable" to exploit-risk ranking Absence of exploitability evidence must not be treated as low risk. Rank the top exploit-risk list by tier (known-exploited > known-high EPSS > unknown EPSS > known-low EPSS) so an unrated finding outranks one with evidence of low likelihood; label unrated findings "EPSS n/a"; and reword the quadrant footnote so excluded findings read as unrated rather than lower risk.
This commit is contained in:
@@ -17,7 +17,7 @@ import { useIsMobile } from '@/hooks/use-is-mobile';
|
||||
import { Masthead, type Tone } from './mobile/mobile-ui';
|
||||
import { SecurityMobileTabs, type SecurityMobileTab } from './security/SecurityMobile';
|
||||
import type { SecurityTab } from '@/lib/events';
|
||||
import type { SecurityOverview, ScanSummary, ScanDetailTab, SecurityRiskTrendPoint, FleetRole } from '@/types/security';
|
||||
import type { SecurityOverview, ScanSummary, ScanDetailTab, SecurityRiskTrendPoint, ExploitIntelFinding, FleetRole } from '@/types/security';
|
||||
import { VulnerabilityScanSheet } from './VulnerabilityScanSheet';
|
||||
import { SuppressionsPanel } from './settings/SuppressionsPanel';
|
||||
import { MisconfigAckPanel } from './settings/MisconfigAckPanel';
|
||||
@@ -75,6 +75,7 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
|
||||
const [summariesLoading, setSummariesLoading] = useState(true);
|
||||
const [summariesError, setSummariesError] = useState(false);
|
||||
const [trend, setTrend] = useState<SecurityRiskTrendPoint[]>([]);
|
||||
const [exploitIntel, setExploitIntel] = useState<ExploitIntelFinding[]>([]);
|
||||
const [isReplica, setIsReplica] = useState(false);
|
||||
// Bumped after a node-wide scan completes to refetch the active node's posture.
|
||||
const [reloadToken, setReloadToken] = useState(0);
|
||||
@@ -114,6 +115,12 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
|
||||
.then((r) => (r.ok ? r.json() : []))
|
||||
.then((t) => (Array.isArray(t) ? t : []))
|
||||
.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(() => []);
|
||||
try {
|
||||
const [overviewRes, summariesRes] = await Promise.all([
|
||||
apiFetch('/security/overview'),
|
||||
@@ -154,8 +161,11 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
|
||||
} finally {
|
||||
if (!cancelled) setSummariesLoading(false);
|
||||
}
|
||||
const trend = await trendPromise;
|
||||
if (!cancelled) setTrend(trend);
|
||||
const [trendData, intelData] = await Promise.all([trendPromise, exploitIntelPromise]);
|
||||
if (!cancelled) {
|
||||
setTrend(trendData);
|
||||
setExploitIntel(intelData);
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, [activeNode?.id, reloadToken]);
|
||||
@@ -202,9 +212,22 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
|
||||
{ value: 'scanner', label: 'Scanner setup' },
|
||||
];
|
||||
|
||||
const subtitle = overview
|
||||
? `${overview.scannedImages} ${overview.scannedImages === 1 ? 'image' : 'images'} scanned · scanner ${overview.scanner.available ? 'ready' : 'not installed'}`
|
||||
: undefined;
|
||||
// The scanner-detections disclaimer rides as an info affordance next to the
|
||||
// scanned-images count rather than a standing caption below the masthead.
|
||||
const subtitle = overview ? (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span>
|
||||
{overview.scannedImages} {overview.scannedImages === 1 ? 'image' : 'images'} scanned · scanner {overview.scanner.available ? 'ready' : 'not installed'}
|
||||
</span>
|
||||
<span
|
||||
className="inline-flex shrink-0 cursor-help text-stat-subtitle/70 hover:text-stat-subtitle"
|
||||
title={SCANNER_DETECTIONS_NOTE}
|
||||
aria-label={SCANNER_DETECTIONS_NOTE}
|
||||
>
|
||||
<Info className="h-3 w-3" strokeWidth={1.5} aria-hidden="true" />
|
||||
</span>
|
||||
</span>
|
||||
) : undefined;
|
||||
|
||||
// The tab panels are identical on desktop and mobile; only the masthead and
|
||||
// the tab strip differ, so the panels are shared between both layouts.
|
||||
@@ -214,8 +237,8 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
|
||||
<OverviewTab
|
||||
overview={overview}
|
||||
loadError={overviewLoadError}
|
||||
summaries={summaries}
|
||||
trend={trend}
|
||||
exploitIntel={exploitIntel}
|
||||
onNavigate={onTabChange}
|
||||
onInspect={onInspect}
|
||||
canScan={canScan}
|
||||
@@ -329,12 +352,11 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
|
||||
return (
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<PageMasthead
|
||||
kicker="SECURITY"
|
||||
state={state}
|
||||
tone={tone}
|
||||
pulsing={pulsing}
|
||||
size="hero"
|
||||
className="rounded-lg mb-2"
|
||||
className="rounded-lg mb-4"
|
||||
subtitle={subtitle}
|
||||
metadata={overview ? [
|
||||
{ label: 'CRITICAL', value: String(overview.critical), tone: overview.critical > 0 ? 'error' : 'value' },
|
||||
@@ -342,9 +364,6 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
|
||||
{ label: 'LAST SCAN', value: overview.lastSuccessfulScanAt ? formatTimeAgo(overview.lastSuccessfulScanAt) : 'never', tone: 'subtitle' },
|
||||
] : undefined}
|
||||
/>
|
||||
<p className="mb-4 max-w-3xl font-mono text-[11px] leading-snug text-stat-subtitle">
|
||||
{SCANNER_DETECTIONS_NOTE}
|
||||
</p>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={(v) => onTabChange(v as SecurityTab)}>
|
||||
<TabsList className="mb-4">
|
||||
|
||||
Reference in New Issue
Block a user