/** * CveIntelService: daily KEV + EPSS refresh, air-gap tolerant, read-time join. */ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest'; import { setupTestDb, cleanupTestDb } from './helpers/setupTestDb'; let tmpDir: string; let DatabaseService: typeof import('../services/DatabaseService').DatabaseService; let CveIntelService: typeof import('../services/CveIntelService').CveIntelService; beforeAll(async () => { tmpDir = await setupTestDb(); ({ DatabaseService } = await import('../services/DatabaseService')); ({ CveIntelService } = await import('../services/CveIntelService')); }); afterAll(() => cleanupTestDb(tmpDir)); function db() { return DatabaseService.getInstance(); } function reset(): void { const raw = (db() as unknown as { db: { prepare: (s: string) => { run: () => void } } }).db; raw.prepare('DELETE FROM cve_intel').run(); raw.prepare('DELETE FROM vulnerability_details').run(); raw.prepare('DELETE FROM vulnerability_scans').run(); db().updateGlobalSetting('cve_intel_enabled', '1'); } beforeEach(reset); afterEach(() => vi.restoreAllMocks()); function jsonOk(body: unknown) { return { ok: true, status: 200, json: async () => body } as unknown as Response; } /** Routes by exact host: www.cisa.gov -> KEV, api.first.org -> EPSS. */ function stubFetch(kev: unknown, epss: unknown): ReturnType { const mock = vi.fn(async (url: string | URL) => { const host = new URL(String(url)).hostname; if (host === 'www.cisa.gov') return jsonOk(kev); if (host === 'api.first.org') return jsonOk(epss); throw new Error(`unexpected url ${String(url)}`); }); vi.stubGlobal('fetch', mock); return mock; } function seedFinding(cve: string): void { const scanId = db().createVulnerabilityScan({ node_id: 1, image_ref: `img-${cve}:1`, image_digest: `sha256:${cve}`, scanned_at: Date.now(), total_vulnerabilities: 1, critical_count: 1, 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: 'CRITICAL', os_info: null, trivy_version: null, scan_duration_ms: null, triggered_by: 'manual', status: 'completed', error: null, stack_context: null, }); db().insertVulnerabilityDetails(scanId, [{ vulnerability_id: cve, pkg_name: 'pkg', installed_version: '1', fixed_version: null, severity: 'CRITICAL', title: null, description: null, primary_url: null, }]); } describe('CveIntelService.refresh', () => { it('upserts KEV membership and joins it at read time', async () => { stubFetch({ vulnerabilities: [{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-01' }] }, { data: [] }); await CveIntelService.getInstance().refresh(); const intel = db().getCveIntel(['CVE-2024-0001']); expect(intel.get('CVE-2024-0001')).toMatchObject({ kev: true, kevDate: '2024-01-01' }); }); it('fetches EPSS only for CVEs present in stored findings', async () => { seedFinding('CVE-2024-1111'); const mock = stubFetch({ vulnerabilities: [] }, { data: [{ cve: 'CVE-2024-1111', epss: '0.5', percentile: '0.9' }] }); await CveIntelService.getInstance().refresh(); expect(db().getCveIntel(['CVE-2024-1111']).get('CVE-2024-1111')).toMatchObject({ epssScore: 0.5, epssPercentile: 0.9 }); expect(mock.mock.calls.some((c) => String(c[0]).includes('CVE-2024-1111'))).toBe(true); }); it('skips the EPSS fetch entirely when no CVEs are present', async () => { const mock = stubFetch({ vulnerabilities: [] }, { data: [] }); await CveIntelService.getInstance().refresh(); expect(mock.mock.calls.some((c) => new URL(String(c[0])).hostname === 'api.first.org')).toBe(false); // KEV is still attempted. expect(mock.mock.calls.some((c) => new URL(String(c[0])).hostname === 'www.cisa.gov')).toBe(true); }); it('keeps the cached intel when a fetch fails (air-gap tolerant)', async () => { db().replaceKev([{ cve_id: 'CVE-2024-0002', date_added: '2023-12-31' }], Date.now()); vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('network down'); })); await expect(CveIntelService.getInstance().refresh()).resolves.toBeUndefined(); expect(db().getCveIntel(['CVE-2024-0002']).get('CVE-2024-0002')?.kev).toBe(true); }); it('does no network fetch when disabled by setting', async () => { db().updateGlobalSetting('cve_intel_enabled', '0'); const mock = stubFetch({ vulnerabilities: [] }, { data: [] }); await CveIntelService.getInstance().refresh(); expect(mock).not.toHaveBeenCalled(); }); it('getCveIntel returns an empty map for no ids and does not crash', () => { expect(db().getCveIntel([]).size).toBe(0); }); });