mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 18:57:09 +00:00
4834e2e51d
Retain scans and History search by digest when available, keep imageRefLike for compatibility, and surface digests in compare.
144 lines
5.2 KiB
TypeScript
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' },
|
|
]);
|
|
});
|
|
});
|