fix: repair chained imports and track blocked evidence dependencies
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1046e-465c-7d43-8c4c-d10f0a1feadd
This commit is contained in:
parent
28b567e588
commit
146f4e1dd3
21 changed files with 869 additions and 95 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue