"""Helpers for rendering resolved config snapshots with redaction.""" from __future__ import annotations from configparser import ConfigParser _REDACT_KEY_PARTS: tuple[str, ...] = ( "password", "smtp_password", "api_key", "token", "secret", "smtp_user", # A Discord webhook URL is a bearer credential: anyone holding it can post # to the channel. Covers ``discord_webhook_urls``. "webhook", ) # Keys whose *prefix* marks a secret. [DiscordBridge] maps channels as # ``bridge. = ``, so the varying part is # the channel name and there is no fixed stem for the substring rule to catch. _REDACT_KEY_PREFIXES: tuple[str, ...] = ( "bridge.", ) def is_sensitive_key(key: str) -> bool: """Return True when a config key should be redacted.""" key_lower = key.lower() if any(key_lower.startswith(prefix) for prefix in _REDACT_KEY_PREFIXES): return True return any(part in key_lower for part in _REDACT_KEY_PARTS) def config_to_redacted_sections(config: ConfigParser) -> dict[str, dict[str, str]]: """Return config sections as key/value maps with sensitive keys redacted.""" sections: dict[str, dict[str, str]] = {} for section in config.sections(): sections[section] = { key: "●●●●●●" if is_sensitive_key(key) else value for key, value in config.items(section, raw=True) } return sections def redacted_sections_to_ini_text(sections: dict[str, dict[str, str]]) -> str: """Render redacted config sections as human-readable INI text.""" lines: list[str] = [] for idx, (section_name, options) in enumerate(sections.items()): if idx > 0: lines.append("") lines.append(f"[{section_name}]") for key, value in options.items(): lines.append(f"{key} = {value}") return "\n".join(lines)