"""Utilitaires de masquage des secrets dans les URLs, textes et exceptions. Ce module centralise la rédaction des données sensibles (tokens, mots de passe, clés d'accès) afin qu'aucun secret ne soit exposé dans les logs, les messages d'erreur ou les traces du pipeline ``pronote-sync``. """ from __future__ import annotations import re from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit _SENSITIVE_QUERY_KEYS = frozenset( { "icalsecurise", "token", "key", "password", "secret", "api_key", "apikey", "access_token", "auth", "authorization", } ) _URL_PATTERN = re.compile(r"https?://[^\s]+", re.IGNORECASE) _AUTH_HEADER_PATTERN = re.compile( r"((?:Proxy-)?Authorization)\s*[:=]\s*\S[^\r\n]*", re.IGNORECASE, ) _ISOLATED_SECRET_PATTERN = re.compile( r"\b(icalsecurise|access_token|api_key|apikey|authorization|token|password|secret|key|auth)" r"(\s*['\"]?\s*[:=]\s*)" r"(['\"]?)" r"([^\s&'\"]+)", re.IGNORECASE, ) _REDACTED = "REDACTED" _REDACTED_URL = "REDACTED_URL" def redact_url(url: str) -> str: """Masque les identifiants et les paramètres sensibles d'une URL. Les informations d'authentification du netloc (``utilisateur:motdepasse@hôte``) sont entièrement masquées (utilisateur et mot de passe), ainsi que les paramètres sensibles de la requête (ex: ``icalsecurise``). :param url: URL pouvant contenir des informations sensibles (ex: ``icalsecurise``). :return: URL avec les éléments sensibles remplacés par ``REDACTED``, ou ``REDACTED_URL`` si le traitement échoue. :rtype: str """ try: parts = urlsplit(url) if parts.username is not None or parts.password is not None: # Netloc sûr : REDACTED@hôte:port. L'utilisateur et le mot de # passe sont entièrement masqués. host = parts.hostname or "" if parts.port is not None: netloc = f"{_REDACTED}@{host}:{parts.port}" else: netloc = f"{_REDACTED}@{host}" parts = parts._replace(netloc=netloc) query: list[tuple[str, str]] = parse_qsl(parts.query, keep_blank_values=True) redacted_query = [ (key, _REDACTED if key.lower() in _SENSITIVE_QUERY_KEYS else value) for key, value in query ] return urlunsplit(parts._replace(query=urlencode(redacted_query, doseq=True))) except Exception: return _REDACTED_URL def redact_secrets(text: str) -> str: """Masque les secrets présents dans un texte arbitraire. Les URLs sont d'abord traitées par :func:`redact_url`, puis les en-têtes d'authentification (``Authorization``, ``Proxy-Authorization``) et les affectations isolées de type ``cle=valeur`` ou ``cle:valeur`` (ex: ``icalsecurise=XXX``, ``"token": "XXX"``) sont masquées, sans distinction de casse. :param text: Texte pouvant contenir des URLs ou des secrets en clair. :return: Texte avec les secrets remplacés par ``REDACTED``. :rtype: str """ redacted = _URL_PATTERN.sub(lambda match: redact_url(match.group(0)), text) redacted = _AUTH_HEADER_PATTERN.sub(r"\1: REDACTED", redacted) return _ISOLATED_SECRET_PATTERN.sub(r"\1\2\3REDACTED", redacted) def redact_exception(exc: Exception) -> str: """Masque les secrets dans la représentation textuelle d'une exception. :param exc: Exception dont le message doit être rédigé. :return: Représentation textuelle de l'exception avec les secrets masqués. :rtype: str """ return redact_secrets(str(exc))