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.
47 lines
1.8 KiB
TypeScript
47 lines
1.8 KiB
TypeScript
/**
|
|
* The shared block message must name the inputs that actually matched, so a
|
|
* KEV-driven block never reads as a severity-threshold block.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { describePolicyBlock } from '../helpers/policyGate';
|
|
import type { PolicyViolation } from '../services/PolicyEnforcement';
|
|
import type { ScanPolicy } from '../services/DatabaseService';
|
|
|
|
const policy = { name: 'prod-gate', max_severity: 'CRITICAL' } as ScanPolicy;
|
|
const violation = (over: Partial<PolicyViolation>): PolicyViolation => ({
|
|
imageRef: 'nginx:1.14', severity: 'LOW', criticalCount: 0, highCount: 0,
|
|
kevCount: 0, fixableCount: 0, reasons: [], scanId: 1, ...over,
|
|
});
|
|
|
|
describe('describePolicyBlock', () => {
|
|
it('names KEV without mentioning a severity threshold', () => {
|
|
const msg = describePolicyBlock(policy, [violation({ kevCount: 1, reasons: ['kev'] })]);
|
|
expect(msg).toContain('known-exploited');
|
|
expect(msg).not.toContain('CRITICAL');
|
|
});
|
|
|
|
it('joins multiple distinct reasons across violations', () => {
|
|
const msg = describePolicyBlock(policy, [
|
|
violation({ reasons: ['kev'] }),
|
|
violation({ imageRef: 'redis:7', reasons: ['fixable'] }),
|
|
]);
|
|
expect(msg).toContain('known-exploited');
|
|
expect(msg).toContain('fixable');
|
|
});
|
|
|
|
it('de-duplicates a reason shared by multiple violations', () => {
|
|
const msg = describePolicyBlock(policy, [
|
|
violation({ reasons: ['kev'] }),
|
|
violation({ imageRef: 'redis:7', reasons: ['kev'] }),
|
|
]);
|
|
expect(msg.match(/known-exploited/g)).toHaveLength(1);
|
|
expect(msg).toContain('2 image(s)');
|
|
});
|
|
|
|
it('uses the supplied action verb and a generic phrase when no reason is set', () => {
|
|
const msg = describePolicyBlock(policy, [violation({})], 'update');
|
|
expect(msg).toContain('blocked update');
|
|
expect(msg).toContain('scan policy conditions');
|
|
});
|
|
});
|