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
+9 -2
View File
@@ -16,7 +16,7 @@ import { SENCHO_SETTINGS_CHANGED } from '@/lib/events';
import type { SenchoSettingsChangedDetail } from '@/lib/events';
import {
Shield, Activity, Bell, Code, Server, Package, X,
Info, Crown, Webhook, Users, Zap, Database, LifeBuoy, Lock, Tag, Route,
Info, Crown, Webhook, Users, Zap, Database, LifeBuoy, Lock, Tag, Route, ShieldCheck,
} from 'lucide-react';
import { NodeManager } from './NodeManager';
import { useNodes } from '@/context/NodeContext';
@@ -33,6 +33,7 @@ import {
NotificationsSection,
NotificationRoutingSection,
WebhooksSection,
SecuritySection,
DeveloperSection,
AppStoreSection,
SupportSection,
@@ -44,7 +45,8 @@ import type { PatchableSettings, SectionId } from './settings';
const GLOBAL_ONLY_SECTIONS: ReadonlySet<SectionId> = new Set<SectionId>([
'account', 'license', 'users', 'sso', 'api-tokens', 'registries',
'labels', 'notifications', 'notification-routing', 'webhooks', 'nodes', 'appstore',
'labels', 'notifications', 'notification-routing', 'webhooks', 'security',
'nodes', 'appstore',
]);
interface SettingsModalProps {
@@ -305,6 +307,8 @@ export function SettingsModal({ isOpen, onClose, initialSection }: SettingsModal
return <NotificationRoutingSection />;
case 'webhooks':
return <WebhooksSection isPaid={isPaid} />;
case 'security':
return <SecuritySection isPaid={isPaid} />;
case 'developer':
return (
<DeveloperSection
@@ -395,6 +399,9 @@ export function SettingsModal({ isOpen, onClose, initialSection }: SettingsModal
{!isRemote && (
<NavButton section="webhooks" icon={<Webhook className="w-4 h-4 mr-2" />} label="Webhooks" locked={!isPaid} />
)}
{!isRemote && isAdmin && (
<NavButton section="security" icon={<ShieldCheck className="w-4 h-4 mr-2" />} label="Security" locked={!isPaid} />
)}
<NavButton
section="developer"
icon={<Code className="w-4 h-4 mr-2" />}