/** * Pins the scheduler's policy-alert fan-out. * * After `trivy.scanAllNodeImages` resolves with one or more policy * violations, `SchedulerService.executeScan` must dispatch a warning-level * notification for each violation so an operator can triage them. Scheduled * scans never auto-quarantine in the current scope; any regression that * silently swallows violations turns the feature back into post-hoc logging. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; const { mockGetDueScheduledTasks, mockCreateScheduledTaskRun, mockUpdateScheduledTaskRun, mockUpdateScheduledTask, mockCleanupOldTaskRuns, mockGetScheduledTask, mockGetNodes, mockGetNode, mockCreateSnapshot, mockInsertSnapshotFiles, mockClearStackUpdateStatus, mockMarkStaleRunsAsFailed, mockDeleteOldScans, mockGetTier, mockDispatchAlert, mockGetProxyTarget, mockIsTrivyAvailable, mockScanAllNodeImages, } = vi.hoisted(() => ({ mockGetDueScheduledTasks: vi.fn().mockReturnValue([]), mockCreateScheduledTaskRun: vi.fn().mockReturnValue(1), mockUpdateScheduledTaskRun: vi.fn(), mockUpdateScheduledTask: vi.fn(), mockCleanupOldTaskRuns: vi.fn(), mockGetScheduledTask: vi.fn(), mockGetNodes: vi.fn().mockReturnValue([]), mockGetNode: vi.fn().mockReturnValue({ id: 1, name: 'local', type: 'local', status: 'online' }), mockCreateSnapshot: vi.fn().mockReturnValue(1), mockInsertSnapshotFiles: vi.fn(), mockClearStackUpdateStatus: vi.fn(), mockMarkStaleRunsAsFailed: vi.fn().mockReturnValue(0), mockDeleteOldScans: vi.fn().mockReturnValue(0), mockGetTier: vi.fn().mockReturnValue('paid'), mockDispatchAlert: vi.fn().mockResolvedValue(undefined), mockGetProxyTarget: vi.fn().mockReturnValue(null), mockIsTrivyAvailable: vi.fn().mockReturnValue(true), mockScanAllNodeImages: vi.fn(), })); vi.mock('../services/DatabaseService', () => ({ DatabaseService: { getInstance: () => ({ getDueScheduledTasks: mockGetDueScheduledTasks, createScheduledTaskRun: mockCreateScheduledTaskRun, updateScheduledTaskRun: mockUpdateScheduledTaskRun, updateScheduledTask: mockUpdateScheduledTask, cleanupOldTaskRuns: mockCleanupOldTaskRuns, getScheduledTask: mockGetScheduledTask, getNodes: mockGetNodes, getNode: mockGetNode, createSnapshot: mockCreateSnapshot, insertSnapshotFiles: mockInsertSnapshotFiles, clearStackUpdateStatus: mockClearStackUpdateStatus, markStaleRunsAsFailed: mockMarkStaleRunsAsFailed, deleteOldScans: mockDeleteOldScans, }), }, })); vi.mock('../services/LicenseService', () => ({ LicenseService: { getInstance: () => ({ getTier: mockGetTier, }), }, })); vi.mock('../services/NotificationService', () => ({ NotificationService: { getInstance: () => ({ dispatchAlert: mockDispatchAlert }), }, })); vi.mock('../services/NodeRegistry', () => ({ NodeRegistry: { getInstance: () => ({ getDefaultNodeId: () => 1, getNode: mockGetNode, getProxyTarget: mockGetProxyTarget, }), }, })); vi.mock('../services/TrivyService', () => ({ default: { getInstance: () => ({ isTrivyAvailable: mockIsTrivyAvailable, scanAllNodeImages: mockScanAllNodeImages, getSource: () => 'managed', detectTrivy: vi.fn().mockResolvedValue(undefined), }), }, })); import { SchedulerService } from '../services/SchedulerService'; function makeScanTask() { return { id: 300, name: 'policy-scan', action: 'scan', cron_expression: '0 2 * * *', enabled: true, target_id: null, node_id: 1, created_by: 'admin', last_status: null, }; } function summaryWith(violations: Array<{ imageRef: string; policyId: number; policyName: string; maxSeverity: string; severity: string; scanId: number; reasons: Array<'severity' | 'kev' | 'fixable'>; }>) { return { scanned: violations.length, skipped: 0, failed: 0, severity: { critical: 0, high: 0, medium: 0, low: 0, unknown: 0 }, violations, }; } describe('SchedulerService - scheduled scan policy alerts', () => { beforeEach(() => { vi.clearAllMocks(); (SchedulerService as unknown as { instance?: SchedulerService }).instance = undefined; }); it('dispatches a warning alert for every violated scan', async () => { mockGetScheduledTask.mockReturnValue(makeScanTask()); mockScanAllNodeImages.mockResolvedValue(summaryWith([ { imageRef: 'nginx:1.14', policyId: 1, policyName: 'prod-high-gate', maxSeverity: 'HIGH', severity: 'MEDIUM', scanId: 42, reasons: ['kev'], }, { imageRef: 'redis:6', policyId: 1, policyName: 'prod-high-gate', maxSeverity: 'HIGH', severity: 'HIGH', scanId: 43, reasons: ['severity'], }, ])); const svc = SchedulerService.getInstance(); await svc.triggerTask(300); const warningCalls = mockDispatchAlert.mock.calls.filter((c) => c[0] === 'warning'); expect(warningCalls).toHaveLength(2); // The first violation matched KEV, so the alert names the known-exploited // input and never the severity ceiling it did not enforce. expect(warningCalls[0][2]).toContain('prod-high-gate'); expect(warningCalls[0][2]).toContain('nginx:1.14'); expect(warningCalls[0][2]).toContain('known-exploited CVE (KEV)'); expect(warningCalls[0][2]).not.toContain('HIGH'); // The second matched the severity threshold. expect(warningCalls[1][2]).toContain('redis:6'); expect(warningCalls[1][2]).toContain('severity threshold'); }); it('does not dispatch any policy alert when no violations occur', async () => { mockGetScheduledTask.mockReturnValue(makeScanTask()); mockScanAllNodeImages.mockResolvedValue(summaryWith([])); const svc = SchedulerService.getInstance(); await svc.triggerTask(300); const warningCalls = mockDispatchAlert.mock.calls.filter( (c) => c[0] === 'warning' && typeof c[2] === 'string' && c[2].includes('Policy'), ); expect(warningCalls).toHaveLength(0); }); it('takes no quarantine action (alerts only, no stack lifecycle calls)', async () => { // The current scope explicitly rejects auto-quarantine. The scheduler // must never call DockerController / ComposeService off the scan path. // We verify by asserting only the DB task-run + notification surfaces // were touched, not any docker or compose mock. mockGetScheduledTask.mockReturnValue(makeScanTask()); mockScanAllNodeImages.mockResolvedValue(summaryWith([ { imageRef: 'nginx:1.14', policyId: 1, policyName: 'block-critical', maxSeverity: 'CRITICAL', severity: 'CRITICAL', scanId: 44, reasons: ['severity'], }, ])); const svc = SchedulerService.getInstance(); await svc.triggerTask(300); // The violation produced a warning alert. const warningCalls = mockDispatchAlert.mock.calls.filter((c) => c[0] === 'warning'); expect(warningCalls.length).toBeGreaterThan(0); // Task-run record written (happy path completion), not an error. expect(mockCreateScheduledTaskRun).toHaveBeenCalled(); }); });