| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105 |
- """Central redaction helpers for data-source requests, logs and responses."""
- import re
- from collections.abc import Mapping
- from urllib.parse import urlsplit, urlunsplit
- REDACTED = "[redacted]"
- SENSITIVE_KEYS = {
- "password",
- "passwd",
- "credential",
- "credentials",
- "credential_ref",
- "api_key",
- "apikey",
- "token",
- "authorization",
- "encrypted_payload",
- "nonce",
- "conn_str",
- "connection_string",
- "connection_url",
- }
- RESPONSE_SECRET_KEYS = SENSITIVE_KEYS | {"username"}
- _URL_IN_TEXT = re.compile(
- r"(?P<scheme>[a-zA-Z][a-zA-Z0-9+.-]*://)"
- r"(?P<userinfo>[^\s/@]+)@"
- )
- _NAMED_SECRET = re.compile(
- r"(?i)(password|passwd|api[_-]?key|token|authorization)"
- r"(\s*[=:]\s*)([^\s,;]+)"
- )
- def _redact_url(value: str) -> str:
- try:
- parsed = urlsplit(value)
- except (TypeError, ValueError):
- return value
- if not parsed.scheme or parsed.hostname is None or "@" not in parsed.netloc:
- return _URL_IN_TEXT.sub(r"\g<scheme>[redacted]@", value)
- host = parsed.hostname
- if ":" in host and not host.startswith("["):
- host = f"[{host}]"
- port = f":{parsed.port}" if parsed.port is not None else ""
- return urlunsplit(
- (
- parsed.scheme,
- f"{REDACTED}@{host}{port}",
- parsed.path,
- parsed.query,
- parsed.fragment,
- )
- )
- def redact_mapping(value):
- """Return a recursively redacted copy suitable for logs."""
- if isinstance(value, Mapping):
- redacted = {}
- for key, item in value.items():
- normalized = str(key).strip().lower()
- redacted[key] = (
- REDACTED
- if normalized in SENSITIVE_KEYS
- else redact_mapping(item)
- )
- return redacted
- if isinstance(value, list):
- return [redact_mapping(item) for item in value]
- if isinstance(value, tuple):
- return tuple(redact_mapping(item) for item in value)
- if isinstance(value, str):
- return _redact_url(value)
- return value
- def strip_sensitive_fields(value):
- """Remove secret field names and values from API response structures."""
- if isinstance(value, Mapping):
- return {
- key: strip_sensitive_fields(item)
- for key, item in value.items()
- if str(key).strip().lower() not in RESPONSE_SECRET_KEYS
- }
- if isinstance(value, list):
- return [strip_sensitive_fields(item) for item in value]
- if isinstance(value, tuple):
- return tuple(strip_sensitive_fields(item) for item in value)
- if isinstance(value, str):
- return _redact_url(value)
- return value
- def sanitize_exception(error: BaseException, limit: int = 1000) -> str:
- """Return a bounded error classification with credentials removed."""
- value = str(error)
- value = _URL_IN_TEXT.sub(r"\g<scheme>[redacted]@", value)
- value = _NAMED_SECRET.sub(
- lambda match: f"{match.group(1)}{match.group(2)}{REDACTED}",
- value,
- )
- return value[: max(0, int(limit))]
|