107 lines
3.0 KiB
Python
107 lines
3.0 KiB
Python
|
|
import re
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
ENV_STYLE_PATTERN = re.compile(
|
||
|
|
r"(?i)(api[_-]?key|token|secret|password|passwd|credential|private[_-]?key)\s*[:=]\s*([\"\']?[^\s,\"\']+[\"\']?)",
|
||
|
|
)
|
||
|
|
URL_CREDENTIAL_PATTERN = re.compile(r"://([^:@]+):([^@]+)@")
|
||
|
|
JWT_PATTERN = re.compile(r"(eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*\.\S+)")
|
||
|
|
AUTH_HEADER_PATTERN = re.compile(r"(bearer\s+|basic\s+|api[_-]?key\s+)([a-zA-Z0-9+/=_-]+)", re.IGNORECASE)
|
||
|
|
|
||
|
|
REDACTED = "***"
|
||
|
|
|
||
|
|
MAX_SANITIZE_DEPTH = 20
|
||
|
|
|
||
|
|
SENSITIVE_CONFIG_KEYS = frozenset(
|
||
|
|
{
|
||
|
|
"app_secret",
|
||
|
|
"bot_token",
|
||
|
|
"signing_secret",
|
||
|
|
"encrypt_key",
|
||
|
|
"verification_token",
|
||
|
|
"api_key",
|
||
|
|
"access_token",
|
||
|
|
"refresh_token",
|
||
|
|
"private_key",
|
||
|
|
"webhook_secret",
|
||
|
|
"client_secret",
|
||
|
|
"secret",
|
||
|
|
"password",
|
||
|
|
"passwd",
|
||
|
|
"auth_token",
|
||
|
|
"jwt",
|
||
|
|
"credential",
|
||
|
|
"app_key",
|
||
|
|
"master_key",
|
||
|
|
"license_key",
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
_SENSITIVE_KEYWORD_RE = re.compile(
|
||
|
|
r"(?:^|[_-])("
|
||
|
|
r"app[_-]?secret|bot[_-]?token|signing[_-]?secret|encrypt[_-]?key|"
|
||
|
|
r"verification[_-]?token|api[_-]?key|access[_-]?token|refresh[_-]?token|"
|
||
|
|
r"private[_-]?key|webhook[_-]?secret|client[_-]?secret|secret|"
|
||
|
|
r"password|passwd|auth[_-]?token|jwt|credential|"
|
||
|
|
r"app[_-]?key|master[_-]?key|license[_-]?key"
|
||
|
|
r")(?:$|[_-])"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _is_sensitive_key(key: str) -> bool:
|
||
|
|
key_lower = key.lower().replace("-", "_")
|
||
|
|
if key_lower in SENSITIVE_CONFIG_KEYS:
|
||
|
|
return True
|
||
|
|
return bool(_SENSITIVE_KEYWORD_RE.search(key_lower))
|
||
|
|
|
||
|
|
|
||
|
|
def sanitize_text(text: str) -> str:
|
||
|
|
text = ENV_STYLE_PATTERN.sub(r"\1=***", text)
|
||
|
|
text = URL_CREDENTIAL_PATTERN.sub(r"://\1:***@", text)
|
||
|
|
text = JWT_PATTERN.sub(REDACTED, text)
|
||
|
|
text = AUTH_HEADER_PATTERN.sub(r"\1***", text)
|
||
|
|
return text
|
||
|
|
|
||
|
|
|
||
|
|
def sanitize_dict(data: dict, _depth: int = 0) -> dict:
|
||
|
|
if _depth > MAX_SANITIZE_DEPTH:
|
||
|
|
return {"__redacted__": REDACTED}
|
||
|
|
result = {}
|
||
|
|
for key, value in data.items():
|
||
|
|
result[key] = _sanitize_value(key, value, _depth)
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
def _sanitize_value(key: str, value: Any, _depth: int = 0) -> Any:
|
||
|
|
if _depth > MAX_SANITIZE_DEPTH:
|
||
|
|
return REDACTED
|
||
|
|
if isinstance(value, dict):
|
||
|
|
return sanitize_dict(value, _depth + 1)
|
||
|
|
if isinstance(value, list):
|
||
|
|
return [_sanitize_value("", item, _depth + 1) for item in value]
|
||
|
|
if isinstance(value, str):
|
||
|
|
if _is_sensitive_key(key):
|
||
|
|
return REDACTED
|
||
|
|
return sanitize_text(value)
|
||
|
|
return value
|
||
|
|
|
||
|
|
|
||
|
|
def sanitize_log_record(record_data: dict) -> dict:
|
||
|
|
return sanitize_dict(record_data)
|
||
|
|
|
||
|
|
|
||
|
|
def sanitize_for_json_dump(obj: Any, _depth: int = 0) -> Any:
|
||
|
|
if _depth > MAX_SANITIZE_DEPTH:
|
||
|
|
return REDACTED
|
||
|
|
if isinstance(obj, dict):
|
||
|
|
return {k: sanitize_for_json_dump(v, _depth + 1) for k, v in obj.items()}
|
||
|
|
if isinstance(obj, list):
|
||
|
|
return [sanitize_for_json_dump(v, _depth + 1) for v in obj]
|
||
|
|
if isinstance(obj, str):
|
||
|
|
return sanitize_text(obj)
|
||
|
|
return obj
|
||
|
|
|
||
|
|
|
||
|
|
sanitize_config_object = sanitize_dict
|