mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 18:32: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.
146 lines
4.9 KiB
TypeScript
146 lines
4.9 KiB
TypeScript
/**
|
|
* Pins the cascade behavior of `DatabaseService.deleteScanPolicy`.
|
|
*
|
|
* Scans are evaluated against a policy at scan time and the result is cached
|
|
* inside `vulnerability_scans.policy_evaluation` as a JSON blob. When the
|
|
* underlying policy is deleted, those blobs must be cleared so the scheduler
|
|
* and UI stop reporting violations from a policy that no longer exists.
|
|
*/
|
|
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);
|
|
});
|
|
|
|
function seedScanWithEvaluation(policyId: number, digest: string): number {
|
|
const db = DatabaseService.getInstance();
|
|
const id = db.createVulnerabilityScan({
|
|
node_id: 1,
|
|
image_ref: `alpine:${digest.slice(-4)}`,
|
|
image_digest: digest,
|
|
scanned_at: Date.now(),
|
|
total_vulnerabilities: 1,
|
|
critical_count: 0,
|
|
high_count: 1,
|
|
medium_count: 0,
|
|
low_count: 0,
|
|
unknown_count: 0,
|
|
fixable_count: 0,
|
|
secret_count: 0,
|
|
misconfig_count: 0,
|
|
scanners_used: 'vuln',
|
|
highest_severity: 'HIGH',
|
|
os_info: 'alpine 3.19',
|
|
trivy_version: '0.56.0',
|
|
scan_duration_ms: 1200,
|
|
triggered_by: 'manual',
|
|
status: 'completed',
|
|
error: null,
|
|
stack_context: null,
|
|
});
|
|
db.updateVulnerabilityScan(id, {
|
|
policy_evaluation: JSON.stringify({
|
|
policyId,
|
|
policyName: 'block-high',
|
|
maxSeverity: 'HIGH',
|
|
violated: true,
|
|
evaluatedAt: Date.now(),
|
|
}),
|
|
});
|
|
return id;
|
|
}
|
|
|
|
describe('deleteScanPolicy', () => {
|
|
it('removes the policy row and clears policy_evaluation on matching scans', () => {
|
|
const db = DatabaseService.getInstance();
|
|
const policy = db.createScanPolicy({
|
|
name: 'block-high',
|
|
node_id: null,
|
|
node_identity: '',
|
|
stack_pattern: '*',
|
|
max_severity: 'HIGH',
|
|
block_on_deploy: 1, block_on_severity: 1, block_on_kev: 0, block_on_fixable: 0,
|
|
enabled: 1,
|
|
replicated_from_control: 0,
|
|
});
|
|
const otherPolicy = db.createScanPolicy({
|
|
name: 'block-critical',
|
|
node_id: null,
|
|
node_identity: '',
|
|
stack_pattern: '*',
|
|
max_severity: 'CRITICAL',
|
|
block_on_deploy: 1, block_on_severity: 1, block_on_kev: 0, block_on_fixable: 0,
|
|
enabled: 1,
|
|
replicated_from_control: 0,
|
|
});
|
|
|
|
const matchingScanId = seedScanWithEvaluation(policy.id, 'sha256:matching');
|
|
const unrelatedScanId = seedScanWithEvaluation(otherPolicy.id, 'sha256:unrelated');
|
|
|
|
db.deleteScanPolicy(policy.id);
|
|
|
|
expect(db.getScanPolicy(policy.id)).toBeNull();
|
|
expect(db.getVulnerabilityScan(matchingScanId)?.policy_evaluation).toBeNull();
|
|
expect(db.getVulnerabilityScan(unrelatedScanId)?.policy_evaluation).not.toBeNull();
|
|
expect(db.getScanPolicy(otherPolicy.id)).not.toBeNull();
|
|
});
|
|
|
|
it('is a no-op for an unknown policy id', () => {
|
|
const db = DatabaseService.getInstance();
|
|
expect(() => db.deleteScanPolicy(999_999)).not.toThrow();
|
|
});
|
|
|
|
it('does not touch scans whose policy_evaluation is NULL', () => {
|
|
const db = DatabaseService.getInstance();
|
|
const policy = db.createScanPolicy({
|
|
name: 'block-medium',
|
|
node_id: null,
|
|
node_identity: '',
|
|
stack_pattern: '*',
|
|
max_severity: 'MEDIUM',
|
|
block_on_deploy: 1, block_on_severity: 1, block_on_kev: 0, block_on_fixable: 0,
|
|
enabled: 1,
|
|
replicated_from_control: 0,
|
|
});
|
|
const unevaluatedScanId = db.createVulnerabilityScan({
|
|
node_id: 1,
|
|
image_ref: 'alpine:none',
|
|
image_digest: 'sha256:nopol',
|
|
scanned_at: Date.now(),
|
|
total_vulnerabilities: 0,
|
|
critical_count: 0,
|
|
high_count: 0,
|
|
medium_count: 0,
|
|
low_count: 0,
|
|
unknown_count: 0,
|
|
fixable_count: 0,
|
|
secret_count: 0,
|
|
misconfig_count: 0,
|
|
scanners_used: 'vuln',
|
|
highest_severity: null,
|
|
os_info: 'alpine 3.19',
|
|
trivy_version: '0.56.0',
|
|
scan_duration_ms: 100,
|
|
triggered_by: 'manual',
|
|
status: 'completed',
|
|
error: null,
|
|
stack_context: null,
|
|
});
|
|
|
|
db.deleteScanPolicy(policy.id);
|
|
|
|
const row = db.getVulnerabilityScan(unevaluatedScanId);
|
|
expect(row).not.toBeNull();
|
|
expect(row?.policy_evaluation).toBeNull();
|
|
});
|
|
});
|