Files
govoplan-core/src/govoplan_core/security/redaction.py
T

48 lines
1.8 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]
return value
def contains_plain_secret(value: object) -> bool:
if not isinstance(value, Mapping):
return False
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 isinstance(item, Mapping) and contains_plain_secret(item):
return True
return False