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
+22
View File
@@ -0,0 +1,22 @@
const MAX_LENGTH = 255;
const IMAGE_REF_PATTERN = /^[a-zA-Z0-9]([a-zA-Z0-9._\-/:@+]*[a-zA-Z0-9])?$/;
const FORBIDDEN_CHARS = /[\s;|&`$(){}[\]<>'"\\!*?#~]/;
export function validateImageRef(ref: unknown): ref is string {
if (typeof ref !== 'string') return false;
const trimmed = ref.trim();
if (trimmed.length === 0 || trimmed.length > MAX_LENGTH) return false;
if (FORBIDDEN_CHARS.test(trimmed)) return false;
if (trimmed.includes('..')) return false;
if (!IMAGE_REF_PATTERN.test(trimmed)) return false;
return true;
}
export function assertImageRef(ref: unknown): string {
if (!validateImageRef(ref)) {
throw new Error('Invalid image reference');
}
return ref;
}
+21
View File
@@ -0,0 +1,21 @@
import type { VulnSeverity } from '../services/DatabaseService';
export const SEVERITY_ORDER: VulnSeverity[] = [
'UNKNOWN',
'LOW',
'MEDIUM',
'HIGH',
'CRITICAL',
];
export function severityRank(severity: VulnSeverity | null | undefined): number {
if (!severity) return -1;
return SEVERITY_ORDER.indexOf(severity);
}
export function isSeverityAtLeast(
actual: VulnSeverity | null | undefined,
threshold: VulnSeverity,
): boolean {
return severityRank(actual) >= severityRank(threshold);
}