feat: add an on-demand node-wide security scan with live progress (#1367)

Add a "Scan this node" action on the Security overview that scans, in one pass,
any combination of three types: image vulnerabilities, image secrets, and
compose misconfigurations. Progress streams live into the deploy-feedback modal.

- TrivyService.scanNode runs the selected scanners across the node's images and,
  for misconfig, every stack's compose file, behind a per-node lock and tolerant
  of per-item failures. The existing scanAllNodeImages becomes a thin vuln-only
  wrapper over the shared image loop, so scheduled scans are unchanged.
- POST /api/security/scan-node (admin, scanner-gated) streams sanitized progress
  to the deploy terminal and returns a combined summary. Secret scans stream
  counts only, never matched values.
- Frontend adds a "scan" action verb and a ScanNodeLauncher wired into the
  overview; the scan stays bound to the node it started on even if the active
  node changes mid-run.
This commit is contained in:
Anso
2026-06-12 19:07:45 -04:00
committed by GitHub
parent ebf66fd92a
commit ef5a3f00a7
11 changed files with 641 additions and 6 deletions
@@ -11,6 +11,7 @@ import {
TopExposedImagesChart,
FindingsByTypeChart,
} from './SecurityCharts';
import { ScanNodeLauncher } from './ScanNodeLauncher';
interface OverviewTabProps {
overview: SecurityOverview | null;
@@ -20,6 +21,10 @@ interface OverviewTabProps {
trend: SecurityRiskTrendPoint[];
onNavigate: (tab: SecurityTab) => void;
onInspect: (scanId: number) => void;
/** Admin on a node with a ready scanner; enables the node-scan launcher. */
canScan: boolean;
/** Refresh the overview after a node-wide scan completes. */
onScanComplete: () => void;
}
const STATUS_ROW_TONE: Record<'value' | 'warn' | 'subtitle', string> = {
@@ -47,7 +52,7 @@ function ChartCard({ title, className, children }: { title: string; className?:
);
}
export function OverviewTab({ overview, loadError, summaries, trend, onNavigate, onInspect }: OverviewTabProps) {
export function OverviewTab({ overview, loadError, summaries, trend, onNavigate, onInspect, canScan, onScanComplete }: OverviewTabProps) {
if (loadError === 'unsupported') {
return (
<div className="flex flex-col items-center justify-center py-20 text-center">
@@ -98,6 +103,17 @@ export function OverviewTab({ overview, loadError, summaries, trend, onNavigate,
return (
<div className="space-y-6">
{canScan && (
<div className="flex items-center justify-between gap-3">
<p className="text-sm text-stat-subtitle">
{overview.scannedImages === 0
? 'No images scanned on this node yet.'
: `${overview.scannedImages} image${overview.scannedImages === 1 ? '' : 's'} scanned.`}
</p>
<ScanNodeLauncher canScan={canScan} onComplete={onScanComplete} />
</div>
)}
{/* Charts lead the dashboard. */}
<div className="grid gap-4 lg:grid-cols-3">
<ChartCard title="Risk trend · 30 days · critical + high" className="lg:col-span-2">