redaction.py 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. """日志、请求头与运行记录脱敏(任务书 P1-B 安全要求)。
  2. 规则:
  3. - 已知 Secret / 密码 / Token 值出现即替换为 ***REDACTED***;
  4. - 敏感 Header 值只保留 AccessKey 后四位,其余整体遮蔽;
  5. - 连接串中 pwd=/password= 段遮蔽。
  6. """
  7. from __future__ import annotations
  8. import re
  9. REDACTED = "***REDACTED***"
  10. SENSITIVE_HEADERS = {
  11. "authorization", "x-signature", "x-access-key", "cookie", "set-cookie",
  12. "proxy-authorization", "x-csrf-token",
  13. }
  14. # 键名命中即整体遮蔽(用于运行记录/报告中的嵌套字段)
  15. SENSITIVE_KEY_RE = re.compile(
  16. r"(password|passwd|pwd|secret|token|signature|access[-_]?key|credential|jwt)", re.IGNORECASE)
  17. _CONN_PWD_RE = re.compile(r"(pwd|password)\s*=\s*[^;]+", re.IGNORECASE)
  18. _BEARER_RE = re.compile(r"(Bearer|Basic)\s+[A-Za-z0-9\-._~+/=]+", re.IGNORECASE)
  19. def mask_access_key(access_key: str | None) -> str:
  20. """报告中 AccessKey 只保留后四位。"""
  21. if not access_key:
  22. return ""
  23. return f"****{access_key[-4:]}" if len(access_key) > 4 else "****"
  24. def redact_headers(headers: dict, secrets: list[str] | None = None) -> dict:
  25. out = {}
  26. for k, v in headers.items():
  27. if str(k).lower() in SENSITIVE_HEADERS:
  28. out[k] = mask_access_key(str(v)) if str(k).lower() == "x-access-key" else REDACTED
  29. else:
  30. out[k] = redact_text(str(v), secrets)
  31. return out
  32. def redact_text(text: str, secrets: list[str] | None = None) -> str:
  33. if not text:
  34. return text
  35. result = text
  36. for secret in secrets or []:
  37. if secret and secret in result:
  38. result = result.replace(secret, REDACTED)
  39. result = _CONN_PWD_RE.sub(lambda m: f"{m.group(1)}={REDACTED}", result)
  40. result = _BEARER_RE.sub(lambda m: f"{m.group(1)} {REDACTED}", result)
  41. return result
  42. def redact_obj(obj, secrets: list[str] | None = None):
  43. """递归脱敏 dict/list/str,用于运行记录与报告。
  44. - 敏感 Header 名:Authorization/签名类整体遮蔽,AccessKey 只留后四位;
  45. - 键名命中 SENSITIVE_KEY_RE 的字段整体遮蔽;
  46. - 其余字符串按已知 Secret 值与连接串/Bearer 模式遮蔽。
  47. """
  48. if isinstance(obj, dict):
  49. out = {}
  50. for k, v in obj.items():
  51. key = str(k).lower()
  52. if key in SENSITIVE_HEADERS or SENSITIVE_KEY_RE.search(key):
  53. out[k] = mask_access_key(str(v)) if "access" in key and "key" in key else REDACTED
  54. else:
  55. out[k] = redact_obj(v, secrets)
  56. return out
  57. if isinstance(obj, list):
  58. return [redact_obj(v, secrets) for v in obj]
  59. if isinstance(obj, str):
  60. return redact_text(obj, secrets)
  61. return obj