Files
CapaKraken/plan.md
T
Hartmut bfdf0a82da security/platform: close audit findings #19–#26
Tests, CSP nonce middleware, SSRF guard, perf-route hardening,
Docker env isolation, migration runbook, RBAC E2E coverage.

Tickets resolved:
- #19: MfaSetup.test.ts — static source tests confirming local QR rendering
- #20: ssrf-guard.test.ts (16 tests) + webhook-procedure-support mock fix
- #21: /api/perf route.test.ts (5 tests) — header-only auth, fail-closed
- #22: middleware.ts (nonce-based CSP) + middleware.test.ts (6 tests);
       layout.tsx async + nonce prop; CSP removed from next.config.ts
- #23: Active-session registry enforcement verified (already in codebase)
- #24: docker-compose.yml REDIS_URL hardcoded (no host-env substitution)
- #25: docker-compose.yml REDIS_URL + docs/developer-runbook.md created
- #26: e2e/dev-system/rbac-data-access.spec.ts (12 tests, 3 roles × 4 procedures)

Quality gates: tsc clean, api 1447/1447, web 189/189 passing.
Turbo concurrency capped at 2 (package.json) to prevent OOM under
parallel test runs.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-01 22:14:20 +02:00

16 KiB
Raw Blame History

CapaKraken — Umsetzungsplan

Gitea-Repo: https://gitea.hartmut-noerenberg.com/Hartmut/plANARCHY Stand: 2026-04-01 | Issues: #19#26


Plan: In-Review-Tickets verifizieren und in main integrieren

Anforderungsanalyse

8 Tickets sind in-review. Ziel: jeden Ticket-Scope im Code verifizieren, Gitea-Kommentar mit Bewertung ( akzeptiert / ⚠️ Nachbesserung nötig) hinterlassen, dann alle offenen Changes in einem sauberen Commit auf main integrieren.

Umfang der Verifikation pro Ticket:

  • Code-Artefakte im Repo prüfen (Dateien, Tests)
  • Quality-Gates (tsc, test:unit) müssen grün sein
  • Akzeptanzkriterien aus dem Ticket-Body abgleichen
  • Gitea-Kommentar: Strukturiertes Urteil mit Befunden

Integration-Strategie:

  • Alle ungestagten Änderungen (git status) gehören zu den Tickets #20, #22, #25, #26
  • Ein einziger Commit auf main mit klarer Commit-Message
  • plan.md und docs/ mitcommiten

Betroffene Pakete & Dateien

Paket Dateien Art der Änderung
apps/web next.config.ts, src/app/layout.tsx, src/middleware.ts, src/middleware.test.ts, src/app/api/perf/route.ts, src/app/api/perf/route.test.ts, src/components/security/MfaSetup.test.ts, e2e/dev-system/rbac-data-access.spec.ts, e2e/dev-system/helpers.ts Verifikation + commit
packages/api src/__tests__/ssrf-guard.test.ts, src/__tests__/webhook-procedure-support.test.ts Verifikation + commit
root package.json, docker-compose.yml, docs/developer-runbook.md, plan.md Verifikation + commit

Task-Liste (atomare Schritte in Reihenfolge)

Phase 1 — Verifikation & Kommentierung (sequenziell, ein Ticket nach dem anderen)

  • V-1: Ticket #19 (MFA-QR) — Code-Check: MfaSetup.tsx + MfaSetup.test.ts; Urteil auf Gitea posten
  • V-2: Ticket #20 (Webhook SSRF) — Code-Check: ssrf-guard.ts + ssrf-guard.test.ts + webhook-procedure-support.test.ts; Urteil posten
  • V-3: Ticket #21 (/api/perf) — Code-Check: route.ts + route.test.ts; Urteil posten
  • V-4: Ticket #22 (CSP) — Code-Check: middleware.ts + middleware.test.ts + next.config.ts + layout.tsx; Urteil posten
  • V-5: Ticket #23 (Active-Session-Registry) — Code-Check: Session-Guard-Middleware, Auth-Flow; Urteil posten
  • V-6: Ticket #24 (Docker reproducibility) — Code-Check: docker-compose.yml, Dockerfile.dev; Urteil posten
  • V-7: Ticket #25 (Env/Migration-Strategie) — Code-Check: docker-compose.yml, docs/developer-runbook.md; Urteil posten
  • V-8: Ticket #26 (RBAC E2E-Tests) — Code-Check: rbac-data-access.spec.ts; Urteil posten

