mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-31 12:48:10 +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:
@@ -6,7 +6,7 @@
|
||||
* when the binary is not available.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import TrivyService from '../services/TrivyService';
|
||||
import TrivyService, { parseTrivyOutput } from '../services/TrivyService';
|
||||
|
||||
describe('TrivyService', () => {
|
||||
let svc: TrivyService;
|
||||
@@ -31,6 +31,12 @@ describe('TrivyService', () => {
|
||||
expect(result).toHaveProperty('version');
|
||||
expect(typeof result.available).toBe('boolean');
|
||||
});
|
||||
|
||||
it('records a detection timestamp after running', async () => {
|
||||
const before = Date.now();
|
||||
await svc.detectTrivy();
|
||||
expect(svc.getDetectionTimestamp()).toBeGreaterThanOrEqual(before);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scanImage', () => {
|
||||
@@ -51,4 +57,99 @@ describe('TrivyService', () => {
|
||||
expect(svc.isScanning(1, 'nginx:latest')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTrivyOutput', () => {
|
||||
it('extracts OS metadata and deduplicates vulnerabilities across targets', () => {
|
||||
const raw = JSON.stringify({
|
||||
Metadata: { OS: { Family: 'alpine', Name: '3.19.0' } },
|
||||
Results: [
|
||||
{
|
||||
Target: 'alpine:3.19 (alpine)',
|
||||
Vulnerabilities: [
|
||||
{
|
||||
VulnerabilityID: 'CVE-2024-0001',
|
||||
PkgName: 'openssl',
|
||||
InstalledVersion: '3.0.0',
|
||||
FixedVersion: '3.0.1',
|
||||
Severity: 'HIGH',
|
||||
},
|
||||
{
|
||||
VulnerabilityID: 'CVE-2024-0002',
|
||||
PkgName: 'curl',
|
||||
InstalledVersion: '8.0',
|
||||
Severity: 'CRITICAL',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
Target: 'other-target',
|
||||
Vulnerabilities: [
|
||||
{
|
||||
VulnerabilityID: 'CVE-2024-0001',
|
||||
PkgName: 'openssl',
|
||||
InstalledVersion: '3.0.0',
|
||||
Severity: 'HIGH',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const parsed = parseTrivyOutput(raw);
|
||||
expect(parsed.os).toBe('alpine 3.19.0');
|
||||
expect(parsed.vulnerabilities.length).toBe(2);
|
||||
const ids = parsed.vulnerabilities.map((v) => v.vulnerabilityId);
|
||||
expect(ids).toContain('CVE-2024-0001');
|
||||
expect(ids).toContain('CVE-2024-0002');
|
||||
});
|
||||
|
||||
it('normalizes unknown severities to UNKNOWN', () => {
|
||||
const raw = JSON.stringify({
|
||||
Results: [
|
||||
{
|
||||
Vulnerabilities: [
|
||||
{
|
||||
VulnerabilityID: 'CVE-X',
|
||||
PkgName: 'libx',
|
||||
InstalledVersion: '1',
|
||||
Severity: 'NEGLIGIBLE',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const parsed = parseTrivyOutput(raw);
|
||||
expect(parsed.vulnerabilities[0].severity).toBe('UNKNOWN');
|
||||
});
|
||||
|
||||
it('drops entries missing VulnerabilityID or PkgName', () => {
|
||||
const raw = JSON.stringify({
|
||||
Results: [
|
||||
{
|
||||
Vulnerabilities: [
|
||||
{ PkgName: 'x', Severity: 'HIGH' },
|
||||
{ VulnerabilityID: 'CVE-1', Severity: 'HIGH' },
|
||||
{ VulnerabilityID: 'CVE-2', PkgName: 'y', Severity: 'LOW' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
const parsed = parseTrivyOutput(raw);
|
||||
expect(parsed.vulnerabilities.length).toBe(1);
|
||||
expect(parsed.vulnerabilities[0].vulnerabilityId).toBe('CVE-2');
|
||||
});
|
||||
|
||||
it('tolerates missing Metadata and empty Results', () => {
|
||||
const parsed = parseTrivyOutput(JSON.stringify({ Results: [] }));
|
||||
expect(parsed.os).toBeNull();
|
||||
expect(parsed.vulnerabilities).toEqual([]);
|
||||
|
||||
const parsedEmpty = parseTrivyOutput(JSON.stringify({}));
|
||||
expect(parsedEmpty.os).toBeNull();
|
||||
expect(parsedEmpty.vulnerabilities).toEqual([]);
|
||||
});
|
||||
|
||||
it('throws a helpful error on malformed JSON', () => {
|
||||
expect(() => parseTrivyOutput('{not-json')).toThrow(/Malformed/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user