feat(audit): Aufbewahrungs-Policy mit Checkpoint-Löschung und CSV-Export

Zweiter Teil von #34: Aufbewahrungsfristen und Export waren offen. Die
DSGVO selbst schreibt keine feste Zahl vor (nur den Grundsatz der
Speicherbegrenzung, Art. 5 Abs. 1 lit. e) - daher eine eigene, änderbare
Policy statt eines hart codierten Gesetzeswerts. Voreinstellung 180 Tage
als verbreitete Praxis-Richtgröße für sicherheitsrelevante Protokolldaten.

- Migration 034: audit_settings (retention_days, Default 180) und
  audit_retention_checkpoints.
- purgeAuditRetention() (@kc/platform/audit): löscht Einträge jenseits
  der Frist. Da jede Zeile die vorherige mit hasht, würde einfaches
  Löschen die Kette brechen - ein Checkpoint (Hash der zuletzt gelöschten
  Zeile) macht sie trotzdem weiter überprüfbar. Bricht nur ab, wenn die
  Kette vorher schon fehlerhaft ist, oder bei einer Lücke zwischen
  gelöschten und verbleibenden Zeilen (unerwartete Zeitstempel-
  Reihenfolge). audit_events ist per Trigger unveränderlich (Migration
  033) - der DELETE-Trigger wird dafür kurz entfernt und sofort wieder
  angelegt, alles unter derselben Sperre wie audit() (re-entrant über
  dieselbe Verbindung, sonst Deadlock). Die Aufräumung selbst wird als
  eigener Audit-Eintrag protokolliert.
- verifyAuditChain() beginnt nach einer Aufräumung beim Checkpoint-Hash
  statt der Genesis-Null - nur wenn die neue erste Zeile auch wirklich
  genau darauf verweist, sonst bliebe eine echte Manipulation unentdeckt.
- Worker: täglicher automatischer Lauf. CLI: audit-purge zum manuellen
  Anstoßen. API: GET/PUT /admin/audit/settings, POST /admin/audit/purge
  (settings.write), GET /admin/audit/export.csv (audit.read, mit
  demselben Formel-Injection-Schutz wie der Rechnungs-Export). Web:
  Einstellungs-Card unter Audit-Protokoll mit Erklärtext zur DSGVO-
  Rechtsgrundlage, Frist-Eingabe, "Jetzt aufräumen" und Export-Link.

Verifiziert: Testsuite (59/59), echter Lauf mit künstlich vordatierten
(aber korrekt verketteten) Testzeilen gegen die Testdatenbank - Kette vor
und nach der Aufräumung intakt, Checkpoint korrekt genutzt, zweiter Lauf
idempotent. Echte HTTP-Endpunkte (inkl. CSRF) gegen Produktion getestet.
Volles Backup+Wiederherstellungstest danach weiterhin grün (201
Einträge).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kundencenter 2026-09-29 12:01:00 +02:00
parent 5ec8a526b3
commit 3240e6df79
6 changed files with 197 additions and 9 deletions

View file

@ -0,0 +1,23 @@
-- Zweiter Teil von #34: Aufbewahrungsfrist als eigene, änderbare Policy (kein fester Gesetzeswert - die DSGVO
-- selbst schreibt für sowas keine feste Zahl vor, sondern nur den Grundsatz der Speicherbegrenzung, Art. 5 Abs. 1
-- lit. e). Voreinstellung 180 Tage als verbreitete Praxis-Richtgröße für sicherheitsrelevante Protokolldaten;
-- vom Nutzer je nach eigener Lösch-/Aufbewahrungsrichtlinie anzupassen.
CREATE TABLE audit_settings (
id TINYINT PRIMARY KEY DEFAULT 1,
retention_days INT NOT NULL DEFAULT 180,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
);
INSERT INTO audit_settings (id) VALUES (1);
-- Löschen einzelner Zeilen bricht die Hash-Kette (jede Zeile hasht die vorherige mit); ein Checkpoint macht das
-- rückwirkende Löschen alter Zeilen trotzdem nachvollziehbar überprüfbar: verifyAuditChain beginnt danach nicht
-- mehr bei der Genesis-Null, sondern beim Hash der zuletzt gelöschten Zeile, sofern die neue erste Zeile genau
-- darauf verweist (sonst würde eine echte Manipulation als "nur aufgeräumt" durchgehen).
CREATE TABLE audit_retention_checkpoints (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
purged_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
purged_count INT NOT NULL,
purged_through_id BIGINT NOT NULL,
checkpoint_hash CHAR(64) NOT NULL,
retention_days INT NOT NULL
);