Files
sencho/backend/src/__tests__/security-overview-route.test.ts
T
Anso 89a13f51e9 fix: keep security posture accurate for secret-only scans and any-severity KEVs (#1475)
The Security overview and exploit-intel surfaces picked the latest scan per
image without restricting to scans that ran the vulnerability scanner, and
counted known-exploited (KEV) findings only among Critical/High. Two effects:

- A newer secret-only node scan became the latest scan for an image and
  clobbered its Critical/High/fixable/KEV posture to zero, which could read a
  false Secure state.
- A Medium or Low severity KEV that the pre-deploy gate blocks on produced zero
  overview and exploit-intel rows, so the page disagreed with the gate.

Posture queries now select the latest vulnerability-bearing scan per image, the
image summary sources its vulnerability counts from that scan via a LEFT JOIN
while still counting secret and misconfiguration findings from the latest scan
overall, and knownExploited is counted from a dedicated any-severity KEV query
that mirrors the gate.
2026-06-26 18:58:05 -04:00

546 lines
28 KiB
TypeScript

/**
* GET /api/security/overview -> node-scoped posture rollup (Community, auth-only)
* GET /api/security/policy-packs -> static catalog (Community, auth-only, identical per tier)
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
import request from 'supertest';
import bcrypt from 'bcrypt';
import { setupTestDb, cleanupTestDb, loginAsTestAdmin } from './helpers/setupTestDb';
let tmpDir: string;
let app: import('express').Express;
let adminCookie: string;
let viewerCookie: string;
let DatabaseService: typeof import('../services/DatabaseService').DatabaseService;
let LicenseService: typeof import('../services/LicenseService').LicenseService;
let TrivyService: typeof import('../services/TrivyService').default;
const DAY = 24 * 60 * 60 * 1000;
beforeAll(async () => {
tmpDir = await setupTestDb();
({ DatabaseService } = await import('../services/DatabaseService'));
({ LicenseService } = await import('../services/LicenseService'));
TrivyService = (await import('../services/TrivyService')).default;
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('community');
// Deterministic scanner readout for the overview's scanner block.
const svc = TrivyService.getInstance();
vi.spyOn(svc, 'isTrivyAvailable').mockReturnValue(true);
vi.spyOn(svc, 'getVersion').mockReturnValue('0.52.0');
vi.spyOn(svc, 'getSource').mockReturnValue('managed');
({ app } = await import('../index'));
adminCookie = await loginAsTestAdmin(app);
const viewerHash = await bcrypt.hash('ovviewer1', 1);
DatabaseService.getInstance().addUser({ username: 'ov-viewer', password_hash: viewerHash, role: 'viewer' });
const res = await request(app).post('/api/auth/login').send({ username: 'ov-viewer', password: 'ovviewer1' });
const cookies = res.headers['set-cookie'] as string | string[];
viewerCookie = Array.isArray(cookies) ? cookies[0] : cookies;
});
afterAll(() => cleanupTestDb(tmpDir));
function db() {
return DatabaseService.getInstance();
}
function seedScan(o: {
node_id?: number;
image_ref: string;
scanned_at: number;
status?: 'completed' | 'failed';
critical?: number;
high?: number;
fixable?: number;
secret?: number;
misconfig?: number;
}): void {
db().createVulnerabilityScan({
node_id: o.node_id ?? 1,
image_ref: o.image_ref,
image_digest: `sha256:${o.image_ref}-${Math.random().toString(16).slice(2)}`,
scanned_at: o.scanned_at,
total_vulnerabilities: (o.critical ?? 0) + (o.high ?? 0),
critical_count: o.critical ?? 0,
high_count: o.high ?? 0,
medium_count: 0,
low_count: 0,
unknown_count: 0,
fixable_count: o.fixable ?? 0,
secret_count: o.secret ?? 0,
misconfig_count: o.misconfig ?? 0,
scanners_used: 'vuln',
highest_severity: (o.critical ?? 0) > 0 ? 'CRITICAL' : null,
os_info: null,
trivy_version: null,
scan_duration_ms: null,
triggered_by: 'manual',
status: o.status ?? 'completed',
error: o.status === 'failed' ? 'boom' : null,
stack_context: o.image_ref.startsWith('stack:') ? o.image_ref.slice(6) : null,
});
}
function resetSecurity(): void {
const raw = (db() as unknown as { db: { prepare: (s: string) => { run: () => void } } }).db;
raw.prepare('DELETE FROM vulnerability_details').run();
raw.prepare('DELETE FROM vulnerability_scans').run();
raw.prepare('DELETE FROM scan_policies').run();
raw.prepare('DELETE FROM cve_suppressions').run();
raw.prepare('DELETE FROM misconfig_acknowledgements').run();
raw.prepare('DELETE FROM cve_intel').run();
}
describe('GET /api/security/overview', () => {
beforeEach(() => {
resetSecurity();
db().updateGlobalSetting('deploy_block_honor_suppressions', '1');
});
it('aggregates node-scoped counts with the documented shape', async () => {
const now = Date.now();
seedScan({ image_ref: 'imgA:1', scanned_at: now - 1000, critical: 2, high: 1, fixable: 3, secret: 1 });
seedScan({ image_ref: 'imgB:1', scanned_at: now - 8 * DAY }); // stale
seedScan({ image_ref: 'stack:web', scanned_at: now - 2000, misconfig: 2 });
// Failed scans (same image) beyond a single row prove the uncapped count.
for (let i = 0; i < 4; i++) seedScan({ image_ref: 'imgA:1', scanned_at: now, status: 'failed' });
// Other node's data must be excluded.
seedScan({ node_id: 2, image_ref: 'other:1', scanned_at: now, critical: 99 });
// One fleet-wide and one this-node block policy count; an other-node one does not.
db().createScanPolicy({ name: 'fw', node_id: null, node_identity: '', stack_pattern: null, max_severity: 'CRITICAL', block_on_deploy: 1, block_on_severity: 1, block_on_kev: 0, block_on_fixable: 0, enabled: 1, replicated_from_control: 0 });
db().createScanPolicy({ name: 'n1', node_id: 1, node_identity: '', stack_pattern: null, max_severity: 'CRITICAL', block_on_deploy: 1, block_on_severity: 1, block_on_kev: 0, block_on_fixable: 0, enabled: 1, replicated_from_control: 0 });
db().createScanPolicy({ name: 'n2', node_id: 2, node_identity: '', stack_pattern: null, max_severity: 'CRITICAL', block_on_deploy: 1, block_on_severity: 1, block_on_kev: 0, block_on_fixable: 0, enabled: 1, replicated_from_control: 0 });
const res = await request(app).get('/api/security/overview').set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
scannedImages: 2, // imgA + imgB, stack:web excluded
critical: 2, // node-2's 99 excluded
high: 1,
fixable: 3,
secrets: 1,
misconfigs: 2,
staleScans: 1, // imgB only
failedScans: 4, // uncapped
});
expect(res.body.lastSuccessfulScanAt).toBeGreaterThan(0);
expect(res.body.scanner).toMatchObject({ available: true, source: 'managed', version: '0.52.0' });
expect(res.body.deployEnforcement).toMatchObject({
honorSuppressionsOnDeploy: true,
eligibleBlockPolicies: 2,
});
});
it('derives suppression- and ack-aware posture facts from detail rows', async () => {
const now = Date.now();
const scanId = db().createVulnerabilityScan({
node_id: 1,
image_ref: 'app:1',
image_digest: `sha256:app-${Math.random().toString(16).slice(2)}`,
scanned_at: now,
total_vulnerabilities: 3,
critical_count: 2,
high_count: 1,
medium_count: 0,
low_count: 0,
unknown_count: 0,
fixable_count: 2,
secret_count: 0,
misconfig_count: 2,
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,
});
const detail = (vulnerability_id: string, severity: 'CRITICAL' | 'HIGH', fixed_version: string | null) => ({
vulnerability_id, pkg_name: `pkg-${vulnerability_id}`, installed_version: '1', fixed_version,
severity, title: null, description: null, primary_url: null,
});
db().insertVulnerabilityDetails(scanId, [
detail('CVE-2024-0001', 'CRITICAL', '2'), // fixable, counts
detail('CVE-2024-0002', 'HIGH', null), // unfixable, does not count
detail('CVE-2024-0003', 'CRITICAL', '9'), // fixable but suppressed -> accepted, not fixable
]);
db().createCveSuppression({
cve_id: 'CVE-2024-0003', pkg_name: null, image_pattern: null, reason: 'accepted risk',
created_by: 'admin', created_at: now, expires_at: null, replicated_from_control: 0,
});
db().insertMisconfigFindings(scanId, [
{ rule_id: 'DS001', check_id: null, severity: 'HIGH', title: null, message: null, resolution: null, target: 'app', primary_url: null },
{ rule_id: 'DS002', check_id: null, severity: 'CRITICAL', title: null, message: null, resolution: null, target: 'app', primary_url: null },
]);
db().createMisconfigAcknowledgement({
rule_id: 'DS001', stack_pattern: null, reason: 'acknowledged',
created_by: 'admin', created_at: now, expires_at: null, replicated_from_control: 0,
});
const res = await request(app).get('/api/security/overview').set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
rawCritical: 2,
rawHigh: 1,
fixableCriticalHigh: 1, // 0001 only (0003 suppressed, 0002 unfixable)
accepted: 1, // 0003 suppressed
dangerousCompose: 1, // DS002 (DS001 acknowledged)
knownExploited: 0,
publiclyExposed: 0,
needsReview: 0,
notAffected: 0,
posture: 'Action needed',
posturePartial: false,
});
});
it('separates not_affected and needs_review triage facts in the overview', async () => {
const now = Date.now();
const scanId = db().createVulnerabilityScan({
node_id: 1, image_ref: 'triage:1', image_digest: 'sha256:triage', scanned_at: now,
total_vulnerabilities: 2, critical_count: 2, 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-2024-0010', pkg_name: 'a', installed_version: '1', fixed_version: null, severity: 'CRITICAL', title: null, description: null, primary_url: null },
{ vulnerability_id: 'CVE-2024-0011', pkg_name: 'b', installed_version: '1', fixed_version: null, severity: 'CRITICAL', title: null, description: null, primary_url: null },
]);
db().createCveSuppression({ cve_id: 'CVE-2024-0010', pkg_name: null, image_pattern: null, reason: 'not affected', created_by: 'admin', created_at: now, expires_at: null, replicated_from_control: 0, status: 'not_affected' });
db().createCveSuppression({ cve_id: 'CVE-2024-0011', pkg_name: null, image_pattern: null, reason: 'reviewing', created_by: 'admin', created_at: now, expires_at: null, replicated_from_control: 0, status: 'needs_review' });
const res = await request(app).get('/api/security/overview').set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(res.body).toMatchObject({ notAffected: 1, needsReview: 1, accepted: 0, fixableCriticalHigh: 0, posture: 'Monitoring' });
});
it('escalates an unfixable known-exploited (KEV) finding to Action needed', async () => {
const now = Date.now();
const scanId = db().createVulnerabilityScan({
node_id: 1, image_ref: 'kev:1', image_digest: 'sha256:kev', scanned_at: 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-2024-9999', pkg_name: 'libkev', installed_version: '1', fixed_version: null,
severity: 'CRITICAL', title: null, description: null, primary_url: null,
}]);
// No fix available, but the CVE is known-exploited: KEV overrides "no fix".
db().replaceKev([{ cve_id: 'CVE-2024-9999', date_added: '2024-01-01' }], now);
const res = await request(app).get('/api/security/overview').set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(res.body).toMatchObject({ knownExploited: 1, fixableCriticalHigh: 0, posture: 'Action needed' });
});
it('preserves vulnerability posture when a newer secret-only scan exists for the same image', async () => {
const now = Date.now();
// Vulnerability scan: one unfixable Critical that is known-exploited.
const vulnScan = db().createVulnerabilityScan({
node_id: 1, image_ref: 'app:1', image_digest: 'sha256:app-vuln', scanned_at: now - 1000,
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(vulnScan, [{
vulnerability_id: 'CVE-2024-5001', pkg_name: 'libkev', installed_version: '1', fixed_version: null,
severity: 'CRITICAL', title: null, description: null, primary_url: null,
}]);
db().replaceKev([{ cve_id: 'CVE-2024-5001', date_added: '2024-01-01' }], now);
// Newer secret-only node scan for the SAME image: no vuln details, carries a secret.
db().createVulnerabilityScan({
node_id: 1, image_ref: 'app:1', image_digest: 'sha256:app-secret', scanned_at: now,
total_vulnerabilities: 0, critical_count: 0, high_count: 0, medium_count: 0, low_count: 0,
unknown_count: 0, fixable_count: 0, secret_count: 1, misconfig_count: 0, scanners_used: 'secret',
highest_severity: null, os_info: null, trivy_version: null, scan_duration_ms: null,
triggered_by: 'manual', status: 'completed', error: null, stack_context: null,
});
const res = await request(app).get('/api/security/overview').set('Cookie', adminCookie);
expect(res.status).toBe(200);
// The secret-only scan must not erase the vulnerability posture, and its
// secret finding is still counted.
expect(res.body).toMatchObject({
critical: 1,
knownExploited: 1,
secrets: 1,
posture: 'Action needed',
});
});
it('counts a Low/Medium known-exploited finding in knownExploited and posture (matches the deploy gate)', async () => {
const now = Date.now();
const scanId = db().createVulnerabilityScan({
node_id: 1, image_ref: 'lowkev:1', image_digest: 'sha256:lowkev', scanned_at: now,
total_vulnerabilities: 1, critical_count: 0, high_count: 0, medium_count: 0, low_count: 1,
unknown_count: 0, fixable_count: 0, secret_count: 0, misconfig_count: 0, scanners_used: 'vuln',
highest_severity: 'LOW', 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-2024-5002', pkg_name: 'liblow', installed_version: '1', fixed_version: null,
severity: 'LOW', title: null, description: null, primary_url: null,
}]);
// The deploy gate blocks a KEV at any severity; the overview must agree.
db().replaceKev([{ cve_id: 'CVE-2024-5002', date_added: '2024-01-01' }], now);
const res = await request(app).get('/api/security/overview').set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(res.body).toMatchObject({ knownExploited: 1, posture: 'Action needed' });
});
it('excludes a suppressed (accepted) KEV from knownExploited', async () => {
const now = Date.now();
const scanId = db().createVulnerabilityScan({
node_id: 1, image_ref: 'suppkev:1', image_digest: 'sha256:suppkev', scanned_at: now,
total_vulnerabilities: 2, critical_count: 0, high_count: 0, medium_count: 2, low_count: 0,
unknown_count: 0, fixable_count: 0, secret_count: 0, misconfig_count: 0, scanners_used: 'vuln',
highest_severity: 'MEDIUM', 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-2024-KEVA', pkg_name: 'a', installed_version: '1', fixed_version: null, severity: 'MEDIUM', title: null, description: null, primary_url: null },
{ vulnerability_id: 'CVE-2024-KEVB', pkg_name: 'b', installed_version: '1', fixed_version: null, severity: 'MEDIUM', title: null, description: null, primary_url: null },
]);
db().replaceKev([{ cve_id: 'CVE-2024-KEVA', date_added: '2024-01-01' }, { cve_id: 'CVE-2024-KEVB', date_added: '2024-01-01' }], now);
// KEVA is accepted (dismissed); only the live KEVB remains actionable.
db().createCveSuppression({ cve_id: 'CVE-2024-KEVA', pkg_name: null, image_pattern: null, reason: 'accepted', created_by: 'admin', created_at: now, expires_at: null, replicated_from_control: 0, status: 'accepted' });
const res = await request(app).get('/api/security/overview').set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(res.body.knownExploited).toBe(1);
});
it('reads Secure when a scan completed with nothing actionable or severe', async () => {
db().createVulnerabilityScan({
node_id: 1, image_ref: 'clean:1', image_digest: 'sha256:clean', 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: null, os_info: null, trivy_version: null, scan_duration_ms: null,
triggered_by: 'manual', status: 'completed', error: null, stack_context: null,
});
const res = await request(app).get('/api/security/overview').set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(res.body.posture).toBe('Secure');
});
it('is reachable by a Community viewer (read-only, auth-only)', async () => {
const res = await request(app).get('/api/security/overview').set('Cookie', viewerCookie);
expect(res.status).toBe(200);
});
it('requires authentication', async () => {
const res = await request(app).get('/api/security/overview');
expect(res.status).toBe(401);
});
});
describe('GET /api/security/overview/trend', () => {
beforeEach(() => resetSecurity());
const dayStart = (daysAgo: number): number => {
const d = new Date(Date.now() - daysAgo * DAY);
d.setUTCHours(0, 0, 0, 0);
return d.getTime();
};
it('returns ascending daily critical/high points, node-scoped and completed only', async () => {
const d1 = dayStart(3);
const d2 = dayStart(2);
seedScan({ image_ref: 'a:1', scanned_at: d1 + 3_600_000, critical: 4, high: 2 });
seedScan({ image_ref: 'a:1', scanned_at: d2 + 3_600_000, critical: 1, high: 5 });
seedScan({ node_id: 2, image_ref: 'x:1', scanned_at: d2 + 3_600_000, critical: 9, high: 9 }); // other node
seedScan({ image_ref: 'f:1', scanned_at: d2 + 3_600_000, critical: 7, high: 7, status: 'failed' }); // failed
const res = await request(app).get('/api/security/overview/trend').set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
expect(res.body).toHaveLength(2);
expect(res.body[0]).toMatchObject({ critical: 4, high: 2 });
expect(res.body[1]).toMatchObject({ critical: 1, high: 5 });
expect(res.body[0].date < res.body[1].date).toBe(true);
});
it('requires authentication', async () => {
const res = await request(app).get('/api/security/overview/trend');
expect(res.status).toBe(401);
});
});
describe('GET /api/security/policy-packs', () => {
it('returns the 5 default packs with fully-formed rules (auth-only)', async () => {
const res = await request(app).get('/api/security/policy-packs').set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
expect(res.body).toHaveLength(5);
for (const pack of res.body) {
expect(pack).toMatchObject({
id: expect.any(String),
name: expect.any(String),
tagline: expect.any(String),
tierCopy: expect.any(String),
});
expect(Array.isArray(pack.rules)).toBe(true);
expect(pack.rules.length).toBeGreaterThan(0);
for (const rule of pack.rules) {
expect(rule).toMatchObject({
id: expect.any(String),
name: expect.any(String),
severity: expect.stringMatching(/^(CRITICAL|HIGH|MEDIUM|LOW)$/),
whatItChecks: expect.any(String),
why: expect.any(String),
howToFix: expect.any(String),
enforcement: expect.stringMatching(/^(warning|enforceable)$/),
});
}
}
});
it('returns 401 unauthenticated', async () => {
const res = await request(app).get('/api/security/policy-packs');
expect(res.status).toBe(401);
});
it('returns an identical catalog regardless of tier', async () => {
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('community');
const community = await request(app).get('/api/security/policy-packs').set('Cookie', adminCookie);
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('paid');
const paid = await request(app).get('/api/security/policy-packs').set('Cookie', adminCookie);
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('community');
expect(paid.body).toEqual(community.body);
});
});
describe('GET /api/security/scans/:scanId/vulnerabilities', () => {
beforeEach(() => resetSecurity());
it('attaches read-time exploit intel (KEV/EPSS) to each finding', async () => {
const now = Date.now();
const scanId = db().createVulnerabilityScan({
node_id: 1, image_ref: 'vex:1', image_digest: 'sha256:vex', scanned_at: 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-2024-7777', pkg_name: 'p', installed_version: '1', fixed_version: null,
severity: 'CRITICAL', title: null, description: null, primary_url: null,
}]);
db().replaceKev([{ cve_id: 'CVE-2024-7777', date_added: '2024-02-02' }], now);
db().upsertEpss([{ cve_id: 'CVE-2024-7777', epss_score: 0.42, epss_percentile: 0.95 }], now);
const res = await request(app).get(`/api/security/scans/${scanId}/vulnerabilities`).set('Cookie', adminCookie);
expect(res.status).toBe(200);
const item = (res.body.items as Array<{ vulnerability_id: string; kev: boolean; epss_score: number }>)
.find((i) => i.vulnerability_id === 'CVE-2024-7777');
expect(item).toMatchObject({ kev: true, epss_score: 0.42, epss_percentile: 0.95 });
});
});
describe('GET /api/security/vex/export (Admiral)', () => {
beforeEach(() => {
resetSecurity();
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('community');
});
afterAll(() => {
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('community');
});
it('is gated to Admiral: 403 for Community', async () => {
const res = await request(app).get('/api/security/vex/export').set('Cookie', adminCookie);
expect(res.status).toBe(403);
});
it('exports an OpenVEX document from triage decisions for Admiral', async () => {
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('paid');
db().createCveSuppression({
cve_id: 'CVE-2024-2222', pkg_name: null, image_pattern: 'nginx*', reason: 'not present in build',
created_by: 'admin', created_at: Date.now(), expires_at: null, replicated_from_control: 0,
status: 'not_affected', justification: 'component_not_present',
});
const res = await request(app).get('/api/security/vex/export').set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(res.body['@context']).toContain('openvex');
const stmt = (res.body.statements as Array<{ vulnerability: { name: string }; status: string; justification?: string; products: string[] }>)
.find((s) => s.vulnerability.name === 'CVE-2024-2222');
expect(stmt).toMatchObject({ status: 'not_affected', justification: 'component_not_present', products: ['nginx*'] });
});
});
describe('GET /api/security/overview/exploit-intel', () => {
beforeEach(() => resetSecurity());
it('returns actionable Crit/High findings with KEV/EPSS joined and dismissed excluded', async () => {
const now = Date.now();
const scanId = db().createVulnerabilityScan({
node_id: 1, image_ref: 'app:1', image_digest: 'sha256:app', scanned_at: now,
total_vulnerabilities: 3, critical_count: 2, high_count: 1, medium_count: 0, low_count: 0,
unknown_count: 0, fixable_count: 2, 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,
});
const d = (id: string, severity: 'CRITICAL' | 'HIGH', cvss: number | null, fixed: string | null) => ({
vulnerability_id: id, pkg_name: `p-${id}`, installed_version: '1', fixed_version: fixed,
severity, title: null, description: null, primary_url: null, cvss_score: cvss,
});
db().insertVulnerabilityDetails(scanId, [
d('CVE-2024-AAAA', 'CRITICAL', 9.8, '2'), // actionable, has KEV + EPSS
d('CVE-2024-BBBB', 'HIGH', 7.2, null), // actionable, no intel yet
d('CVE-2024-CCCC', 'CRITICAL', 8.1, '3'), // dismissed -> excluded
]);
db().replaceKev([{ cve_id: 'CVE-2024-AAAA', date_added: '2024-01-01' }], now);
db().upsertEpss([{ cve_id: 'CVE-2024-AAAA', epss_score: 0.6, epss_percentile: 0.97 }], now);
db().createCveSuppression({
cve_id: 'CVE-2024-CCCC', pkg_name: null, image_pattern: null, reason: 'accepted',
created_by: 'admin', created_at: now, expires_at: null, replicated_from_control: 0, status: 'accepted',
});
const res = await request(app).get('/api/security/overview/exploit-intel').set('Cookie', adminCookie);
expect(res.status).toBe(200);
const items = res.body.items as Array<{ vulnerability_id: string; cvss_score: number | null; epss_score: number | null; kev: boolean; severity: string; scan_id: number }>;
const ids = items.map((i) => i.vulnerability_id);
expect(ids).toContain('CVE-2024-AAAA');
expect(ids).toContain('CVE-2024-BBBB');
expect(ids).not.toContain('CVE-2024-CCCC'); // dismissed triage decision
expect(items.find((i) => i.vulnerability_id === 'CVE-2024-AAAA')).toMatchObject({ cvss_score: 9.8, epss_score: 0.6, kev: true, severity: 'CRITICAL', scan_id: scanId });
expect(items.find((i) => i.vulnerability_id === 'CVE-2024-BBBB')).toMatchObject({ cvss_score: 7.2, epss_score: null, kev: false });
expect(res.body.truncated).toBe(false);
});
it('includes a Medium/Low known-exploited finding alongside Critical/High', async () => {
const now = Date.now();
const scanId = db().createVulnerabilityScan({
node_id: 1, image_ref: 'mix:1', image_digest: 'sha256:mix', scanned_at: now,
total_vulnerabilities: 2, critical_count: 0, high_count: 0, medium_count: 1, low_count: 1,
unknown_count: 0, fixable_count: 0, secret_count: 0, misconfig_count: 0, scanners_used: 'vuln',
highest_severity: 'MEDIUM', 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-2024-MEDK', pkg_name: 'm', installed_version: '1', fixed_version: null, severity: 'MEDIUM', title: null, description: null, primary_url: null, cvss_score: 5.5 },
{ vulnerability_id: 'CVE-2024-LOWN', pkg_name: 'l', installed_version: '1', fixed_version: null, severity: 'LOW', title: null, description: null, primary_url: null, cvss_score: 3.1 },
]);
// Only the Medium finding is known-exploited; the Low non-KEV stays out.
db().replaceKev([{ cve_id: 'CVE-2024-MEDK', date_added: '2024-01-01' }], now);
const res = await request(app).get('/api/security/overview/exploit-intel').set('Cookie', adminCookie);
expect(res.status).toBe(200);
const ids = (res.body.items as Array<{ vulnerability_id: string }>).map((i) => i.vulnerability_id);
expect(ids).toContain('CVE-2024-MEDK');
expect(ids).not.toContain('CVE-2024-LOWN');
});
it('requires authentication', async () => {
const res = await request(app).get('/api/security/overview/exploit-intel');
expect(res.status).toBe(401);
});
});