# ============================================================ # 审计合规导出与保留脱敏 v1(moduleId: core-audit-export, 可重生 ✅) # plan.md §3.6 / 矩阵「全审计」:合规导出(json/csv)+ 敏感字段脱敏 # ============================================================ from __future__ import annotations # 前向类型引用 import csv # CSV 导出 import io # 内存流 import json # JSON 序列化 from datetime import datetime # 时间戳 from typing import Any # 类型标注 REDACTED = "***REDACTED***" # 精确敏感键集合(避免子串误伤 worldKey/monkey 等字段) _SENSITIVE_KEYS = frozenset({ "token", "access_token", "refresh_token", "id_token", "session_token", "password", "passwd", "secret", "client_secret", "api_key", "apikey", "authorization", "credential", "credentials", "private_key", }) def _is_sensitive_key(key: str) -> bool: low = str(key).lower().strip() return low in _SENSITIVE_KEYS or low.endswith("_secret") def sanitize_value(value: Any) -> Any: """递归脱敏:敏感键名的值替换为掩码,其余原样保留。""" if isinstance(value, dict): return { k: (REDACTED if _is_sensitive_key(k) else sanitize_value(v)) for k, v in value.items() } if isinstance(value, list): return [sanitize_value(v) for v in value] return value def sanitize_event(event: dict[str, Any]) -> dict[str, Any]: """对单条审计事件脱敏(顶层与嵌套均覆盖敏感键)。""" return sanitize_value(dict(event)) def export_events(events: list[dict[str, Any]], fmt: str = "json") -> str: """导出当前作用域审计事件(脱敏后)。 fmt: json(列表 + 元信息)/ csv(表头 + 行)。 """ cleaned = [sanitize_event(e) for e in events] if fmt == "json": try: # 完整 Merkle 根(矩阵 104 行) from server.agent_core.audit_merkle import merkle_root merkle = merkle_root(events) except Exception: merkle = None payload = { "exportedAt": _now_iso(), "count": len(cleaned), "chainOk": None, # 调用方可通过 verify 填充 "merkleRoot": merkle, # 二叉树 Merkle 根(可复算校验) "events": cleaned, } return json.dumps(payload, ensure_ascii=False, indent=1) if fmt == "csv": buf = io.StringIO() writer = csv.writer(buf) if cleaned: writer.writerow(sorted(cleaned[0].keys())) for ev in cleaned: row = [] for key in sorted(cleaned[0].keys()): val = ev.get(key) if isinstance(val, (dict, list)): val = json.dumps(val, ensure_ascii=False, sort_keys=True, default=str) row.append("" if val is None else str(val)) writer.writerow(row) return buf.getvalue() raise ValueError(f"unsupported export format: {fmt!r}") def _now_iso() -> str: from server.timeutil import fmt_dt return fmt_dt(datetime.now())