from __future__ import annotations import hashlib import uuid from datetime import UTC, datetime, timedelta, timezone from pshelpers import needs_pg from packetstation import audit, db from packetstation.settings import Settings AT = datetime(2026, 10, 15, 12, 0, 0, 123456, tzinfo=UTC) def test_canonical_is_stable_and_normalised() -> None: uid = uuid.uuid4() a = audit.canonical({"at": AT, "action": "x", "user_id": uid, "detail": {"b": 1, "a": [1, 2]}, "ip": "10.0.0.1"}) denver = AT.astimezone(timezone(timedelta(hours=-6))) b = audit.canonical( {"detail": {"a": [1, 2], "b": 1}, "ip": "10.0.0.1/32", "user_id": str(uid), "action": "x", "at": denver} ) assert a == b assert b'"at":"2026-10-15T12:00:00.123456+00:00"' in a assert b" " not in a def test_row_hash_chains() -> None: f1 = {"at": AT, "action": "one"} h1 = audit.row_hash(None, f1) assert h1 == hashlib.sha256(audit.canonical(f1)).digest() h2 = audit.row_hash(h1, {"at": AT, "action": "two"}) assert h2 == hashlib.sha256(h1 + audit.canonical({"at": AT, "action": "two"})).digest() assert audit.row_hash(None, {"at": AT, "action": "two"}) != h2 @needs_pg def test_pg_chain_verifies_and_detects_tamper(pg_settings: Settings) -> None: with db.connect_as("app", pg_settings, autocommit=True) as conn: first = audit.append( conn, action="a.one", user_id=uuid.uuid4(), detail={"n": 1, "s": "x"}, ip="10.1.2.3", packet_id=5, subject_type="packet", subject_id=5, ) second = audit.append(conn, action="a.two", detail=None) assert second.id > first.id prev = conn.execute("SELECT prev_hash FROM audit_events WHERE id = %s", (second.id,)).fetchone() assert bytes(prev[0]) == first.row_hash ok, n, detail = audit.verify_chain(conn) assert ok, detail assert n == 2 # Tamper as the superuser with the trigger disabled, then verification must fail import psycopg from pshelpers import PG_URL from psycopg.conninfo import make_conninfo assert PG_URL with psycopg.connect(make_conninfo(PG_URL, dbname=pg_settings.pg_database), autocommit=True) as su: su.execute("ALTER TABLE audit_events DISABLE TRIGGER audit_events_append_only") su.execute("UPDATE audit_events SET action = 'forged' WHERE id = %s", (first.id,)) with db.connect_as("app", pg_settings) as conn: ok, _, detail = audit.verify_chain(conn) assert not ok and "row_hash" in detail