* 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.
* feat(security): make CVE suppressions optionally honored by deploy-block policies
Block-on-deploy policies evaluate the raw scan result, so a CVE an admin
has accepted in CVE Suppressions still blocks the deploy. Add an opt-in,
per-instance toggle ("Honor suppressions in deploy blocks", Settings ->
Security) that, when on, re-derives each image's severity from the
suppression-filtered findings before comparing to the policy threshold. A
deploy that proceeds only because suppressions dropped it below the gate is
recorded in the audit log. Default off, so the strict raw-scan behavior is
unchanged unless an operator enables it.
The setting governs the instance that runs the deploy and is not
fleet-replicated. The gate fails safe: a suppression-read error or an
empty detail set falls back to raw scan severity rather than dropping it.
Also surface a previously swallowed error in the CVE suppressions and
misconfig acknowledgement settings panels so a failed list load shows a
toast instead of an empty list.
* fix(security): gate on raw severity when preflight detail rows are truncated
The suppression-aware deploy gate re-derived image severity from the stored
vulnerability_details rows, assuming any non-empty set was complete. A cached
pre-deploy scan keeps the full aggregate counts but copies only a bounded slice
of detail rows, so recomputing from that slice could drop an unsuppressed
blocking CVE below the threshold and let a deploy through.
Guard the recompute: when the loaded detail rows do not match the scan's total
finding count, gate on the raw scan severity (never drops severity). Suppression
awareness still applies for scans whose details are stored in full, which is the
common case.
* fix(security): gate admin-only scan affordances on isAdmin
Backend already required admin for SBOM, SARIF, scan policies, Trivy
install/update/uninstall, the auto-update toggle, CVE suppressions, and
misconfig acknowledgements. The matching frontend surfaces were gated
only on isPaid (or only on isReplica), so non-admin users at the same
tier saw buttons that returned 403 on click.
Threads isAdmin from useAuth() into SecuritySection, SuppressionsPanel,
and MisconfigAckPanel. Updates the scan-result sheet caller in
ResourcesView so SBOM and SARIF render only when paid AND admin; passes
canManageSuppressions to the stack-misconfig sheet so admins can ack
misconfigs from that surface too.
Read paths remain visible to non-admins (policy list, suppression list,
ack list, scan history) since the GET routes are auth-only on both
sides.
* fix(security): close 3 remaining scan-sheet parity gaps from review
Code review surfaced three sites missed in the first pass:
1. SecurityHistoryView opened the scan sheet with canGenerateSbom set
only on isPaid, so Skipper non-admins saw SBOM and SARIF buttons
even though the backend requires admin+paid. Now ANDed with isAdmin.
2. ResourcesView passed onRescan unconditionally, and the sheet
renders a Re-scan primary action whenever onRescan is defined.
Non-admins reaching the sheet via the severity-badge shortcut saw
the button; clicking it called POST /security/scan, which the
backend requires admin for. onRescan is now undefined for
non-admins.
3. ShellOverlays and ResourcesView passed canManageSuppressions=isAdmin
without considering the replica gate, so a replica admin saw
suppress and ack columns whose backend writes blockIfReplica. The
sheet now probes /fleet/role internally and ANDs !isReplica into
the effective canManageSuppressions, so the column hides on a
replica regardless of how the caller wired the prop.
* fix(security): clear isReplica state on every scan-sheet probe
The previous probe only flipped the state to true on a replica response
and never wrote false on a control, non-OK, or skipped probe. With the
sheet kept mounted by ResourcesView, SecurityHistoryView, and
ShellOverlays, an admin who first viewed a scan on a replica would
keep suppress/ack controls hidden even after switching to a control
instance, because the stale true value persisted across re-opens.
The effect now resets isReplica to false at the start of every probe
and assigns the result of /fleet/role directly. Probe failures and
skips leave the state at false, so the UI is permissive and the
backend blockIfReplica guard remains the source of truth.
Bring the two settings panels onto §10 Modal primitives.
LabelsSection:
- Create/edit form -> Modal at sm with kicker LABELS · NEW or
LABELS · EDIT
- Delete confirm -> destructive ConfirmModal with kicker
LABELS · DELETE · IRREVERSIBLE
- handleDelete now closes from finally so the dialog clears on errors
too (the new ConfirmModal Promise-aware behaviour keeps it open until
state closes it)
SuppressionsPanel:
- New suppression form -> Modal at md with kicker SUPPRESSIONS · NEW
- Remove confirm -> destructive ConfirmModal with kicker
SUPPRESSIONS · REMOVE · IRREVERSIBLE
- handleDelete already closed from finally; no change needed there
Operators can accept known-benign findings once and have Sencho filter
them out of scan drawers, comparison views, and other read surfaces.
Suppressions replicate from the control instance to every remote node.
* New cve_suppressions table with a COALESCE-based unique index so NULL
scope slots collide the way users expect
* Admin + paid-tier CRUD routes; writes are rejected on replicas
* Read-time filter enriches vulnerability details and compare payloads
without mutating stored counts
* Settings > Security panel for managing rules, per-CVE suppress action
in the scan drawer, dimmed rows with a shield-off indicator
* Vitest unit tests for the filter (glob, expiry, specificity) and
route tests (auth, tier, replica, UNIQUE conflict)