mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-06 00:47:52 +00:00
6527bc971b
Scan-policy deploy gates can now block on a known-exploited CVE (CISA KEV) and on a fixable Critical/High finding, in addition to an optional severity threshold. New policies default risk-first (KEV and fixable on, severity off); existing policies keep their severity-only behavior. CVSS stays captured for context but is never the sole basis for a block, and a finding whose exploitability cannot be confirmed is treated as risky rather than safe (incomplete scan detail fails closed on KEV/fixable inputs). The decision logic is shared between the pre-deploy gate and the informational post-scan banner via a pure helper, so the two never disagree. Block messages and the block dialog now name the conditions an image matched. Backend and frontend gates move together, the new inputs replicate across the fleet, and a blocking policy with no active input is rejected on both sides.
112 lines
4.3 KiB
TypeScript
112 lines
4.3 KiB
TypeScript
/**
|
|
* Pins the matching and ordering behavior of `DatabaseService.getMatchingPolicy`.
|
|
*
|
|
* The matcher must be deterministic across replicas: two policies in the same
|
|
* scope class (e.g. both fleet-wide stack-wildcard) need to resolve to the same
|
|
* winner regardless of SQLite row-iteration order. The chosen tiebreaker is
|
|
* lowest id wins, so the oldest-defined policy stays authoritative.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import { setupTestDb, cleanupTestDb } from './helpers/setupTestDb';
|
|
|
|
let tmpDir: string;
|
|
let DatabaseService: typeof import('../services/DatabaseService').DatabaseService;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = await setupTestDb();
|
|
({ DatabaseService } = await import('../services/DatabaseService'));
|
|
});
|
|
|
|
afterAll(() => {
|
|
cleanupTestDb(tmpDir);
|
|
});
|
|
|
|
describe('getMatchingPolicy tiebreaker', () => {
|
|
it('returns the lowest-id row when two policies tie on scope class', () => {
|
|
const db = DatabaseService.getInstance();
|
|
const first = db.createScanPolicy({
|
|
name: 'first-fleet-wide',
|
|
node_id: null,
|
|
node_identity: '',
|
|
stack_pattern: null,
|
|
max_severity: 'HIGH',
|
|
block_on_deploy: 0, block_on_severity: 1, block_on_kev: 0, block_on_fixable: 0,
|
|
enabled: 1,
|
|
replicated_from_control: 0,
|
|
});
|
|
const second = db.createScanPolicy({
|
|
name: 'second-fleet-wide',
|
|
node_id: null,
|
|
node_identity: '',
|
|
stack_pattern: null,
|
|
max_severity: 'CRITICAL',
|
|
block_on_deploy: 0, block_on_severity: 1, block_on_kev: 0, block_on_fixable: 0,
|
|
enabled: 1,
|
|
replicated_from_control: 0,
|
|
});
|
|
const winner = db.getMatchingPolicy(1, 'web', 'local');
|
|
expect(winner?.id).toBe(first.id);
|
|
// Sanity: with first deleted, the next-lowest takes over.
|
|
db.deleteScanPolicy(first.id);
|
|
const next = db.getMatchingPolicy(1, 'web', 'local');
|
|
expect(next?.id).toBe(second.id);
|
|
});
|
|
|
|
it('prefers node-scoped over fleet-wide regardless of id order', () => {
|
|
const db = DatabaseService.getInstance();
|
|
const fleetWide = db.createScanPolicy({
|
|
name: 'tie-fleet',
|
|
node_id: null,
|
|
node_identity: '',
|
|
stack_pattern: null,
|
|
max_severity: 'LOW',
|
|
block_on_deploy: 0, block_on_severity: 1, block_on_kev: 0, block_on_fixable: 0,
|
|
enabled: 1,
|
|
replicated_from_control: 0,
|
|
});
|
|
const nodeScoped = db.createScanPolicy({
|
|
name: 'tie-node',
|
|
node_id: 1,
|
|
node_identity: 'local',
|
|
stack_pattern: null,
|
|
max_severity: 'CRITICAL',
|
|
block_on_deploy: 0, block_on_severity: 1, block_on_kev: 0, block_on_fixable: 0,
|
|
enabled: 1,
|
|
replicated_from_control: 0,
|
|
});
|
|
const winner = db.getMatchingPolicy(1, 'web', 'local');
|
|
// Node-scoped wins by class, even though its id is higher than the fleet-wide row.
|
|
expect(winner?.id).toBe(nodeScoped.id);
|
|
db.deleteScanPolicy(fleetWide.id);
|
|
db.deleteScanPolicy(nodeScoped.id);
|
|
});
|
|
|
|
it('respects identity matching for replicated rows', () => {
|
|
const db = DatabaseService.getInstance();
|
|
const otherIdentity = db.createScanPolicy({
|
|
name: 'replicated-other',
|
|
node_id: null,
|
|
node_identity: 'https://other.example',
|
|
stack_pattern: null,
|
|
max_severity: 'CRITICAL',
|
|
block_on_deploy: 0, block_on_severity: 1, block_on_kev: 0, block_on_fixable: 0,
|
|
enabled: 1,
|
|
replicated_from_control: 1,
|
|
});
|
|
const ourIdentity = db.createScanPolicy({
|
|
name: 'replicated-self',
|
|
node_id: null,
|
|
node_identity: 'https://me.example',
|
|
stack_pattern: null,
|
|
max_severity: 'HIGH',
|
|
block_on_deploy: 0, block_on_severity: 1, block_on_kev: 0, block_on_fixable: 0,
|
|
enabled: 1,
|
|
replicated_from_control: 1,
|
|
});
|
|
const winner = db.getMatchingPolicy(1, 'web', 'https://me.example');
|
|
expect(winner?.id).toBe(ourIdentity.id);
|
|
db.deleteScanPolicy(otherIdentity.id);
|
|
db.deleteScanPolicy(ourIdentity.id);
|
|
});
|
|
});
|