Files
sencho/frontend/src/components/stack/PreDeployScanDialog.tsx
T
Anso 7ce045accb feat: pre-deploy scan visibility and pinned scanner version (#1378)
* feat: pre-deploy scan visibility and pinned scanner version

Pin managed Trivy installs and add an opt-in pre-deploy scan advisory so a
manual deploy can surface each image's latest scan before it runs.

- Managed Trivy now installs a pinned, known-good version by default for
  reproducible installs. Auto-update still tracks the latest release, and an
  explicit update always pulls the latest.
- Add an opt-in pre-deploy scan advisory: when enabled, deploying a stack from
  the editor first shows each image's latest cached scan severity for review.
  It is visibility only and never blocks; deploy enforcement is unchanged.
- Backend: pre_deploy_scan_advisory setting, PUT
  /security/pre-deploy-scan-advisory, a cache-only GET
  /security/stacks/:name/pre-deploy-summary, and a node-scoped
  getLatestVulnScanByDigestForNode lookup.
- Frontend: advisory toggle on the Security page scanner setup, and a
  PreDeployScanDialog wired into the editor deploy flow that fails open when the
  summary is unavailable.
- Docs: scanner configuration, version pinning, and the advisory.

* fix: harden pre-deploy advisory guard, toggle visibility, and installer busy state

Addresses review findings on the pre-deploy advisory.

- Block a second editor deploy during the async advisory window with a
  synchronous pending ref, cleared on cancel and in the deploy's finally, so a
  double-click can no longer start two deploys.
- Keep the pre-deploy advisory toggle visible to admins whenever the setting is
  on, so it can still be turned off after the scanner becomes unavailable.
- Resolve the managed Trivy version inside the install lock so the busy state and
  serialization cover the latest-version fetch and the managed-install check.
2026-06-16 00:42:33 -04:00

79 lines
2.9 KiB
TypeScript

import {
Modal,
ModalHeader,
ModalBody,
ModalFooter,
} from '@/components/ui/modal';
import { Button } from '@/components/ui/button';
import { SeverityChip } from '@/components/VulnerabilityScanSheet';
import { formatTimeAgo } from '@/lib/relativeTime';
import type { PreDeployScanImage } from '@/types/security';
interface PreDeployScanDialogProps {
open: boolean;
stackName: string;
images: PreDeployScanImage[];
onCancel: () => void;
onDeploy: () => void;
}
/**
* Advisory pre-deploy review. Shows the latest cached scan for each image in a
* manual deploy so the operator can review the security posture before
* proceeding. Unlike PolicyBlockDialog this never blocks: anyone can deploy or
* cancel, and there is no override gate (blocking is the paid deploy-block
* policy). Opened opt-in via the pre-deploy scan advisory setting.
*/
export function PreDeployScanDialog({ open, stackName, images, onCancel, onDeploy }: PreDeployScanDialogProps) {
return (
<Modal open={open} onOpenChange={(next) => { if (!next) onCancel(); }} size="xl">
<ModalHeader
kicker={`${stackName.toUpperCase()} · PRE-DEPLOY · SCAN REVIEW`}
title="Review scan results before deploying"
description="The latest vulnerability scan for each image in this deploy. Advisory only; it does not block the deploy."
/>
<ModalBody>
<div className="border border-glass-border bg-card/60 shadow-card-bevel divide-y divide-glass-border">
{images.length === 0 ? (
<div className="px-4 py-3 text-sm text-muted-foreground">No images found for this stack.</div>
) : (
images.map((img) => (
<div key={img.imageRef} className="px-4 py-3 flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="font-mono text-sm truncate">{img.imageRef}</div>
<div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle tabular-nums">
{img.scan
? `${img.scan.criticalCount} critical · ${img.scan.highCount} high · ${img.scan.mediumCount} medium · ${img.scan.lowCount} low · scanned ${formatTimeAgo(img.scan.scannedAt)}`
: 'not scanned'}
</div>
</div>
{img.scan?.highestSeverity ? (
<SeverityChip severity={img.scan.highestSeverity} />
) : null}
</div>
))
)}
</div>
</ModalBody>
<ModalFooter
secondary={
<Button variant="outline" size="sm" onClick={onCancel}>
Cancel
</Button>
}
primary={
<Button
size="sm"
onClick={(e) => {
e.preventDefault();
onDeploy();
}}
>
Deploy
</Button>
}
/>
</Modal>
);
}