Phase 2 — Quality Gates

  • Q-1: pnpm --filter @capakraken/web exec tsc --noEmit — keine Fehler
  • Q-2: pnpm --filter @capakraken/api test:unit — alle Tests grün
  • Q-3: pnpm --filter @capakraken/web test:unit — alle Tests grün

Phase 3 — Git-Integration

  • G-1: git add aller ungestagten Änderungen (alle Ticket-Artefakte)
  • G-2: Commit mit Message: security/platform: close audit findings #19#26 (tests, CSP nonce, SSRF guard, runbook)
  • G-3: git push origin main

Abhängigkeiten

  • V-1 bis V-8 sind unabhängig voneinander, aber sequenziell (ein Kommentar pro Ticket)
  • Q-1 bis Q-3 können nach allen V-Tasks parallel laufen
  • G-1/G-2/G-3 müssen nach Q-1..3 erfolgen (kein Commit bei roten Tests)

Akzeptanzkriterien

  • Alle 8 in-review-Tickets haben einen Bewertungskommentar
  • pnpm test:unit läuft grün (beide Pakete)
  • tsc --noEmit ohne Fehler
  • Alle neuen Dateien in einem sauberen Commit auf main
  • git status danach: working tree clean

Risiken & offene Fragen

  • Ticket #23 (Active-Session): Die Implementierung in früheren Sessions könnte durch spätere Auth-Fixes (jti→sid) überholt worden sein — genau prüfen
  • Ticket #24 vs. #25: Überlappen in docker-compose.yml — beide Tickets betreffen dieselbe Datei; ein Commit deckt beide ab
  • Push auf main: Direkt auf main — kein PR da kein Remote-Review-Prozess konfiguriert. Sicherstellen dass alle Tests grün sind


Ticket #25 — Docker/Env/Migration-Strategie

Anforderungsanalyse

Ziel: Docker-Container-Lifecycle ohne manuelle Eingriffe.

