from __future__ import annotations import re from collections.abc import Mapping def sensitive_key_tokens(key: object) -> set[str]: value = str(key).strip() value = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", value) return {item for item in re.split(r"[^A-Za-z0-9]+", value.lower()) if item} def is_sensitive_key(key: object) -> bool: tokens = sensitive_key_tokens(key) if not tokens: return False if tokens & {"authorization", "cookie", "credential", "credentials", "password", "passwd", "secret", "token"}: return True return bool({"api", "key"} <= tokens or {"access", "key"} <= tokens or {"private", "key"} <= tokens) def redact_secret_values(value: object, *, placeholder: str = "") -> object: if isinstance(value, Mapping): result: dict[str, object] = {} for key, item in value.items(): if is_sensitive_key(key): result[str(key)] = placeholder else: result[str(key)] = redact_secret_values(item, placeholder=placeholder) return result if isinstance(value, list): return [redact_secret_values(item, placeholder=placeholder) for item in value] if isinstance(value, tuple): return tuple(redact_secret_values(item, placeholder=placeholder) for item in value) return value def contains_plain_secret(value: object) -> bool: if isinstance(value, Mapping): for key, item in value.items(): normalized_key = str(key).strip().casefold().replace("-", "_") if normalized_key in {"credential_ref", "secret_ref", "secret_reference"}: continue if is_sensitive_key(key) and item not in (None, "", {"secret_ref": ""}): # nosec B105 - empty redaction sentinels. return True if contains_plain_secret(item): return True elif isinstance(value, (list, tuple)): return any(contains_plain_secret(item) for item in value) return False