diff --git a/docs/features/security.mdx b/docs/features/security.mdx index 353acc6e..9cb70d69 100644 --- a/docs/features/security.mdx +++ b/docs/features/security.mdx @@ -1,6 +1,6 @@ --- title: "Security" -description: "The command center for your fleet's security posture: an overview dashboard, image and Compose findings, secrets, scan history, suppressions, policy packs, and scanner setup, all in one place." +description: "The command center for your fleet's security posture: an overview dashboard, image and Compose findings, secrets, scan history, suppressions, and scanner setup, all in one place." --- Security is a primary surface in Sencho. The **Security** page in the top navigation brings the @@ -30,32 +30,23 @@ SBOM. ## Compose risks -Compose risks surface misconfigurations in your stack definitions rather than image CVEs: privileged -containers, Docker socket mounts, host networking, broad host bind mounts, missing healthchecks, public -database ports, containers running as root, unpinned image tags, and missing restart policies. Each -entry opens its scan report with the specific findings and how to fix them. The [Policy packs](#policy-packs) -tab explains each category in depth. +Compose risks surface the security misconfigurations Trivy finds in your stack definitions rather than +image CVEs: privileged containers, Docker socket mounts, host networking, broad host bind mounts, public +database ports, and containers running as root. Each entry opens its scan report with the specific +findings and how to fix them. This is a security audit of the Compose file; for deploy-readiness checks +(port conflicts, missing bind paths, unset variables, no healthcheck), use +[Compose Doctor](/features/compose-doctor) from the stack page instead. ## Secrets The secrets tab lists images where Trivy detected exposed credentials or keys, and opens straight to the secret findings for a scan. -## Policy packs +## Policies -Policy packs are curated bundles of security expectations for a deployment posture. Sencho ships five -defaults: - -- **Homelab baseline**: gentle defaults for a single-operator homelab. -- **Production hardening**: sensible hardening for services that face real traffic. -- **Strict production**: a zero-tolerance posture for critical workloads. -- **Public edge service**: focused on services exposed to the public internet. -- **Internal service**: least-privilege defaults for east-west internal services. - -Each pack lists its rules with the severity, what the rule checks, why it matters, and how to fix it. -Every rule is marked **warning** or **enforceable**. In Community, packs are advisory: they explain -what good looks like. Block-on-deploy enforcement is an Admiral capability, configured under -[scan policies](/features/vulnerability-scanning#scan-policies). +The Policies tab manages deploy-enforcement scan policies: severity thresholds that block or warn on a +deploy, scoped by stack pattern. Enforcement is an Admiral capability; see +[scan policies](/features/vulnerability-scanning#scan-policies) for the full configuration. ## Suppressions diff --git a/docs/features/vulnerability-scanning.mdx b/docs/features/vulnerability-scanning.mdx index ce5ef874..94dd8a4f 100644 --- a/docs/features/vulnerability-scanning.mdx +++ b/docs/features/vulnerability-scanning.mdx @@ -3,7 +3,7 @@ title: "Vulnerability Scanning" description: "Scan container images and stack compose files for CVEs, secrets, and misconfigurations. Surface severity badges in the Resources Hub, compare scans over time, and gate deploys on policy violations." --- -Sencho integrates with [Trivy](https://trivy.dev) to scan container images and Compose files for vulnerabilities (CVEs), hardcoded secrets, and misconfigurations. Findings surface as severity badges in the Resources Hub and as drillable reports in the scan drawer. The dedicated [Security page](/features/security) is the command center for risk review: overview, image findings, Compose risks, secrets, scan history, suppressions, policy packs, and scanner setup all live there. Manual scanning, secret and misconfig detection, scan history, comparison, scheduled fleet scans, CVE suppressions, single-scan SBOM export, and managed Trivy auto-update are available on every tier. Admiral adds policy enforcement and SARIF export. +Sencho integrates with [Trivy](https://trivy.dev) to scan container images and Compose files for vulnerabilities (CVEs), hardcoded secrets, and misconfigurations. Findings surface as severity badges in the Resources Hub and as drillable reports in the scan drawer. The dedicated [Security page](/features/security) is the command center for risk review: overview, image findings, Compose risks, secrets, scan history, suppressions, and scanner setup all live there. Manual scanning, secret and misconfig detection, scan history, comparison, scheduled fleet scans, CVE suppressions, single-scan SBOM export, and managed Trivy auto-update are available on every tier. Admiral adds policy enforcement and SARIF export. Resources Hub Images table with severity badges (CRITICAL, HIGH, MEDIUM) on managed image rows alongside the Scan history button @@ -153,7 +153,7 @@ Policies define severity thresholds that govern whether a stack can deploy. A po See [Deploy Enforcement](/features/deploy-enforcement) for the full pre-flight flow, admin bypass path, and audit-log behavior. - The Security page Policies tab showing the policy-pack catalog, the Deploy enforcement policies section with an Add policy button, and the No scan policies configured empty state + The Security page Policies tab showing the Deploy enforcement policies section with an Add policy button and the No scan policies configured empty state ### Creating a policy @@ -246,6 +246,10 @@ Full scans take longer than vulnerability-only scans because Trivy reads every f Beyond package CVEs, Sencho can run `trivy config` against a stack's Compose file to flag insecure defaults before you deploy. Typical checks cover privileged containers, missing resource limits, host networking, mounted Docker sockets, and overly broad capabilities. + + This is a **security** audit of the Compose file, using Trivy's hardening rules. It is distinct from [Compose Doctor](/features/compose-doctor), which runs **deploy-readiness** checks (port conflicts, missing bind paths, unset variables, no healthcheck) before a deploy. Use Compose Doctor to confirm a stack will deploy cleanly; use Compose risk scanning to audit its security posture. + + From any stack page, open the **More actions** overflow menu next to the Update button and select **Scan config**. Sencho runs the scanner against the stack's working directory and opens the scan drawer on the **Misconfigs** tab. diff --git a/frontend/src/components/SecurityView.tsx b/frontend/src/components/SecurityView.tsx index 39b37a7e..ab58d66f 100644 --- a/frontend/src/components/SecurityView.tsx +++ b/frontend/src/components/SecurityView.tsx @@ -21,7 +21,6 @@ import { MisconfigAckPanel } from './settings/MisconfigAckPanel'; import { OverviewTab } from './security/OverviewTab'; import { ImagesTab } from './security/ImagesTab'; import { FindingsTab } from './security/FindingsTab'; -import { PolicyPacksTab } from './security/PolicyPacksTab'; import { ScanPolicyManager } from './security/ScanPolicyManager'; import { ScannerSetupTab } from './security/ScannerSetupTab'; import { HistoryTab } from './security/HistoryTab'; @@ -163,6 +162,12 @@ export function SecurityView({ activeTab, onTabChange }: SecurityViewProps) { return () => { cancelled = true; }; }, [isRemote, activeNode?.id]); + // The Policies tab hosts only the paid enforcement manager, so it is hidden for + // Community; redirect off it if a deep-link lands a Community user there. + useEffect(() => { + if (!isPaid && activeTab === 'policies') onTabChange('overview'); + }, [isPaid, activeTab, onTabChange]); + const { state, tone } = deriveMasthead(overview, overviewLoadError !== null); const pulsing = tone === 'live' && !!overview?.scanner.available; @@ -201,9 +206,11 @@ export function SecurityView({ activeTab, onTabChange }: SecurityViewProps) { Secrets - - Policies - + {isPaid && ( + + Policies + + )} Suppressions @@ -226,6 +233,7 @@ export function SecurityView({ activeTab, onTabChange }: SecurityViewProps) { onInspect={onInspect} canScan={canScan} onScanComplete={() => setReloadToken((t) => t + 1)} + isPaid={isPaid} /> @@ -255,12 +263,11 @@ export function SecurityView({ activeTab, onTabChange }: SecurityViewProps) { - -
+ {isPaid && ( + - -
-
+ + )} {isRemote ? ( diff --git a/frontend/src/components/security/FindingsTab.tsx b/frontend/src/components/security/FindingsTab.tsx index 981e2bf9..f0bc2d6f 100644 --- a/frontend/src/components/security/FindingsTab.tsx +++ b/frontend/src/components/security/FindingsTab.tsx @@ -35,8 +35,8 @@ const COPY: Record {count} - onInspect(s.scan_id, copy.detailTab)} /> + onInspect(s.scan_id, copy.detailTab)} /> ); diff --git a/frontend/src/components/security/OverviewTab.tsx b/frontend/src/components/security/OverviewTab.tsx index eca8d3dd..2e196a95 100644 --- a/frontend/src/components/security/OverviewTab.tsx +++ b/frontend/src/components/security/OverviewTab.tsx @@ -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 (
@@ -176,7 +178,7 @@ export function OverviewTab({ overview, loadError, summaries, trend, onNavigate, tone="subtitle" />

- 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.

diff --git a/frontend/src/components/security/PolicyPacksTab.tsx b/frontend/src/components/security/PolicyPacksTab.tsx deleted file mode 100644 index aa59f550..00000000 --- a/frontend/src/components/security/PolicyPacksTab.tsx +++ /dev/null @@ -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 = { - CRITICAL: 'text-destructive', - HIGH: 'text-warning', - MEDIUM: 'text-warning', - LOW: 'text-muted-foreground', -}; - -function EnforcementBadge({ enforcement }: { enforcement: PolicyPackRule['enforcement'] }) { - const enforceable = enforcement === 'enforceable'; - return ( - - {enforceable ? 'Enforceable' : 'Warning'} - - ); -} - -export function PolicyPacksTab() { - const [packs, setPacks] = useState(null); - const [error, setError] = useState(false); - const [expanded, setExpanded] = useState>(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 ( -

- Policy packs could not be loaded. -

- ); - } - - if (!packs) { - return ( -
- - -
- ); - } - - return ( -
-

- 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. -

- -
- {packs.map((pack) => { - const isOpen = expanded.has(pack.id); - return ( -
- - - {isOpen && ( -
-

{pack.tierCopy}

-
    - {pack.rules.map((rule) => ( -
  • -
    -
    - {rule.name} - - {rule.severity} - -
    - -
    -
    -
    Checks
    -
    {rule.whatItChecks}
    -
    Why
    -
    {rule.why}
    -
    Fix
    -
    {rule.howToFix}
    -
    -
  • - ))} -
-
- )} -
- ); - })} -
-
- ); -} diff --git a/frontend/src/components/security/ScanPolicyManager.tsx b/frontend/src/components/security/ScanPolicyManager.tsx index 956b28c6..dd77b562 100644 --- a/frontend/src/components/security/ScanPolicyManager.tsx +++ b/frontend/src/components/security/ScanPolicyManager.tsx @@ -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 ( diff --git a/frontend/src/components/security/__tests__/PolicyPacksTab.test.tsx b/frontend/src/components/security/__tests__/PolicyPacksTab.test.tsx deleted file mode 100644 index 902332e8..00000000 --- a/frontend/src/components/security/__tests__/PolicyPacksTab.test.tsx +++ /dev/null @@ -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; - -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(); - 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(); - 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(); -}); diff --git a/frontend/src/types/security.ts b/frontend/src/types/security.ts index dd5d286a..75539e4f 100644 --- a/frontend/src/types/security.ts +++ b/frontend/src/types/security.ts @@ -214,23 +214,3 @@ export interface SecurityRiskTrendPoint { critical: number; high: number; } - -export type PolicyRuleEnforcement = 'warning' | 'enforceable'; - -export interface PolicyPackRule { - id: string; - name: string; - severity: Exclude; - whatItChecks: string; - why: string; - howToFix: string; - enforcement: PolicyRuleEnforcement; -} - -export interface PolicyPack { - id: string; - name: string; - tagline: string; - tierCopy: string; - rules: PolicyPackRule[]; -}