Konkrete Mängel:

  1. REDIS_URL in docker-compose.yml nutzt ${REDIS_URL:-redis://redis:6379} — Host-Env-Var kann Docker-internen Wert überschreiben (gleiche Klasse wie das behobene DATABASE_URL-Problem)
  2. Migration-Strategy undokumentiert: DB per db push aufgebaut, dann Migration hinzugefügt → migrate deploy scheitert mit P3005, erforderte migrate resolve --applied
  3. Kein Developer-Runbook (Setup, Restart, DB-Ops fehlen)

Betroffene Dateien

Datei Änderung
docker-compose.yml REDIS_URL hardcoden
docs/developer-runbook.md create

Task-Liste

  • #25-T1: docker-compose.ymlREDIS_URL: redis://redis:6379 (Literal, kein ${})
  • #25-T2: docs/developer-runbook.md erstellen mit: Erstmaligem Setup, DB-Migration-Strategie inkl. P3005-Recovery, E2E_TEST_MODE-Erklärung, Container-Neustart-Checkliste

Ticket #26 — RBAC Datenzugriffs-Matrix E2E-Tests

Anforderungsanalyse

Neue Testdatei apps/web/e2e/dev-system/rbac-data-access.spec.ts mit Netzwerk-Ebene tRPC-Response-Assertions (nicht nur UI-Sichtbarkeit).

Grundlage docs/route-access-matrix.md:

tRPC-Prozedur Audience Admin Manager Viewer
user.list admin-only ✓ 200 FORBIDDEN FORBIDDEN
allocation.listView planning-read ✓ 200 ✓ 200 FORBIDDEN
resource.listSummaries resource-overview ✓ 200 ✓ 200 FORBIDDEN
user.listAssignable manager-write ✓ 200 ✓ 200 FORBIDDEN

Technik: page.evaluate() mit fetch() gegen /api/trpc/<proc>?batch=1&input=... — läuft im Browser-Kontext der gespeicherten Session.

tRPC GET-Format:

GET /api/trpc/<proc>?batch=1&input={"0":{"json":null}}
Erfolg: [{"result":{"data":{"json":[...]}}}]
Fehler: [{"error":{"json":{"data":{"code":"FORBIDDEN","httpStatus":403}}}}]

Betroffene Dateien

Datei Änderung
apps/web/e2e/dev-system/rbac-data-access.spec.ts create

Task-Liste

  • #26-T1: Datei erstellen mit trpcQuery(page, procedure, input?) Helper-Funktion
  • #26-T2: Admin-Describe-Block (4 Tests: alle 4 Prozeduren → Erfolg erwartet)
  • #26-T3: Manager-Describe-Block (4 Tests: user.list → FORBIDDEN, Rest → Erfolg)
  • #26-T4: Viewer-Describe-Block (4 Tests: alle → FORBIDDEN)
  • #26-T5: Smoke-Run: pnpm exec playwright test e2e/dev-system/rbac-data-access.spec.ts --config=playwright.dev.config.ts

Risiken

  • allocation.listView könnte ein Pflicht-Input-Objekt erfordern → Falls BAD_REQUEST statt FORBIDDEN, Schema im Router prüfen und minimalen Input übergeben
  • Viewer-Permissions aus DB-Seed (SystemRoleConfig) — prüfen ob VIEWER tatsächlich kein VIEW_PLANNING hat


Anforderungsanalyse

Alle 6 Issues stammen aus einem Security-Audit und einem Plattform-Review.
5 davon sind Security-Findings (OWASP A02/A05/A07/A09), 1 ist ein Plattform-Thema (Docker-Reproduzierbarkeit).
Die Security-Issues sind weitgehend unabhängig voneinander; lediglich #23 greift in den tRPC-Request-Pfad ein und sollte zuletzt umgesetzt werden, da es den Haupt-Auth-Pfad berührt.


Betroffene Pakete & Dateien

Issue Paket/Pfad Datei Art
#21 apps/web src/app/api/perf/route.ts edit
#19 apps/web src/components/security/MfaSetup.tsx edit
#19 apps/web package.json edit (neue Dep: qrcode + @types/qrcode)
#20 packages/api src/lib/webhook-dispatcher.ts edit
#20 packages/api src/router/webhook-support.ts edit
#20 packages/api src/lib/ssrf-guard.ts create
#22 apps/web next.config.ts edit
#23 apps/web src/app/api/trpc/[trpc]/route.ts edit
#23 apps/web src/server/auth.ts edit (Doku/Cleanup)
#24 root Dockerfile.dev, Dockerfile.prod, docker-compose.yml, docker-compose.prod.yml, tooling/docker/app-dev-start.sh edit

Task-Liste (in empfohlener Reihenfolge)

Issue #21 — /api/perf fail-closed + Query-Token entfernen

  • Task 1: GET-Handler in apps/web/src/app/api/perf/route.ts ändern:

    • if (cronSecret)if (!cronSecret) return 401/403 (fail-closed)
    • queryToken-Zweig vollständig entfernen
    • Nur noch Authorization: Bearer <secret> prüfen
    • → Datei: apps/web/src/app/api/perf/route.ts
  • Task 2: Unit-Tests für /api/perf:

    • Test: autorisiert per Header → 200
    • Test: kein Secret → 401
    • Test: Query-Param-Token → 401 (nicht mehr akzeptiert)
    • Test: fehlende CRON_SECRET-Env → fail-closed (kein Metrics-Leak)
    • → Datei: apps/web/src/app/api/perf/route.test.ts (neu)

Issue #19 — MFA QR lokal rendern

  • Task 3: qrcode-Paket und @types/qrcode zu apps/web/package.json hinzufügen, pnpm install ausführen.

  • Task 4: MfaSetup.tsx umschreiben:

    • <img src="https://api.qrserver.com/..."> durch lokale QR-Generierung ersetzen
    • qrcode.toDataURL(uri) im Client-Effekt aufrufen und als <img src={dataUrl}> rendern
    • Sicherstellen: der otpauth://-URI verlässt den Browser nicht mehr
    • → Datei: apps/web/src/components/security/MfaSetup.tsx
  • Task 5: Test sicherstellen, dass kein Rendering-Request an externe QR-URL geht:

    • Unit-Test oder Playwright-Test der prüft, dass kein <img src> mit qrserver.com oder chart.googleapis.com gerendert wird
    • → Datei: apps/web/src/components/security/MfaSetup.test.tsx (neu oder bestehend ergänzen)

Issue #20 — Webhook SSRF-Schutz

  • Task 6: ssrf-guard.ts erstellen mit einer assertWebhookUrlAllowed(url: string): void-Funktion:

    • Parst die URL, löst Hostname auf (DNS-Check via Node dns.lookup)
    • Blockt: Loopback (127.0.0.0/8, ::1), RFC1918 (10.x, 172.1631.x, 192.168.x), Link-Local (169.254.x), Cloud-Metadata (169.254.169.254)
    • Blockt: alle Schemes außer https (und http nur wenn expliziter Dev-Override gesetzt)
    • Wirft TRPCError({ code: "BAD_REQUEST" }) mit allgemeiner Fehlermeldung (ohne IP preiszugeben)
    • → Datei: packages/api/src/lib/ssrf-guard.ts
  • Task 7: ssrf-guard in webhook-support.ts und webhook-dispatcher.ts integrieren:

    • Vor Speicherung + vor Dispatch assertWebhookUrlAllowed(url) aufrufen
    • → Dateien: packages/api/src/router/webhook-support.ts, packages/api/src/lib/webhook-dispatcher.ts
  • Task 8: Unit-Tests für ssrf-guard.ts:

    • Erlaubt: https://example.com/hook
    • Blockt: http://localhost/…, http://127.0.0.1/…, http://10.0.0.1/…, http://192.168.1.1/…, http://169.254.169.254/…, ftp://…
    • → Datei: packages/api/src/__tests__/ssrf-guard.test.ts (neu)

Issue #22 — CSP härten

  • Task 9: CSP in apps/web/next.config.ts überarbeiten:

    • unsafe-eval entfernen oder nur für NODE_ENV === "development" erlauben
    • unsafe-inline aus script-src entfernen
    • Nonce-basierte Inline-Scripts prüfen: Next.js 15 unterstützt CSP-Nonces via nonce-Prop auf <Script> — recherchieren ob tatsächliche Inline-Scripts existieren, die Nonces brauchen
    • Unnötige connect-src-Origins bereinigen
    • → Datei: apps/web/next.config.ts
  • Task 10: Smoke-Test: App unter gehärteter CSP starten, Browser-DevTools auf CSP-Violations prüfen (mindestens Login → Dashboard → Timeline → Allocations).
    Gefundene Violations entweder beheben (Move to external file / Nonce) oder als Known Exception dokumentieren.


Issue #23 — Active-Session-Registry bei jedem Request prüfen

  • Task 11: Im tRPC-Route-Handler (apps/web/src/app/api/trpc/[trpc]/route.ts) nach dem auth()-Call die jti aus dem Session-Token lesen und gegen prisma.activeSession validieren:

    const jti = session?.user?.jti as string | undefined;
    if (jti) {
      const active = await prisma.activeSession.findUnique({ where: { jti } });
      if (!active) {
        return NextResponse.json({ error: "Session revoked" }, { status: 401 });
      }
    }
    
    • Gilt für alle authentisierten tRPC-Requests
    • Gilt auch für nicht-tRPC Auth-Pfade wenn vorhanden (Route-Handler prüfen)
    • → Datei: apps/web/src/app/api/trpc/[trpc]/route.ts
  • Task 12: Unit-/Integrations-Tests:

    • gültige aktive Session → Request durch
    • ausgeloggte (gelöschte) Session → 401
    • durch Concurrent-Session-Limit verdrängte Session → 401
    • → Datei: apps/web/src/app/api/trpc/[trpc]/route.test.ts (neu oder ergänzen)

Issue #24 — Docker-Setup host-unabhängig

  • Task 13: Dockerfile.dev prüfen und absichern:

    • pnpm install muss im Container ablaufen (kein Volume-Mount der Host-node_modules)
    • prisma generate muss Teil des Starts sein (nicht als Host-Voraussetzung)
    • Fehlende Systempakete (z. B. OpenSSL für Prisma) explizit installieren
    • → Datei: Dockerfile.dev
  • Task 14: Dockerfile.prod prüfen:

    • Multi-Stage-Build: Build-Stage hat pnpm + alle Dev-Deps; Runtime-Stage nur Prod-Artefakte
    • Generierte Prisma-Artefakte (node_modules/.prisma) korrekt aus Build-Stage kopiert
    • → Datei: Dockerfile.prod
  • Task 15: docker-compose.yml absichern:

    • node_modules-Volume-Override korrekt gesetzt damit Host-Modules nicht reinmappen
    • app-dev-start.sh ausführbar und alle Schritte (generate, migrate, start) enthalten
    • → Dateien: docker-compose.yml, tooling/docker/app-dev-start.sh
  • Task 16: Frischer-Checkout-Smoke-Test dokumentieren:

    git clone … && cd capakraken
    docker compose up --build
    # → App erreichbar, Login funktioniert, keine Host-Abhängigkeiten
    

    → Schritt in docs/ oder README.md festhalten


Abhängigkeiten

Task 1 → Task 2          (Tests setzen fertige Impl voraus)
Task 3 → Task 4 → Task 5 (Dep-Install → Impl → Test)
Task 6 → Task 7 → Task 8 (Guard-Lib → Integration → Tests)
Task 9 → Task 10         (CSP-Änderung → Smoke-Test)
Task 11 → Task 12        (Session-Impl → Tests)
Task 1316 unabhängig von allen anderen (Docker-only)

Parallel möglich (nach Task-Gruppe):
- #21 (Tasks 12) || #19 (Tasks 35) || #24 (Tasks 1316)
- #20 (Tasks 68) || #22 (Tasks 910) — erst nachdem #21/#19 fertig sind (separater Branch)
- #23 (Tasks 1112) — zuletzt, da Auth-Pfad berührt

Akzeptanzkriterien

  • pnpm test:unit läuft grün
  • pnpm --filter @capakraken/web exec tsc --noEmit — keine neuen Errors
  • pnpm lint — sauber
  • #21: /api/perf ohne CRON_SECRET gibt 401/403, Query-Token wird nicht mehr akzeptiert
  • #19: MfaSetup macht keinen Request an qrserver.com oder chart.googleapis.com
  • #20: Webhook-Save und Dispatch für http://127.0.0.1/… gibt BAD_REQUEST
  • #22: Browser-DevTools zeigt keine CSP-Violations für Haupt-User-Flows in Production-Mode
  • #23: Request mit gelöschter ActiveSession-jti gibt 401
  • #24: docker compose up --build auf sauberem Checkout bootet die App ohne Host-Abhängigkeiten

Risiken & offene Fragen

Risiko Einschätzung Maßnahme
CSP-Nonces in Next.js 15: <Script> und Tailwind CSS benötigen ggf. Nonces Mittel Vor Task 9 in Next.js-15-Doku recherchieren; ggf. nur unsafe-eval entfernen als erster Schritt
Session-Registry-Check (#23) erhöht DB-Load: jeder tRPC-Request = 1 DB-Read Mittel Redis-Cache mit kurzer TTL (30s) als Opt-in; erst messen ob nötig
SSRF-Guard DNS-Lookup: async, könnte Race-Condition durch DNS-Rebinding haben Niedrig Nach DNS-Lookup Socket-Verbindung ebenfalls gegen IP prüfen (defense-in-depth)
Docker #24: node_modules-Volume-Semantik bei pnpm-Workspaces komplex Mittel Symlink-Struktur von pnpm in Container testen; ggf. --shamefully-hoist Flag
jti im Session-Token: Auth.js-Version muss jti ins JWT schreiben Offen In auth.ts prüfen ob token.jti tatsächlich im JWT-Callback persistiert wird