refactor: drop the advisory policy-packs section and the findings cursor tooltip (#1369)

* refactor: drop the advisory policy-packs section and the findings cursor tooltip

Two Security-page cleanups from review.

- Remove the advisory policy-packs catalog from the Policies tab. It was
  information-only and disconnected from the scan_policies enforcement engine, so
  it read as duplicated. The tab now hosts only the enforcement manager, which is
  paid, so the Policies tab is hidden for Community (with a deep-link guard) and
  the Overview's enforcement hint is gated to match. The backend policy-packs
  catalog and route are kept as a dormant foundation. Delete the orphaned
  PolicyPacksTab component, its test, and the unused frontend pack types.
- Drop the cursor-follow tooltip from the findings severity badge (Secrets and
  Compose risks), matching the Images table.
- Clarify that Compose risks is a Trivy security-misconfig audit, distinct from
  Compose Doctor's deploy-readiness preflight, in the tab copy and the docs.

* chore: re-run CI
This commit is contained in:
Anso
2026-06-12 23:24:32 -04:00
committed by GitHub
parent 4610a433e6
commit 3c116466d9
9 changed files with 43 additions and 274 deletions
@@ -35,8 +35,8 @@ const COPY: Record<FindingsKind, {
detailTab: 'misconfigs',
countField: 'misconfig_count',
emptyTitle: 'No Compose risks found',
emptyBody: 'Scan a stack from Resources to surface misconfigurations like privileged containers, host mounts, or missing healthchecks.',
intro: 'Compose risks are misconfigurations in your stack definitions, such as privileged containers, Docker socket mounts, host networking, broad bind mounts, or missing healthchecks. Open a result for the specific findings and how to fix them; the Policy packs tab explains each category.',
emptyBody: 'Run a node scan or a per-stack config scan to surface security misconfigurations like privileged containers, Docker socket mounts, or host networking.',
intro: 'Compose risks are the security misconfigurations Trivy finds in your stack definitions: privileged containers, Docker socket mounts, host networking, or broad capabilities. This is a security audit of the compose file. For deploy-readiness checks like port conflicts, missing bind paths, unset variables, or no healthcheck, run Compose Doctor from the stack page instead. Open a result for the specific findings and how to fix them.',
},
};
@@ -109,7 +109,7 @@ export function FindingsTab({ kind, summaries, loading, error, onInspect }: Find
</td>
<td className="px-4 py-2.5 text-right font-mono tabular-nums text-xs text-stat-value">{count}</td>
<td className="px-4 py-2.5 text-right max-md:hidden">
<SeverityBadge summary={s} onClick={() => onInspect(s.scan_id, copy.detailTab)} />
<SeverityBadge summary={s} tooltip={false} onClick={() => onInspect(s.scan_id, copy.detailTab)} />
</td>
</tr>
);
@@ -25,6 +25,8 @@ interface OverviewTabProps {
canScan: boolean;
/** Refresh the overview after a node-wide scan completes. */
onScanComplete: () => void;
/** Paid licensees can manage enforcement policies (the Policies tab is hidden otherwise). */
isPaid: boolean;
}
const STATUS_ROW_TONE: Record<'value' | 'warn' | 'subtitle', string> = {
@@ -52,7 +54,7 @@ function ChartCard({ title, className, children }: { title: string; className?:
);
}
export function OverviewTab({ overview, loadError, summaries, trend, onNavigate, onInspect, canScan, onScanComplete }: OverviewTabProps) {
export function OverviewTab({ overview, loadError, summaries, trend, onNavigate, onInspect, canScan, onScanComplete, isPaid }: OverviewTabProps) {
if (loadError === 'unsupported') {
return (
<div className="flex flex-col items-center justify-center py-20 text-center">
@@ -176,7 +178,7 @@ export function OverviewTab({ overview, loadError, summaries, trend, onNavigate,
tone="subtitle"
/>
<p className="mt-2 text-xs text-muted-foreground">
Manage enforcement policies on the Policies tab. This is a read-only posture for the active node.
{isPaid ? 'Manage enforcement policies on the Policies tab. ' : ''}This is a read-only posture for the active node.
</p>
</div>
</div>
@@ -1,145 +0,0 @@
import { useEffect, useState } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { Skeleton } from '@/components/ui/skeleton';
import { cn } from '@/lib/utils';
import { apiFetch } from '@/lib/api';
import type { PolicyPack, PolicyPackRule } from '@/types/security';
const SEVERITY_TEXT: Record<PolicyPackRule['severity'], string> = {
CRITICAL: 'text-destructive',
HIGH: 'text-warning',
MEDIUM: 'text-warning',
LOW: 'text-muted-foreground',
};
function EnforcementBadge({ enforcement }: { enforcement: PolicyPackRule['enforcement'] }) {
const enforceable = enforcement === 'enforceable';
return (
<span
className={cn(
'inline-flex items-center rounded border px-1.5 py-0.5 font-mono text-[10px] uppercase tracking-[0.18em]',
enforceable
? 'border-brand/30 bg-brand/10 text-brand'
: 'border-card-border bg-muted/30 text-stat-subtitle',
)}
>
{enforceable ? 'Enforceable' : 'Warning'}
</span>
);
}
export function PolicyPacksTab() {
const [packs, setPacks] = useState<PolicyPack[] | null>(null);
const [error, setError] = useState(false);
const [expanded, setExpanded] = useState<Set<string>>(new Set());
const toggle = (id: string) =>
setExpanded((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
useEffect(() => {
let cancelled = false;
(async () => {
try {
// The catalog is global/static, so target the local control regardless
// of which node is active.
const res = await apiFetch('/security/policy-packs', { localOnly: true });
if (!res.ok) throw new Error('Failed to load policy packs');
const data = (await res.json()) as PolicyPack[];
if (!cancelled) setPacks(Array.isArray(data) ? data : []);
} catch (err) {
// The catalog is a static, always-available route, so a failure here is a
// real bug (routing/proxy/auth) worth a breadcrumb, not a silent empty state.
console.error('[Security] Failed to load policy packs:', err);
if (!cancelled) setError(true);
}
})();
return () => { cancelled = true; };
}, []);
if (error) {
return (
<p className="text-sm text-muted-foreground py-16 text-center">
Policy packs could not be loaded.
</p>
);
}
if (!packs) {
return (
<div className="space-y-3" aria-busy="true">
<Skeleton className="h-16 w-full rounded-lg" />
<Skeleton className="h-16 w-full rounded-lg" />
</div>
);
}
return (
<div className="space-y-5">
<p className="text-sm text-muted-foreground max-w-2xl">
Policy packs are curated security expectations for a deployment posture. Packs are advisory in
Community: they explain what good looks like. Block-on-deploy enforcement is an Admiral capability.
</p>
<div className="space-y-3">
{packs.map((pack) => {
const isOpen = expanded.has(pack.id);
return (
<div key={pack.id} className="rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel overflow-hidden">
<button
type="button"
onClick={() => toggle(pack.id)}
aria-expanded={isOpen}
className="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-glass-highlight transition-colors"
>
{isOpen
? <ChevronDown className="w-4 h-4 text-stat-subtitle shrink-0" strokeWidth={1.5} />
: <ChevronRight className="w-4 h-4 text-stat-subtitle shrink-0" strokeWidth={1.5} />}
<div className="min-w-0 flex-1">
<h3 className="font-display italic text-[18px] leading-6 text-stat-value">{pack.name}</h3>
<p className="text-sm text-muted-foreground">{pack.tagline}</p>
</div>
<span className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle shrink-0 tabular-nums">
{pack.rules.length} rule{pack.rules.length === 1 ? '' : 's'}
</span>
</button>
{isOpen && (
<div className="border-t border-card-border">
<p className="px-4 py-2 text-xs text-stat-subtitle">{pack.tierCopy}</p>
<ul className="divide-y divide-card-border/40 border-t border-card-border/40">
{pack.rules.map((rule) => (
<li key={rule.id} className="px-4 py-3">
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-center gap-2 min-w-0">
<span className="font-medium text-sm">{rule.name}</span>
<span className={cn('font-mono text-[10px] uppercase tracking-[0.18em]', SEVERITY_TEXT[rule.severity])}>
{rule.severity}
</span>
</div>
<EnforcementBadge enforcement={rule.enforcement} />
</div>
<dl className="mt-2 grid gap-1.5 text-xs sm:grid-cols-[7rem_1fr]">
<dt className="font-mono uppercase tracking-[0.18em] text-stat-subtitle">Checks</dt>
<dd className="text-stat-subtitle">{rule.whatItChecks}</dd>
<dt className="font-mono uppercase tracking-[0.18em] text-stat-subtitle">Why</dt>
<dd className="text-stat-subtitle">{rule.why}</dd>
<dt className="font-mono uppercase tracking-[0.18em] text-stat-subtitle">Fix</dt>
<dd className="text-stat-subtitle">{rule.howToFix}</dd>
</dl>
</li>
))}
</ul>
</div>
)}
</div>
);
})}
</div>
</div>
);
}
@@ -243,8 +243,9 @@ export function ScanPolicyManager() {
}
};
// Enforcement management is a paid governance surface; Community sees only the
// policy-pack catalog above it.
// Enforcement management is a paid governance surface; the Policies tab is
// hidden for Community entirely (gated in SecurityView), so this is a
// defensive guard.
if (!isPaid) return null;
return (
@@ -1,71 +0,0 @@
/**
* PolicyPacksTab renders the static catalog and, crucially, fetches it with
* { localOnly: true } so the global catalog is available regardless of which
* node is active.
*/
import { it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
vi.mock('@/lib/api', () => ({ apiFetch: vi.fn() }));
import { apiFetch } from '@/lib/api';
import { PolicyPacksTab } from '../PolicyPacksTab';
import type { PolicyPack } from '@/types/security';
const mockedFetch = apiFetch as unknown as ReturnType<typeof vi.fn>;
function jsonResponse(status: number, body: unknown): Response {
return { ok: status >= 200 && status < 300, status, json: async () => body } as unknown as Response;
}
const PACKS: PolicyPack[] = [
{
id: 'homelab-baseline',
name: 'Homelab baseline',
tagline: 'Gentle defaults.',
tierCopy: 'Advisory.',
rules: [
{ id: 'pin-image-tag', name: 'Pin image tags', severity: 'LOW', whatItChecks: 'tags', why: 'reproducible', howToFix: 'pin', enforcement: 'warning' },
],
},
{
id: 'strict-production',
name: 'Strict production',
tagline: 'Zero tolerance.',
tierCopy: 'Strict.',
rules: [
{ id: 'no-privileged', name: 'No privileged containers', severity: 'CRITICAL', whatItChecks: 'priv', why: 'escape', howToFix: 'drop', enforcement: 'enforceable' },
],
},
];
beforeEach(() => {
vi.clearAllMocks();
mockedFetch.mockResolvedValue(jsonResponse(200, PACKS));
});
it('fetches the catalog with localOnly and reveals rules when a pack is expanded', async () => {
const user = userEvent.setup();
render(<PolicyPacksTab />);
await waitFor(() => expect(screen.getByText('Homelab baseline')).toBeInTheDocument());
expect(screen.getByText('Strict production')).toBeInTheDocument();
expect(mockedFetch).toHaveBeenCalledWith('/security/policy-packs', { localOnly: true });
// Rules are collapsed behind the accordion until the pack header is clicked.
expect(screen.queryByText('Pin image tags')).not.toBeInTheDocument();
await user.click(screen.getByText('Homelab baseline'));
await user.click(screen.getByText('Strict production'));
expect(screen.getByText('Pin image tags')).toBeInTheDocument();
expect(screen.getByText('No privileged containers')).toBeInTheDocument();
});
it('labels expanded rules as warning or enforceable', async () => {
const user = userEvent.setup();
render(<PolicyPacksTab />);
await waitFor(() => expect(screen.getByText('Homelab baseline')).toBeInTheDocument());
await user.click(screen.getByText('Homelab baseline'));
await user.click(screen.getByText('Strict production'));
expect(screen.getByText('Warning')).toBeInTheDocument();
expect(screen.getByText('Enforceable')).toBeInTheDocument();
});