diff --git a/frontend/src/components/EditorLayout/ShellOverlays.tsx b/frontend/src/components/EditorLayout/ShellOverlays.tsx index 5de73521..eb5f654e 100644 --- a/frontend/src/components/EditorLayout/ShellOverlays.tsx +++ b/frontend/src/components/EditorLayout/ShellOverlays.tsx @@ -129,6 +129,7 @@ export function ShellOverlays({ setStackMisconfigScanId(null)} + canManageSuppressions={isAdmin} /> {/* Compose diff preview */} diff --git a/frontend/src/components/ResourcesView.tsx b/frontend/src/components/ResourcesView.tsx index 0ea89846..da2b11ee 100644 --- a/frontend/src/components/ResourcesView.tsx +++ b/frontend/src/components/ResourcesView.tsx @@ -1343,8 +1343,8 @@ export default function ResourcesView() { setInspectScanId(null)} - onRescan={(imageRef) => { setInspectScanId(null); handleScanImage(imageRef, { force: true }); }} - canGenerateSbom={isPaid} + onRescan={isAdmin ? (imageRef) => { setInspectScanId(null); handleScanImage(imageRef, { force: true }); } : undefined} + canGenerateSbom={isPaid && isAdmin} canCompare canManageSuppressions={isAdmin} /> diff --git a/frontend/src/components/SecurityHistoryView.tsx b/frontend/src/components/SecurityHistoryView.tsx index 9196f074..10feb134 100644 --- a/frontend/src/components/SecurityHistoryView.tsx +++ b/frontend/src/components/SecurityHistoryView.tsx @@ -314,7 +314,7 @@ export function SecurityHistoryView({ open, onClose }: SecurityHistoryViewProps) setInspectScanId(null)} - canGenerateSbom={isPaid} + canGenerateSbom={isPaid && isAdmin} canCompare={false} canManageSuppressions={isAdmin} /> diff --git a/frontend/src/components/VulnerabilityScanSheet.tsx b/frontend/src/components/VulnerabilityScanSheet.tsx index 620264eb..f47a59d6 100644 --- a/frontend/src/components/VulnerabilityScanSheet.tsx +++ b/frontend/src/components/VulnerabilityScanSheet.tsx @@ -110,8 +110,30 @@ export function VulnerabilityScanSheet({ onRescan, canGenerateSbom = false, canCompare = false, - canManageSuppressions = false, + canManageSuppressions: canManageSuppressionsProp = false, }: VulnerabilityScanSheetProps) { + const [isReplica, setIsReplica] = useState(false); + useEffect(() => { + // Reset on every probe so a stale `true` from a previous replica view + // does not survive switching to a control instance with the sheet kept + // mounted by its parent. Defense in depth: if the probe never resolves + // the UI stays permissive and the backend blockIfReplica guard runs. + setIsReplica(false); + if (!canManageSuppressionsProp || scanId == null) return; + let cancelled = false; + (async () => { + try { + const res = await apiFetch('/fleet/role', { localOnly: true }); + if (cancelled || !res.ok) return; + const data = await res.json(); + if (!cancelled) setIsReplica(data?.role === 'replica'); + } catch (err) { + console.warn('Failed to probe fleet role for replica gate:', err); + } + })(); + return () => { cancelled = true; }; + }, [canManageSuppressionsProp, scanId]); + const canManageSuppressions = canManageSuppressionsProp && !isReplica; const [scan, setScan] = useState(null); const [details, setDetails] = useState([]); const [totalDetails, setTotalDetails] = useState(0); diff --git a/frontend/src/components/settings/MisconfigAckPanel.tsx b/frontend/src/components/settings/MisconfigAckPanel.tsx index 8b48cfca..2e72602a 100644 --- a/frontend/src/components/settings/MisconfigAckPanel.tsx +++ b/frontend/src/components/settings/MisconfigAckPanel.tsx @@ -10,6 +10,7 @@ import { ChevronLeft, ChevronRight, Plus, ShieldCheck, Trash2 } from 'lucide-rea import { toast } from '@/components/ui/toast-store'; import { apiFetch } from '@/lib/api'; import type { MisconfigAcknowledgement } from '@/types/security'; +import { useAuth } from '@/context/AuthContext'; const RULE_RE = /^[A-Z0-9][A-Z0-9_-]{0,199}$/i; const PAGE_SIZE = 8; @@ -33,6 +34,7 @@ interface MisconfigAckPanelProps { } export function MisconfigAckPanel({ isReplica }: MisconfigAckPanelProps) { + const { isAdmin } = useAuth(); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); const [dialogOpen, setDialogOpen] = useState(false); @@ -179,7 +181,7 @@ export function MisconfigAckPanel({ isReplica }: MisconfigAckPanelProps) { )} - {!isReplica && ( + {isAdmin && !isReplica && ( )} - {trivy.source === 'managed' && ( + {isAdmin && trivy.source === 'managed' && ( )} - {!isReplica && ( + {isAdmin && !isReplica && (