mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-25 17:57:06 +00:00
fix(security): harden Trivy scan lifecycle, logging, and docs (#639)
* fix(security): harden Trivy scan lifecycle, logging, and docs - Call TrivyService.initialize() at startup so capability state is accurate before first request; add periodic re-detect to the scheduler so newly installed Trivy binaries light up without a restart. - Add markStaleScansAsFailed sweep (+ idx_vuln_scans_status index) to recover any scan row left in_progress after a crash or timeout; sweep runs before the paid-tier gate so every tier self-heals. - Split scanImage persistence into beginScan/finishScan so the manual scan route owns a single code path and can return a scanId synchronously while work continues asynchronously. - Validate image refs on /api/security/scan and /sbom via new utility; defense-in-depth against shell-metacharacter payloads. - Dispatch a warning-level alert when a post-deploy scan fails so the operator has a user-visible path to the failure instead of a silent log. - Share DIGEST_CACHE_TTL_MS and severity ordering across service and route layers; remove dead invalidateDetection(). - Add [Trivy:diag] logging gated behind developer_mode for support diagnostics; production logs unchanged. - Frontend: defensive toast fallback chain, sr-only SheetDescription, and a truncation badge when the 500-item detail fetch is capped. - Tests: extend trivy-service and vulnerability-db suites; add image-ref and severity unit tests. - Docs: expand vulnerability-scanning troubleshooting with recovery, re-detect, and diagnostic-log guidance; link Dockerfile comment to trivy-setup. * fix(security): drop unnecessary escape in image-ref forbidden-char regex
This commit is contained in:
@@ -296,6 +296,68 @@ describe('Vulnerability scan storage (in-memory SQLite)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── markStaleScansAsFailed ───────────────────────────────────────
|
||||
|
||||
describe('markStaleScansAsFailed', () => {
|
||||
function markStale(olderThanMs: number): number {
|
||||
const cutoff = Date.now() - olderThanMs;
|
||||
const result = db
|
||||
.prepare(
|
||||
`UPDATE vulnerability_scans
|
||||
SET status = 'failed',
|
||||
error = 'Scan did not complete within expected time',
|
||||
scan_duration_ms = ? - scanned_at
|
||||
WHERE status = 'in_progress' AND scanned_at < ?`,
|
||||
)
|
||||
.run(Date.now(), cutoff);
|
||||
return result.changes;
|
||||
}
|
||||
|
||||
it('flips in_progress scans older than the cutoff to failed', () => {
|
||||
const now = Date.now();
|
||||
const stale = insertScan({ status: 'in_progress', scanned_at: now - 30 * 60 * 1000 });
|
||||
const fresh = insertScan({ status: 'in_progress', scanned_at: now - 1_000 });
|
||||
|
||||
const changed = markStale(15 * 60 * 1000);
|
||||
expect(changed).toBe(1);
|
||||
|
||||
const staleRow = db.prepare('SELECT status, error FROM vulnerability_scans WHERE id = ?').get(stale) as {
|
||||
status: string;
|
||||
error: string;
|
||||
};
|
||||
const freshRow = db.prepare('SELECT status FROM vulnerability_scans WHERE id = ?').get(fresh) as {
|
||||
status: string;
|
||||
};
|
||||
expect(staleRow.status).toBe('failed');
|
||||
expect(staleRow.error).toMatch(/did not complete/i);
|
||||
expect(freshRow.status).toBe('in_progress');
|
||||
});
|
||||
|
||||
it('leaves completed and failed rows untouched', () => {
|
||||
const now = Date.now();
|
||||
const completed = insertScan({ status: 'completed', scanned_at: now - 30 * 60 * 1000 });
|
||||
const failed = insertScan({ status: 'failed', scanned_at: now - 30 * 60 * 1000 });
|
||||
|
||||
const changed = markStale(15 * 60 * 1000);
|
||||
expect(changed).toBe(0);
|
||||
|
||||
const rows = db
|
||||
.prepare('SELECT id, status FROM vulnerability_scans WHERE id IN (?, ?)')
|
||||
.all(completed, failed) as Array<{ id: number; status: string }>;
|
||||
expect(rows.find((r) => r.id === completed)?.status).toBe('completed');
|
||||
expect(rows.find((r) => r.id === failed)?.status).toBe('failed');
|
||||
});
|
||||
|
||||
it('is idempotent when called repeatedly', () => {
|
||||
const now = Date.now();
|
||||
insertScan({ status: 'in_progress', scanned_at: now - 30 * 60 * 1000 });
|
||||
|
||||
expect(markStale(15 * 60 * 1000)).toBe(1);
|
||||
expect(markStale(15 * 60 * 1000)).toBe(0);
|
||||
expect(markStale(15 * 60 * 1000)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── vulnerability_details cascade ────────────────────────────────
|
||||
|
||||
describe('vulnerability_details cascade', () => {
|
||||
|
||||
Reference in New Issue
Block a user