Workstream 11: Tests & Sicherheitshärtung (Phase 7)
Beweist die Auth-Gating-Garantie und härtet das System ab (Definition of Done #1, #2, #3, #7, #8): - Routen-Manifest (tests/e2e/routes.manifest.ts) als einzige Quelle der Wahrheit; anonyme Seite -> Redirect /login, anonyme API -> 401. - Kritische auth-gating.spec.ts: genau ein Fall je Manifest-Eintrag, ohne Daten-Leak. - Driftschutz (routes.manifest.spec.ts + tests/unit/routes-manifest.test.ts): keine ungetestete neue Route unter src/app/**. - Default-Deny-Beweis für Server Actions (server-actions-guard.spec.ts + tests/unit/server-actions-guard.test.ts): jede "use server"-Funktion ruft als erste Anweisung einen Guard; Login-Actions per Allowlist ausgenommen. - Wiederverwendbare reine Scanner unter tests/support (route-scan, guard-scan) — offline lauffähig, in Vitest und Playwright geteilt. - rbac-scoping, search-eta, login-ratelimit, security-headers Specs (gegen geseedeten Server; in der Sandbox deferred, per test.skip abgesichert). - global-setup (Migration + Seed) und auth.setup (Login je Konto -> storageState); Playwright-Projekte setup -> chromium verdrahtet. - src/lib/security/headers.test.ts: statischer Beleg für CSP, HSTS, X-Frame-Options DENY, nosniff, Permissions-Policy. - vitest.config.ts: Coverage-Schwellen (>=90 %) für src/lib/search + src/lib/geo. - package.json: Scripts test:unit, test:coverage, test:e2e, test:e2e:gating. - docs/reference/sicherheitshaertung-checkliste.md: jeder Härtungspunkt mit Test/Befehl und Negativ-Probe. Offline verifiziert: tsc --noEmit (0), vitest run (229 passed / 7 db-skipped), drizzle-kit check (ok), next build (exit 0), next lint (0 Fehler), playwright --list (98 Tests, 15 Dateien). DB-/Server-/Browser-abhängige E2E-Läufe sind deferred (kein Postgres/Server in der Sandbox). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
68
tests/support/guard-scan.ts
Normal file
68
tests/support/guard-scan.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { globSync } from "node:fs";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/**
|
||||
* Statischer Default-Deny-Scanner für Server Actions (Querschnittsstandard 3).
|
||||
*
|
||||
* Reine Funktionen, damit der Beweis sowohl im Vitest-Unit-Test (offline, ohne
|
||||
* Server/DB) als auch in der Playwright-Suite (`server-actions-guard.spec.ts`)
|
||||
* wiederverwendet werden kann.
|
||||
*/
|
||||
|
||||
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
|
||||
/** Erkennt einen der fünf kanonischen Guards (guards.ts). */
|
||||
export const GUARD_REGEX =
|
||||
/require(Session|Role|OwnBrigade|PlatformAdmin|WehrAdmin)\s*\(/;
|
||||
|
||||
/**
|
||||
* Genuin öffentliche "use server"-Funktionen (Login VOR der Authentifizierung).
|
||||
* Diese DÜRFEN keinen Session-Guard haben — sie sind der Einstieg. Schlüssel:
|
||||
* `<repo-relativer Pfad>:<Funktionsname>`.
|
||||
*/
|
||||
export const PUBLIC_ACTION_ALLOWLIST: ReadonlySet<string> = new Set([
|
||||
"src/app/(auth)/login/actions.ts:loginAction",
|
||||
"src/app/(auth)/login/actions.ts:authentikLoginAction",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Prüft den Quelltext EINER Datei. Gibt eine Liste von Verstößen
|
||||
* (`<file>: <funktionsname>`) zurück. Dateien ohne "use server"-Direktive
|
||||
* liefern nie Verstöße.
|
||||
*/
|
||||
export function findUnguardedActions(
|
||||
file: string,
|
||||
src: string,
|
||||
allowlist: ReadonlySet<string> = PUBLIC_ACTION_ALLOWLIST,
|
||||
): string[] {
|
||||
if (!src.includes('"use server"')) return [];
|
||||
const offenders: string[] = [];
|
||||
const fns = src.split(/export async function /).slice(1);
|
||||
for (const body of fns) {
|
||||
const name = body.slice(0, body.indexOf("(")).trim();
|
||||
if (allowlist.has(`${file}:${name}`)) continue;
|
||||
// Erste ~600 Zeichen des Funktionskörpers müssen einen Guard enthalten.
|
||||
if (!GUARD_REGEX.test(body.slice(0, 600))) {
|
||||
offenders.push(`${file}: ${name}`);
|
||||
}
|
||||
}
|
||||
return offenders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scannt das echte Repository (src/**) nach ungeschützten Server Actions.
|
||||
*/
|
||||
export function findUnguardedActionsInRepo(): string[] {
|
||||
const files = globSync("src/**/*.{ts,tsx}", { cwd: REPO_ROOT });
|
||||
const offenders: string[] = [];
|
||||
for (const rel of files) {
|
||||
const abs = resolve(REPO_ROOT, rel);
|
||||
const src = readFileSync(abs, "utf8");
|
||||
if (!src.includes('"use server"')) continue;
|
||||
const repoRel = relative(REPO_ROOT, abs);
|
||||
offenders.push(...findUnguardedActions(repoRel, src));
|
||||
}
|
||||
return offenders;
|
||||
}
|
||||
Reference in New Issue
Block a user