fix: name matched risk inputs in policy scan banner and alerts (#1473)

The pre-deploy gate names the inputs that matched a scan policy (a
known-exploited CVE, a fixable Critical/High, or a severity threshold),
but the informational post-scan surfaces still framed every violation as
a severity ceiling. The scan detail banner read "blocks severities at or
above X, highest severity is Y" and the scheduled-scan alert read
"<severity> exceeds <maxSeverity>", which is wrong for a KEV- or
fixable-only policy that never gated on severity.

Persist the matched reasons on the policy evaluation, carry them on the
scheduled-scan violation, and render them on the banner so every policy
surface names the input that actually matched. Evaluations persisted
before this change carry no reasons: the parser defaults the field to an
empty array and the banner falls back to a plain violation notice.
This commit is contained in:
Anso
2026-06-26 16:43:26 -04:00
committed by GitHub
parent d9b7911f12
commit 1de49f8b1a
9 changed files with 150 additions and 39 deletions
+22
View File
@@ -0,0 +1,22 @@
import { describe, it, expect } from 'vitest';
import { formatPolicyReasons } from './policyReasons';
describe('formatPolicyReasons', () => {
it('names a KEV match without citing a severity ceiling', () => {
expect(formatPolicyReasons(['kev'], 'HIGH')).toBe('a known-exploited CVE (KEV)');
});
it('names the configured ceiling for a severity match', () => {
expect(formatPolicyReasons(['severity'], 'CRITICAL')).toBe('severity at or above CRITICAL');
});
it('joins every matched reason in order', () => {
expect(formatPolicyReasons(['severity', 'kev', 'fixable'], 'HIGH')).toBe(
'severity at or above HIGH, a known-exploited CVE (KEV), a fixable Critical/High',
);
});
it('returns an empty string when no reason was recorded', () => {
expect(formatPolicyReasons([], 'HIGH')).toBe('');
});
});
+27
View File
@@ -0,0 +1,27 @@
import type { PolicyBlockReason, VulnSeverity } from '@/types/security';
/**
* Human-readable description of the policy inputs that matched, for the scan
* detail banner. Echoes the deploy-gate dialog's phrasing (PolicyBlockDialog)
* so the banner and the block dialog read consistently, and names the
* configured severity ceiling so the severity input stays specific. Returns an
* empty string when no reason was recorded (evaluations persisted before
* reason tracking).
*/
export function formatPolicyReasons(
reasons: PolicyBlockReason[],
maxSeverity: VulnSeverity,
): string {
return reasons
.map((reason) => {
switch (reason) {
case 'severity':
return `severity at or above ${maxSeverity}`;
case 'kev':
return 'a known-exploited CVE (KEV)';
case 'fixable':
return 'a fixable Critical/High';
}
})
.join(', ');
}