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
+120
View File
@@ -326,6 +326,18 @@ export interface FleetSyncStatus {
last_error: string | null;
}
export interface CveSuppression {
id: number;
cve_id: string;
pkg_name: string | null;
image_pattern: string | null;
reason: string;
created_by: string;
created_at: number;
expires_at: number | null;
replicated_from_control: number;
}
export interface ScanSummary {
image_ref: string;
highest_severity: VulnSeverity | null;
@@ -633,6 +645,23 @@ export class DatabaseService {
PRIMARY KEY (node_id, resource)
);
CREATE TABLE IF NOT EXISTS cve_suppressions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cve_id TEXT NOT NULL,
pkg_name TEXT,
image_pattern TEXT,
reason TEXT NOT NULL DEFAULT '',
created_by TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER,
replicated_from_control INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_cve_suppressions_cve ON cve_suppressions(cve_id);
CREATE INDEX IF NOT EXISTS idx_cve_suppressions_expires ON cve_suppressions(expires_at);
-- COALESCE makes NULL scope slots collide the way users expect (NULL == NULL here).
CREATE UNIQUE INDEX IF NOT EXISTS idx_cve_suppressions_unique
ON cve_suppressions(cve_id, COALESCE(pkg_name, ''), COALESCE(image_pattern, ''));
CREATE TABLE IF NOT EXISTS stack_labels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL DEFAULT 0,
@@ -2515,6 +2544,97 @@ export class DatabaseService {
.all(resource, cutoff) as FleetSyncStatus[];
}
// --- CVE Suppressions ---
public getCveSuppressions(): CveSuppression[] {
return this.db
.prepare('SELECT * FROM cve_suppressions ORDER BY cve_id, pkg_name')
.all() as CveSuppression[];
}
public getCveSuppression(id: number): CveSuppression | null {
return (
(this.db.prepare('SELECT * FROM cve_suppressions WHERE id = ?')
.get(id) as CveSuppression | undefined) ?? null
);
}
public createCveSuppression(
suppression: Omit<CveSuppression, 'id'>,
): CveSuppression {
const result = this.db
.prepare(
`INSERT INTO cve_suppressions
(cve_id, pkg_name, image_pattern, reason, created_by, created_at, expires_at, replicated_from_control)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
suppression.cve_id,
suppression.pkg_name,
suppression.image_pattern,
suppression.reason,
suppression.created_by,
suppression.created_at,
suppression.expires_at,
suppression.replicated_from_control ?? 0,
);
return { ...suppression, id: result.lastInsertRowid as number };
}
public updateCveSuppression(
id: number,
updates: Partial<Pick<CveSuppression, 'reason' | 'image_pattern' | 'expires_at'>>,
): CveSuppression | null {
const existing = this.getCveSuppression(id);
if (!existing) return null;
const ALLOWED = new Set(['reason', 'image_pattern', 'expires_at']);
const fields: string[] = [];
const values: unknown[] = [];
for (const [key, value] of Object.entries(updates)) {
if (!ALLOWED.has(key)) continue;
fields.push(`${key} = ?`);
values.push(value);
}
if (fields.length === 0) return existing;
values.push(id);
this.db
.prepare(`UPDATE cve_suppressions SET ${fields.join(', ')} WHERE id = ?`)
.run(...(values as never[]));
return this.getCveSuppression(id);
}
public deleteCveSuppression(id: number): void {
this.db.prepare('DELETE FROM cve_suppressions WHERE id = ?').run(id);
}
/**
* Replace all replicated CVE suppressions in a single transaction.
* Preserves rows flagged as locally created on this instance.
*/
public replaceReplicatedCveSuppressions(rows: Array<Omit<CveSuppression, 'id'>>): void {
const deleteStmt = this.db.prepare('DELETE FROM cve_suppressions WHERE replicated_from_control = 1');
const insertStmt = this.db.prepare(
`INSERT INTO cve_suppressions
(cve_id, pkg_name, image_pattern, reason, created_by, created_at, expires_at, replicated_from_control)
VALUES (?, ?, ?, ?, ?, ?, ?, 1)`,
);
const txn = this.db.transaction((items: Array<Omit<CveSuppression, 'id'>>) => {
deleteStmt.run();
for (const s of items) {
insertStmt.run(
s.cve_id,
s.pkg_name,
s.image_pattern,
s.reason,
s.created_by,
s.created_at,
s.expires_at,
);
}
});
txn(rows);
}
// --- Stack Labels ---
public getLabels(nodeId: number): Label[] {
+30 -4
View File
@@ -1,8 +1,14 @@
import axios, { AxiosError } from 'axios';
import { DatabaseService, Node, ScanPolicy } from './DatabaseService';
import { CveSuppression, DatabaseService, Node, ScanPolicy } from './DatabaseService';
import { NodeRegistry } from './NodeRegistry';
export type FleetResource = 'scan_policies';
export type FleetResource = 'scan_policies' | 'cve_suppressions';
export const FLEET_RESOURCES: readonly FleetResource[] = ['scan_policies', 'cve_suppressions'];
export function isFleetResource(value: unknown): value is FleetResource {
return typeof value === 'string' && (FLEET_RESOURCES as readonly string[]).includes(value);
}
export type FleetRole = 'control' | 'replica';
@@ -140,14 +146,20 @@ export class FleetSyncService {
* This promotes the instance to 'replica' mode if not already, caches
* the target identity it was told, and replaces replicated rows atomically.
*/
public applyIncomingSync(resource: FleetResource, rows: ScanPolicy[], targetIdentity: string): void {
public applyIncomingSync(
resource: FleetResource,
rows: ScanPolicy[] | Array<Omit<CveSuppression, 'id'>>,
targetIdentity: string,
): void {
const db = DatabaseService.getInstance();
db.setSystemState('fleet_role', 'replica');
if (targetIdentity) {
db.setSystemState('fleet_self_identity', targetIdentity);
}
if (resource === 'scan_policies') {
db.replaceReplicatedScanPolicies(rows);
db.replaceReplicatedScanPolicies(rows as ScanPolicy[]);
} else if (resource === 'cve_suppressions') {
db.replaceReplicatedCveSuppressions(rows as Array<Omit<CveSuppression, 'id'>>);
}
}
@@ -168,6 +180,20 @@ export class FleetSyncService {
updated_at: p.updated_at,
}));
}
if (resource === 'cve_suppressions') {
return db
.getCveSuppressions()
.filter((s) => s.replicated_from_control === 0)
.map((s) => ({
cve_id: s.cve_id,
pkg_name: s.pkg_name,
image_pattern: s.image_pattern,
reason: s.reason,
created_by: s.created_by,
created_at: s.created_at,
expires_at: s.expires_at,
}));
}
return [];
}