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:
Anso
2026-04-16 20:32:38 -04:00
committed by GitHub
parent f8eb1b4e88
commit dc8370f5a4
16 changed files with 563 additions and 140 deletions
+33 -1
View File
@@ -14,11 +14,15 @@ import { NodeRegistry } from './NodeRegistry';
import { NotificationService } from './NotificationService';
import TrivyService from './TrivyService';
const TRIVY_REDETECT_INTERVAL_MS = 10 * 60 * 1000;
const STALE_SCAN_THRESHOLD_MS = 15 * 60 * 1000;
export class SchedulerService {
private static instance: SchedulerService;
private intervalId: ReturnType<typeof setInterval> | null = null;
private isProcessing = false;
private runningTasks = new Set<number>();
private lastTrivyRedetect = 0;
private constructor() {}
@@ -56,6 +60,19 @@ export class SchedulerService {
}
}
private async maybeRedetectTrivy(): Promise<void> {
const now = Date.now();
if (now - this.lastTrivyRedetect < TRIVY_REDETECT_INTERVAL_MS) return;
this.lastTrivyRedetect = now;
try {
await TrivyService.getInstance().detectTrivy();
} catch (error) {
if (isDebugEnabled()) {
console.warn('[SchedulerService:debug] Trivy re-detect failed:', error);
}
}
}
public calculateNextRun(cronExpression: string): number {
const expr = CronExpressionParser.parse(cronExpression);
return expr.next().toDate().getTime();
@@ -68,12 +85,27 @@ export class SchedulerService {
}
this.isProcessing = true;
try {
const db = DatabaseService.getInstance();
// Vulnerability scanning is available on every tier, so the stale-scan sweep
// and Trivy re-detect run before the paid-tier gate below.
try {
const staleScans = db.markStaleScansAsFailed(STALE_SCAN_THRESHOLD_MS);
if (staleScans > 0) {
console.log(
`[SchedulerService] Marked ${staleScans} stale vulnerability scan(s) as failed`,
);
}
} catch (error) {
console.error('[SchedulerService] Stale scan sweep failed:', error);
}
await this.maybeRedetectTrivy();
const ls = LicenseService.getInstance();
const isPaid = ls.getTier() === 'paid';
const isAdmiral = isPaid && ls.getVariant() === 'admiral';
if (!isPaid) return;
const db = DatabaseService.getInstance();
const now = Date.now();
const dueTasks = db.getDueScheduledTasks(now);