feat(security): polish scan sheets, fix CVE links, surface policy violations (#721)

* feat(security): polish scan sheets, fix CVE links, surface policy violations

Adds cveUrl helper that rewrites Trivy's 404-ing avd.aquasec.com links to
cve.org for CVE-prefixed IDs (GHSA and misconfig URLs pass through unchanged).
Redesigns both scan sheets with shadow-card-bevel chips, tracked-mono kickers,
severity row tinting with a left accent rail, and tabular-nums timestamps.
Surfaces a destructive policy-violation banner on scans whose policy_evaluation
row flags a block, and fixes the compare sheet's delta ribbon so CRITICAL
net-positive deltas render in destructive (not warning) tone. Backend parses
the JSON policy_evaluation column at the API boundary so the UI receives a
structured object.

* chore(security): suppress CVE-2026-32281 and CVE-2026-32283 in Trivy scan

Both CVEs affect Go stdlib crypto/x509 and TLS in Docker CLI 29.4.0
(Go 1.26.1) and Compose v5.1.2 (Go 1.25.8). No upstream static binary
has been released with the patched Go 1.26.2 or 1.25.9 runtimes yet.

Exposure analysis: the Docker CLI and compose plugin connect to the local
Docker socket (Unix socket, no TLS) and to public registries with well-known
CAs. Neither CVE is exploitable in this configuration. Added alongside
sibling entries already in .trivyignore for the same binary versions.

Revisit on next Docker CLI and Compose upstream release.
This commit is contained in:
Anso
2026-04-21 08:51:35 -04:00
committed by GitHub
parent e4fdb1cd6c
commit 12c2b37510
12 changed files with 295 additions and 83 deletions
@@ -46,6 +46,8 @@ import { ScanComparisonSheet } from './ScanComparisonSheet';
import { apiFetch } from '@/lib/api';
import { toast } from '@/components/ui/toast-store';
import { cn } from '@/lib/utils';
import { cveUrl } from '@/lib/cveUrl';
import { SEVERITY_ROW_TINT } from '@/lib/severityStyles';
import type {
VulnerabilityScan,
VulnerabilityDetail,
@@ -88,7 +90,7 @@ function SeverityChip({ severity }: { severity: VulnSeverity }) {
return (
<span
className={cn(
'inline-flex items-center gap-1 rounded border px-1.5 py-0.5 text-[10px] font-mono tabular-nums uppercase tracking-wide',
'inline-flex items-center gap-1 rounded border px-1.5 py-0.5 text-[10px] font-mono tabular-nums uppercase tracking-[0.18em] shadow-card-bevel',
SEVERITY_CLASSES[severity],
)}
>
@@ -372,10 +374,13 @@ export function VulnerabilityScanSheet({
return (
<Sheet open={scanId != null} onOpenChange={(open) => !open && onClose()}>
<SheetContent className="sm:max-w-2xl flex flex-col p-0">
<SheetHeader className="p-6 pb-4 border-b">
<SheetTitle className="flex items-center gap-2 pr-6">
<ShieldCheck className="w-4 h-4 text-muted-foreground" strokeWidth={1.5} />
<span className="font-mono text-sm truncate">
<SheetHeader className="px-6 pt-6 pb-4 pr-14 border-b border-border space-y-2">
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">
Vulnerability scan · {scan?.triggered_by ?? '-'}
</div>
<SheetTitle className="flex items-center gap-2 font-display italic text-2xl">
<ShieldCheck className="w-5 h-5 text-muted-foreground" strokeWidth={1.5} />
<span className="font-mono text-base truncate not-italic">
{scan?.image_ref ?? 'Loading...'}
</span>
</SheetTitle>
@@ -396,29 +401,55 @@ export function VulnerabilityScanSheet({
<div className="flex flex-col flex-1 min-h-0">
{/* Summary stats */}
<div className="px-6 py-4 border-b space-y-3">
{scan.policy_evaluation?.violated && (
<div
role="alert"
className="relative rounded border border-destructive/40 bg-destructive/10 px-3 py-2 pl-4 shadow-card-bevel"
>
<span
aria-hidden="true"
className="absolute left-0 top-0 bottom-0 w-[3px] bg-destructive/80 rounded-l"
/>
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-destructive">
Policy violation
</div>
<div className="text-sm text-stat-value mt-0.5">
<span className="font-mono">{scan.policy_evaluation.policyName}</span> blocks
severities at or above{' '}
<span className="font-mono tabular-nums">
{scan.policy_evaluation.maxSeverity}
</span>
. This scan's highest severity is{' '}
<span className="font-mono tabular-nums">
{scan.highest_severity ?? 'UNKNOWN'}
</span>
.
</div>
</div>
)}
<div className="flex flex-wrap gap-2">
{scan.critical_count > 0 && (
<span className={cn('rounded border px-2 py-1 text-xs font-mono tabular-nums', SEVERITY_CLASSES.CRITICAL)}>
<span className={cn('rounded border px-2 py-1 text-xs font-mono tabular-nums uppercase tracking-[0.18em] shadow-card-bevel', SEVERITY_CLASSES.CRITICAL)}>
{scan.critical_count} CRITICAL
</span>
)}
{scan.high_count > 0 && (
<span className={cn('rounded border px-2 py-1 text-xs font-mono tabular-nums', SEVERITY_CLASSES.HIGH)}>
<span className={cn('rounded border px-2 py-1 text-xs font-mono tabular-nums uppercase tracking-[0.18em] shadow-card-bevel', SEVERITY_CLASSES.HIGH)}>
{scan.high_count} HIGH
</span>
)}
{scan.medium_count > 0 && (
<span className={cn('rounded border px-2 py-1 text-xs font-mono tabular-nums', SEVERITY_CLASSES.MEDIUM)}>
<span className={cn('rounded border px-2 py-1 text-xs font-mono tabular-nums uppercase tracking-[0.18em] shadow-card-bevel', SEVERITY_CLASSES.MEDIUM)}>
{scan.medium_count} MEDIUM
</span>
)}
{scan.low_count > 0 && (
<span className={cn('rounded border px-2 py-1 text-xs font-mono tabular-nums', SEVERITY_CLASSES.LOW)}>
<span className={cn('rounded border px-2 py-1 text-xs font-mono tabular-nums uppercase tracking-[0.18em] shadow-card-bevel', SEVERITY_CLASSES.LOW)}>
{scan.low_count} LOW
</span>
)}
{scan.total_vulnerabilities === 0 && (
<span className="rounded border border-success/40 bg-success/10 text-success px-2 py-1 text-xs font-mono tabular-nums uppercase">
<span className="rounded border border-success/40 bg-success/10 text-success px-2 py-1 text-xs font-mono tabular-nums uppercase tracking-[0.18em] shadow-card-bevel">
No vulnerabilities
</span>
)}
@@ -426,20 +457,20 @@ export function VulnerabilityScanSheet({
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
<div>
<div className="text-stat-subtitle uppercase tracking-wide">Total</div>
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">Total</div>
<div className="font-mono tabular-nums text-stat-value">{scan.total_vulnerabilities}</div>
</div>
<div>
<div className="text-stat-subtitle uppercase tracking-wide">Fixable</div>
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">Fixable</div>
<div className="font-mono tabular-nums text-success">{scan.fixable_count}</div>
</div>
<div>
<div className="text-stat-subtitle uppercase tracking-wide">Triggered</div>
<div className="font-mono text-stat-value">{scan.triggered_by}</div>
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">Triggered</div>
<div className="font-mono tabular-nums text-stat-value">{scan.triggered_by}</div>
</div>
<div>
<div className="text-stat-subtitle uppercase tracking-wide">Scanned</div>
<div className="font-mono text-stat-value">
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">Scanned</div>
<div className="font-mono tabular-nums text-stat-value">
{new Date(scan.scanned_at).toLocaleString()}
</div>
</div>
@@ -521,7 +552,7 @@ export function VulnerabilityScanSheet({
</div>
) : (
<>
<div className="text-xs text-stat-subtitle uppercase tracking-wide">
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">
Compare against
</div>
<Combobox
@@ -635,18 +666,23 @@ export function VulnerabilityScanSheet({
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[180px]">CVE</TableHead>
<TableHead>Package</TableHead>
<TableHead className="w-[100px]">Severity</TableHead>
<TableHead className="w-[110px]">Installed</TableHead>
<TableHead className="w-[110px]">Fixed</TableHead>
<TableHead className="w-[180px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">CVE</TableHead>
<TableHead className="text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Package</TableHead>
<TableHead className="w-[100px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Severity</TableHead>
<TableHead className="w-[110px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Installed</TableHead>
<TableHead className="w-[110px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Fixed</TableHead>
{canManageSuppressions && <TableHead className="w-[40px]" />}
</TableRow>
</TableHeader>
<TableBody>
{pageItems.map((d) => (
<TableRow key={d.id} className={d.suppressed ? 'opacity-60' : undefined}>
<TableCell className="font-mono text-xs">
{pageItems.map((d) => {
const href = cveUrl(d.vulnerability_id, d.primary_url);
return (
<TableRow
key={d.id}
className={cn(SEVERITY_ROW_TINT[d.severity], d.suppressed && 'opacity-60')}
>
<TableCell className="font-mono text-xs tabular-nums">
<span className="inline-flex items-center gap-1.5">
{d.suppressed && (
<ShieldOff
@@ -655,9 +691,9 @@ export function VulnerabilityScanSheet({
aria-label="Suppressed"
/>
)}
{d.primary_url ? (
{href ? (
<a
href={d.primary_url}
href={href}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center gap-1 hover:underline"
@@ -706,7 +742,8 @@ export function VulnerabilityScanSheet({
</TableCell>
)}
</TableRow>
))}
);
})}
</TableBody>
</Table>
)}
@@ -757,15 +794,15 @@ export function VulnerabilityScanSheet({
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Severity</TableHead>
<TableHead className="w-[160px]">Rule</TableHead>
<TableHead>Title</TableHead>
<TableHead className="w-[260px]">Target</TableHead>
<TableHead className="w-[100px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Severity</TableHead>
<TableHead className="w-[160px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Rule</TableHead>
<TableHead className="text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Title</TableHead>
<TableHead className="w-[260px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Target</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{secretsPageItems.map((s) => (
<TableRow key={s.id}>
<TableRow key={s.id} className={SEVERITY_ROW_TINT[s.severity]}>
<TableCell>
<SeverityChip severity={s.severity} />
</TableCell>
@@ -850,16 +887,16 @@ export function VulnerabilityScanSheet({
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Severity</TableHead>
<TableHead className="w-[140px]">Check</TableHead>
<TableHead>Title</TableHead>
<TableHead className="w-[200px]">Target</TableHead>
<TableHead className="w-[220px]">Fix</TableHead>
<TableHead className="w-[100px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Severity</TableHead>
<TableHead className="w-[140px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Check</TableHead>
<TableHead className="text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Title</TableHead>
<TableHead className="w-[200px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Target</TableHead>
<TableHead className="w-[220px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Fix</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{misconfigsPageItems.map((m) => (
<TableRow key={m.id}>
<TableRow key={m.id} className={SEVERITY_ROW_TINT[m.severity]}>
<TableCell>
<SeverityChip severity={m.severity} />
</TableCell>