aps-agent/server/agent_core/audit_export.py

84 lines
3.4 KiB
Python
Raw Normal View History

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