feat: chart-led Security overview with sortable Images and History tables (#1364)

* feat: chart-led Security overview with sortable Images and History tables

Refine the Security page around the existing design system and add the
data the dashboard needs.

- Overview leads with four charts (30-day risk trend, severity donut, top
  exposed images, findings by type); the signal-rail counts become a
  secondary summary, and the scanner and deploy-enforcement posture follow.
- Images becomes a recessed table with search, a severity filter, sortable
  columns, a last-scan column, and inline scan actions; the findings cell is
  clickable into the scan sheet, and the per-row cursor tooltip is dropped
  where the columns already carry that information.
- Policies puts deploy-enforcement first, collapses the policy packs into an
  accordion, and uses the standard primary button for Add policy.
- Suppressions and acknowledgements move their titles and Add buttons outside
  the cards, matching the Fleet tab layout.
- History switches from the detail sheet to an inline table (search, sortable
  columns, two-scan compare, pagination); the now-unreachable scan-history
  overlay is removed.
- Add GET /api/security/overview/trend, a node-scoped daily critical/high
  rollup backing the risk-trend chart.
- Extract the shared image-scan hook and the severity classifier, and harden
  the overview data fetch so a malformed non-critical response can never read
  as a clean security state.

* fix: treat malformed Security responses as errors, not empty or clean states

Address an independent review of the data-fetch paths so a 200 with an
unexpected shape can never read as a benign "no findings" view.

- SecurityView: validate that the image-summaries body is a scan-summary map; an
  unexpected shape now sets the error state instead of an empty map. Isolate the
  trend fetch in its own self-catching promise so a transport failure on the
  non-critical chart can no longer poison the overview or summaries error state.
- useImageScan: only a "completed" poll counts as success (a malformed or unknown
  status now throws), and a failed post-scan summaries refresh is logged instead
  of silently dropped.
- HistoryTab: a 200 whose body lacks an items array is treated as an error, not
  an empty "no completed scans" list.
This commit is contained in:
Anso
2026-06-12 14:35:03 -04:00
committed by GitHub
parent 1b96f3b980
commit 3d39d856a3
31 changed files with 1570 additions and 931 deletions
+40
View File
@@ -4515,6 +4515,46 @@ export class DatabaseService {
).cnt;
}
/**
* Daily Critical/High totals for the node over the last `days` days, for the
* Security overview risk-trend chart. For each calendar day with scans, takes
* the latest completed scan per image (so a re-scan replaces, not adds) and
* sums the critical and high counts across images. Days with no scans are
* omitted from the result.
*/
public getDailyRiskTrend(
nodeId: number,
days = 30,
): Array<{ date: string; critical: number; high: number }> {
const window = Math.max(1, Math.min(days, 365));
const cutoffMs = Date.now() - window * 24 * 60 * 60 * 1000;
const rows = this.db
.prepare(
`WITH daily_latest AS (
SELECT
DATE(scanned_at / 1000, 'unixepoch') AS day,
image_ref,
critical_count,
high_count,
ROW_NUMBER() OVER (
PARTITION BY DATE(scanned_at / 1000, 'unixepoch'), image_ref
ORDER BY scanned_at DESC
) AS rn
FROM vulnerability_scans
WHERE node_id = ? AND status = 'completed' AND scanned_at >= ?
)
SELECT day,
SUM(critical_count) AS critical,
SUM(high_count) AS high
FROM daily_latest
WHERE rn = 1
GROUP BY day
ORDER BY day ASC`,
)
.all(nodeId, cutoffMs) as Array<{ day: string; critical: number; high: number }>;
return rows.map((r) => ({ date: r.day, critical: r.critical ?? 0, high: r.high ?? 0 }));
}
/**
* Count of enabled block-on-deploy policies that are eligible to apply to
* this node: fleet-wide (node_id IS NULL) or scoped to this node. Built on