Schliesst die in QA-05 gefundene Luecke: der zentrale Audit-Log (AUD-01/02)
existierte und war getestet, wurde aber von keinem Produktions-Handler
befuellt. Additive WithAudit(...)-Methode je Store (Konvention aus
lockout.Store.WithPolicy uebernommen, audit==nil bleibt gueltig, kein
Verhaltensbruch fuer bestehende Aufrufer):
- internal/policy.Store.Grant/Revoke -> policy.grant/policy.revoke
- internal/tenant.Registry (Suspend/Reactivate/ScheduleDeletion/
CancelDeletion via transition) -> tenant.transition
- internal/lockout.Store.RecordFailure/Unlock -> auth.login_failed/
auth.account_locked/auth.account_unlocked
- internal/kek.Store.RotateTenantKEK/RotateMasterKey -> kek.tenant_rotated/
kek.master_rotated
Neues Testpaket internal/audit/wiring_test.go: fuer jeden der vier Bereiche
eine reale Aktion ausgefuehrt und per direkter audit_events-Abfrage
nachgewiesen (derselbe Nachweisstil wie der QA-05-Stichprobenabgleich, der
die Luecke fand). Alle bestehenden Tests der vier Pakete bleiben gruen.
51/51 Pakete gruen auf 192.168.1.131.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
internal/audit/export.go: StreamCSV/StreamJSON filtern nach Tenant, Akteur,
Aktion und Zeitraum (Akzeptanzkriterium 1) und schreiben Zeile fuer Zeile
ueber rows.Next() DIREKT auf den uebergebenen io.Writer — zu keinem
Zeitpunkt wird das komplette Ergebnis im Speicher aufgebaut (Akzeptanz-
kriterium 3). JSON-Export als JSON Lines statt einem grossen Array, um
Streaming ohne Sonderbehandlung von Klammern/Kommas zu ermoeglichen.
ExportHandler (Akzeptanzkriterium 2) schreibt direkt auf http.ResponseWriter
— derselbe Streaming-Pfad wie in Tests, kein Zwischenpuffer nur fuer HTTP.
Authorize ist eine schmale Schnittstelle (Vorbild: AUD-05 RetentionRegistrar-
Muster), da die eigentliche Rollenpruefung RBAC-02 (Policy-Enforcement) ist
und nicht Teil dieser Kachel — der Handler kennt nur "darf dieser Aufrufer
exportieren", nicht wie das entschieden wird.
Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Export mit hoher Eintragszahl ohne uebermaessigen Speicherverbrauch —
TestExport_StreamsLargeResultWithoutExcessiveMemory: 20.000 Eintraege,
Heap-Wachstum waehrend Export nur ~1.8KB (Schwelle 3MB). PASS.
2. Filterkombinationen automatisiert gegen erwartete Ergebnismengen —
TestExport_FilterCombinations (Tenant/Actor/Action einzeln und kombiniert)
und TestExport_TimeRangeFilter (innerhalb/ausserhalb Zeitraum). PASS.
3. Zugriff ohne passende Berechtigung abgewiesen —
TestExportHandler_RejectsWithoutAuthorization: fehlender/falscher caller
-> 403, berechtigter caller -> 200. PASS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
internal/audit/retention.go: RegisterWithArchive meldet audit_log_entry als
Objekttyp bei Archives Retention-Engine an (Default-Frist 10 Jahre, GoBD-
Buchungsbeleg-Frist, tenant-ueberschreibbar). RetentionRegistrar ist der
RET-05-Modul-Adapter-Vertrag, wie Core ihn konsumiert — die eigentliche
Implementierung lebt im Archive-Modul.
WICHTIGER HINWEIS: Archive (RET-01 Retention-Objektmodell, RET-02 Fristen-
Engine, RET-05 Modul-Adapter) existiert zum Zeitpunkt dieser Kachel NICHT
als Code — nur als Planung in archive-kanban/. Diese Kachel implementiert
ausschliesslich die Core-Seite (Registrierungsaufruf gegen die Schnittstelle)
und testet sie gegen einen lokalen Fake, der den RET-05-Vertrag simuliert.
Das ist KEIN Ersatz fuer eine echte Integrationspruefung gegen Archive.
Core implementiert bewusst keine eigene Loeschlogik fuer Audit-Eintraege
(Akzeptanzkriterium 3) — es gibt in diesem Paket keinen Delete-Codepfad
ausser dem durch AUD-02 technisch unterbundenen.
Pruefungen:
1. Registrierung bei Archive erfolgreich getestet, Objekttyp taucht in
Archives Retention-Konfiguration auf — NICHT durchfuehrbar, da Archive
nicht existiert. Stattdessen TestRegisterWithArchive_UsesCorrectObjectTypeAndRetention
gegen Fake: bestaetigt korrekten Aufruf mit objectType=audit_log_entry,
10 Jahre, tenantOverridable=true. Ausgefuehrt, PASS — aber die eigentliche
Pruefung bleibt OFFEN bis Archive RET-05 existiert.
2. Audit-Eintrag mit abgelaufener Frist wird von Archive korrekt als
loeschfaellig markiert, Core greift nicht ein — NICHT durchfuehrbar ohne
Archive RET-02. Offen.
3. Legal Hold aus Archive verhindert Loeschung trotz abgelaufener Frist —
NICHT durchfuehrbar ohne Archive RET-01/02. Offen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Append-only per Trigger (nicht nur GRANT/REVOKE): audit_events_prevent_mutation()
wirft bei jedem UPDATE/DELETE auf audit_events eine Exception, unabhaengig
von der verbindenden Rolle (Akzeptanzkriterium 1).
internal/audit/four_eyes.go: Vier-Augen-Prinzip fuer sicherheitskritische
Entscheidungen (Loeschbestaetigung, Rechtevergabe), 1:1 nach archivdms-
Vorbild. Request erzeugt einen Klartext-Code (wird ausserhalb des Systems an
eine ZWEITE Person uebermittelt) und speichert nur dessen SHA-256-Hash.
Confirm sperrt die Zeile mit FOR UPDATE (Akzeptanzkriterium 2 — serialisiert
zwei gleichzeitige Bestaetigungsversuche, verhindert doppelte Ausfuehrung),
weist eine Bestaetigung durch dieselbe Person wie die anfordernde ab
(ErrSameActor, echtes Vier-Augen-Prinzip statt nur Code-Pruefung), und
vergleicht den Code timing-safe (Akzeptanzkriterium 3).
internal/audit/timingsafe.go: timingSafeEqual als projektweite Referenz-
implementierung (subtle.ConstantTimeCompare) fuer sicherheitsrelevante
Vergleiche — andere Module (z.B. Archive CMP-06 Freigabelinks) uebernehmen
dasselbe Muster laut IAM-02-Konvention.
Nebenbei behoben: AUD-01s eigener Test nutzte einen festen Tenant-Slug mit
DELETE-basiertem Cleanup — seit dem neuen Append-only-Trigger schlaegt dieses
Cleanup lautlos fehl, wodurch Zeilen sich ueber Testlaeufe hinweg summierten
und die Zaehl-Assertion brach. Auf eindeutigen Slug pro Lauf umgestellt
(direkte, notwendige Folge dieser Kachel, keine Umgestaltung von AUD-01
selbst).
Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Direkter UPDATE/DELETE-Versuch von der Datenbank abgewiesen —
TestAppendOnly_RejectsUpdateAndDelete: beide Operationen scheitern,
Eintrag bleibt unveraendert erhalten. PASS.
2. Vier-Augen-Prinzip mit FOR-UPDATE-Lock race-frei unter parallelen
Anfragen — TestFourEyes_ConcurrentConfirmIsRaceFree: zwei gleichzeitige
Bestaetigungsversuche fuer denselben Vorgang, genau einer erfolgreich,
der andere ErrAlreadyDecided. PASS.
3. Timing-safe Vergleich per Laufzeitmessung stichprobenartig verifiziert —
TestTimingSafeEqual_NoEarlyExitTiming: Mismatch am Anfang (603µs) vs. am
Ende (574µs) ueber 20000 Iterationen, kein Hinweis auf Short-Circuit-
Vergleich (Ratio innerhalb Faktor 3 Toleranz). PASS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
internal/audit: eigenes, strukturiertes Audit-Datenmodell (Akteur, Aktion,
Zielobjekt, Zeitpunkt, Tenant) in der Registry-DB, getrennt von jedem
allgemeinen Anwendungs-Log (eigenes Paket, eigene Tabelle audit_events,
kein Logging-Framework). Log.Record ist der EINE zentrale Schreibpfad —
es gibt keine zweite Schreibmoeglichkeit, ueber die ein Handler die
Validierung umgehen koennte.
Fehlender Tenant-Bezug wird zweifach verhindert (Akzeptanzkriterium 2):
Log.Record weist leeren TenantSlug direkt ab (ErrMissingTenant), zusaetzlich
erzwingt eine CHECK-Constraint in der Migration dasselbe auf Datenbankebene,
selbst wenn Log.Record umgangen wuerde. Mandantenuebergreifende Ereignisse
(z.B. Superadmin-Aktionen) nutzen den reservierten Wert audit.SystemTenant
statt NULL oder leerem String — es gibt keinen Weg, ganz ohne Tenant-Bezug
zu schreiben.
Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Automatisierter Test belegt genau einen Audit-Eintrag pro
sicherheitsrelevantem Vorgang — TestRecord_PersistsExactlyOneEventPerSecurityIncident
(simulierter fehlgeschlagener Login), Feldinhalte verifiziert. PASS.
2. Fehlender Tenant-Bezug durch Constraint/Test verhindert —
TestRecord_RejectsMissingTenant (App-Ebene) UND
TestConstraint_RejectsMissingTenantAtDatabaseLevel (direkter INSERT unter
Umgehung von Log.Record, durch CHECK-Constraint abgewiesen). PASS.
3. Datenmodell von zweiter Person gegen Dokumentation geprueft — NICHT
durchgefuehrt (keine zweite Person in dieser Session verfuegbar). Offen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>