fix: repair chained imports and track blocked evidence dependencies
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1046e-465c-7d43-8c4c-d10f0a1feadd
This commit is contained in:
tegwick 2026-10-04 11:36:06 +02:00
parent 28b567e588
commit 146f4e1dd3
21 changed files with 869 additions and 95 deletions

View file

@ -45,6 +45,7 @@ from audit_core.integrity import (
chain_link,
evaluate_tamper_evidence,
load_attestation,
order_chain_rows,
verify_rows,
)
from audit_core.redaction import Finding
@ -164,6 +165,21 @@ MIGRATIONS: list[tuple[str, str]] = [
ALTER TABLE {schema}.events ADD COLUMN IF NOT EXISTS chain_prev text;
""",
),
(
"0007-import-receipts",
"""
CREATE INDEX IF NOT EXISTS events_chain_prev_idx ON {schema}.events (chain_prev);
CREATE INDEX IF NOT EXISTS events_chain_hash_idx ON {schema}.events (chain_hash);
CREATE TABLE IF NOT EXISTS {schema}.import_receipts (
source_id text NOT NULL,
kind text NOT NULL,
record_id text NOT NULL,
content_hash text NOT NULL,
occurrences bigint NOT NULL DEFAULT 0,
PRIMARY KEY (source_id, kind, record_id)
);
""",
),
]
# Rejection reasons whose payload must never be persisted. Storing the body of
@ -380,11 +396,7 @@ class PostgresAuditBackend:
try:
with self.pool.connection() as conn, conn.transaction():
conn.execute("SELECT pg_advisory_xact_lock(%s)", (CHAIN_LOCK_KEY,))
head = conn.execute(
f"SELECT chain_hash FROM {self._events} "
"ORDER BY accepted_at DESC, event_id DESC LIMIT 1"
).fetchone()
previous = head[0] if head and head[0] else GENESIS
previous = self._chain_head(conn)
link = chain_link(previous, payload_hash, event.event_id)
inserted = conn.execute(
f"""
@ -445,7 +457,8 @@ class PostgresAuditBackend:
return [{"accepted_at": _iso(at), **rec} for rec, at in rows]
def event_counts(
self, source: str, since: str, until: str, tenant: str | None = None
self, source: str, since: str, until: str, tenant: str | None = None,
*, stream_instance_id: str | None = None
) -> list[dict]:
"""Per-class counts for one source over a bounded window.
@ -462,6 +475,10 @@ class PostgresAuditBackend:
if tenant is not None:
sql += " AND tenant = %s"
params += (tenant,)
if stream_instance_id is not None:
sql += " AND jsonb_typeof(record->'details'->'data'->'stream_instance_id') = 'string'"
sql += " AND record->'details'->'data'->>'stream_instance_id' = %s"
params += (stream_instance_id,)
sql += " GROUP BY action ORDER BY action"
return [{"class": action, "count": count} for action, count in self._query(sql, params)]
@ -566,7 +583,7 @@ class PostgresAuditBackend:
(),
)
return verify_rows(
[
order_chain_rows([
ChainRow(
event_id=r[0],
payload_hash=r[1],
@ -575,7 +592,7 @@ class PostgresAuditBackend:
accepted_at=_iso(r[4]),
)
for r in rows
],
]),
attestation=attestation,
)
@ -634,6 +651,23 @@ class PostgresAuditBackend:
except Exception as exc: # pool/libpq can raise outside psycopg.Error
raise BackendUnavailableError(str(exc)) from exc
def _chain_head(self, conn) -> str:
"""Find the unreferenced link while holding CHAIN_LOCK_KEY.
accepted_at is historical evidence, not insertion order: an imported
record may predate a record already held without rewriting its chain.
"""
heads = conn.execute(
f"SELECT e.chain_hash FROM {self._events} e WHERE NOT EXISTS ("
f"SELECT 1 FROM {self._events} successor "
"WHERE successor.chain_prev = e.chain_hash) LIMIT 2"
).fetchall()
if len(heads) == 1:
return heads[0][0]
if heads or conn.execute(f"SELECT EXISTS (SELECT 1 FROM {self._events})").fetchone()[0]:
raise BackendUnavailableError("audit chain has no unique head")
return GENESIS
def _backfill_chain(self, conn) -> None:
"""Fill chain columns on rows accepted before migration 0006.