sanitizer.py 1.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
  1. from __future__ import annotations
  2. import json
  3. import re
  4. from typing import Any
  5. _SECRET_PATTERNS = (
  6. re.compile(
  7. r"(?i)([\"']?authorization[\"']?\s*[:=]\s*[\"']?)(?:(?:bearer|basic)\s+)?([^\s,\"'\]}]+)"
  8. ),
  9. re.compile(
  10. r"(?i)([\"']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|password|secret)[\"']?\s*[:=]\s*[\"']?)([^\s,\"'\]}]+)"
  11. ),
  12. re.compile(r"(?i)(mysql(?:\+pymysql)?://[^:/\s]+:)([^@\s]+)(@)"),
  13. re.compile(r"(?i)((?:postgres(?:ql)?|redis|mongodb(?:\+srv)?|amqp)://[^:/\s]+:)([^@\s]+)(@)"),
  14. )
  15. def redact_text(value: str) -> str:
  16. result = value
  17. for pattern in _SECRET_PATTERNS:
  18. if pattern.groups == 3:
  19. result = pattern.sub(r"\1***\3", result)
  20. else:
  21. result = pattern.sub(r"\1***", result)
  22. return result
  23. def sanitize(value: Any, *, max_text: int | None = 120_000) -> Any:
  24. if isinstance(value, str):
  25. redacted = redact_text(value)
  26. if max_text is not None and len(redacted) > max_text:
  27. return redacted[:max_text] + f"\n…[truncated {len(redacted) - max_text} chars]"
  28. return redacted
  29. if isinstance(value, dict):
  30. result = {}
  31. for key, nested in value.items():
  32. key_text = str(key)
  33. if re.search(r"(?i)(authorization|api[_-]?key|access[_-]?token|secret|password|credential|dsn|database[_-]?url|connection[_-]?string|data[_-]?source[_-]?url)", key_text):
  34. result[key_text] = "***"
  35. else:
  36. result[key_text] = sanitize(nested, max_text=max_text)
  37. return result
  38. if isinstance(value, (list, tuple)):
  39. return [sanitize(v, max_text=max_text) for v in value]
  40. try:
  41. json.dumps(value)
  42. return value
  43. except TypeError:
  44. return str(value)