import { useCallback, useEffect, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Modal, ModalHeader, ModalBody, ModalFooter, ConfirmModal } from '@/components/ui/modal'; import { ChevronLeft, ChevronRight, Plus, Trash2 } from 'lucide-react'; import { toast } from '@/components/ui/toast-store'; import { apiFetch } from '@/lib/api'; import { FleetTabHeading } from '@/components/fleet/FleetEmptyState'; import type { CveSuppression } from '@/types/security'; import { useAuth } from '@/context/AuthContext'; const CVE_ID_RE = /^(CVE-\d{4}-\d{4,}|GHSA-[\w-]{14,})$/; const PAGE_SIZE = 8; interface SuppressionFormState { cveId: string; pkgName: string; imagePattern: string; reason: string; expiresInDays: string; } const EMPTY_FORM: SuppressionFormState = { cveId: '', pkgName: '', imagePattern: '', reason: '', expiresInDays: '', }; interface SuppressionsPanelProps { isReplica: boolean; } export function SuppressionsPanel({ isReplica }: SuppressionsPanelProps) { const { isAdmin } = useAuth(); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [dialogOpen, setDialogOpen] = useState(false); const [form, setForm] = useState(EMPTY_FORM); const [saving, setSaving] = useState(false); const [deleteRow, setDeleteRow] = useState(null); const [page, setPage] = useState(0); const load = useCallback(async () => { try { const res = await apiFetch('/security/suppressions', { localOnly: true }); if (!res.ok) throw new Error('Failed to load suppressions'); const data = await res.json(); setRows(Array.isArray(data) ? data : []); } catch (err) { console.error('Failed to load suppressions:', err); toast.error('Failed to load suppressions'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE)); const safePage = Math.min(page, totalPages - 1); const pageItems = rows.slice(safePage * PAGE_SIZE, (safePage + 1) * PAGE_SIZE); const needsPagination = rows.length > PAGE_SIZE; const openCreate = () => { setForm(EMPTY_FORM); setDialogOpen(true); }; const handleSave = async () => { const cveId = form.cveId.trim(); if (!CVE_ID_RE.test(cveId)) { toast.error('CVE must look like CVE-YYYY-NNNN or GHSA-xxxx-xxxx-xxxx.'); return; } const reason = form.reason.trim(); if (!reason) { toast.error('A reason is required.'); return; } let expiresAt: number | null = null; const days = form.expiresInDays.trim(); if (days) { const n = Number(days); if (!Number.isFinite(n) || n <= 0) { toast.error('Expiry must be a positive number of days or blank.'); return; } expiresAt = Date.now() + n * 24 * 60 * 60 * 1000; } setSaving(true); try { const res = await apiFetch('/security/suppressions', { method: 'POST', localOnly: true, body: JSON.stringify({ cve_id: cveId, pkg_name: form.pkgName.trim() || null, image_pattern: form.imagePattern.trim() || null, reason, expires_at: expiresAt, }), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body?.error || 'Failed to create suppression'); } toast.success('Suppression created'); setDialogOpen(false); await load(); } catch (err) { toast.error((err as Error)?.message || 'Failed to create suppression'); } finally { setSaving(false); } }; const handleDelete = async () => { if (!deleteRow) return; try { const res = await apiFetch(`/security/suppressions/${deleteRow.id}`, { method: 'DELETE', localOnly: true, }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body?.error || 'Failed to delete suppression'); } toast.success('Suppression removed'); await load(); } catch (err) { toast.error((err as Error)?.message || 'Failed to delete suppression'); } finally { setDeleteRow(null); } }; const formatExpiry = (row: CveSuppression): string => { if (row.expires_at === null) return 'Never'; const d = new Date(row.expires_at); return d.toLocaleDateString(); }; return (
Add suppression ) : undefined } />
{needsPagination && !loading && rows.length > 0 && (
{safePage + 1} / {totalPages}
)} {loading && (
)} {!loading && rows.length === 0 && (
No suppressions yet. Accept a CVE from any scan result to silence it fleet-wide.
)} {!loading && rows.length > 0 && (
    {pageItems.map((row) => (
  • {row.cve_id} {row.pkg_name && ( {row.pkg_name} )} {row.image_pattern && ( {row.image_pattern} )} {!row.active && ( expired )} {row.replicated_from_control === 1 && ( replicated )}
    {row.reason}
    by {row.created_by} - expires {formatExpiry(row)}
    {isAdmin && !isReplica && row.replicated_from_control === 0 && ( )}
  • ))}
)}
setForm({ ...form, cveId: e.target.value })} />
setForm({ ...form, pkgName: e.target.value })} />
setForm({ ...form, imagePattern: e.target.value })} />