"""安全审计:操作日志记录、查询与不可篡改链式校验。""" from __future__ import annotations import hashlib import json import threading import time import uuid from dataclasses import dataclass, field, asdict from typing import Optional @dataclass class AuditEntry: action: str # 操作类型,如 user.create / billing.refund actor: str # 操作人 target: str = "" # 操作对象 detail: str = "" # 附加说明 ip: str = "" entry_id: str = field(default_factory=lambda: uuid.uuid4().hex) ts: float = field(default_factory=time.time) prev_hash: str = "" entry_hash: str = "" def canonical(self) -> str: d = asdict(self) d.pop("entry_hash", None) return json.dumps(d, sort_keys=True, ensure_ascii=False) class AuditLogger: """线程安全的内存审计日志(哈希链防篡改),生产可替换为 DB 存储。""" def __init__(self) -> None: self._entries: list[AuditEntry] = [] self._lock = threading.Lock() def log(self, action: str, actor: str, target: str = "", detail: str = "", ip: str = "") -> AuditEntry: with self._lock: prev = self._entries[-1].entry_hash if self._entries else "GENESIS" entry = AuditEntry(action=action, actor=actor, target=target, detail=detail, ip=ip, prev_hash=prev) entry.entry_hash = hashlib.sha256(entry.canonical().encode("utf-8")).hexdigest() self._entries.append(entry) return entry def query(self, *, actor: Optional[str] = None, action: Optional[str] = None, since: Optional[float] = None, until: Optional[float] = None) -> list[AuditEntry]: with self._lock: result = list(self._entries) if actor is not None: result = [e for e in result if e.actor == actor] if action is not None: result = [e for e in result if e.action == action] if since is not None: result = [e for e in result if e.ts >= since] if until is not None: result = [e for e in result if e.ts <= until] return result def verify_chain(self) -> bool: """校验哈希链完整性;任何条目被篡改都会返回 False""" with self._lock: entries = list(self._entries) prev = "GENESIS" for e in entries: if e.prev_hash != prev: return False if hashlib.sha256(e.canonical().encode("utf-8")).hexdigest() != e.entry_hash: return False prev = e.entry_hash return True def __len__(self) -> int: return len(self._entries)