84 lines
3.4 KiB
Python
84 lines
3.4 KiB
Python
# ============================================================
|
||
# 审计合规导出与保留脱敏 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())
|