mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 18:32:52 +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:
@@ -623,8 +623,8 @@ export default function ResourcesView() {
|
||||
}
|
||||
throw new Error('Scan timed out');
|
||||
} catch (error) {
|
||||
const err = error as { message?: string };
|
||||
toast.error(err?.message || 'Scan failed');
|
||||
const err = error as { message?: string; error?: string; data?: { error?: string } };
|
||||
toast.error(err?.message || err?.error || err?.data?.error || 'Scan failed');
|
||||
} finally {
|
||||
toast.dismiss(loadingId);
|
||||
setScanningImageRef(null);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -75,18 +75,21 @@ export function VulnerabilityScanSheet({
|
||||
}: VulnerabilityScanSheetProps) {
|
||||
const [scan, setScan] = useState<VulnerabilityScan | null>(null);
|
||||
const [details, setDetails] = useState<VulnerabilityDetail[]>([]);
|
||||
const [totalDetails, setTotalDetails] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [severityFilter, setSeverityFilter] = useState<SeverityFilter>('ALL');
|
||||
const [page, setPage] = useState(0);
|
||||
const [downloadingSbom, setDownloadingSbom] = useState(false);
|
||||
|
||||
const DETAIL_FETCH_LIMIT = 500;
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (scanId == null) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const [scanRes, detailsRes] = await Promise.all([
|
||||
apiFetch(`/security/scans/${scanId}`),
|
||||
apiFetch(`/security/scans/${scanId}/vulnerabilities?limit=500`),
|
||||
apiFetch(`/security/scans/${scanId}/vulnerabilities?limit=${DETAIL_FETCH_LIMIT}`),
|
||||
]);
|
||||
if (!scanRes.ok) throw new Error('Failed to fetch scan');
|
||||
if (!detailsRes.ok) throw new Error('Failed to fetch vulnerabilities');
|
||||
@@ -94,6 +97,7 @@ export function VulnerabilityScanSheet({
|
||||
const detailsData = await detailsRes.json();
|
||||
setScan(scanData);
|
||||
setDetails(Array.isArray(detailsData.items) ? detailsData.items : []);
|
||||
setTotalDetails(typeof detailsData.total === 'number' ? detailsData.total : 0);
|
||||
setPage(0);
|
||||
} catch (err) {
|
||||
toast.error((err as Error)?.message || 'Failed to load scan');
|
||||
@@ -107,6 +111,7 @@ export function VulnerabilityScanSheet({
|
||||
else {
|
||||
setScan(null);
|
||||
setDetails([]);
|
||||
setTotalDetails(0);
|
||||
setSeverityFilter('ALL');
|
||||
setPage(0);
|
||||
}
|
||||
@@ -191,6 +196,11 @@ export function VulnerabilityScanSheet({
|
||||
{scan?.image_ref ?? 'Loading...'}
|
||||
</span>
|
||||
</SheetTitle>
|
||||
<SheetDescription className="sr-only">
|
||||
{scan
|
||||
? `Vulnerability scan results for ${scan.image_ref}: ${scan.total_vulnerabilities} total findings.`
|
||||
: 'Vulnerability scan details.'}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{loading && !scan && (
|
||||
@@ -336,6 +346,12 @@ export function VulnerabilityScanSheet({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{totalDetails > details.length && (
|
||||
<div className="px-6 pt-2 text-xs text-stat-subtitle font-mono">
|
||||
Showing first {details.length} of {totalDetails}. Export CSV for the complete list.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ScrollArea className="flex-1 min-h-0">
|
||||
<div className="px-6 py-3">
|
||||
{pageItems.length === 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user