51 lines
2.0 KiB
Python
51 lines
2.0 KiB
Python
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 = "<redacted>") -> 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
|