diff --git a/apps/api/test/core.test.ts b/apps/api/test/core.test.ts index d1f493f..bb846dc 100644 --- a/apps/api/test/core.test.ts +++ b/apps/api/test/core.test.ts @@ -132,13 +132,22 @@ describe('Rechte', () => { }); describe('Audit', () => { - it('führt eine intakte Hash-Kette, maskiert Geheimnisse und erkennt Manipulation', async () => { + it('führt eine intakte Hash-Kette, maskiert Geheimnisse und erkennt eine eingeschleuste Fälschung', async () => { expect((await verifyAuditChain()).brokenAt).toBeNull(); const dump = JSON.stringify(await query('SELECT before_json, after_json FROM audit_events')); expect(dump).not.toMatch(/passwort-owner|correct-horse/); - const first = await one('SELECT id FROM audit_events ORDER BY id LIMIT 1 OFFSET 3'); - await run("UPDATE audit_events SET action = 'manipuliert' WHERE id = ?", [first!.id]); - expect((await verifyAuditChain()).brokenAt).toBe(first!.id); + // audit_events ist auf DB-Ebene unveränderlich (Migration 033); eine Manipulation ist daher nur noch als + // eingeschleuste Fälschung denkbar (z. B. Wiedereinspielen einer alten Sicherung neben der echten Kette), + // nicht mehr als nachträgliches UPDATE einer bestehenden Zeile. + await run("INSERT INTO audit_events (actor_type, action, result, prev_hash, hash, hash_version) VALUES ('system','gefaelscht','success', REPEAT('0',64), REPEAT('f',64), 2)"); + const fake = await one("SELECT id FROM audit_events WHERE action = 'gefaelscht'"); + expect((await verifyAuditChain()).brokenAt).toBe(fake!.id); + }); + + it('verweigert UPDATE und DELETE auf audit_events auf DB-Ebene (append-only)', async () => { + const first = await one('SELECT id FROM audit_events ORDER BY id LIMIT 1'); + await expect(run("UPDATE audit_events SET action = 'manipuliert' WHERE id = ?", [first!.id])).rejects.toThrow(/unveraenderlich/); + await expect(run('DELETE FROM audit_events WHERE id = ?', [first!.id])).rejects.toThrow(/unveraenderlich/); }); }); diff --git a/migrations/033_audit_immutable.sql b/migrations/033_audit_immutable.sql new file mode 100644 index 0000000..7fa3365 --- /dev/null +++ b/migrations/033_audit_immutable.sql @@ -0,0 +1,10 @@ +-- Nightbot-Befund #34 (Teil "eigene DB-Rolle für Audit ohne UPDATE/DELETE"): eine echte separate DB-Rolle +-- ist mit dem bestehenden Muster (audit() schreibt oft in derselben Transaktion wie die Fachaktion, die sie +-- protokolliert) nicht sauber vereinbar, ohne diese Atomarität zu verlieren. Stattdessen: Trigger, die UPDATE/ +-- DELETE auf audit_events unabhängig vom verbindenden DB-Nutzer grundsätzlich verweigern (append-only auf +-- DB-Ebene, nicht nur per Konvention im Anwendungscode). INSERT/SELECT bleiben uneingeschränkt möglich. +CREATE TRIGGER audit_events_no_update BEFORE UPDATE ON audit_events +FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'audit_events ist unveraenderlich (append-only) - UPDATE nicht erlaubt'; + +CREATE TRIGGER audit_events_no_delete BEFORE DELETE ON audit_events +FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'audit_events ist unveraenderlich (append-only) - DELETE nicht erlaubt';