aps-agent/server/agent_core/audit_export.py

84 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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