"""Хуки before_send для GlitchTip/Sentry SDK (tradein-local, #396, #tgsupport). Redact-ит consumer-PII (client_name / client_phone / client_email и пр.) из error events до отправки в GlitchTip — estimator/trade-in flow таскает эти поля, а send_default_pii=False их не покрывает (это user-data в request.data / extra / contexts, не PII-заголовки). `redact_telegram_bot_token` — отдельный хук (#tgsupport review): Telegram Bot API токен живёт в URL-пути (`https://api.telegram.org/bot:/...`), а не в query/userinfo, поэтому НЕ покрывается sentry_sdk `sanitize_url` (тот режет только `user:pass@` и query-параметры). Токен утекает ДВУМЯ путями, которые `_scrub`/`scrub_pii_event` (ключ-based, PII-словарь) не ловят: 1. `include_local_variables=True` (sentry_sdk default) кладёт locals stack-фрейма (`self._base`, `url` в `TelegramClient._request`) в traceback → полный токен открытым текстом. 2. `HttpxIntegration` кладёт полный request URL в span `data` (виден при любом ненулевом `traces_sample_rate`), а не только в traceback. Поэтому редактор — НЕ ключ-based, а regex full-text по КАЖДОЙ строке во всём event (глубокий обход dict/list/tuple) — токен может всплыть в любом поле. """ from __future__ import annotations import re from typing import Any from sentry_sdk.types import Event _REDACTED = "[REDACTED]" # Ключи consumer-PII (нижний регистр; сверка case-insensitive). _PII_KEYS = frozenset({"client_name", "client_phone", "client_email", "phone", "email", "name"}) # Telegram Bot API токен в пути URL: /bot:/. # Матчим ровно этот сегмент (не весь URL) — сохраняет остальной путь/query # читаемым для диагностики (метод API, error code и т.п.). _TG_BOT_TOKEN_RE = re.compile(r"/bot\d+:[A-Za-z0-9_-]+") _TG_BOT_TOKEN_REPLACEMENT = "/bot[REDACTED]" # Тот же токен БЕЗ префикса `/bot` — форма `:` сама по себе # (напр. локаль `token` в конструкторе TelegramClient, или если его кто-то # засунет в log-сообщение). Сейчас единственный путь такой формы в event — # locals стек-фрейма, а они выключены через include_local_variables=False в # tgbot_main. Но именно на отказ того флага этот редактор и страхует: без этой # ветки рубеж был бы один, а не два. Формат токена BotFather: 8-12 цифр `:` 35 # символов base64url — нижние границы взяты с запасом, чтобы не промахнуться # на нестандартных id, но остаться уже, чем `\d+:\S+` (тот бил бы по любым # `id:value` в логах, напр. `chat_id:12345`). _TG_BOT_TOKEN_BARE_RE = re.compile(r"\b\d{6,12}:[A-Za-z0-9_-]{30,}\b") def _scrub(obj: Any) -> None: """Рекурсивно заменить значения PII-ключей в dict на [REDACTED] (in-place).""" if isinstance(obj, dict): for key, value in obj.items(): if isinstance(key, str) and key.lower() in _PII_KEYS: obj[key] = _REDACTED else: _scrub(value) elif isinstance(obj, list): for item in obj: _scrub(item) def scrub_pii_event(event: Event, _hint: dict[str, Any]) -> Event | None: """Redact consumer-PII из error event перед отправкой. Возвращает event (не None).""" if not isinstance(event, dict): return event request = event.get("request") if isinstance(request, dict): _scrub(request.get("data")) _scrub(event.get("extra")) _scrub(event.get("contexts")) return event def _redact_strings(obj: Any) -> Any: """Рекурсивно проходит dict/list/tuple и прогоняет обе токен-регулярки по КАЖДОЙ строке (не только по конкретным ключам) — токен может оказаться в locals stack-фрейма, span data, breadcrumb message, request.url и т.д. Возвращает НОВУЮ структуру (не мутирует `obj` — в отличие от `_scrub`, чтобы не зависеть от того, какие контейнеры sentry_sdk считает mutable в своём event dict).""" if isinstance(obj, str): redacted = _TG_BOT_TOKEN_RE.sub(_TG_BOT_TOKEN_REPLACEMENT, obj) return _TG_BOT_TOKEN_BARE_RE.sub(_REDACTED, redacted) if isinstance(obj, dict): return {k: _redact_strings(v) for k, v in obj.items()} if isinstance(obj, list): return [_redact_strings(v) for v in obj] if isinstance(obj, tuple): return tuple(_redact_strings(v) for v in obj) return obj def redact_telegram_bot_token(event: Event, _hint: dict[str, Any]) -> Event | None: """Full-text regex redaction Telegram Bot API токена по ВСЕМУ event (#tgsupport). Ловит оба вектора утечки токена в GlitchTip, которые ключ-based `scrub_pii_event` не покрывает: locals stack-фреймов (`include_local_variables=True`) и httpx-span `data` (полный request URL). Композировать с `scrub_pii_event`, не вместо него — разные классы секретов (PII полей формы vs bot-токен в URL). """ if not isinstance(event, dict): return event return _redact_strings(event) # type: ignore[return-value]