feat(security): severity-aware scheduled scan notifications (#654)

Enrich scheduled vulnerability scan completion notifications with
per-severity CVE counts so recipients can triage from the message body
alone. Expose the scan action in the schedule creation UI, require an
explicit node_id, and harden fire-and-forget alert dispatches so a
failing webhook cannot crash the scheduler.

Notification body now reports scanned/skipped/failed counts plus
critical/high/medium totals aggregated across fresh and cached scans,
reflecting the current node posture rather than only what was newly
scanned on this run.
This commit is contained in:
Anso
2026-04-17 11:08:46 -04:00
committed by GitHub
parent f9dc86bbfd
commit 29ed0524c1
9 changed files with 280 additions and 27 deletions
@@ -22,6 +22,7 @@ const ACTION_OPTIONS = [
{ value: 'restart', label: 'Restart Stack', targetType: 'stack' as const },
{ value: 'snapshot', label: 'Fleet Snapshot', targetType: 'fleet' as const },
{ value: 'prune', label: 'System Prune', targetType: 'system' as const },
{ value: 'scan', label: 'Vulnerability Scan', targetType: 'system' as const },
];
interface ScheduledOperationsViewProps {
@@ -194,6 +195,9 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }:
body.target_id = formTargetId;
body.node_id = formNodeId ? parseInt(formNodeId, 10) : null;
}
if (formAction === 'scan') {
body.node_id = formNodeId ? parseInt(formNodeId, 10) : null;
}
if (formAction === 'prune' && formPruneTargets.length > 0) {
body.prune_targets = formPruneTargets;
}
@@ -480,6 +484,19 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }:
</>
)}
{formAction === 'scan' && (
<div className="space-y-2">
<Label>Node</Label>
<Combobox
options={nodes.map(n => ({ value: String(n.id), label: n.name }))}
value={formNodeId}
onValueChange={setFormNodeId}
placeholder="Select node..."
/>
<p className="text-xs text-muted-foreground">Every image on the selected node will be scanned.</p>
</div>
)}
{formAction === 'prune' && (
<>
<div className="space-y-2">
@@ -531,7 +548,7 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }:
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setDialogOpen(false)}>Cancel</Button>
<Button onClick={handleSave} disabled={saving || !formName || !formCron || (targetType === 'stack' && (!formTargetId || !formNodeId)) || (formAction === 'prune' && formPruneTargets.length === 0)}>
<Button onClick={handleSave} disabled={saving || !formName || !formCron || (targetType === 'stack' && (!formTargetId || !formNodeId)) || (formAction === 'scan' && !formNodeId) || (formAction === 'prune' && formPruneTargets.length === 0)}>
{saving ? 'Saving...' : editingTask ? 'Update' : 'Create'}
</Button>
</DialogFooter>