audit-core/tests/test_integrity.py

140 lines
4.6 KiB
Python
Raw Normal View History

import json
from audit_core.ingestion import IngestionApplication
from audit_core.integrity import GENESIS, chain_link, load_attestation, write_attestation
from audit_core.interface import AuditEvent
from audit_core.sqlite_backend import SQLiteAuditBackend
from test_ingestion import invoke
def _event(event_id: str, **kw) -> AuditEvent:
fields = dict(
event_id=event_id,
source="user-engine",
action="membership.added",
resource="membership-1",
outcome="recorded",
tenant="tenant:friendly:binky",
scope="tenant",
details={"correlation_id": "corr-1"},
observed_at="2026-08-09T00:00:00+00:00",
)
fields.update(kw)
return AuditEvent(**fields)
def _digest(event: AuditEvent) -> str:
import hashlib
return hashlib.sha256(
json.dumps(event.as_record(), sort_keys=True).encode()
).hexdigest()
def test_first_accept_sets_genesis(tmp_path):
backend = SQLiteAuditBackend(str(tmp_path / "c.db"))
event = _event("e1")
backend.accept(event, _digest(event))
row = backend.db.execute(
"SELECT chain_prev, chain_hash FROM events WHERE event_id = 'e1'"
).fetchone()
assert row[0] == GENESIS
assert row[1] == chain_link(GENESIS, _digest(event), "e1")
def test_second_accept_links(tmp_path):
backend = SQLiteAuditBackend(str(tmp_path / "c.db"))
first = _event("e1")
second = _event("e2")
backend.accept(first, _digest(first))
backend.accept(second, _digest(second))
head = backend.db.execute(
"SELECT chain_hash FROM events WHERE event_id = 'e1'"
).fetchone()[0]
prev = backend.db.execute(
"SELECT chain_prev FROM events WHERE event_id = 'e2'"
).fetchone()[0]
assert prev == head
def test_duplicate_does_not_fork(tmp_path):
backend = SQLiteAuditBackend(str(tmp_path / "c.db"))
event = _event("e1")
digest = _digest(event)
assert backend.accept(event, digest).duplicate is False
assert backend.accept(event, digest).duplicate is True
count = backend.db.execute("SELECT count(*) FROM events").fetchone()[0]
assert count == 1
assert backend.verify_chain().events == 1
def test_verify_clean_on_fresh_store(tmp_path):
backend = SQLiteAuditBackend(str(tmp_path / "c.db"))
empty = backend.verify_chain()
assert empty.intact is True
assert empty.events == 0
assert empty.head == GENESIS
first = _event("e1")
second = _event("e2")
backend.accept(first, _digest(first))
backend.accept(second, _digest(second))
report = backend.verify_chain()
assert report.intact is True
assert report.events == 2
assert report.first_break is None
def test_rewritten_payload_fails_verify(tmp_path):
backend = SQLiteAuditBackend(str(tmp_path / "c.db"))
first = _event("e1")
second = _event("e2")
backend.accept(first, _digest(first))
backend.accept(second, _digest(second))
backend.db.execute("UPDATE events SET payload_hash = 'deadbeef' WHERE event_id = 'e2'")
report = backend.verify_chain()
assert report.intact is False
assert report.first_break == "e2"
def test_attestation_mismatch(tmp_path):
backend = SQLiteAuditBackend(str(tmp_path / "c.db"))
event = _event("e1")
backend.accept(event, _digest(event))
path = tmp_path / "head.json"
write_attestation(path, backend.verify_chain())
cited = load_attestation(path)
assert backend.verify_chain(cited).attestation_match is True
cited["chain_hash"] = "f" * 64
broken = backend.verify_chain(cited)
assert broken.intact is False
assert broken.first_break == "attestation_mismatch"
assert broken.attestation_match is False
def test_attestation_still_matches_after_growth(tmp_path):
backend = SQLiteAuditBackend(str(tmp_path / "c.db"))
first = _event("e1")
backend.accept(first, _digest(first))
cited = backend.attest_chain()
second = _event("e2")
backend.accept(second, _digest(second))
report = backend.verify_chain(cited)
assert report.intact is True
assert report.attestation_match is True
assert report.events == 2
def test_http_integrity_requires_read(tmp_path):
backend = SQLiteAuditBackend(str(tmp_path / "c.db"))
app = IngestionApplication(backend, "opaque")
status, _ = invoke(app, None, path="/v1/integrity", method="GET", body=b"")
assert status.startswith("200")
event = _event("e1")
backend.accept(event, _digest(event))
status, body = invoke(app, None, path="/v1/integrity", method="GET", body=b"")
assert status.startswith("200")
assert body["intact"] is True
assert body["events"] == 1
assert "record" not in body