diff --git a/tradein-mvp/backend/app/api/v1/auth.py b/tradein-mvp/backend/app/api/v1/auth.py index b4180e8f..cedc699d 100644 --- a/tradein-mvp/backend/app/api/v1/auth.py +++ b/tradein-mvp/backend/app/api/v1/auth.py @@ -219,10 +219,18 @@ def _saturated_429(ip: str) -> HTTPException: дублируется. Первый отказ отчитывается сразу, а не в конце окна: одиночная аномалия обязана быть видна, даже если продолжения не будет. + Уровень ERROR, а не WARNING, — не косметика: бэкенд поднят с + `LoggingIntegration(level=INFO, event_level=ERROR)` (app/main.py), то есть + ровно с ERROR запись становится событием GlitchTip, а WARNING остаётся + строкой в docker-логе, которая умирает с ротацией и редеплоем. Цена + прецедента известна (#2674): монитор писал WARNING про протухшие куки — и + событий было ноль. Спама не будет: запись не чаще раза в окно, и все они + группируются в один issue (шаблон сообщения один). + Чего это НЕ делает: у GlitchTip-проекта нет ни правил, ни получателей - (#2673), так что уведомление никому не уйдёт — след появляется в аудите и - в логе, а не в чьём-то телефоне. Проверить доставку поведенчески сейчас - нельзя, и утверждать её здесь было бы враньём. + (#2673), так что уведомление никому не уйдёт — событие будет видно в + интерфейсе, но не в чьём-то телефоне. Проверить доставку поведенчески + сейчас не на чем, и утверждать её здесь было бы враньём. `username=""` — не заглушка: имя не пишем ПОТОМУ, что отказ случился до того, как мы на него посмотрели. Записывай мы присланное, атакующий @@ -237,7 +245,7 @@ def _saturated_429(ip: str) -> HTTPException: if now - _saturation_reported_at >= _SATURATION_REPORT_WINDOW_S: rejected, _saturation_rejected = _saturation_rejected, 0 _saturation_reported_at = now - logger.warning( + logger.error( "login rejected: password verify saturated — %d отказов с прошлой записи " "(не чаще раза в %.0fс), последний ip=%s", rejected, diff --git a/tradein-mvp/backend/tests/test_auth_api.py b/tradein-mvp/backend/tests/test_auth_api.py index 8c802b87..3ff4e00a 100644 --- a/tradein-mvp/backend/tests/test_auth_api.py +++ b/tradein-mvp/backend/tests/test_auth_api.py @@ -1057,6 +1057,10 @@ def test_saturation_is_reported_once_per_window_and_lands_in_audit( lines = [r for r in caplog.records if "saturated" in r.getMessage()] assert len(lines) == 1, f"20 отказов дали {len(lines)} строк в логе — агрегации нет" + # ERROR, а не WARNING: бэкенд поднят с LoggingIntegration(event_level=ERROR) + # (app/main.py), и только с ERROR запись становится событием GlitchTip. + # Понижение уровня выключило бы канал молча — прецедент #2674. + assert lines[0].levelno == logging.ERROR saturated = [e for e in events if e["event_type"] == "login_verify_saturated"] assert len(saturated) == 1, saturated