feat(security): fleet-replicated CVE suppression list (#650)

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)
This commit is contained in:
Anso
2026-04-17 05:16:34 -04:00
committed by GitHub
parent 708d15b2b3
commit 732fc95415
16 changed files with 1568 additions and 41 deletions
@@ -28,6 +28,7 @@ import { ScanComparisonSheet } from './ScanComparisonSheet';
import { SeverityChip } from './VulnerabilityScanSheet';
import { VulnerabilityScanSheet } from './VulnerabilityScanSheet';
import { useLicense } from '@/context/LicenseContext';
import { useAuth } from '@/context/AuthContext';
import { useNodes } from '@/context/NodeContext';
import type { VulnerabilityScan } from '@/types/security';
@@ -56,6 +57,7 @@ function groupByImage(scans: VulnerabilityScan[]): GroupedScans[] {
export function SecurityHistoryView() {
const { isPaid } = useLicense();
const { isAdmin } = useAuth();
const { activeNode } = useNodes();
const [scans, setScans] = useState<VulnerabilityScan[]>([]);
const [loading, setLoading] = useState(false);
@@ -304,6 +306,7 @@ export function SecurityHistoryView() {
onClose={() => setInspectScanId(null)}
canGenerateSbom={isPaid}
canCompare={false}
canManageSuppressions={isPaid && isAdmin}
/>
</div>
);