feat(security): scan comparison UI (#648)

Side-by-side vulnerability scan comparison with two entry points:

- Compare button plus inline baseline picker inside the scan drawer.
- New Scan History page reachable from the Resources Hub, grouping
  completed scans by image with a checkbox selection flow.

The comparison sheet shows a severity delta ribbon, Added/Removed/Unchanged
filters, and a paginated CVE table. Cross-image comparisons are allowed
but flagged with a warning. Compare access is gated to Skipper and
Admiral tiers; the underlying /security/compare endpoint is unchanged.
This commit is contained in:
Anso
2026-04-16 23:15:36 -04:00
committed by GitHub
parent e660d2a658
commit 8ee0c0c476
8 changed files with 806 additions and 6 deletions
+19 -1
View File
@@ -17,10 +17,11 @@ import { Switch } from "@/components/ui/switch";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { apiFetch } from '@/lib/api';
import { toast } from '@/components/ui/toast-store';
import { Trash2, HardDrive, Network, PackageMinus, MonitorX, MoreVertical, AlertTriangle, ShieldCheck, Plus, Eye, Copy, Container, Loader2 } from 'lucide-react';
import { Trash2, HardDrive, Network, PackageMinus, MonitorX, MoreVertical, AlertTriangle, ShieldCheck, Plus, Eye, Copy, Container, Loader2, History } from 'lucide-react';
import { CursorProvider, CursorContainer, Cursor, CursorFollow } from '@/components/animate-ui/primitives/animate/cursor';
import { useTrivyStatus } from '@/hooks/useTrivyStatus';
import { VulnerabilityScanSheet } from './VulnerabilityScanSheet';
import { SENCHO_NAVIGATE_EVENT, type SenchoNavigateDetail } from './NodeManager';
import type { ScanSummary, VulnSeverity } from '@/types/security';
import { useNodes } from '@/context/NodeContext';
import { useAuth } from '@/context/AuthContext';
@@ -705,6 +706,22 @@ export default function ResourcesView() {
{activeNode?.type === 'remote' && (
<span className="text-sm text-muted-foreground">- {activeNode.name}</span>
)}
{trivy.available && isPaid && (
<Button
variant="outline"
size="sm"
className="ml-auto border-border"
onClick={() => {
window.dispatchEvent(new CustomEvent<SenchoNavigateDetail>(SENCHO_NAVIGATE_EVENT, {
detail: { view: 'security-history' },
}));
}}
title="View completed vulnerability scans and compare them"
>
<History className="w-4 h-4 mr-2" strokeWidth={1.5} />
Scan history
</Button>
)}
</div>
{/* Top row: Footprint + Quick Clean */}
@@ -1473,6 +1490,7 @@ export default function ResourcesView() {
onClose={() => setInspectScanId(null)}
onRescan={(imageRef) => { setInspectScanId(null); handleScanImage(imageRef, true); }}
canGenerateSbom={isPaid}
canCompare={isPaid}
/>
</div>
);