Четыре независимых security-audit находки:
1. rotate_proxy_ip (admin.py) отдавал str(httpx exc) клиенту и в Sentry —
mobileproxy changeip-URL несёт API-ключ в query-string. Ответ теперь
нейтральный ("changeip request failed"); sentry_scrub.py получил
composable full-text редактор секрет-подобных query-параметров
(?token=/?proxy_key=/?api_key=/... — не завязан на конкретного провайдера),
встроенный в scrub_pii_event (in-place, сохраняет event identity) — main.py
не тронут, редактор подключается автоматически через существующую ссылку.
2. Меняющие состояние /api/v1/admin/* ручки (куки, авто-логin, прокси,
настройки скрапера, bulk) были полностью исключены из user_events —
установить, кто их вызвал, было нельзя. RequestAuditMiddleware теперь
пишет `admin_action` для POST/PUT/PATCH/DELETE на /admin/* с атрибуцией
(username/ip/path/method/status), БЕЗ тела запроса (там секреты). GET
дашборды по-прежнему не логируются (design как раньше — не шумят).
3. login-событие писалось безусловно, без учёта response.status_code —
отражённая RBAC-попытка (протухший внутренний секрет / неизвестная роль /
scope-блок) была неотличима от настоящего входа. Теперь event_type
расходится на login/login_failed по фактическому статусу ответа; payload
несёт status_code. Дедуп-бакет (once/user+ip+ua+day) оставлен как есть —
разбивка на success/fail потребовала бы правки user_events.py (вне
scope); задокументировано как известный trade-off.
Побочный вопрос (шум basic_auth 401 от сканеров) не требует доп. фильтра
здесь: Caddy гейтит basic_auth ДО проксирования — трафик без валидного
X-Authenticated-User в этот код вообще не попадает.
4. POST /lead принимал estimate_id без проверки владельца — можно было
привязать заявку к чужой оценке. Переиспользован owner-or-admin guard
_assert_estimate_access из trade_in.py (тот же подход, что #690).
Тесты на каждый пункт (secret не в ответе/событии, admin_action с
атрибуцией и без body, login vs login_failed, чужой estimate_id -> 404) +
regression на существующие сьюты. Полный `pytest -q --deselect
tests/test_search_api.py::test_search_cache_hit`: 2635 passed, 8 skipped.