mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-17 14:08:19 +00:00
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:
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user