Files
sencho/backend/src/__tests__/policy-risk.test.ts
T
Anso ca496c89dc fix: name matched risk inputs in policy block messages (#1471)
The auto-update, bulk-label, scheduler, and blueprint deploy block
messages hardcoded "image(s) exceed <max_severity>", which is wrong
under the risk-first policy model: a block can be driven by a
known-exploited (KEV) or fixable Critical/High input while the severity
threshold was never the trigger. In those cases the message named a
severity ceiling the policy did not enforce.

Route all four message paths through a shared summarizeBlockReasons
helper (the same reason text the deploy-gate 409 response and the block
dialog already use), so every surface names the inputs that actually
matched. Falls back to a generic phrase when no reason was recorded.
2026-06-26 15:34:24 -04:00

112 lines
4.3 KiB
TypeScript

/**
* Pins the pure risk-decision helper shared by the pre-deploy gate and the
* informational post-scan evaluation. KEV membership is supplied as a test
* predicate, so these cases stay free of DB and intel-cache setup.
*/
import { describe, it, expect } from 'vitest';
import {
evaluatePolicyRisk,
describeReason,
describePolicyInputs,
summarizeBlockReasons,
type PolicyRiskInputs,
type RiskFinding,
} from '../utils/policy-risk';
const noKev = () => false;
const finding = (over: Partial<RiskFinding> = {}): RiskFinding => ({
vulnerability_id: 'CVE-2026-0001',
severity: 'HIGH',
fixed_version: null,
...over,
});
const inputs = (over: Partial<PolicyRiskInputs> = {}): PolicyRiskInputs => ({
blockOnSeverity: false,
blockOnKev: false,
blockOnFixable: false,
maxSeverity: 'HIGH',
...over,
});
describe('evaluatePolicyRisk', () => {
it('matches severity only when the highest finding meets the threshold', () => {
const high = evaluatePolicyRisk([finding({ severity: 'HIGH' })], noKev, inputs({ blockOnSeverity: true, maxSeverity: 'HIGH' }));
expect(high.reasons).toEqual(['severity']);
const low = evaluatePolicyRisk([finding({ severity: 'LOW' })], noKev, inputs({ blockOnSeverity: true, maxSeverity: 'HIGH' }));
expect(low.reasons).toEqual([]);
});
it('matches KEV when a finding is known-exploited, regardless of severity', () => {
const isKev = (id: string) => id === 'CVE-2026-9999';
const out = evaluatePolicyRisk(
[finding({ vulnerability_id: 'CVE-2026-9999', severity: 'LOW' })],
isKev,
inputs({ blockOnKev: true }),
);
expect(out.reasons).toEqual(['kev']);
expect(out.kevCount).toBe(1);
});
it('counts a Critical/High finding with a fix as fixable, but not one without', () => {
const fixable = evaluatePolicyRisk([finding({ severity: 'CRITICAL', fixed_version: '1.2.3' })], noKev, inputs({ blockOnFixable: true }));
expect(fixable.reasons).toEqual(['fixable']);
expect(fixable.fixableCount).toBe(1);
const noFix = evaluatePolicyRisk([finding({ severity: 'CRITICAL', fixed_version: null })], noKev, inputs({ blockOnFixable: true }));
expect(noFix.reasons).toEqual([]);
const lowFix = evaluatePolicyRisk([finding({ severity: 'MEDIUM', fixed_version: '1.0' })], noKev, inputs({ blockOnFixable: true }));
expect(lowFix.reasons).toEqual([]);
});
it('reports every input that matches, in display order', () => {
const isKev = () => true;
const out = evaluatePolicyRisk(
[finding({ severity: 'CRITICAL', fixed_version: '2.0' })],
isKev,
inputs({ blockOnSeverity: true, blockOnKev: true, blockOnFixable: true, maxSeverity: 'HIGH' }),
);
expect(out.reasons).toEqual(['severity', 'kev', 'fixable']);
});
it('returns no reasons when no input is enabled', () => {
const out = evaluatePolicyRisk([finding({ severity: 'CRITICAL' })], () => true, inputs());
expect(out.reasons).toEqual([]);
});
});
describe('describeReason / describePolicyInputs', () => {
it('labels each reason', () => {
expect(describeReason('severity')).toContain('severity');
expect(describeReason('kev')).toContain('KEV');
expect(describeReason('fixable')).toContain('fixable');
});
it('lists active inputs and notes when none are active', () => {
expect(describePolicyInputs(inputs({ blockOnSeverity: true, maxSeverity: 'HIGH' }))).toContain('severity>=HIGH');
expect(describePolicyInputs(inputs({ blockOnKev: true, blockOnFixable: true }))).toBe('KEV, fixable Critical/High');
expect(describePolicyInputs(inputs())).toBe('no active inputs');
});
});
describe('summarizeBlockReasons', () => {
it('names a KEV-driven block as known-exploited, not a severity threshold', () => {
expect(summarizeBlockReasons([{ reasons: ['kev'] }])).toBe('known-exploited CVE (KEV)');
});
it('joins and de-duplicates reasons across violations', () => {
const summary = summarizeBlockReasons([
{ reasons: ['kev'] },
{ reasons: ['fixable', 'kev'] },
]);
expect(summary).toBe('known-exploited CVE (KEV) + fixable Critical/High');
});
it('falls back to a generic phrase when no reason was recorded', () => {
expect(summarizeBlockReasons([{ reasons: [] }])).toBe('scan policy conditions');
expect(summarizeBlockReasons([])).toBe('scan policy conditions');
});
});