mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-28 03:06:57 +00:00
ca496c89dc
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.
112 lines
4.3 KiB
TypeScript
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');
|
|
});
|
|
});
|