mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-24 08:57:25 +00:00
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:
@@ -47,6 +47,7 @@ import { cn } from '@/lib/utils';
|
||||
import { cveUrl } from '@/lib/cveUrl';
|
||||
import { SEVERITY_ROW_TINT } from '@/lib/severityStyles';
|
||||
import { formatTimeAgo } from '@/lib/relativeTime';
|
||||
import { formatPolicyReasons } from '@/lib/policyReasons';
|
||||
import type {
|
||||
VulnerabilityScan,
|
||||
VulnerabilityDetail,
|
||||
@@ -629,16 +630,13 @@ export function VulnerabilityScanSheet({
|
||||
Policy violation
|
||||
</div>
|
||||
<div className="text-sm text-stat-value mt-0.5">
|
||||
<span className="font-mono">{scan.policy_evaluation.policyName}</span> blocks
|
||||
severities at or above{' '}
|
||||
<span className="font-mono tabular-nums">
|
||||
{scan.policy_evaluation.maxSeverity}
|
||||
</span>
|
||||
. This scan's highest severity is{' '}
|
||||
<span className="font-mono tabular-nums">
|
||||
{scan.highest_severity ?? 'UNKNOWN'}
|
||||
</span>
|
||||
.
|
||||
This scan violates{' '}
|
||||
<span className="font-mono">{scan.policy_evaluation.policyName}</span>
|
||||
{scan.policy_evaluation.reasons.length > 0 ? (
|
||||
<>: matched {formatPolicyReasons(scan.policy_evaluation.reasons, scan.policy_evaluation.maxSeverity)}.</>
|
||||
) : (
|
||||
<>.</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
@@ -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(', ');
|
||||
}
|
||||
@@ -4,10 +4,16 @@ export type VulnSeverity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | 'UNKNOWN';
|
||||
export type VulnScanStatus = 'in_progress' | 'completed' | 'failed';
|
||||
export type VulnScanTrigger = 'manual' | 'scheduled' | 'deploy' | 'deploy-preflight';
|
||||
|
||||
export type PolicyBlockReason = 'severity' | 'kev' | 'fixable';
|
||||
|
||||
export interface ScanPolicyEvaluation {
|
||||
policyId: number;
|
||||
policyName: string;
|
||||
maxSeverity: VulnSeverity;
|
||||
// Inputs that matched (severity / kev / fixable). Empty for evaluations
|
||||
// persisted before reason tracking; the banner falls back to a plain
|
||||
// violation notice in that case.
|
||||
reasons: PolicyBlockReason[];
|
||||
violated: boolean;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user