Files
sencho/backend/src/__tests__/security-scans-route.test.ts
T
Anso 4834e2e51d feat(security): prefer digest identity in scan history (#1610)
Retain scans and History search by digest when available, keep imageRefLike for compatibility, and surface digests in compare.
2026-07-10 22:10:09 -04:00

144 lines
5.2 KiB
TypeScript

/**
* Route wiring for GET /api/security/scans: auth, legacy imageRefLike,
* additive imageIdentityLike, exact imageDigest, node isolation, and
* cappedIdentities response shape.
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
import request from 'supertest';
import { setupTestDb, cleanupTestDb, loginAsTestAdmin } from './helpers/setupTestDb';
let tmpDir: string;
let app: import('express').Express;
let DatabaseService: typeof import('../services/DatabaseService').DatabaseService;
let adminCookie: string;
beforeAll(async () => {
tmpDir = await setupTestDb();
({ DatabaseService } = await import('../services/DatabaseService'));
const { LicenseService } = await import('../services/LicenseService');
vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('community');
({ app } = await import('../index'));
adminCookie = await loginAsTestAdmin(app);
});
afterAll(() => cleanupTestDb(tmpDir));
function seedScan(overrides: {
node_id?: number;
image_ref?: string;
image_digest?: string | null;
scanned_at?: number;
} = {}): number {
const db = DatabaseService.getInstance();
return db.createVulnerabilityScan({
node_id: overrides.node_id ?? 1,
image_ref: overrides.image_ref ?? 'alpine:3.19',
image_digest: overrides.image_digest === undefined
? `sha256:${Math.random().toString(16).slice(2)}`
: overrides.image_digest,
scanned_at: overrides.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,
});
}
beforeEach(() => {
DatabaseService.getInstance().getDb().prepare('DELETE FROM vulnerability_scans').run();
});
describe('GET /api/security/scans query wiring', () => {
it('requires authentication', async () => {
const res = await request(app).get('/api/security/scans');
expect(res.status).toBe(401);
});
it('forwards legacy imageRefLike as reference-only', async () => {
seedScan({ image_ref: 'alpine:3.19', image_digest: 'sha256:aaa', scanned_at: 1 });
seedScan({ image_ref: 'nginx:1', image_digest: 'sha256:alpinezzz', scanned_at: 2 });
const res = await request(app)
.get('/api/security/scans?imageRefLike=alpine&status=completed')
.set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(res.body.total).toBe(1);
expect(res.body.items[0].image_ref).toBe('alpine:3.19');
expect(Array.isArray(res.body.cappedIdentities)).toBe(true);
});
it('forwards imageIdentityLike matching ref or digest', async () => {
seedScan({ image_ref: 'nginx:1', image_digest: 'sha256:deadbeef99', scanned_at: 1 });
seedScan({ image_ref: 'redis:7', image_digest: 'sha256:other', scanned_at: 2 });
const byDigest = await request(app)
.get('/api/security/scans?imageIdentityLike=deadbeef&status=completed')
.set('Cookie', adminCookie);
expect(byDigest.status).toBe(200);
expect(byDigest.body.total).toBe(1);
expect(byDigest.body.items[0].image_ref).toBe('nginx:1');
const byRef = await request(app)
.get('/api/security/scans?imageIdentityLike=redis&status=completed')
.set('Cookie', adminCookie);
expect(byRef.body.total).toBe(1);
expect(byRef.body.items[0].image_ref).toBe('redis:7');
});
it('forwards exact imageDigest', async () => {
const digest = 'sha256:exactroute01';
seedScan({ image_ref: 'a:1', image_digest: digest, scanned_at: 1 });
seedScan({ image_ref: 'a:2', image_digest: 'sha256:other', scanned_at: 2 });
const res = await request(app)
.get(`/api/security/scans?imageDigest=${encodeURIComponent(digest)}&status=completed`)
.set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(res.body.total).toBe(1);
expect(res.body.items[0].image_digest).toBe(digest);
});
it('scopes listing and cappedIdentities to the request node', async () => {
const db = DatabaseService.getInstance();
db.updateGlobalSetting('scan_history_per_image_limit', '2');
db.getDb()
.prepare(`INSERT OR IGNORE INTO nodes (id, name, type, compose_dir, is_default, status, created_at)
VALUES (2, 'Peer', 'remote', '/tmp', 0, 'online', ?)`)
.run(Date.now());
const digest = 'sha256:noderoute00';
for (let i = 0; i < 4; i++) {
seedScan({ node_id: 1, image_ref: 'a:1', image_digest: digest, scanned_at: 1000 + i });
}
for (let i = 0; i < 4; i++) {
seedScan({ node_id: 2, image_ref: 'a:1', image_digest: digest, scanned_at: 2000 + i });
}
const res = await request(app)
.get('/api/security/scans?status=completed&limit=50')
.set('Cookie', adminCookie);
expect(res.status).toBe(200);
expect(res.body.total).toBe(2);
expect(res.body.items.every((s: { node_id: number }) => s.node_id === 1)).toBe(true);
expect(res.body.cappedIdentities).toEqual([
{ key: digest, kind: 'digest', displayRef: 'a:1' },
]);
});
});