mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 19:57:37 +00:00
b91025dc8b
Remove the leftover paid-only blockingEnabled switch so enabled block-on-deploy policies enforce on every tier, matching the documented every-tier security surface. Existing Community policies begin blocking immediately with no migration.
799 lines
38 KiB
TypeScript
799 lines
38 KiB
TypeScript
/**
|
|
* Covers the pre-deploy policy gate: no-policy, disabled-policy, Trivy-missing
|
|
* (fail open + notification de-dup), violation, admin bypass (audit-logged),
|
|
* and compose-parse-failure (fail closed) paths.
|
|
*
|
|
* The gate is the only code path that can block a `docker compose up`, so
|
|
* regressions here are high-impact. We stub the four collaborators the helper
|
|
* talks to rather than spinning up the real services, since the contracts
|
|
* between them are stable and already exercised by their own tests.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import type { ScanPolicy, VulnerabilityScan } from '../services/DatabaseService';
|
|
|
|
interface TrivyStub {
|
|
isTrivyAvailable: ReturnType<typeof vi.fn>;
|
|
scanImagePreflight: ReturnType<typeof vi.fn>;
|
|
}
|
|
interface ComposeStub {
|
|
listStackImages: ReturnType<typeof vi.fn>;
|
|
}
|
|
interface DbStub {
|
|
getMatchingPolicy: ReturnType<typeof vi.fn>;
|
|
insertAuditLog: ReturnType<typeof vi.fn>;
|
|
getGlobalSettings: ReturnType<typeof vi.fn>;
|
|
getAllVulnerabilityDetails: ReturnType<typeof vi.fn>;
|
|
getCveSuppressions: ReturnType<typeof vi.fn>;
|
|
getCveIntel: ReturnType<typeof vi.fn>;
|
|
}
|
|
interface NotificationStub {
|
|
dispatchAlert: ReturnType<typeof vi.fn>;
|
|
}
|
|
|
|
const trivyStub: TrivyStub = {
|
|
isTrivyAvailable: vi.fn(),
|
|
scanImagePreflight: vi.fn(),
|
|
};
|
|
const composeStub: ComposeStub = {
|
|
listStackImages: vi.fn(),
|
|
};
|
|
const dbStub: DbStub = {
|
|
getMatchingPolicy: vi.fn(),
|
|
insertAuditLog: vi.fn(),
|
|
getGlobalSettings: vi.fn(),
|
|
getAllVulnerabilityDetails: vi.fn(),
|
|
getCveSuppressions: vi.fn(),
|
|
getCveIntel: vi.fn(),
|
|
};
|
|
const notificationStub: NotificationStub = {
|
|
dispatchAlert: vi.fn(),
|
|
};
|
|
|
|
vi.mock('../services/TrivyService', () => ({
|
|
default: { getInstance: () => trivyStub },
|
|
}));
|
|
vi.mock('../services/ComposeService', () => ({
|
|
ComposeService: { getInstance: () => composeStub },
|
|
}));
|
|
vi.mock('../services/DatabaseService', () => ({
|
|
DatabaseService: { getInstance: () => dbStub },
|
|
}));
|
|
vi.mock('../services/NotificationService', () => ({
|
|
NotificationService: { getInstance: () => notificationStub },
|
|
}));
|
|
vi.mock('../services/FleetSyncService', () => ({
|
|
FleetSyncService: { getSelfIdentity: () => 'self-node' },
|
|
}));
|
|
|
|
import {
|
|
_resetTrivyMissingNotificationStateForTests,
|
|
enforcePolicyForImageRefs,
|
|
enforcePolicyPreDeploy,
|
|
} from '../services/PolicyEnforcement';
|
|
|
|
function mkPolicy(overrides: Partial<ScanPolicy> = {}): ScanPolicy {
|
|
return {
|
|
id: 1,
|
|
name: 'block-high',
|
|
node_id: null,
|
|
node_identity: 'self-node',
|
|
stack_pattern: '*',
|
|
max_severity: 'HIGH',
|
|
block_on_deploy: 1,
|
|
enabled: 1,
|
|
block_on_severity: 1,
|
|
block_on_kev: 0,
|
|
block_on_fixable: 0,
|
|
replicated_from_control: 0,
|
|
created_at: Date.now(),
|
|
updated_at: Date.now(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function mkScan(overrides: Partial<VulnerabilityScan> = {}): VulnerabilityScan {
|
|
return {
|
|
id: 1,
|
|
node_id: 1,
|
|
image_ref: 'nginx:1.14',
|
|
image_digest: null,
|
|
scanned_at: Date.now(),
|
|
total_vulnerabilities: 0,
|
|
critical_count: 0,
|
|
high_count: 0,
|
|
medium_count: 0,
|
|
low_count: 0,
|
|
unknown_count: 0,
|
|
fixable_count: 0,
|
|
secret_count: 0,
|
|
misconfig_count: 0,
|
|
scanners_used: 'vuln',
|
|
highest_severity: 'LOW',
|
|
os_info: null,
|
|
trivy_version: '0.50.0',
|
|
scan_duration_ms: null,
|
|
triggered_by: 'deploy-preflight',
|
|
status: 'completed',
|
|
error: null,
|
|
stack_context: 'web',
|
|
policy_evaluation: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('enforcePolicyPreDeploy', () => {
|
|
beforeEach(() => {
|
|
trivyStub.isTrivyAvailable.mockReset();
|
|
trivyStub.scanImagePreflight.mockReset();
|
|
composeStub.listStackImages.mockReset();
|
|
dbStub.getMatchingPolicy.mockReset();
|
|
dbStub.insertAuditLog.mockReset();
|
|
dbStub.getGlobalSettings.mockReset();
|
|
dbStub.getAllVulnerabilityDetails.mockReset();
|
|
dbStub.getCveSuppressions.mockReset();
|
|
// Default: suppression-aware blocking off, so behavior matches the raw-scan
|
|
// path unless a test opts in.
|
|
dbStub.getGlobalSettings.mockReturnValue({});
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([]);
|
|
dbStub.getCveSuppressions.mockReturnValue([]);
|
|
dbStub.getCveIntel.mockReset().mockReturnValue(new Map());
|
|
notificationStub.dispatchAlert.mockReset();
|
|
_resetTrivyMissingNotificationStateForTests();
|
|
});
|
|
|
|
it('allows deploy when no matching policy exists', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(null);
|
|
|
|
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.bypassed).toBe(false);
|
|
expect(result.violations).toEqual([]);
|
|
expect(result.policy).toBeUndefined();
|
|
expect(trivyStub.isTrivyAvailable).not.toHaveBeenCalled();
|
|
expect(composeStub.listStackImages).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('allows deploy when the matching policy is disabled', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ enabled: 0 }));
|
|
|
|
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.bypassed).toBe(false);
|
|
expect(result.violations).toEqual([]);
|
|
expect(composeStub.listStackImages).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('allows deploy when the matching policy does not block on deploy', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_deploy: 0 }));
|
|
|
|
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.violations).toEqual([]);
|
|
expect(composeStub.listStackImages).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('fails open with a warning alert when Trivy is not installed', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
|
|
trivyStub.isTrivyAvailable.mockReturnValue(false);
|
|
|
|
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.trivyMissing).toBe(true);
|
|
expect(result.violations).toEqual([]);
|
|
expect(notificationStub.dispatchAlert).toHaveBeenCalledTimes(1);
|
|
expect(notificationStub.dispatchAlert.mock.calls[0][0]).toBe('warning');
|
|
expect(notificationStub.dispatchAlert.mock.calls[0][1]).toBe('scan_finding');
|
|
expect(notificationStub.dispatchAlert.mock.calls[0][2]).toContain('Trivy not installed');
|
|
expect(composeStub.listStackImages).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('collapses repeated trivy-missing notifications for the same stack within the cooldown', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
|
|
trivyStub.isTrivyAvailable.mockReturnValue(false);
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
expect(result.trivyMissing).toBe(true);
|
|
}
|
|
|
|
expect(notificationStub.dispatchAlert).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('dispatches a separate trivy-missing notification per stack', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
|
|
trivyStub.isTrivyAvailable.mockReturnValue(false);
|
|
|
|
await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
await enforcePolicyPreDeploy('db', 1, { bypass: false, actor: 'u' });
|
|
await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
await enforcePolicyPreDeploy('db', 1, { bypass: false, actor: 'u' });
|
|
|
|
expect(notificationStub.dispatchAlert).toHaveBeenCalledTimes(2);
|
|
const stackNames = notificationStub.dispatchAlert.mock.calls.map((c) => (c[3] as { stackName: string }).stackName);
|
|
expect(stackNames).toEqual(['web', 'db']);
|
|
});
|
|
|
|
it('dispatches a separate trivy-missing notification per node for the same stack name', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
|
|
trivyStub.isTrivyAvailable.mockReturnValue(false);
|
|
|
|
await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
await enforcePolicyPreDeploy('web', 2, { bypass: false, actor: 'u' });
|
|
await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
await enforcePolicyPreDeploy('web', 2, { bypass: false, actor: 'u' });
|
|
|
|
expect(notificationStub.dispatchAlert).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('redispatches the trivy-missing notification after the cooldown elapses', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
|
|
trivyStub.isTrivyAvailable.mockReturnValue(false);
|
|
const nowSpy = vi.spyOn(Date, 'now');
|
|
const start = 1_700_000_000_000;
|
|
nowSpy.mockReturnValue(start);
|
|
|
|
await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
nowSpy.mockReturnValue(start + 30 * 60 * 1000);
|
|
await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
expect(notificationStub.dispatchAlert).toHaveBeenCalledTimes(1);
|
|
|
|
nowSpy.mockReturnValue(start + 60 * 60 * 1000 + 1);
|
|
await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
expect(notificationStub.dispatchAlert).toHaveBeenCalledTimes(2);
|
|
|
|
nowSpy.mockRestore();
|
|
});
|
|
|
|
it('blocks deploy when a scanned image exceeds the policy severity', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
|
|
trivyStub.isTrivyAvailable.mockReturnValue(true);
|
|
composeStub.listStackImages.mockResolvedValue(['nginx:1.14']);
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({
|
|
id: 99,
|
|
highest_severity: 'CRITICAL',
|
|
critical_count: 2,
|
|
high_count: 5,
|
|
}));
|
|
|
|
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.bypassed).toBe(false);
|
|
expect(result.violations).toHaveLength(1);
|
|
expect(result.violations[0]).toMatchObject({
|
|
imageRef: 'nginx:1.14',
|
|
severity: 'CRITICAL',
|
|
criticalCount: 2,
|
|
highCount: 5,
|
|
scanId: 99,
|
|
});
|
|
expect(dbStub.insertAuditLog).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('allows deploy on admin bypass and records a policy.bypass audit entry', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
|
|
trivyStub.isTrivyAvailable.mockReturnValue(true);
|
|
composeStub.listStackImages.mockResolvedValue(['nginx:1.14']);
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({
|
|
id: 99,
|
|
highest_severity: 'CRITICAL',
|
|
critical_count: 2,
|
|
}));
|
|
|
|
const result = await enforcePolicyPreDeploy('web', 1, {
|
|
bypass: true,
|
|
actor: 'admin',
|
|
ip: '10.0.0.1',
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.bypassed).toBe(true);
|
|
expect(result.violations).toHaveLength(1);
|
|
expect(dbStub.insertAuditLog).toHaveBeenCalledTimes(1);
|
|
const entry = dbStub.insertAuditLog.mock.calls[0][0];
|
|
expect(entry.username).toBe('admin');
|
|
expect(entry.ip_address).toBe('10.0.0.1');
|
|
expect(entry.node_id).toBe(1);
|
|
expect(entry.summary).toContain('policy.bypass');
|
|
expect(entry.summary).toContain('nginx:1.14');
|
|
});
|
|
|
|
it('fails closed with a synthetic violation when compose parse fails', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
|
|
trivyStub.isTrivyAvailable.mockReturnValue(true);
|
|
composeStub.listStackImages.mockRejectedValue(new Error('compose file missing'));
|
|
|
|
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations).toHaveLength(1);
|
|
expect(result.violations[0].imageRef).toBe('(compose parse error)');
|
|
expect(result.violations[0].severity).toBe('UNKNOWN');
|
|
// The block is unactionable without naming why the gate ran a synthetic block.
|
|
expect(result.violations[0].error).toMatch(/compose file missing/i);
|
|
expect(trivyStub.scanImagePreflight).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('records a violation when an individual image scan throws', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
|
|
trivyStub.isTrivyAvailable.mockReturnValue(true);
|
|
composeStub.listStackImages.mockResolvedValue(['nginx:1.14']);
|
|
trivyStub.scanImagePreflight.mockRejectedValue(new Error('trivy crashed'));
|
|
|
|
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations).toHaveLength(1);
|
|
expect(result.violations[0].imageRef).toBe('nginx:1.14');
|
|
expect(result.violations[0].severity).toBe('UNKNOWN');
|
|
expect(result.violations[0].scanId).toBe(0);
|
|
// The synthetic violation must carry the scan failure reason so the block is
|
|
// actionable rather than an unexplained zero-count block.
|
|
expect(result.violations[0].error).toMatch(/trivy crashed/i);
|
|
});
|
|
|
|
it('records an evaluation-failure violation that keeps the scan id and names the reason', async () => {
|
|
// A KEV policy forces the per-finding detail path, and an intel lookup that
|
|
// throws after a successful scan exercises the evaluation-failure catch,
|
|
// which (unlike a scan failure) keeps the real scan id.
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_severity: 0, block_on_kev: 1 }));
|
|
trivyStub.isTrivyAvailable.mockReturnValue(true);
|
|
composeStub.listStackImages.mockResolvedValue(['nginx:1.14']);
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 42, total_vulnerabilities: 1, highest_severity: 'CRITICAL', critical_count: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([
|
|
{ id: 1, scan_id: 42, vulnerability_id: 'CVE-2024-1', pkg_name: 'p', installed_version: '1', fixed_version: null, severity: 'CRITICAL', title: null, description: null, primary_url: null },
|
|
]);
|
|
dbStub.getCveIntel.mockImplementation(() => { throw new Error('intel db read failed'); });
|
|
|
|
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations).toHaveLength(1);
|
|
expect(result.violations[0].scanId).toBe(42);
|
|
expect(result.violations[0].error).toMatch(/policy evaluation failed/i);
|
|
});
|
|
|
|
it('skips image refs that fail validation without calling the scanner', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
|
|
trivyStub.isTrivyAvailable.mockReturnValue(true);
|
|
composeStub.listStackImages.mockResolvedValue(['not a valid ref!!!', 'nginx:1.14']);
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ highest_severity: 'LOW' }));
|
|
|
|
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.violations).toEqual([]);
|
|
expect(trivyStub.scanImagePreflight).toHaveBeenCalledTimes(1);
|
|
expect(trivyStub.scanImagePreflight).toHaveBeenCalledWith('nginx:1.14', 1, 'web');
|
|
});
|
|
|
|
it('allows deploy when all scans fall below the policy threshold', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ max_severity: 'CRITICAL' }));
|
|
trivyStub.isTrivyAvailable.mockReturnValue(true);
|
|
composeStub.listStackImages.mockResolvedValue(['nginx:1.14', 'redis:7']);
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ highest_severity: 'HIGH' }));
|
|
|
|
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.violations).toEqual([]);
|
|
expect(trivyStub.scanImagePreflight).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('enforces a supplied image list without reading compose from disk', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
|
|
trivyStub.isTrivyAvailable.mockReturnValue(true);
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({
|
|
id: 42,
|
|
highest_severity: 'HIGH',
|
|
high_count: 1,
|
|
}));
|
|
|
|
const result = await enforcePolicyForImageRefs('blueprint-web', 1, ['nginx:1.27-alpine'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations).toHaveLength(1);
|
|
expect(result.violations[0]).toMatchObject({
|
|
imageRef: 'nginx:1.27-alpine',
|
|
severity: 'HIGH',
|
|
highCount: 1,
|
|
scanId: 42,
|
|
});
|
|
expect(composeStub.listStackImages).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('fails closed on invalid supplied image refs when requested', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
|
|
trivyStub.isTrivyAvailable.mockReturnValue(true);
|
|
|
|
const result = await enforcePolicyForImageRefs('blueprint-web', 1, ['${IMAGE}'], { bypass: false, actor: 'u' }, undefined, true);
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations).toHaveLength(1);
|
|
expect(result.violations[0]).toMatchObject({
|
|
imageRef: '${IMAGE}',
|
|
severity: 'UNKNOWN',
|
|
scanId: 0,
|
|
});
|
|
expect(result.violations[0].error).toMatch(/invalid image reference/i);
|
|
expect(trivyStub.scanImagePreflight).not.toHaveBeenCalled();
|
|
expect(composeStub.listStackImages).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
interface FindingStub {
|
|
vulnerability_id: string;
|
|
pkg_name: string;
|
|
severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | 'UNKNOWN';
|
|
fixed_version: string | null;
|
|
}
|
|
|
|
function mkFinding(overrides: Partial<FindingStub> = {}): FindingStub {
|
|
return { vulnerability_id: 'CVE-2026-0001', pkg_name: 'openssl', severity: 'CRITICAL', fixed_version: null, ...overrides };
|
|
}
|
|
|
|
function mkSuppression(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
id: 1,
|
|
cve_id: 'CVE-2026-0001',
|
|
pkg_name: null,
|
|
image_pattern: null,
|
|
reason: 'accepted after review',
|
|
created_by: 'admin',
|
|
created_at: Date.now(),
|
|
expires_at: null,
|
|
replicated_from_control: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('enforcePolicyForImageRefs with suppression-aware blocking', () => {
|
|
beforeEach(() => {
|
|
trivyStub.isTrivyAvailable.mockReset().mockReturnValue(true);
|
|
trivyStub.scanImagePreflight.mockReset();
|
|
composeStub.listStackImages.mockReset();
|
|
dbStub.getMatchingPolicy.mockReset().mockReturnValue(mkPolicy());
|
|
dbStub.insertAuditLog.mockReset();
|
|
dbStub.getGlobalSettings.mockReset().mockReturnValue({ deploy_block_honor_suppressions: '1' });
|
|
dbStub.getAllVulnerabilityDetails.mockReset().mockReturnValue([]);
|
|
dbStub.getCveSuppressions.mockReset().mockReturnValue([]);
|
|
dbStub.getCveIntel.mockReset().mockReturnValue(new Map());
|
|
notificationStub.dispatchAlert.mockReset();
|
|
_resetTrivyMissingNotificationStateForTests();
|
|
});
|
|
|
|
it('allows the deploy and audits when a suppression covers the sole blocking CVE', async () => {
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 7, highest_severity: 'CRITICAL', critical_count: 1, total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-0001', severity: 'CRITICAL' })]);
|
|
dbStub.getCveSuppressions.mockReturnValue([mkSuppression({ cve_id: 'CVE-2026-0001' })]);
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'admin', ip: '10.0.0.2' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.bypassed).toBe(false);
|
|
expect(result.violations).toEqual([]);
|
|
expect(dbStub.insertAuditLog).toHaveBeenCalledTimes(1);
|
|
const entry = dbStub.insertAuditLog.mock.calls[0][0];
|
|
expect(entry.summary).toContain('policy.suppression_pass');
|
|
expect(entry.summary).toContain('CVE-2026-0001');
|
|
expect(entry.summary).toContain('nginx:1.14');
|
|
expect(entry.username).toBe('admin');
|
|
});
|
|
|
|
it('still blocks when only some of the blocking findings are suppressed, with recomputed counts', async () => {
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 8, highest_severity: 'CRITICAL', critical_count: 1, high_count: 1, total_vulnerabilities: 2 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([
|
|
mkFinding({ vulnerability_id: 'CVE-2026-0001', pkg_name: 'openssl', severity: 'CRITICAL' }),
|
|
mkFinding({ vulnerability_id: 'CVE-2026-0002', pkg_name: 'zlib', severity: 'HIGH' }),
|
|
]);
|
|
dbStub.getCveSuppressions.mockReturnValue([mkSuppression({ cve_id: 'CVE-2026-0001' })]);
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations).toHaveLength(1);
|
|
expect(result.violations[0]).toMatchObject({ imageRef: 'nginx:1.14', severity: 'HIGH', criticalCount: 0, highCount: 1, scanId: 8 });
|
|
expect(dbStub.insertAuditLog).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not audit when the raw scan was already below the threshold', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ max_severity: 'HIGH' }));
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 9, highest_severity: 'LOW', total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-0003', severity: 'LOW' })]);
|
|
dbStub.getCveSuppressions.mockReturnValue([mkSuppression({ cve_id: 'CVE-2026-0003' })]);
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.violations).toEqual([]);
|
|
expect(dbStub.insertAuditLog).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('still blocks when the matching suppression has expired', async () => {
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 10, highest_severity: 'CRITICAL', critical_count: 1, total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-0001', severity: 'CRITICAL' })]);
|
|
dbStub.getCveSuppressions.mockReturnValue([mkSuppression({ cve_id: 'CVE-2026-0001', expires_at: Date.now() - 1000 })]);
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations[0]).toMatchObject({ severity: 'CRITICAL', scanId: 10 });
|
|
expect(dbStub.insertAuditLog).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('honors an image-pattern-scoped suppression that matches the deployed image', async () => {
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 11, highest_severity: 'CRITICAL', critical_count: 1, total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-0001', severity: 'CRITICAL' })]);
|
|
dbStub.getCveSuppressions.mockReturnValue([mkSuppression({ cve_id: 'CVE-2026-0001', image_pattern: '*nginx*' })]);
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.violations).toEqual([]);
|
|
expect(dbStub.insertAuditLog).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('aggregates a suppression-driven pass across multiple images and de-dupes CVEs in the audit', async () => {
|
|
trivyStub.scanImagePreflight
|
|
.mockResolvedValueOnce(mkScan({ id: 20, highest_severity: 'CRITICAL', critical_count: 1, total_vulnerabilities: 1 }))
|
|
.mockResolvedValueOnce(mkScan({ id: 21, highest_severity: 'CRITICAL', critical_count: 2, total_vulnerabilities: 2 }));
|
|
dbStub.getAllVulnerabilityDetails
|
|
.mockReturnValueOnce([mkFinding({ vulnerability_id: 'CVE-2026-0001', severity: 'CRITICAL' })])
|
|
.mockReturnValueOnce([
|
|
mkFinding({ vulnerability_id: 'CVE-2026-0001', severity: 'CRITICAL' }),
|
|
mkFinding({ vulnerability_id: 'CVE-2026-0009', pkg_name: 'curl', severity: 'CRITICAL' }),
|
|
]);
|
|
dbStub.getCveSuppressions.mockReturnValue([
|
|
mkSuppression({ id: 1, cve_id: 'CVE-2026-0001' }),
|
|
mkSuppression({ id: 2, cve_id: 'CVE-2026-0009' }),
|
|
]);
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14', 'redis:7'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.violations).toEqual([]);
|
|
expect(dbStub.insertAuditLog).toHaveBeenCalledTimes(1);
|
|
const summary = dbStub.insertAuditLog.mock.calls[0][0].summary as string;
|
|
expect(summary).toContain('nginx:1.14');
|
|
expect(summary).toContain('redis:7');
|
|
// CVE-2026-0001 appears on both images but must be listed once.
|
|
expect(summary.match(/CVE-2026-0001/g)).toHaveLength(1);
|
|
expect(summary).toContain('CVE-2026-0009');
|
|
});
|
|
|
|
it('does not audit a suppression pass when another image still violates the policy', async () => {
|
|
trivyStub.scanImagePreflight
|
|
.mockResolvedValueOnce(mkScan({ id: 22, highest_severity: 'CRITICAL', critical_count: 1, total_vulnerabilities: 1 }))
|
|
.mockResolvedValueOnce(mkScan({ id: 23, highest_severity: 'CRITICAL', critical_count: 1, total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails
|
|
.mockReturnValueOnce([mkFinding({ vulnerability_id: 'CVE-2026-0001', severity: 'CRITICAL' })])
|
|
.mockReturnValueOnce([mkFinding({ vulnerability_id: 'CVE-2026-0099', severity: 'CRITICAL' })]);
|
|
dbStub.getCveSuppressions.mockReturnValue([mkSuppression({ cve_id: 'CVE-2026-0001' })]);
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14', 'redis:7'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations).toHaveLength(1);
|
|
expect(result.violations[0]).toMatchObject({ imageRef: 'redis:7', severity: 'CRITICAL' });
|
|
expect(dbStub.insertAuditLog).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('short-circuits before reading findings when the policy does not block on deploy', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_deploy: 0 }));
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(trivyStub.scanImagePreflight).not.toHaveBeenCalled();
|
|
expect(dbStub.getAllVulnerabilityDetails).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('still blocks when a suppression is scoped to a non-matching image pattern', async () => {
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 24, highest_severity: 'CRITICAL', critical_count: 1, total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-0001', severity: 'CRITICAL' })]);
|
|
dbStub.getCveSuppressions.mockReturnValue([mkSuppression({ cve_id: 'CVE-2026-0001', image_pattern: 'registry.internal/*' })]);
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations[0]).toMatchObject({ severity: 'CRITICAL', scanId: 24 });
|
|
expect(dbStub.insertAuditLog).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('gates on raw severity when stored detail rows are incomplete (cache-truncated scan)', async () => {
|
|
// The scan aggregate reports 1500 findings but only one detail row is
|
|
// present (a cache hit copies a bounded slice). The lone loaded finding is
|
|
// suppressed, but the recompute must not trust the truncated set: a
|
|
// blocking CVE could live in the rows that were not copied, so gate on raw.
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 30, highest_severity: 'CRITICAL', critical_count: 5, total_vulnerabilities: 1500 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-0001', severity: 'CRITICAL' })]);
|
|
dbStub.getCveSuppressions.mockReturnValue([mkSuppression({ cve_id: 'CVE-2026-0001' })]);
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations[0]).toMatchObject({ severity: 'CRITICAL', criticalCount: 5, scanId: 30 });
|
|
expect(dbStub.insertAuditLog).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('still allows the deploy when the suppression-pass audit write fails', async () => {
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 31, highest_severity: 'CRITICAL', critical_count: 1, total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-0001', severity: 'CRITICAL' })]);
|
|
dbStub.getCveSuppressions.mockReturnValue([mkSuppression({ cve_id: 'CVE-2026-0001' })]);
|
|
dbStub.insertAuditLog.mockImplementation(() => { throw new Error('audit buffer full'); });
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.violations).toEqual([]);
|
|
expect(dbStub.insertAuditLog).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('ignores suppressions entirely when the toggle is off (identical to raw blocking)', async () => {
|
|
dbStub.getGlobalSettings.mockReturnValue({ deploy_block_honor_suppressions: '0' });
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 12, highest_severity: 'CRITICAL', critical_count: 1 }));
|
|
dbStub.getCveSuppressions.mockReturnValue([mkSuppression({ cve_id: 'CVE-2026-0001' })]);
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations[0]).toMatchObject({ severity: 'CRITICAL', scanId: 12 });
|
|
expect(dbStub.getAllVulnerabilityDetails).not.toHaveBeenCalled();
|
|
expect(dbStub.insertAuditLog).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('enforcePolicyForImageRefs - risk-based inputs (KEV / fixable / optional severity)', () => {
|
|
beforeEach(() => {
|
|
trivyStub.isTrivyAvailable.mockReset().mockReturnValue(true);
|
|
trivyStub.scanImagePreflight.mockReset();
|
|
composeStub.listStackImages.mockReset();
|
|
dbStub.getMatchingPolicy.mockReset();
|
|
dbStub.insertAuditLog.mockReset();
|
|
// Suppressions off by default; KEV/fixable still force a detail read.
|
|
dbStub.getGlobalSettings.mockReset().mockReturnValue({});
|
|
dbStub.getAllVulnerabilityDetails.mockReset().mockReturnValue([]);
|
|
dbStub.getCveSuppressions.mockReset().mockReturnValue([]);
|
|
dbStub.getCveIntel.mockReset().mockReturnValue(new Map());
|
|
notificationStub.dispatchAlert.mockReset();
|
|
_resetTrivyMissingNotificationStateForTests();
|
|
});
|
|
|
|
const kevIntel = (cve: string) => new Map([[cve, { kev: true, kevDate: null, epssScore: null, epssPercentile: null }]]);
|
|
|
|
it('blocks a KEV-only policy when a known-exploited CVE is present, even at LOW severity', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_severity: 0, block_on_kev: 1, block_on_fixable: 0 }));
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 40, highest_severity: 'LOW', total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-1000', severity: 'LOW' })]);
|
|
dbStub.getCveIntel.mockReturnValue(kevIntel('CVE-2026-1000'));
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations[0]).toMatchObject({ reasons: ['kev'], kevCount: 1, scanId: 40 });
|
|
});
|
|
|
|
it('allows a KEV-only policy when no finding is known-exploited', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_severity: 0, block_on_kev: 1, block_on_fixable: 0 }));
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 41, highest_severity: 'CRITICAL', critical_count: 1, total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-2000', severity: 'CRITICAL' })]);
|
|
dbStub.getCveIntel.mockReturnValue(new Map());
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.violations).toEqual([]);
|
|
});
|
|
|
|
it('blocks a fixable-only policy when a Critical/High finding has a fix available', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_severity: 0, block_on_kev: 0, block_on_fixable: 1 }));
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 42, highest_severity: 'HIGH', high_count: 1, total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-3000', severity: 'HIGH', fixed_version: '1.2.3' })]);
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations[0]).toMatchObject({ reasons: ['fixable'], fixableCount: 1, scanId: 42 });
|
|
});
|
|
|
|
it('allows a fixable-only policy when the Critical/High findings have no fix', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_severity: 0, block_on_kev: 0, block_on_fixable: 1 }));
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 43, highest_severity: 'CRITICAL', critical_count: 1, total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-3001', severity: 'CRITICAL', fixed_version: null })]);
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.violations).toEqual([]);
|
|
});
|
|
|
|
it('does not gate on severity when the severity input is off (KEV/fixable only)', async () => {
|
|
// A CRITICAL image with no KEV and no fix must pass a risk-first policy that
|
|
// leaves severity off: severity alone is no longer a blocking basis.
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_severity: 0, block_on_kev: 1, block_on_fixable: 1, max_severity: 'LOW' }));
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 44, highest_severity: 'CRITICAL', critical_count: 1, total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-4000', severity: 'CRITICAL', fixed_version: null })]);
|
|
dbStub.getCveIntel.mockReturnValue(new Map());
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.violations).toEqual([]);
|
|
});
|
|
|
|
it('fails closed on a KEV input when detail rows are truncated (assume it is automatable)', async () => {
|
|
// Aggregate reports 1500 findings but only one detail row is present; KEV
|
|
// membership cannot be proven absent, so the gate must block.
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_severity: 0, block_on_kev: 1, block_on_fixable: 0 }));
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 45, highest_severity: 'LOW', total_vulnerabilities: 1500 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-5000', severity: 'LOW' })]);
|
|
dbStub.getCveIntel.mockReturnValue(new Map());
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations[0].reasons).toContain('kev');
|
|
});
|
|
|
|
it('fails closed on a KEV input when the detail read throws (transient DB error)', async () => {
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_severity: 0, block_on_kev: 1, block_on_fixable: 0 }));
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 47, highest_severity: 'LOW', total_vulnerabilities: 3 }));
|
|
dbStub.getAllVulnerabilityDetails.mockImplementation(() => { throw new Error('database is locked'); });
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.violations[0].reasons).toContain('kev');
|
|
});
|
|
|
|
it('still allows when only the severity input is set and the detail read throws', async () => {
|
|
// Severity stays verifiable from the aggregate; a below-threshold image passes
|
|
// even though the (honor-suppressions) detail read failed.
|
|
dbStub.getGlobalSettings.mockReturnValue({ deploy_block_honor_suppressions: '1' });
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_severity: 1, block_on_kev: 0, block_on_fixable: 0, max_severity: 'HIGH' }));
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 48, highest_severity: 'LOW', total_vulnerabilities: 2 }));
|
|
dbStub.getAllVulnerabilityDetails.mockImplementation(() => { throw new Error('database is locked'); });
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.violations).toEqual([]);
|
|
});
|
|
|
|
it('does not audit a suppression pass when the suppressed finding is not KEV (KEV policy)', async () => {
|
|
// A non-KEV finding is suppressed under a KEV-only policy. The raw set carries
|
|
// no KEV finding, so there is nothing the suppression "saved": no audit.
|
|
dbStub.getGlobalSettings.mockReturnValue({ deploy_block_honor_suppressions: '1' });
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_severity: 0, block_on_kev: 1, block_on_fixable: 0 }));
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 49, highest_severity: 'CRITICAL', critical_count: 1, total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-7000', severity: 'CRITICAL' })]);
|
|
dbStub.getCveSuppressions.mockReturnValue([mkSuppression({ cve_id: 'CVE-2026-7000' })]);
|
|
dbStub.getCveIntel.mockReturnValue(new Map()); // CVE-2026-7000 is not KEV
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'u' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(dbStub.insertAuditLog).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('allows and audits when an honored suppression covers the sole KEV finding', async () => {
|
|
dbStub.getGlobalSettings.mockReturnValue({ deploy_block_honor_suppressions: '1' });
|
|
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_severity: 0, block_on_kev: 1, block_on_fixable: 0 }));
|
|
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ id: 46, highest_severity: 'CRITICAL', critical_count: 1, total_vulnerabilities: 1 }));
|
|
dbStub.getAllVulnerabilityDetails.mockReturnValue([mkFinding({ vulnerability_id: 'CVE-2026-6000', severity: 'CRITICAL' })]);
|
|
dbStub.getCveSuppressions.mockReturnValue([mkSuppression({ cve_id: 'CVE-2026-6000' })]);
|
|
dbStub.getCveIntel.mockReturnValue(kevIntel('CVE-2026-6000'));
|
|
|
|
const result = await enforcePolicyForImageRefs('web', 1, ['nginx:1.14'], { bypass: false, actor: 'admin' });
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.violations).toEqual([]);
|
|
expect(dbStub.insertAuditLog).toHaveBeenCalledTimes(1);
|
|
expect(dbStub.insertAuditLog.mock.calls[0][0].summary).toContain('policy.suppression_pass');
|
|
});
|
|
});
|