fix(ptica): скраб ПДн перестаёт утекать то, что защищает + проводка проверяется поведением (#2753) #2787

Merged
bot-backend merged 2 commits from fix/2753-pii-scrub into main 2026-08-07 10:11:37 +00:00
Collaborator

Что было

Страховка try/except вокруг скраба ПДн логировала сбой через logger.exception — то есть при падении скрабера в мониторинг уходило НОВОЕ событие, в локальных переменных которого лежал неочищенный event целиком. Проводка обработчиков при этом проверялась поиском подстроки по тексту исходника.

Что сделано

1. include_local_variables=False — главное

Дефолт SDK — True, Птица его нигде не переопределяла (grep по живому контейнеру gendesign-backend-1: пусто). При ЛЮБОМ исключении кадр стека нёс значения аргументов — телефон заявки, адрес, токен — под ПРОИЗВОЛЬНЫМИ именами. Скраб сверяет ИМЕНА ключей, поэтому такое не ловит по построению: это не дополнительная мера, а условие его полноты. У МЕРЫ флаг стоит с #2737.

Проверено пробой: до фикса значение локальной переменной applicant_note доезжало до транспорта в "vars" кадра.

2. Сбой скраба не становится событием

ignore_logger(__name__) + логирование без трассировки и без str(exc) (только имя класса исключения — данных не несёт). Рекурсия из тела задачи проверена ИСПОЛНЕНИЕМ, а не рассуждением: на коде до фикса процесс не завершается, 1000+ вложенных трассировок за минуту. Диагностика осталась в stdout контейнера — текст трассировки значений переменных не печатает.

3. Проводка — по поведению, а не по подстроке

backend/tests/_sentry_wiring_probe.py поднимает настоящий sentry_sdk.init() в подпроцессе, подменяет транспорт и смотрит, что до него доехало: тело запроса (error), тело запроса (transaction), кадр стека, повторный вход при сбое скраба. Наружу не уходит ничего: DSN на несуществующий хост, capture_envelope подменён до первого события, маркеры генерируются случайно (в кадре стека едут ещё и строки исходника).

4. Подписи НДС — отдельным коммитом

Не связано с приватностью, поэтому вторым коммитом. Три места, не два (третье нашлось при проверке): сноска отрицала коммерцию, строка каскада подписана «НДС (паркинг)», и та же сноска добивала «Коммерческие и офисные площади не учитываются» — при том что сам компонент рисует строку «Выручка — нежилое (1-й этаж)» из revenue_office_rub.

Доказательство, что тест краснеет

Отдельное дерево на origin/main + новые тесты, app/ не тронут:

FAILED test_local_variables_never_reach_transport[app.main]      значение локальной переменной ушло в мониторинг
FAILED test_local_variables_never_reach_transport[app.workers.celery_app]
FAILED test_scrub_failure_does_not_spawn_second_event[app.main]  assert 4 == 1
FAILED test_scrub_failure_does_not_spawn_second_event[app.workers.celery_app]
4 failed, 2 passed

Отдельно — разрыв проводки при сохранённом комментарии (удалены строки before_send=scrub_event,, комментарий переформулирован так, что несёт те же имена аргументов):

старый грепный гейт:      2 passed   ← зелёный на разорванной проводке
новый поведенческий гейт: 2 failed

Test plan

  • pytest backend/ целиком: 4614 passed, 48 skipped, 0 failed
  • ruff check на изменённых файлах
  • красный прогон новых проверок на origin/main
  • проверка не отправляла настоящих ПДн никуда: транспорт подставной, DSN на localhost.invalid
  • после деплоя: grep include_local_variables в живом контейнере

Closes #2753

## Что было Страховка `try/except` вокруг скраба ПДн логировала сбой через `logger.exception` — то есть при падении скрабера в мониторинг уходило НОВОЕ событие, в локальных переменных которого лежал неочищенный `event` целиком. Проводка обработчиков при этом проверялась поиском подстроки по тексту исходника. ## Что сделано ### 1. `include_local_variables=False` — главное Дефолт SDK — `True`, Птица его нигде не переопределяла (`grep` по живому контейнеру `gendesign-backend-1`: пусто). При ЛЮБОМ исключении кадр стека нёс значения аргументов — телефон заявки, адрес, токен — под ПРОИЗВОЛЬНЫМИ именами. Скраб сверяет ИМЕНА ключей, поэтому такое не ловит по построению: это не дополнительная мера, а условие его полноты. У МЕРЫ флаг стоит с #2737. Проверено пробой: до фикса значение локальной переменной `applicant_note` доезжало до транспорта в `"vars"` кадра. ### 2. Сбой скраба не становится событием `ignore_logger(__name__)` + логирование без трассировки и без `str(exc)` (только имя класса исключения — данных не несёт). Рекурсия из тела задачи проверена ИСПОЛНЕНИЕМ, а не рассуждением: на коде до фикса процесс не завершается, 1000+ вложенных трассировок за минуту. Диагностика осталась в stdout контейнера — текст трассировки значений переменных не печатает. ### 3. Проводка — по поведению, а не по подстроке `backend/tests/_sentry_wiring_probe.py` поднимает настоящий `sentry_sdk.init()` в подпроцессе, подменяет транспорт и смотрит, что до него доехало: тело запроса (error), тело запроса (transaction), кадр стека, повторный вход при сбое скраба. Наружу не уходит ничего: DSN на несуществующий хост, `capture_envelope` подменён до первого события, маркеры генерируются случайно (в кадре стека едут ещё и строки исходника). ### 4. Подписи НДС — отдельным коммитом Не связано с приватностью, поэтому вторым коммитом. Три места, не два (третье нашлось при проверке): сноска отрицала коммерцию, строка каскада подписана «НДС (паркинг)», и та же сноска добивала «Коммерческие и офисные площади не учитываются» — при том что сам компонент рисует строку «Выручка — нежилое (1-й этаж)» из `revenue_office_rub`. ## Доказательство, что тест краснеет Отдельное дерево на `origin/main` + новые тесты, `app/` не тронут: ``` FAILED test_local_variables_never_reach_transport[app.main] значение локальной переменной ушло в мониторинг FAILED test_local_variables_never_reach_transport[app.workers.celery_app] FAILED test_scrub_failure_does_not_spawn_second_event[app.main] assert 4 == 1 FAILED test_scrub_failure_does_not_spawn_second_event[app.workers.celery_app] 4 failed, 2 passed ``` Отдельно — разрыв проводки при сохранённом комментарии (удалены строки `before_send=scrub_event,`, комментарий переформулирован так, что несёт те же имена аргументов): ``` старый грепный гейт: 2 passed ← зелёный на разорванной проводке новый поведенческий гейт: 2 failed ``` ## Test plan - [x] `pytest backend/` целиком: 4614 passed, 48 skipped, 0 failed - [x] `ruff check` на изменённых файлах - [x] красный прогон новых проверок на `origin/main` - [x] проверка не отправляла настоящих ПДн никуда: транспорт подставной, DSN на `localhost.invalid` - [ ] после деплоя: `grep include_local_variables` в живом контейнере Closes #2753
bot-backend added 2 commits 2026-08-07 09:46:05 +00:00
Три дыры в скрабе ПДн перед отправкой в мониторинг, и все три проверялись
поиском подстроки в исходнике — гейтом, который зелен на сломанной проводке.

1. include_local_variables=False в обеих точках входа (main.py, celery_app.py).
   Дефолт SDK — True, Птица его нигде не переопределяла: при ЛЮБОМ исключении
   кадр стека нёс значения аргументов (телефон заявки, адрес, токен) под
   произвольными именами. Скраб сверяет ИМЕНА ключей — такое он не ловит по
   построению, то есть это не дополнительная мера, а условие его полноты.
   У МЕРЫ флаг стоит с #2737.

2. Сбой самого скраба больше не уходит в мониторинг: ignore_logger на модуль +
   логирование без трассировки и без str(exc). До этого logger.exception внутри
   before_send создавал НОВОЕ событие, в локальных переменных которого лежал
   неочищенный event целиком, и это событие снова падало в тот же обработчик.
   Проверено исполнением: рекурсия не завершается, 1000+ вложенных трассировок
   за минуту. Диагностика осталась в stdout — текст трассировки значений
   переменных не печатает.

3. Проводка проверяется ПОВЕДЕНИЕМ, а не текстом файла. tests/_sentry_wiring_
   probe.py поднимает настоящий sentry_sdk.init() в подпроцессе, подменяет
   транспорт и смотрит, что до него доехало: тело запроса, транзакция, кадр
   стека, повторный вход при сбое скраба. Наружу не уходит ничего — DSN на
   несуществующий хост, capture_envelope подменён до первого события, маркеры
   случайные.

Старый гейт зелен на разорванной проводке: удалить before_send=scrub_event и
переформулировать соседний комментарий, назвав в нём тот же аргумент, — 2 passed.
Новый на том же коде — 2 failed. На коде до этого коммита новые проверки красные
(локальные переменные ушли в транспорт; сбой скраба вошёл в обработчик 4 раза).
fix(ptica/concept): подписи НДС во фронте перестают отрицать коммерцию (#2753)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 11s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m43s
CI / openapi-codegen-check (pull_request) Successful in 3m16s
CI / backend-tests (pull_request) Successful in 16m40s
3e4a5c769e
Хвост #2457: бэкенд считает НДС на нежилой value-added ОБОИХ частей — паркинг
и коммерция/офисы 1-го этажа (financial.py:747), экспортёры так и подписывают
(«НДС (паркинг + коммерция)»), а карточка концепции утверждала обратное.

Три места, не два: сноска говорила «НДС начисляется только на паркинг»,
строка каскада — «НДС (паркинг)», и та же сноска добивала «Коммерческие и
офисные площади не учитываются» — при том что двумя десятками строк выше сам
компонент рисует строку «Выручка — нежилое (1-й этаж)» из revenue_office_rub.
Третье утверждение сильнее двух первых и без него правка была бы
самопротиворечивой в пределах одного абзаца.
bot-backend merged commit c3a35a31d4 into main 2026-08-07 10:11:37 +00:00
bot-backend deleted branch fix/2753-pii-scrub 2026-08-07 10:11:38 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2787
No description provided.