/** * Vollständigkeitscheck der serverseitigen Modul-Durchsetzung (§3.4, Phase-1-Härtung). * * Jede mutierende Server-Action eines gegateten Moduls MUSS über einen * `moduleGuard("")`-Guard laufen (siehe src/server/action-guard.ts), damit ein * für den Mandanten deaktiviertes Modul auch Writes serverseitig abweist. * * Dieses Script erzwingt das statisch: Es kennt die Zuordnung Action-Datei → Modul * und schlägt fehl (Exit 1 → Build/Test rot), sobald * - eine neue Action-Datei nicht zugeordnet ist ("vergessener Endpoint"), * - eine gegatete Datei den erwarteten moduleGuard nicht verwendet, oder * - eine exportierte Action nicht über `await guard(...)` läuft. * * Neue Action-Datei anlegen ⇒ hier eintragen (Modul-Key oder "EXEMPT"). */ import { readdirSync, readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { MODULE_KEYS } from "../src/lib/modules"; const ACTIONS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "server", "actions"); /** Zuordnung Action-Datei → Modul-Key. "EXEMPT" = kein gegatetes Fachmodul (eigene Auth). */ const ACTION_MODULE: Record = { "assets.ts": "assets", // M2 Strukturanalyse: primäre Informations-Assets (Dedup/Autocomplete) → Asset-Modul. "structure.ts": "assets", "processes.ts": "bia", "risks.ts": "risk", "risk-catalog.ts": "risk", "measures.ts": "measures", "tasks.ts": "tasks", "incidents.ts": "incidents", // IM-D: mandantenseitige Pflege der E-Mail-Intake-Konfiguration (moduleGuard("incidents") + tenant:manage). "incident-intake.ts": "incidents", "suppliers.ts": "suppliers", "services.ts": "suppliers", "software.ts": "suppliers", "projects.ts": "assets", "onboarding.ts": "onboarding", "onboarding-facts.ts": "onboarding", "onboarding-steps.ts": "onboarding", "onboarding-team.ts": "onboarding", "soa.ts": "onboarding", // AP3: ISO-Anwendbarkeitserklärung (eigenes Modul „soa"). "soa-entries.ts": "soa", // AP4: Managementklauseln (Kennzahlen/Managementbewertung/CAPA) im Modul „review". "review.ts": "review", "gap.ts": "onboarding", // Audit-Vorbereitung — Modul `audit`. "audits.ts": "audit", "audit-evidence.ts": "audit", "control-descriptions.ts": "audit", "policies.ts": "policies", "policy-package.ts": "policies", // AP5: Dokumentenlenkung (Prüfzyklus, Neuversion/Historie, Lesebestätigung). "policy-control.ts": "policies", "policy-upload.ts": "policies", "hints.ts": "policies", "register.ts": "policies", // Plattform-Betrieb (eigene Auth) und Kunden-Einstellungen (tenant:manage) sind // keine per TenantModule gegateten Fachmodule — eigene Autorisierung, kein moduleGuard. "admin.ts": "EXEMPT", // SEC1: Mail-Betriebsfunktionen der Plattform-Administration (Auth über die // Plattform-Session), kein per TenantModule gegatetes Fachmodul. "mail.ts": "EXEMPT", // Backup-Portal: Enqueue-Actions für Portal-Restore/Export/DSGVO-Zustellung. // Betreiber-/Plattform-Fähigkeit (Auth über requirePlatformFullAdmin + MFA-Step-up), // kein per TenantModule gegatetes Fachmodul. "backup-admin.ts": "EXEMPT", "backup-settings.ts": "EXEMPT", // IM-D: Betreiber-Provisionierung/Verifizierung der Intake-Konfiguration + Inbound-Review. // Plattform-Fähigkeit (requirePlatformFullAdmin), kein per TenantModule gegatetes Fachmodul. "incident-intake-admin.ts": "EXEMPT", // SEC2: Passwort-Self-Service. Die Reset-Abläufe laufen bewusst OHNE Session // (der Nutzer ist ausgesperrt); abgesichert über Rate-Limit, Enumeration- // Neutralität und single-use-Tokens. Die angemeldeten Abläufe nutzen // requireSession bzw. requirePlatformSession. "auth-recovery.ts": "EXEMPT", "platform.ts": "EXEMPT", "platform-users.ts": "EXEMPT", "tenant-users.ts": "EXEMPT", "account.ts": "EXEMPT", "tenant-switch.ts": "EXEMPT", "webauthn.ts": "EXEMPT", "platform-admins.ts": "EXEMPT", "policy-templates.ts": "EXEMPT", "tenant-settings.ts": "EXEMPT", }; const errors: string[] = []; const files = readdirSync(ACTIONS_DIR).filter((f) => f.endsWith(".ts")); for (const file of files) { const mapped = ACTION_MODULE[file]; if (!mapped) { errors.push( `Nicht zugeordnete Action-Datei: ${file} — in scripts/check-module-guards.ts eintragen (Modul-Key oder "EXEMPT").` ); continue; } const src = readFileSync(join(ACTIONS_DIR, file), "utf8"); if (mapped === "EXEMPT") { // Auth-Nachweis: entweder ein require*-Guard ODER ein direkter auth()-Aufruf // (z. B. tenant-switch.ts, das im No-Tenant-Zustand kein requireSession nutzen // kann, aber die Identity + Mitgliedschaftszugehörigkeit selbst prüft). if (!/require(Session|Platform\w*|Permission)|\bauth\(\)/.test(src)) { errors.push(`${file}: als EXEMPT markiert, aber keine erkennbare Auth-Prüfung.`); } continue; } if (!MODULE_KEYS.includes(mapped)) { errors.push(`${file}: unbekannter Modul-Key "${mapped}" (nicht in src/lib/modules.ts).`); } if (!src.includes(`moduleGuard("${mapped}")`)) { errors.push(`${file}: erwartet moduleGuard("${mapped}") — Modul-Gating fehlt oder falscher Key.`); } // Jede exportierte Server-Action muss über await guard(...) laufen. const exportRe = /export async function (\w+)\s*\(/g; const positions: { name: string; index: number }[] = []; let m: RegExpExecArray | null; while ((m = exportRe.exec(src))) positions.push({ name: m[1], index: m.index }); for (let i = 0; i < positions.length; i++) { const start = positions[i].index; const end = i + 1 < positions.length ? positions[i + 1].index : src.length; if (!/await guard\(/.test(src.slice(start, end))) { errors.push( `${file}: Action "${positions[i].name}" läuft nicht über await guard(...) — Modul-/Rechte-Guard fehlt.` ); } } } if (errors.length) { console.error("✗ Modul-Guard-Vollständigkeitscheck fehlgeschlagen:"); for (const e of errors) console.error(" - " + e); process.exit(1); } console.log( `✓ Modul-Guard-Vollständigkeitscheck: ${files.length} Action-Dateien geprüft — alle mutierenden Actions sind modul- und rechtegegated.` );