SPB Git forge
1commits 1branches 0releases
189.0 KBsize
maindefault branch
5 h agolast push
Python 54.7% TypeScript 42.5% SQL 1.9%
1.6 KB · 38 lines python
Raw Blame History
1"""Immutable, hash-chained audit trail. Every risk decision, mode change, order and kill-switch flip goes here."""23from __future__ import annotations45import hashlib6import json7from typing import Any89import asyncpg1011from ktrade.logging import correlation_id121314def _hash(prev: str | None, actor: str, action: str, entity: str | None, details: dict[str, Any]) -> str:15    payload = json.dumps({"p": prev, "a": actor, "x": action, "e": entity, "d": details}, sort_keys=True, default=str)16    return hashlib.sha256(payload.encode()).hexdigest()171819async def audit(pool: asyncpg.Pool, actor: str, action: str, entity: str | None = None, details: dict[str, Any] | None = None) -> int:20    details = details or {}21    async with pool.acquire() as conn, conn.transaction():22        prev = await conn.fetchval("SELECT hash FROM audit_log ORDER BY id DESC LIMIT 1 FOR UPDATE")23        h = _hash(prev, actor, action, entity, details)24        return await conn.fetchval(25            "INSERT INTO audit_log (actor, action, entity, details, cid, prev_hash, hash) VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id",26            actor, action, entity, json.loads(json.dumps(details, default=str)), correlation_id.get(), prev, h,27        )282930async def verify_chain(pool: asyncpg.Pool, limit: int = 10000) -> tuple[bool, int]:31    rows = await pool.fetch("SELECT actor, action, entity, details, prev_hash, hash FROM audit_log ORDER BY id ASC LIMIT $1", limit)32    prev = None33    for i, r in enumerate(rows):34        if r["prev_hash"] != prev or _hash(prev, r["actor"], r["action"], r["entity"], r["details"]) != r["hash"]:35            return False, i36        prev = r["hash"]37    return True, len(rows)38