second commit
This commit is contained in:
70
app/security/audit.py
Normal file
70
app/security/audit.py
Normal file
@ -0,0 +1,70 @@
|
||||
"""
|
||||
Manipulationssicheres, hash-verkettetes Audit-Log (siehe Konzept 4.7 / 6.1).
|
||||
|
||||
Jeder Eintrag verkettet sich kryptographisch mit seinem Vorgaenger:
|
||||
entry_hash = sha256(prev_hash || ts || event_type || details_json)
|
||||
|
||||
Nachtraegliches Aendern oder Herausloeschen eines Eintrags bricht die Kette
|
||||
ab dieser Stelle - erkennbar durch verify_chain(). Zusaetzlich verhindern
|
||||
DB-Trigger (0001_initial.sql) UPDATE/DELETE auf Anwendungsebene.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import aiosqlite
|
||||
|
||||
GENESIS_HASH = "0" * 64
|
||||
|
||||
|
||||
def _entry_hash(prev_hash: str, ts: str, event_type: str, details_json: str) -> str:
|
||||
payload = f"{prev_hash}|{ts}|{event_type}|{details_json}".encode()
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
async def write_audit_event(
|
||||
conn: aiosqlite.Connection,
|
||||
*,
|
||||
event_type: str,
|
||||
user_id: int | None,
|
||||
client_ip: str | None,
|
||||
details: dict[str, Any],
|
||||
) -> None:
|
||||
"""Schreibt einen Audit-Eintrag; haengt ihn an die bestehende Hash-Chain an.
|
||||
|
||||
Muss innerhalb derselben Transaktion wie die fachliche Aktion laufen (oder
|
||||
zumindest unmittelbar danach), damit kein Ereignis unauditiert bleibt.
|
||||
"""
|
||||
cursor = await conn.execute("SELECT entry_hash FROM audit_log ORDER BY id DESC LIMIT 1")
|
||||
row = await cursor.fetchone()
|
||||
prev_hash = row[0] if row else GENESIS_HASH
|
||||
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
details_json = json.dumps(details, sort_keys=True, ensure_ascii=False)
|
||||
entry_hash = _entry_hash(prev_hash, ts, event_type, details_json)
|
||||
|
||||
await conn.execute(
|
||||
"INSERT INTO audit_log (ts, user_id, client_ip, event_type, details_json, prev_hash, entry_hash) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(ts, user_id, client_ip, event_type, details_json, prev_hash, entry_hash),
|
||||
)
|
||||
|
||||
|
||||
async def verify_chain(conn: aiosqlite.Connection) -> tuple[bool, int | None]:
|
||||
"""Prueft die gesamte Audit-Log-Kette. Rueckgabe: (intakt?, erste kaputte id)."""
|
||||
prev_hash = GENESIS_HASH
|
||||
cursor = await conn.execute(
|
||||
"SELECT id, ts, event_type, details_json, prev_hash, entry_hash FROM audit_log ORDER BY id ASC"
|
||||
)
|
||||
async for row in cursor:
|
||||
entry_id, ts, event_type, details_json, stored_prev, stored_entry = row
|
||||
if stored_prev != prev_hash:
|
||||
return False, entry_id
|
||||
expected = _entry_hash(prev_hash, ts, event_type, details_json)
|
||||
if expected != stored_entry:
|
||||
return False, entry_id
|
||||
prev_hash = stored_entry
|
||||
return True, None
|
||||
Reference in New Issue
Block a user