/** * Covers the pre-deploy policy gate across the six behaviours defined in the * PR 1 plan: no-policy, disabled-policy, Trivy-missing (fail open), * violation, admin bypass (audit-logged), compose-parse-failure (fail closed). * * 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; scanImagePreflight: ReturnType; } interface ComposeStub { listStackImages: ReturnType; } interface DbStub { getMatchingPolicy: ReturnType; insertAuditLog: ReturnType; } interface NotificationStub { dispatchAlert: ReturnType; } const trivyStub: TrivyStub = { isTrivyAvailable: vi.fn(), scanImagePreflight: vi.fn(), }; const composeStub: ComposeStub = { listStackImages: vi.fn(), }; const dbStub: DbStub = { getMatchingPolicy: vi.fn(), insertAuditLog: 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 { enforcePolicyForImageRefs, enforcePolicyPreDeploy } from '../services/PolicyEnforcement'; function mkPolicy(overrides: Partial = {}): 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, replicated_from_control: 0, created_at: Date.now(), updated_at: Date.now(), ...overrides, }; } function mkScan(overrides: Partial = {}): 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(); notificationStub.dispatchAlert.mockReset(); }); 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('allows deploy without scanning when paid-tier blocking is disabled', async () => { dbStub.getMatchingPolicy.mockReturnValue(mkPolicy()); const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u', blockingEnabled: false, }); expect(result.ok).toBe(true); expect(result.violations).toEqual([]); expect(trivyStub.isTrivyAvailable).not.toHaveBeenCalled(); 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('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'); 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); }); 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(trivyStub.scanImagePreflight).not.toHaveBeenCalled(); expect(composeStub.listStackImages).not.toHaveBeenCalled(); }); });