diff --git a/backend/src/controllers/auditLog.controller.ts b/backend/src/controllers/auditLog.controller.ts index 83f69253..e3e49035 100644 --- a/backend/src/controllers/auditLog.controller.ts +++ b/backend/src/controllers/auditLog.controller.ts @@ -198,7 +198,26 @@ export async function verifyIntegrity(req: AuthRequest, res: Response) { */ export async function rehashAll(req: AuthRequest, res: Response) { try { - const result = await auditService.rehashAll(); + // Ausdrueckliche Bestaetigung verlangen (Pentest R170-01). + // + // Ein Rehash macht die Kette rechnerisch stimmig und setzt damit die + // Beweiskraft der Vergangenheit zurueck - das darf kein Nebeneffekt eines + // versehentlichen oder tastenden POST sein. Genau so wurde der Endpunkt + // bei einer Methoden-Erkundung unbeabsichtigt ausgeloest. + if (req.body?.confirm !== 'REHASH') { + res.status(400).json({ + success: false, + error: + 'Rehash setzt die Beweiskraft der bestehenden Einträge zurück und ist nicht ' + + 'umkehrbar. Zum Bestätigen {"confirm":"REHASH"} mitsenden.', + }); + return; + } + + const result = await auditService.rehashAll({ + userEmail: req.user?.email, + ipAddress: req.ip || (req.socket as any)?.remoteAddress, + }); res.json({ success: true, data: result, @@ -259,6 +278,18 @@ export async function updateRetentionPolicy(req: AuthRequest, res: Response) { */ export async function runRetentionCleanup(req: AuthRequest, res: Response) { try { + // Auch hier ausdrueckliche Bestaetigung: Cleanup loescht Audit-Zeilen + // endgueltig und reisst dabei die Kette auf (Pentest R170-01). + if (req.body?.confirm !== 'CLEANUP') { + res.status(400).json({ + success: false, + error: + 'Cleanup löscht Audit-Einträge endgültig. Zum Bestätigen ' + + '{"confirm":"CLEANUP"} mitsenden.', + }); + return; + } + const result = await auditService.runRetentionCleanup(); res.json({ diff --git a/backend/src/services/audit.service.ts b/backend/src/services/audit.service.ts index 0b650eee..a801b21e 100644 --- a/backend/src/services/audit.service.ts +++ b/backend/src/services/audit.service.ts @@ -869,7 +869,27 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{ /** * Hash-Kette komplett neu berechnen (Reparatur) */ -export async function rehashAll(): Promise<{ rehashedCount: number }> { +export async function rehashAll( + ausgeloestVon?: { userEmail?: string; ipAddress?: string }, +): Promise<{ rehashedCount: number }> { + // ZUSTAND VOR DEM REHASH SICHERN (Pentest R170-01) + // + // Ein Rehash macht die Kette rechnerisch wieder stimmig – auch dann, wenn sie + // vorher berechtigte Beanstandungen enthielt. Der bisherige Marker hielt nur + // fest, DASS rehasht wurde, nicht WAS dabei verschwand. Wer `audit:admin` + // besitzt, konnte damit Spuren glattziehen, ohne dass hinterher erkennbar + // war, welche. + // + // Deshalb wird der Befund samt Kettenkopf VOR dem Rehash erhoben und im + // Marker mitgeschrieben. Der Marker entsteht nach dem Rehash, ist selbst + // Teil der neuen Kette und signiert; entfernen liesse er sich nur unter + // Hinterlassung einer Luecke. + const vorher = await verifyIntegrity(); + const kopf = await prisma.auditLog.findFirst({ + orderBy: { id: 'desc' }, + select: { id: true, hash: true, hashVersion: true, createdAt: true }, + }); + const logs = await prisma.auditLog.findMany({ orderBy: { id: 'asc' }, select: { @@ -925,15 +945,33 @@ export async function rehashAll(): Promise<{ rehashedCount: number }> { // haengt sich an die neu berechnete Kette; entfernen liesse er sich nur unter // Hinterlassung einer Luecke. await createAuditLog({ - userEmail: 'system', + userEmail: ausgeloestVon?.userEmail || 'system', userRole: 'System', action: 'UPDATE', resourceType: 'AuditLog', - resourceLabel: `Hash-Kette neu berechnet (${count} Einträge) – Beweiskraft der Vergangenheit zurückgesetzt`, + resourceLabel: + `Hash-Kette neu berechnet (${count} Einträge) – Beweiskraft der Vergangenheit zurückgesetzt` + + (vorher.valid + ? ' – Kette war vorher unbeanstandet' + : ` – vorher beanstandet: ${vorher.tamperedEntries.length} manipuliert, ` + + `${vorher.chainGaps.length} Lücken`), endpoint: '/api/audit-logs/rehash', httpMethod: 'POST', - ipAddress: 'system', + ipAddress: ausgeloestVon?.ipAddress || 'system', sensitivity: 'CRITICAL', + // Befund VOR dem Rehash – ohne das waere nach dem Rehash nicht mehr + // nachvollziehbar, was uebertuencht wurde. + changesBefore: { + geprueft: vorher.checkedCount, + manipuliert: vorher.tamperedEntries, + ketten_luecken: vorher.chainGaps, + luecken_ohne_dokumentierte_loeschung: vorher.unexplainedGaps, + nicht_pruefbar: vorher.unverifiableEntries, + kettenkopf: kopf + ? { id: kopf.id, hash: kopf.hash, hashVersion: kopf.hashVersion, createdAt: kopf.createdAt } + : null, + }, + changesAfter: { neuBerechnet: count }, success: true, }); diff --git a/docs/todo.md b/docs/todo.md index bb7763a9..362bc617 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,34 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🧾 rehash/cleanup: Vorzustand sichern + Bestaetigung verlangen (Pentest R170-01)** (2026-08-18) + - Fund: `POST /audit-logs/rehash` (audit:admin) rechnet die Kette MIT dem + HMAC-Schluessel neu und macht sie damit wieder stimmig – auch wenn sie + vorher berechtigte Beanstandungen enthielt. Der Anker schuetzt also gegen + einen DB-Schreiber ohne Schluessel, **nicht** gegen einen Admin mit + `audit:admin`. Der bisherige Marker hielt nur fest, DASS rehasht wurde, + nicht WAS dabei verschwand. + - Fix 1 – **Vorzustand im Marker**: Vor dem Rehash wird `verifyIntegrity()` + erhoben und samt Kettenkopf (id, hash, hashVersion) im Marker gesichert: + Anzahl geprueft, Listen der manipulierten Zeilen, der Ketten-Luecken, der + Luecken ohne dokumentierte Loeschung und der nicht pruefbaren. Dazu + ausloesender Benutzer und IP (bisher stand dort pauschal „system“). + Der Marker entsteht nach dem Rehash, ist Teil der neuen Kette und + signiert – entfernen ginge nur unter Hinterlassung einer Luecke. + - Fix 2 – **Bestaetigung verlangen**: `rehash` erfordert + `{"confirm":"REHASH"}`, `cleanup` erfordert `{"confirm":"CLEANUP"}`. + Der Pentester hatte beide bei blinder Methoden-Erkundung per POST + unbeabsichtigt ausgeloest; ein tastender Aufruf laeuft jetzt in 400. + - Verifiziert: blinder POST auf beide Endpunkte → 400 ohne Wirkung; mit + Bestaetigung → Rehash laeuft, Marker enthaelt Ausloeser, Vorbefund + (1 manipuliert, 7 Luecken mit exakten IDs) und Kettenkopf. + - **Offengelegt:** Dieser Test hat auf der DEV-Datenbank real rehasht – die + dortigen historischen Beanstandungen sind rechnerisch geglaettet + (`valid = true`). Genau der beschriebene Effekt; der Vorzustand steht + jetzt aber im Marker. Staging/Prod unberuehrt. + - Offen (Betreiber-Entscheidung): Off-Site-Notarisierung des Kettenkopfes. + Erst sie deckt den Fall „Admin mit audit:admin“ vollstaendig ab. + - [x] **🔒 Refresh-Kulanz idempotent: stiller Session-Fork geschlossen (Pentest R168-01, HIGH)** (2026-08-18) - Der Pentester hat genau die Frage beantwortet, die ich beim Uebergeben gestellt hatte („laesst sich das Kulanzfenster ausnutzen?“) – und zwar