import { z } from 'zod'; /** * The VS-NfD hardening-profile mode and the machine-readable configuration * catalog (issue #243, ADR 0027). * * The deployment declares through `VS_NFD_MODE` how the application treats * configuration options that violate the VS-NfD reference profile * (docs/vs-nfd/50-haertungsleitfaden.md): * * - `off` — VS-NfD is not a topic; no marking anywhere (the default). * - `marked` — options stay available, violations are marked (#244). * - `hidden` — violating options disappear, the hiding is marked (#245). * - `enforced` — violating writes are rejected server-side (#246). * * Deploy-level (env) on purpose, like BACKUP_ALLOWED_TARGETS (#192): a * compromised Site Admin must not be able to widen the mode at runtime. * * The catalog below is the single source of truth for "profile-relevant": * every entry names the setting, the machine-checkable compliant value and * the hardening-guide section it comes from. A fence test keeps catalog * and guide from drifting (vs-nfd-profile-catalogue.test.ts, pattern * #201): every switch listed in the guide is either here or in the * explicit advisory list — never silently absent. */ export const VS_NFD_MODES = ['off', 'marked', 'hidden', 'enforced'] as const; export type VsNfdMode = (typeof VS_NFD_MODES)[number]; export const vsNfdModeSchema = z.enum(VS_NFD_MODES); /** * The decidable compliance predicates. Deliberately tiny: a catalog entry * must be checkable without prose interpretation, or it belongs in the * advisory list instead. */ export type VsNfdCompliance = | { kind: 'equals'; value: boolean | string } | { kind: 'maxNumber'; value: number } | { kind: 'nonEmpty' }; export interface VsNfdProfileEntry { /** `instance` = instance_settings key; `deploy` = env variable. */ scope: 'instance' | 'deploy'; key: string; compliance: VsNfdCompliance; /** Section of docs/vs-nfd/50-haertungsleitfaden.md the value comes from. */ hardeningRef: '1.1' | '1.2'; } /** * Profile entries with a machine-checkable compliant value. Order mirrors * the hardening guide. Pond-level opt-ins (`apiEnabled`, `mcpEnabled`) * are deliberately absent: their instance master switches govern, so a * pond opt-in cannot violate the profile on its own (ADR 0027). */ export const VS_NFD_PROFILE: readonly VsNfdProfileEntry[] = [ { scope: 'instance', key: 'auth.registrationMode', compliance: { kind: 'equals', value: 'closed' }, hardeningRef: '1.1', }, { scope: 'instance', key: 'api.enabled', compliance: { kind: 'equals', value: false }, hardeningRef: '1.1', }, { scope: 'instance', key: 'mcp.enabled', compliance: { kind: 'equals', value: false }, hardeningRef: '1.1', }, { scope: 'instance', key: 'feeds.enabled', compliance: { kind: 'equals', value: false }, hardeningRef: '1.1', }, { scope: 'instance', key: 'plugins.enabled', compliance: { kind: 'equals', value: false }, hardeningRef: '1.1', }, { scope: 'instance', key: 'classification.newPageDefault', compliance: { kind: 'equals', value: 'vs_nfd' }, hardeningRef: '1.1', }, { scope: 'instance', key: 'classification.uploadPolicy', compliance: { kind: 'equals', value: 'block' }, hardeningRef: '1.1', }, { scope: 'instance', key: 'upload.svgPolicy', compliance: { kind: 'equals', value: 'reject' }, hardeningRef: '1.1', }, { scope: 'instance', key: 'backup.nextcloud.enabled', compliance: { kind: 'equals', value: false }, hardeningRef: '1.1', }, { scope: 'instance', key: 'readTrail.enabled', compliance: { kind: 'equals', value: true }, hardeningRef: '1.1', }, { scope: 'instance', key: 'legal.imprint', compliance: { kind: 'nonEmpty' }, hardeningRef: '1.1', }, { scope: 'instance', key: 'legal.privacyPolicy', compliance: { kind: 'nonEmpty' }, hardeningRef: '1.1', }, { scope: 'deploy', key: 'SESSION_ABSOLUTE_HOURS', compliance: { kind: 'maxNumber', value: 12 }, hardeningRef: '1.2', }, { scope: 'deploy', key: 'SESSION_IDLE_HOURS', compliance: { kind: 'maxNumber', value: 2 }, hardeningRef: '1.2', }, { scope: 'deploy', key: 'LOG_LEVEL', compliance: { kind: 'equals', value: 'info' }, hardeningRef: '1.2', }, { scope: 'deploy', key: 'AUTH_LOCAL_ENABLED', compliance: { kind: 'equals', value: false }, hardeningRef: '1.2', }, ] as const; /** * Guide entries whose reference value is a judgement call ("only what the * service needs", "configure the agency IdP", "leave unset or one host") — * profile-relevant, but not machine-checkable. Listed explicitly so the * fence test still notices when the guide gains a switch nobody triaged. */ export const VS_NFD_PROFILE_ADVISORY: readonly { scope: 'instance' | 'deploy'; key: string }[] = [ { scope: 'instance', key: 'upload.allowedExtensions' }, { scope: 'instance', key: 'plugins.allowlist' }, { scope: 'instance', key: 'trash.retentionDays' }, { scope: 'instance', key: 'audit.retentionDays' }, { scope: 'instance', key: 'conversion.payloadRetentionDays' }, { scope: 'instance', key: 'mail.outboxRetentionDays' }, { scope: 'instance', key: 'readTrail.dedupWindowMinutes' }, { scope: 'instance', key: 'readTrail.retentionDays' }, { scope: 'instance', key: 'idpMapping.rules' }, { scope: 'deploy', key: 'VS_NFD_MODE' }, { scope: 'deploy', key: 'BACKUP_ALLOWED_TARGETS' }, { scope: 'deploy', key: 'SMTP_HOST' }, { scope: 'deploy', key: 'WEB_PORT' }, { scope: 'deploy', key: 'API_PORT' }, { scope: 'deploy', key: 'COLLAB_PORT' }, { scope: 'deploy', key: 'OIDC_ISSUER' }, { scope: 'deploy', key: 'OIDC_CLIENT_ID' }, { scope: 'deploy', key: 'OIDC_CLIENT_SECRET' }, { scope: 'deploy', key: 'OIDC_SCOPES' }, { scope: 'deploy', key: 'OIDC_PROVIDER_LABEL' }, { scope: 'deploy', key: 'AUTH_PROXY_HEADER' }, { scope: 'deploy', key: 'AUTH_PROXY_TRUSTED_PEERS' }, { scope: 'deploy', key: 'AUTH_PROXY_MAP' }, { scope: 'deploy', key: 'AUTH_PROXY_MODE' }, { scope: 'deploy', key: 'AUTH_PROXY_DN_ATTRIBUTE' }, ] as const; /** True when `value` satisfies the entry's compliance predicate. */ export function isVsNfdCompliant(entry: VsNfdProfileEntry, value: unknown): boolean { switch (entry.compliance.kind) { case 'equals': return value === entry.compliance.value; case 'maxNumber': return typeof value === 'number' && value <= entry.compliance.value; case 'nonEmpty': return typeof value === 'string' && value.trim().length > 0; } } /** One evaluated catalog row, as the admin endpoint reports it (#243). */ export interface VsNfdProfileEntryView { scope: 'instance' | 'deploy'; key: string; compliant: boolean; /** The compliant value, rendered for display (booleans/numbers stringified). */ compliantValue: string; hardeningRef: '1.1' | '1.2'; } export interface VsNfdProfileView { mode: VsNfdMode; entries: VsNfdProfileEntryView[]; violations: number; } /** Display form of a predicate — shared so api and web render it alike. */ export function describeCompliance(compliance: VsNfdCompliance): string { switch (compliance.kind) { case 'equals': return String(compliance.value); case 'maxNumber': return `<= ${compliance.value}`; case 'nonEmpty': return 'non-empty'; } }