feat(images): Trivy-powered vulnerability scanning (#635)

* feat(images): Trivy-powered vulnerability scanning

Scan container images for known CVEs via Trivy. On-demand scanning and
severity badges are available on every tier; scheduled scans, scan
policies, SBOM generation, and scan history are gated to Skipper+.

- New TrivyService (binary detection, per-image scan, SBOM, digest cache)
- Three new tables: vulnerability_scans, vulnerability_details, scan_policies
- 12 routes under /api/security (scan, results, summaries, SBOM, policies, compare)
- Post-deploy async scans wired into all five deploy paths, with a
  per-deploy opt-out toggle in the App Store deploy sheet
- "scan" action type added to SchedulerService for fleet-wide recurring scans
- Frontend: severity badges in Resources Hub with animated cursor detail,
  scan results drawer with vulnerability table and filters, and a new
  Security section in Settings for scan policy CRUD
- Policy threshold violations dispatch a warning or critical alert based on
  the policy's block_on_deploy flag; deploys themselves are never blocked

* fix(security): compute scan age in useEffect to satisfy react-hooks/purity
This commit is contained in:
Anso
2026-04-16 15:03:36 -04:00
committed by GitHub
parent 4c5aa73196
commit c9cd6990d2
23 changed files with 3452 additions and 18 deletions
+29 -3
View File
@@ -6,7 +6,8 @@ import { Label } from "@/components/ui/label";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter } from "@/components/ui/sheet";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import { Search, Rocket, Loader2, Info, ExternalLink, Star } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { Search, Rocket, Loader2, Info, ExternalLink, Star, ShieldCheck } from "lucide-react";
import { toast } from "@/components/ui/toast-store";
import { cn } from '@/lib/utils';
import { apiFetch } from '@/lib/api';
@@ -78,9 +79,17 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
const [newEnvKey, setNewEnvKey] = useState('');
const [portsInUse, setPortsInUse] = useState<Record<string, PortInUseInfo>>({});
const [newEnvVal, setNewEnvVal] = useState('');
const [autoScan, setAutoScan] = useState(true);
const [trivyAvailable, setTrivyAvailable] = useState(false);
useEffect(() => {
fetchTemplates();
apiFetch('/security/trivy-status')
.then(r => r.ok ? r.json() : null)
.then(d => { if (d) setTrivyAvailable(!!d.available); })
.catch((err) => {
console.error('Failed to fetch Trivy status:', err);
});
}, []);
const fetchTemplates = async () => {
@@ -215,7 +224,8 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
body: JSON.stringify({
stackName: stackName.trim(),
template: modifiedTemplate,
envVars: finalEnvVars
envVars: finalEnvVars,
skip_scan: !autoScan
})
});
const data = await res.json();
@@ -546,7 +556,23 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
</ScrollArea>
<SheetFooter className="pt-4 mt-auto border-t sm:justify-start">
<div className="flex flex-col w-full gap-2">
<div className="flex flex-col w-full gap-3">
{trivyAvailable && (
<div className="flex items-center gap-2">
<Checkbox
id="auto-scan"
checked={autoScan}
onCheckedChange={(checked) => setAutoScan(!!checked)}
/>
<Label
htmlFor="auto-scan"
className="text-sm text-muted-foreground cursor-pointer flex items-center gap-1.5"
>
<ShieldCheck className="w-3.5 h-3.5" strokeWidth={1.5} />
Scan images for vulnerabilities after deploy
</Label>
</div>
)}
<Button
onClick={handleDeploy}
disabled={isDeploying || !stackName.trim() || !can('stack:create')}