From cb1dd530d3fbc0020ca70ced2410b1d16d19527f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sat, 25 Jul 2026 01:23:36 +0200 Subject: [PATCH 1/4] =?UTF-8?q?fix(rollbar):=20przesta=C5=84=20zamazywa?= =?UTF-8?q?=C4=87=20linie=20kodu=20w=20tracebackach?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROLLBAR_SCRUB_FIELDS zawierało "code" — dodane świadomie, dla kodu autoryzacyjnego OAuth w POST do /o/token/. Problem: pyrollbar trzyma pod tą SAMĄ nazwą linię kodu źródłowego każdej ramki tracebacku, a ScrubRedactTransform dopasowuje ścieżkę klucza po SUFIKSIE — więc wpis trafiał w oba naraz. Od pyrollbara 1.4.0 skutek jest taki, że KAŻDY traceback w produkcyjnym Rollbarze ma wszystkie linie kodu zamazane na "****". Widać to gołym okiem w danych: item #379 (pyrollbar 1.3.0) ma "code": "sent = conn.send_messages([dict_to_email(message)])" a item #1554 (1.4.0), ten sam błąd, ma "code": "****" we wszystkich dziesięciu ramkach. Każde śledztwo zaczyna się więc bez najważniejszej informacji. Naprawa: "code" znika z ROLLBAR_SCRUB_FIELDS, a zamazywanie przejmuje ScrubKoduAutoryzacyjnego (bpp/rollbar_config.py) — podklasa ScrubTransform, która patrzy na CAŁĄ ścieżkę klucza i odpuszcza, gdy prowadzi ona przez "frames". Czyli: ("request", "POST", "code") → zamazane (sekret OAuth) ("body", "trace", "frames", 0, "code") → nietknięte (linia kodu) Transform wpinany jest w configure_rollbar(), nie w settings.ROLLBAR, żeby nie importować bpp.* na etapie ładowania ustawień. To bezpieczne: configure_rollbar biegnie z AppConfig.ready(), czyli PRZED middlewarem django-rollbar, a rollbar.init buduje łańcuch transformów tylko przy pierwszym wywołaniu. Pozostałe pola wrażliwe (password, code_verifier, refresh_token...) zostają na liście i są nadal zamazywane wszędzie, także w zmiennych lokalnych ramek — pilnuje tego osobny test. Zweryfikowane mutacją: przywrócenie "code" na listę, transform nie zamazujący nic oraz transform zamazujący wszędzie — każde łapane przez testy. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NcAqeqyqBzNEkkVnhpHDaH --- .../+rollbar-scrub-nie-zjada-kodu.bugfix.rst | 6 + src/bpp/rollbar_config.py | 51 +++++++- src/bpp/tests/test_rollbar_config.py | 112 +++++++++++++++++- src/django_bpp/settings/base.py | 6 +- 4 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 src/bpp/newsfragments/+rollbar-scrub-nie-zjada-kodu.bugfix.rst diff --git a/src/bpp/newsfragments/+rollbar-scrub-nie-zjada-kodu.bugfix.rst b/src/bpp/newsfragments/+rollbar-scrub-nie-zjada-kodu.bugfix.rst new file mode 100644 index 000000000..24f1db923 --- /dev/null +++ b/src/bpp/newsfragments/+rollbar-scrub-nie-zjada-kodu.bugfix.rst @@ -0,0 +1,6 @@ +Zgłoszenia błędów wysyłane do monitoringu znów zawierają linie kodu +w miejscu awarii. Reguła maskowania danych wrażliwych obejmowała pole o nazwie +``code`` — a pod tą samą nazwą biblioteka monitoringu przechowuje linię kodu +źródłowego każdej ramki śladu wywołań. Od jednej z ostatnich aktualizacji +skutkowało to zamazaniem całych śladów wywołań, co utrudniało diagnostykę. +Kod autoryzacyjny OAuth jest nadal maskowany. diff --git a/src/bpp/rollbar_config.py b/src/bpp/rollbar_config.py index d4d3de941..88a4a3591 100644 --- a/src/bpp/rollbar_config.py +++ b/src/bpp/rollbar_config.py @@ -1,5 +1,6 @@ import rollbar from django.conf import settings +from rollbar.lib.transforms.scrub import ScrubTransform # Wyjątki, które Rollbar domyślnie rozbija na wiele itemów, bo zmienna treść # w tracebacku (np. wyrenderowany raport z nazwiskiem autora w zmiennej @@ -10,6 +11,37 @@ } +class ScrubKoduAutoryzacyjnego(ScrubTransform): + """Zamazuje pole ``code``, ale NIE w ramkach stosu. + + Problem: ``code`` to jednocześnie nazwa parametru OAuth (kod autoryzacyjny + w POST do ``/o/token/``, do zamazania) i nazwa pola, w którym pyrollbar + trzyma LINIĘ KODU ŹRÓDŁOWEGO każdej ramki tracebacku (do zachowania). + + ``ScrubRedactTransform`` dopasowuje ścieżkę klucza po SUFIKSIE, więc + ``"code"`` na liście ``scrub_fields`` trafiał w oba naraz. Od pyrollbara + 1.4.0 skutkowało to tym, że KAŻDY traceback w Rollbarze miał wszystkie + linie kodu zamazane na ``"****"`` — czyli każde śledztwo zaczynało się bez + najważniejszej informacji. (Porównaj item #379 na pyrollbarze 1.3.0, gdzie + kod jest widoczny, z #1554 na 1.4.0, gdzie już nie.) + + Rozwiązanie: ``"code"`` znika z ``ROLLBAR_SCRUB_FIELDS``, a zamazywanie + przejmuje ten transform, który patrzy na CAŁĄ ścieżkę klucza i odpuszcza, + gdy prowadzi ona przez ``frames`` — czyli przez traceback. + + Pozostałe pola (``password``, ``code_verifier``, ``refresh_token`` itd.) + zostają na liście ``scrub_fields`` i są nadal zamazywane wszędzie, także + w zmiennych lokalnych ramek. + """ + + def in_scrub_fields(self, key): + if not key or key[-1] != "code": + return False + # ("body", "trace", "frames", 0, "code") → linia kodu, zostawiamy. + # ("request", "POST", "code") → sekret OAuth, zamazujemy. + return "frames" not in key + + def add_hostname_to_payload(payload, **kw): """ Global payload handler that adds DJANGO_BPP_HOSTNAME to all Rollbar payloads. @@ -54,6 +86,23 @@ def collapse_noisy_fingerprints(payload, **kw): _initialized = False +def ustawienia_rollbara(): + """``settings.ROLLBAR`` wzbogacone o nasze własne transformy payloadu. + + Transform dokładamy TUTAJ, a nie w ``settings.ROLLBAR``, żeby nie + importować ``bpp.*`` na etapie ładowania ustawień — ``configure_rollbar`` + i tak biegnie z ``AppConfig.ready()`` (patrz ``bpp/apps.py``), czyli PRZED + inicjalizacją middleware'u django-rollbar. To istotne: ``rollbar.init`` + buduje łańcuch transformów tylko przy PIERWSZYM wywołaniu, więc gdyby + ubiegł nas middleware, nasz transform nigdy by nie wszedł. + """ + ustawienia = dict(settings.ROLLBAR) + wlasne = list(ustawienia.get("custom_transforms") or []) + wlasne.append(ScrubKoduAutoryzacyjnego(redact_char="*")) + ustawienia["custom_transforms"] = wlasne + return ustawienia + + def configure_rollbar(): """ Initialize Rollbar and register the hostname payload handler. @@ -63,7 +112,7 @@ def configure_rollbar(): if _initialized: return - rollbar.init(**settings.ROLLBAR) + rollbar.init(**ustawienia_rollbara()) rollbar.events.add_payload_handler(add_hostname_to_payload) # PO hostname: collapse_noisy_fingerprints czyta hosta z custom. rollbar.events.add_payload_handler(collapse_noisy_fingerprints) diff --git a/src/bpp/tests/test_rollbar_config.py b/src/bpp/tests/test_rollbar_config.py index ad8c3c5a6..0af67542b 100644 --- a/src/bpp/tests/test_rollbar_config.py +++ b/src/bpp/tests/test_rollbar_config.py @@ -1,6 +1,10 @@ """Testy payload-handlerów Rollbara (src/bpp/rollbar_config.py).""" -from bpp.rollbar_config import collapse_noisy_fingerprints +from bpp.rollbar_config import ( + ScrubKoduAutoryzacyjnego, + collapse_noisy_fingerprints, + ustawienia_rollbara, +) def _docx_payload(host="publikacje.up.lublin.pl"): @@ -91,3 +95,109 @@ def test_payload_bez_body_nie_wybucha(): result = collapse_noisy_fingerprints(payload) assert "fingerprint" not in result["data"] + + +# --- Scrub pola `code`: sekret OAuth TAK, linia kodu w tracebacku NIE ------- + + +def _przepusc_przez_scrub(fragment, klucz_startowy): + """Uruchamia łańcuch scrubujący dokładnie tak, jak robi to pyrollbar. + + ``rollbar._build_payload`` woła ``_transform`` osobno dla każdego klucza + najwyższego poziomu, zasiewając ścieżkę jako ``(klucz,)`` — dlatego ramki + stosu widzi jako ``("body", "trace", "frames", 0, "code")``, a parametry + żądania jako ``("request", "POST", "code")``. + """ + from rollbar.lib import transforms + from rollbar.lib.transforms.scrub_redact import ScrubRedactTransform + + from django_bpp.settings.base import ROLLBAR_SCRUB_FIELDS + + lancuch = [ + ScrubRedactTransform( + suffixes=[(pole,) for pole in ROLLBAR_SCRUB_FIELDS], redact_char="*" + ) + ] + list(ustawienia_rollbara()["custom_transforms"]) + + return transforms.transform(fragment, lancuch, key=(klucz_startowy,)) + + +def test_linia_kodu_w_tracebacku_nie_jest_zamazywana(): + """Regresja: od pyrollbara 1.4.0 KAŻDY traceback miał `code: "****"`. + + ``ROLLBAR_SCRUB_FIELDS`` zawierało ``"code"`` (dla parametru OAuth), a + ``ScrubRedactTransform`` dopasowuje ścieżkę klucza po SUFIKSIE — więc + trafiało też w ``body.trace.frames[*].code``, czyli linie kodu źródłowego. + Efekt: każde śledztwo w Rollbarze zaczynało się bez kodu. + """ + body = { + "trace": { + "frames": [ + { + "filename": "/app/src/bpp/models/autor.py", + "lineno": 690, + "code": "autor_str = str(self.autor) if self.autor_id else '???'", + } + ] + } + } + + out = _przepusc_przez_scrub(body, "body") + + assert out["trace"]["frames"][0]["code"] == ( + "autor_str = str(self.autor) if self.autor_id else '???'" + ) + + +def test_kod_autoryzacyjny_oauth_w_zadaniu_nadal_jest_zamazywany(): + """Druga strona kontraktu — bez niej poprawka byłaby regresją bezpieczeństwa. + + ``/o/token/`` przyjmuje ``code`` (kod autoryzacyjny OAuth) w POST. Gdyby + ten endpoint zwrócił 500, Rollbar wysłałby aktywny kod w czystej postaci. + """ + request = { + "POST": { + "code": "AKTYWNY_KOD_AUTORYZACYJNY", + "code_verifier": "TAJNY_VERIFIER", + "grant_type": "authorization_code", + } + } + + out = _przepusc_przez_scrub(request, "request") + + assert "AKTYWNY_KOD" not in out["POST"]["code"] + assert "TAJNY_VERIFIER" not in out["POST"]["code_verifier"] + # Wartość niewrażliwa zostaje nietknięta — scrub nie może być zbyt szeroki. + assert out["POST"]["grant_type"] == "authorization_code" + + +def test_pozostale_pola_wrazliwe_nadal_zamazywane_takze_w_ramkach(): + """`password` w zmiennych lokalnych ramki MUSI zniknąć — inaczej niż `code`.""" + body = { + "trace": {"frames": [{"filename": "a.py", "locals": {"password": "tajne123"}}]} + } + + out = _przepusc_przez_scrub(body, "body") + + assert out["trace"]["frames"][0]["locals"]["password"] != "tajne123" + + +def test_configure_rollbar_przekazuje_nasz_transform_do_inicjalizacji(mocker): + """Sam transform nic nie da, jeśli nie trafi do ``rollbar.init``. + + Kolejność ma znaczenie: ``rollbar.init`` buduje łańcuch transformów tylko + przy PIERWSZYM wywołaniu. ``configure_rollbar`` biegnie z + ``AppConfig.ready()``, czyli przed middlewarem django-rollbar — gdyby było + odwrotnie, nasz transform nigdy by nie wszedł. + """ + import bpp.rollbar_config as rc + + init = mocker.patch("bpp.rollbar_config.rollbar.init") + mocker.patch("bpp.rollbar_config.rollbar.events.add_payload_handler") + mocker.patch.object(rc, "_initialized", False) + + rc.configure_rollbar() + + assert init.called + transformy = init.call_args.kwargs["custom_transforms"] + assert any(isinstance(t, ScrubKoduAutoryzacyjnego) for t in transformy) diff --git a/src/django_bpp/settings/base.py b/src/django_bpp/settings/base.py index 67d4589ca..0dfaefcf0 100644 --- a/src/django_bpp/settings/base.py +++ b/src/django_bpp/settings/base.py @@ -1738,7 +1738,11 @@ def iter_namespace(ns_pkg): "token", "refresh_token", "refreshToken", - "code", + # UWAGA: "code" celowo NIE jest tutaj. pyrollbar trzyma pod tą nazwą także + # LINIĘ KODU ŹRÓDŁOWEGO każdej ramki tracebacku, a dopasowanie idzie po + # sufiksie ścieżki klucza — więc wpis na tej liście zamazywał wszystkie + # tracebacki na "****". Kod autoryzacyjny OAuth zamazuje zamiast tego + # bpp.rollbar_config.ScrubKoduAutoryzacyjnego, który pomija ramki stosu. "code_verifier", "codeVerifier", "client_secret", From 52212296c08fdc1fe7d1217e68d4238e50d14bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sat, 25 Jul 2026 01:44:37 +0200 Subject: [PATCH 2/4] =?UTF-8?q?fix(rollbar):=20zamknij=20trzy=20wycieki,?= =?UTF-8?q?=20kt=C3=B3re=20otworzy=C5=82a=20poprzednia=20wersja=20poprawki?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review bezpieczeństwa wykazał, że pierwszy commit tego PR-a otwierał kanały wycieku kodu autoryzacyjnego, które WCZEŚNIEJ były zamknięte. Wszystkie trzy potwierdzone empirycznie na prawdziwym łańcuchu transformów. 1. ?code= w URL-ach przestawało być czyszczone. Wbudowany ScrubUrlTransform dostaje params_to_scrub=SETTINGS["scrub_fields"], więc zdjęcie stamtąd "code" odbierało mu wiedzę o tym parametrze — i to WSZĘDZIE, bo jego in_scrub_fields zwraca True dla każdego stringa (URL, Referer, zmienne lokalne). To nie było teoretyczne: /orcid/callback/ przyjmuje kod autoryzacyjny w query stringu, a pyrollbar zapisuje pełny request.build_absolute_uri(). Naprawa: własny ScrubUrlTransform znający "code". 2. frames[N].locals.code i frames[N].kwargs.code przestawały być czyszczone — czyli DOKŁADNIE ten sekret, dla którego "code" trafiło na listę. W django-oauth-toolkit `code` jest parametrem kilkunastu metod walidatora (validate_code, save_authorization_code, invalidate_authorization_code...), więc wyjątek w którejkolwiek wystawiłby aktywny kod. 3. Warunek `"frames" not in key` dawał się obejść: request.POST.frames.code oraz custom.frames[0].code przechodziły nietknięte. Wyjątek jest teraz DOKŁADNĄ listą dwóch ścieżek, pod którymi pyrollbar trzyma linię kodu, a dopasowanie nazwy klucza wróciło do case-insensitive (bez tego POST.Code wyciekało). Sprostowania: - Twierdzenie "od pyrollbara 1.4.0" było FAŁSZYWE. 1.3.0 zachowuje się identycznie. Prawdziwa przyczyna to commit 13ce70be3 z 2026-07-11, który dodał "code" do ROLLBAR_SCRUB_FIELDS; różnica wersji notifiera między itemami #379 i #1554 to korelacja, nie przyczyna. Poprawione w docstringu i newsfragmencie. - Komentarz przy ROLLBAR_SCRUB_FIELDS twierdził "NIE po sufiksie" — nieprawda od początku (build_key_matcher(..., type="suffix")) i to właśnie ta nieprawda uśpiła czujność przy dodawaniu "code". Poprawiony, wraz z ostrzeżeniem o sprzężeniu ze ScrubUrlTransform. Testy jadą teraz PRAWDZIWYM łańcuchem (rollbar.init + _build_payload) zamiast jego rekonstrukcji — poprzednia wersja składała listę transformów ręcznie i pomijała ScrubUrlTransform, więc świeciła na zielono przy wyciekającym URL-u. 18 testów, w tym 9 na wyciek. Każda z czterech poprawek zweryfikowana mutacją: usunięcie ScrubUrlTransform (4 failed), luźny warunek frames (3), brak case-insensitivity (1), powrót "code" na listę (2). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NcAqeqyqBzNEkkVnhpHDaH --- .../+rollbar-scrub-nie-zjada-kodu.bugfix.rst | 12 +- src/bpp/rollbar_config.py | 80 +++++-- src/bpp/tests/test_rollbar_config.py | 213 ++++++++++++------ src/django_bpp/settings/base.py | 25 +- 4 files changed, 232 insertions(+), 98 deletions(-) diff --git a/src/bpp/newsfragments/+rollbar-scrub-nie-zjada-kodu.bugfix.rst b/src/bpp/newsfragments/+rollbar-scrub-nie-zjada-kodu.bugfix.rst index 24f1db923..a428d2263 100644 --- a/src/bpp/newsfragments/+rollbar-scrub-nie-zjada-kodu.bugfix.rst +++ b/src/bpp/newsfragments/+rollbar-scrub-nie-zjada-kodu.bugfix.rst @@ -1,6 +1,6 @@ -Zgłoszenia błędów wysyłane do monitoringu znów zawierają linie kodu -w miejscu awarii. Reguła maskowania danych wrażliwych obejmowała pole o nazwie -``code`` — a pod tą samą nazwą biblioteka monitoringu przechowuje linię kodu -źródłowego każdej ramki śladu wywołań. Od jednej z ostatnich aktualizacji -skutkowało to zamazaniem całych śladów wywołań, co utrudniało diagnostykę. -Kod autoryzacyjny OAuth jest nadal maskowany. +Zgłoszenia błędów wysyłane do monitoringu znów zawierają linie kodu w miejscu +awarii. Reguła maskowania danych wrażliwych obejmowała pole o nazwie ``code``, +a pod tą samą nazwą biblioteka monitoringu przechowuje linię kodu źródłowego +każdej ramki śladu wywołań — od 11 lipca 2026 skutkowało to zamazywaniem +całych śladów wywołań i utrudniało diagnostykę. Kody autoryzacyjne OAuth są +nadal maskowane, również w adresach URL i zmiennych lokalnych. diff --git a/src/bpp/rollbar_config.py b/src/bpp/rollbar_config.py index 88a4a3591..aa0b4d573 100644 --- a/src/bpp/rollbar_config.py +++ b/src/bpp/rollbar_config.py @@ -1,6 +1,7 @@ import rollbar from django.conf import settings from rollbar.lib.transforms.scrub import ScrubTransform +from rollbar.lib.transforms.scruburl import ScrubUrlTransform # Wyjątki, które Rollbar domyślnie rozbija na wiele itemów, bo zmienna treść # w tracebacku (np. wyrenderowany raport z nazwiskiem autora w zmiennej @@ -10,36 +11,60 @@ "DocxConversionError", } +#: Domyślne `url_fields` pyrollbara — klucze, pod którymi spodziewa się URL-i. +#: `settings.ROLLBAR` ich nie nadpisuje, a `ScrubUrlTransform.in_scrub_fields` +#: i tak zwraca True dla każdego stringa; podajemy je dla zgodności. +URL_FIELDS = ("url", "link", "href") + class ScrubKoduAutoryzacyjnego(ScrubTransform): - """Zamazuje pole ``code``, ale NIE w ramkach stosu. + """Zamazuje pole ``code`` WSZĘDZIE POZA dwiema ścieżkami z linią kodu. Problem: ``code`` to jednocześnie nazwa parametru OAuth (kod autoryzacyjny - w POST do ``/o/token/``, do zamazania) i nazwa pola, w którym pyrollbar - trzyma LINIĘ KODU ŹRÓDŁOWEGO każdej ramki tracebacku (do zachowania). + — w POST do ``/o/token/`` oraz w GET do ``/orcid/callback/``, do + zamazania) i nazwa pola, w którym pyrollbar trzyma LINIĘ KODU ŹRÓDŁOWEGO + każdej ramki tracebacku (do zachowania). ``ScrubRedactTransform`` dopasowuje ścieżkę klucza po SUFIKSIE, więc - ``"code"`` na liście ``scrub_fields`` trafiał w oba naraz. Od pyrollbara - 1.4.0 skutkowało to tym, że KAŻDY traceback w Rollbarze miał wszystkie - linie kodu zamazane na ``"****"`` — czyli każde śledztwo zaczynało się bez - najważniejszej informacji. (Porównaj item #379 na pyrollbarze 1.3.0, gdzie - kod jest widoczny, z #1554 na 1.4.0, gdzie już nie.) - - Rozwiązanie: ``"code"`` znika z ``ROLLBAR_SCRUB_FIELDS``, a zamazywanie - przejmuje ten transform, który patrzy na CAŁĄ ścieżkę klucza i odpuszcza, - gdy prowadzi ona przez ``frames`` — czyli przez traceback. - - Pozostałe pola (``password``, ``code_verifier``, ``refresh_token`` itd.) - zostają na liście ``scrub_fields`` i są nadal zamazywane wszędzie, także - w zmiennych lokalnych ramek. + ``"code"`` na liście ``scrub_fields`` trafiał w oba naraz i zamazywał całe + tracebacki (patrz komentarz przy ``ROLLBAR_SCRUB_FIELDS``). + + Wyjątek jest zdefiniowany jako DOKŁADNA lista dwóch ścieżek, a nie jako + „ścieżka zawiera ``frames``". Luźniejszy warunek dawał się obejść — + ``request.POST.frames.code`` czy ``custom.frames[0].code`` przechodziłyby + nietknięte — a co gorsza pomijał ``frames[N].locals.code`` + i ``frames[N].kwargs.code``, czyli DOKŁADNIE ten sekret, dla którego + ``"code"`` w ogóle trafiło na listę: w django-oauth-toolkit ``code`` jest + parametrem kilkunastu metod walidatora (``validate_code``, + ``invalidate_authorization_code``, ``save_authorization_code``…), więc + wyjątek w którejkolwiek z nich wystawiłby aktywny kod w zmiennych + lokalnych ramki. """ + @staticmethod + def _czy_linia_kodu_ramki(key): + """Czy to JEDNA z dwóch ścieżek, pod którymi pyrollbar trzyma kod. + + Kształty zrzucone z działającego łańcucha transformów: + ``("body", "trace", "frames", , "code")`` oraz + ``("body", "trace_chain", , "frames", , "code")``. + """ + if len(key) == 5 and key[:3] == ("body", "trace", "frames"): + return isinstance(key[3], int) + if len(key) == 6 and key[:2] == ("body", "trace_chain"): + return ( + isinstance(key[2], int) + and key[3] == "frames" + and isinstance(key[4], int) + ) + return False + def in_scrub_fields(self, key): - if not key or key[-1] != "code": + # Case-insensitive jak `build_key_matcher` pyrollbara — bez tego + # `POST.Code` / `POST.CODE` przestałyby być zamazywane. + if not key or str(key[-1]).lower() != "code": return False - # ("body", "trace", "frames", 0, "code") → linia kodu, zostawiamy. - # ("request", "POST", "code") → sekret OAuth, zamazujemy. - return "frames" not in key + return not self._czy_linia_kodu_ramki(tuple(key)) def add_hostname_to_payload(payload, **kw): @@ -97,8 +122,23 @@ def ustawienia_rollbara(): ubiegł nas middleware, nasz transform nigdy by nie wszedł. """ ustawienia = dict(settings.ROLLBAR) + pola = list(ustawienia.get("scrub_fields") or []) + wlasne = list(ustawienia.get("custom_transforms") or []) wlasne.append(ScrubKoduAutoryzacyjnego(redact_char="*")) + # Wbudowany ScrubUrlTransform pyrollbara czyści parametry w URL-ach na + # podstawie `scrub_fields` (`params_to_scrub=SETTINGS['scrub_fields']`), + # więc zdjęcie stamtąd "code" odebrałoby mu wiedzę o TYM parametrze — + # a `?code=` w URL-u to realny wektor: /orcid/callback/ dostaje kod + # autoryzacyjny w query stringu, a pyrollbar zapisuje pełny + # `request.build_absolute_uri()` (także w nagłówku Referer i w zmiennych + # lokalnych). Dokładamy więc własny ScrubUrlTransform, który zna "code". + wlasne.append( + ScrubUrlTransform( + suffixes=[(pole,) for pole in URL_FIELDS], + params_to_scrub=pola + ["code"], + ) + ) ustawienia["custom_transforms"] = wlasne return ustawienia diff --git a/src/bpp/tests/test_rollbar_config.py b/src/bpp/tests/test_rollbar_config.py index 0af67542b..d2308ca61 100644 --- a/src/bpp/tests/test_rollbar_config.py +++ b/src/bpp/tests/test_rollbar_config.py @@ -1,10 +1,9 @@ """Testy payload-handlerów Rollbara (src/bpp/rollbar_config.py).""" -from bpp.rollbar_config import ( - ScrubKoduAutoryzacyjnego, - collapse_noisy_fingerprints, - ustawienia_rollbara, -) +import pytest +from rollbar.lib.transforms.scruburl import ScrubUrlTransform + +from bpp.rollbar_config import collapse_noisy_fingerprints def _docx_payload(host="publikacje.up.lublin.pl"): @@ -98,97 +97,179 @@ def test_payload_bez_body_nie_wybucha(): # --- Scrub pola `code`: sekret OAuth TAK, linia kodu w tracebacku NIE ------- +# +# UWAGA METODOLOGICZNA: te testy jadą PRAWDZIWYM łańcuchem transformów +# pyrollbara (`rollbar.init` + `rollbar._build_payload`), a nie jego +# rekonstrukcją. Wcześniejsza wersja składała listę transformów ręcznie +# i przez to POMIJAŁA `ScrubUrlTransform` — a właśnie tam siedział najgroźniejszy +# wyciek (`?code=` w URL-u). Testy świeciły na zielono przy dziurawym kodzie. -def _przepusc_przez_scrub(fragment, klucz_startowy): - """Uruchamia łańcuch scrubujący dokładnie tak, jak robi to pyrollbar. +@pytest.fixture +def zbuduj_payload(monkeypatch): + """Zwraca funkcję ``data -> payload`` przepuszczony przez pełny pyrollbar.""" + import rollbar - ``rollbar._build_payload`` woła ``_transform`` osobno dla każdego klucza - najwyższego poziomu, zasiewając ścieżkę jako ``(klucz,)`` — dlatego ramki - stosu widzi jako ``("body", "trace", "frames", 0, "code")``, a parametry - żądania jako ``("request", "POST", "code")``. - """ - from rollbar.lib import transforms - from rollbar.lib.transforms.scrub_redact import ScrubRedactTransform + from bpp.rollbar_config import ustawienia_rollbara + + monkeypatch.setattr(rollbar, "_initialized", False) + monkeypatch.setattr(rollbar, "send_payload", lambda p, t: None) - from django_bpp.settings.base import ROLLBAR_SCRUB_FIELDS + ustawienia = ustawienia_rollbara() + ustawienia["access_token"] = "atrapa" + ustawienia["environment"] = "test" + ustawienia["handler"] = "blocking" + ustawienia["suppress_reinit_warning"] = True + rollbar.init(**ustawienia) - lancuch = [ - ScrubRedactTransform( - suffixes=[(pole,) for pole in ROLLBAR_SCRUB_FIELDS], redact_char="*" - ) - ] + list(ustawienia_rollbara()["custom_transforms"]) + return lambda data: rollbar._build_payload(data)["data"] - return transforms.transform(fragment, lancuch, key=(klucz_startowy,)) +SEKRET = "AUTHCODE_SUPERSECRET_XYZ" +LINIA_KODU = "autor_str = str(self.autor) if self.autor_id else '???'" -def test_linia_kodu_w_tracebacku_nie_jest_zamazywana(): - """Regresja: od pyrollbara 1.4.0 KAŻDY traceback miał `code: "****"`. + +def test_linia_kodu_w_tracebacku_nie_jest_zamazywana(zbuduj_payload): + """Regresja: całe tracebacki w Rollbarze miały `code: "****"`. ``ROLLBAR_SCRUB_FIELDS`` zawierało ``"code"`` (dla parametru OAuth), a ``ScrubRedactTransform`` dopasowuje ścieżkę klucza po SUFIKSIE — więc trafiało też w ``body.trace.frames[*].code``, czyli linie kodu źródłowego. - Efekt: każde śledztwo w Rollbarze zaczynało się bez kodu. """ - body = { - "trace": { - "frames": [ - { - "filename": "/app/src/bpp/models/autor.py", - "lineno": 690, - "code": "autor_str = str(self.autor) if self.autor_id else '???'", - } - ] - } - } + out = zbuduj_payload( + {"body": {"trace": {"frames": [{"filename": "a.py", "code": LINIA_KODU}]}}} + ) + + assert out["body"]["trace"]["frames"][0]["code"] == LINIA_KODU - out = _przepusc_przez_scrub(body, "body") - assert out["trace"]["frames"][0]["code"] == ( - "autor_str = str(self.autor) if self.autor_id else '???'" +def test_linia_kodu_w_trace_chain_tez_nie_jest_zamazywana(zbuduj_payload): + """Wyjątki łańcuchowe mają inną ścieżkę klucza — też musi być pokryta.""" + out = zbuduj_payload( + { + "body": { + "trace_chain": [{"frames": [{"filename": "a.py", "code": LINIA_KODU}]}] + } + } ) + assert out["body"]["trace_chain"][0]["frames"][0]["code"] == LINIA_KODU + + +@pytest.mark.parametrize( + "opis,data,sciezka", + [ + ( + "POST /o/token/", + {"request": {"POST": {"code": SEKRET}}}, + ("request", "POST", "code"), + ), + ( + "GET /orcid/callback/", + {"request": {"GET": {"code": SEKRET}}}, + ("request", "GET", "code"), + ), + ( + "inna wielkosc liter", + {"request": {"POST": {"Code": SEKRET}}}, + ("request", "POST", "Code"), + ), + ( + "kolizja klucza `frames` poza tracebackiem", + {"request": {"POST": {"frames": {"code": SEKRET}}}}, + ("request", "POST", "frames", "code"), + ), + ( + "zmienna lokalna ramki (django-oauth-toolkit: validate_code)", + {"body": {"trace": {"frames": [{"locals": {"code": SEKRET}}]}}}, + ("body", "trace", "frames", 0, "locals", "code"), + ), + ( + "argument nazwany ramki", + {"body": {"trace": {"frames": [{"kwargs": {"code": SEKRET}}]}}}, + ("body", "trace", "frames", 0, "kwargs", "code"), + ), + ], +) +def test_kod_autoryzacyjny_jest_zamazywany(zbuduj_payload, opis, data, sciezka): + """Druga strona kontraktu — bez niej poprawka byłaby regresją bezpieczeństwa.""" + out = zbuduj_payload(data) + + biezacy = out + for element in sciezka: + biezacy = biezacy[element] -def test_kod_autoryzacyjny_oauth_w_zadaniu_nadal_jest_zamazywany(): - """Druga strona kontraktu — bez niej poprawka byłaby regresją bezpieczeństwa. + assert SEKRET not in str(biezacy), f"WYCIEK sekretu: {opis}" - ``/o/token/`` przyjmuje ``code`` (kod autoryzacyjny OAuth) w POST. Gdyby - ten endpoint zwrócił 500, Rollbar wysłałby aktywny kod w czystej postaci. + +@pytest.mark.parametrize( + "opis,data,sciezka", + [ + ( + "request.url", + { + "request": { + "url": f"https://bpp.example.pl/orcid/callback/?code={SEKRET}" + } + }, + ("request", "url"), + ), + ( + "naglowek Referer", + { + "request": { + "headers": {"Referer": f"https://bpp.example.pl/cb?code={SEKRET}"} + } + }, + ("request", "headers", "Referer"), + ), + ( + "URL w zmiennej lokalnej ramki", + { + "body": { + "trace": { + "frames": [{"locals": {"url": f"https://x/cb?code={SEKRET}"}}] + } + } + }, + ("body", "trace", "frames", 0, "locals", "url"), + ), + ], +) +def test_kod_autoryzacyjny_w_URL_tez_jest_zamazywany( + zbuduj_payload, opis, data, sciezka +): + """Najgroźniejszy wyciek, jaki wyszedł w self-review. + + Wbudowany ``ScrubUrlTransform`` czyści parametry URL na podstawie + ``scrub_fields`` — zdjęcie stamtąd ``"code"`` rozbroiłoby go dla tego + parametru. ``/orcid/callback/`` dostaje kod autoryzacyjny w query stringu, + a pyrollbar zapisuje pełny ``request.build_absolute_uri()``. """ - request = { - "POST": { - "code": "AKTYWNY_KOD_AUTORYZACYJNY", - "code_verifier": "TAJNY_VERIFIER", - "grant_type": "authorization_code", - } - } + out = zbuduj_payload(data) - out = _przepusc_przez_scrub(request, "request") + biezacy = out + for element in sciezka: + biezacy = biezacy[element] - assert "AKTYWNY_KOD" not in out["POST"]["code"] - assert "TAJNY_VERIFIER" not in out["POST"]["code_verifier"] - # Wartość niewrażliwa zostaje nietknięta — scrub nie może być zbyt szeroki. - assert out["POST"]["grant_type"] == "authorization_code" + assert SEKRET not in str(biezacy), f"WYCIEK sekretu w URL: {opis}" -def test_pozostale_pola_wrazliwe_nadal_zamazywane_takze_w_ramkach(): +def test_pozostale_pola_wrazliwe_nadal_zamazywane_takze_w_ramkach(zbuduj_payload): """`password` w zmiennych lokalnych ramki MUSI zniknąć — inaczej niż `code`.""" - body = { - "trace": {"frames": [{"filename": "a.py", "locals": {"password": "tajne123"}}]} - } - - out = _przepusc_przez_scrub(body, "body") + out = zbuduj_payload( + {"body": {"trace": {"frames": [{"locals": {"password": "tajne123"}}]}}} + ) - assert out["trace"]["frames"][0]["locals"]["password"] != "tajne123" + assert "tajne123" not in str(out["body"]["trace"]["frames"][0]["locals"]) -def test_configure_rollbar_przekazuje_nasz_transform_do_inicjalizacji(mocker): +def test_configure_rollbar_przekazuje_nasze_transformy_do_inicjalizacji(mocker): """Sam transform nic nie da, jeśli nie trafi do ``rollbar.init``. Kolejność ma znaczenie: ``rollbar.init`` buduje łańcuch transformów tylko przy PIERWSZYM wywołaniu. ``configure_rollbar`` biegnie z - ``AppConfig.ready()``, czyli przed middlewarem django-rollbar — gdyby było - odwrotnie, nasz transform nigdy by nie wszedł. + ``AppConfig.ready()``, czyli przed middlewarem django-rollbar. """ import bpp.rollbar_config as rc @@ -198,6 +279,6 @@ def test_configure_rollbar_przekazuje_nasz_transform_do_inicjalizacji(mocker): rc.configure_rollbar() - assert init.called transformy = init.call_args.kwargs["custom_transforms"] - assert any(isinstance(t, ScrubKoduAutoryzacyjnego) for t in transformy) + assert any(isinstance(t, rc.ScrubKoduAutoryzacyjnego) for t in transformy) + assert any(isinstance(t, ScrubUrlTransform) for t in transformy) diff --git a/src/django_bpp/settings/base.py b/src/django_bpp/settings/base.py index 0dfaefcf0..055653611 100644 --- a/src/django_bpp/settings/base.py +++ b/src/django_bpp/settings/base.py @@ -1713,12 +1713,25 @@ def iter_namespace(ns_pkg): # ROLLBAR settings # -# pyrollbar dopasowuje pola do scrubu po DOKŁADNEJ nazwie klucza (case-insensitive, -# NIE po sufiksie) i PODMIENIA swoją domyślną listę, gdy podamy własną. Dlatego -# odtwarzamy tu domyślny zestaw pyrollbara i DOKŁADAMY sekrety OAuth/MCP i PBN -# (uwaga reviewera #3/#5): DOT oznacza jako wrażliwe tylko password/client_secret, -# a Rollbar bez tej listy wysłałby aktywny refresh_token / code / code_verifier / -# token przy nieoczekiwanym 500 na /o/token/ czy /o/revoke_token/. +# pyrollbar dopasowuje pola po nazwie klucza (case-insensitive) NA DOWOLNYM +# POZIOMIE ZAGNIEŻDŻENIA: matcher jest budowany jako `type="suffix"` nad +# ŚCIEŻKĄ klucza, więc wpis trafia w każdy klucz o tej nazwie, gdziekolwiek +# w payloadzie. (Poprzedni komentarz twierdził tu „NIE po sufiksie" — to była +# nieprawda i to ona doprowadziła do zamazywania CAŁYCH tracebacków przez +# niewinnie wyglądający wpis "code"; patrz bpp.rollbar_config.) +# +# Podana lista PODMIENIA domyślną listę pyrollbara, więc odtwarzamy tu jego +# domyślny zestaw i DOKŁADAMY sekrety OAuth/MCP i PBN: DOT oznacza jako +# wrażliwe tylko password/client_secret, a Rollbar bez tej listy wysłałby +# aktywny refresh_token / code / code_verifier / token przy nieoczekiwanym 500 +# na /o/token/ czy /o/revoke_token/. +# +# UWAGA: ta lista zasila TAKŻE `ScrubUrlTransform` +# (`params_to_scrub=SETTINGS["scrub_fields"]`), czyszczący parametry w query +# stringach. Zdjęcie czegoś stąd rozbraja — dla tego pola — również czyszczenie +# URL-i, i to WSZĘDZIE (ten transform skanuje każdy string, nie tylko klucz +# "url"). Dlatego usunięcie "code" wymagało dołożenia własnego +# `ScrubUrlTransform` w bpp.rollbar_config. ROLLBAR_SCRUB_FIELDS = [ # domyślne pyrollbara (zachowujemy — nasza lista je nadpisuje): "pw", From 428a3d2ff3c3c94db66928380e879e2cbc9a5a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sat, 25 Jul 2026 02:18:00 +0200 Subject: [PATCH 3/4] =?UTF-8?q?fix(oauth=5Fmcp):=20testuj=20gwarancj=C4=99?= =?UTF-8?q?=20maskowania=20sekret=C3=B3w,=20nie=20list=C4=99=20p=C3=B3l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI wywalił test_rollbar_scrubuje_sekrety_oauth — asertował, że "code" jest na liście ROLLBAR_SCRUB_FIELDS, a ta zmiana świadomie stamtąd je zdejmuje (zamazywało też linie kodu w tracebackach). Sekret nadal JEST maskowany, tylko innym mechanizmem — test sprawdzał implementację zamiast gwarancji. Przepisany: dla każdego z ośmiu pól wrażliwych budujemy payload przez prawdziwy łańcuch pyrollbara i sprawdzamy, że wartość NIE wychodzi. Dwie ścieżki: request.POST (wymiana kodu na token) oraz frames[N].locals — django-oauth-toolkit przekazuje te sekrety jako argumenty metod walidatora, a to właśnie tę ścieżkę przeoczyła pierwsza wersja ScrubKoduAutoryzacyjnego. 16 asercji zamiast 8, i odporne na kolejną zmianę mechanizmu. Przy okazji: atrapy sekretów w testach dostały nazwy nieprzypominające prawdziwych tokenów (GitGuardian zapalał się na "AUTHCODE_SUPERSECRET_XYZ"). Przegapiłem ten plik, bo lokalnie puszczałem src/bpp/tests + src/django_bpp/tests. Teraz przebieg obejmuje WSZYSTKIE pliki testowe dotykające Rollbara w repo (26 plików, 397 passed). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NcAqeqyqBzNEkkVnhpHDaH --- src/bpp/tests/test_rollbar_config.py | 2 +- src/oauth_mcp/tests/test_rollbar_scrub.py | 87 ++++++++++++++++++----- 2 files changed, 69 insertions(+), 20 deletions(-) diff --git a/src/bpp/tests/test_rollbar_config.py b/src/bpp/tests/test_rollbar_config.py index d2308ca61..8de178213 100644 --- a/src/bpp/tests/test_rollbar_config.py +++ b/src/bpp/tests/test_rollbar_config.py @@ -125,7 +125,7 @@ def zbuduj_payload(monkeypatch): return lambda data: rollbar._build_payload(data)["data"] -SEKRET = "AUTHCODE_SUPERSECRET_XYZ" +SEKRET = "wartosc-ktora-ma-zniknac-z-payloadu" LINIA_KODU = "autor_str = str(self.autor) if self.autor_id else '???'" diff --git a/src/oauth_mcp/tests/test_rollbar_scrub.py b/src/oauth_mcp/tests/test_rollbar_scrub.py index 77b6bcfd5..4150343e3 100644 --- a/src/oauth_mcp/tests/test_rollbar_scrub.py +++ b/src/oauth_mcp/tests/test_rollbar_scrub.py @@ -4,24 +4,73 @@ ``password`` i ``client_secret``; pyrollbar domyślnie NIE scrubuje ``refresh_token``, ``code``, ``code_verifier`` ani ``token``. Nieoczekiwany wyjątek 500 podczas wymiany/odświeżenia/rewokacji wysłałby aktywny sekret do -Rollbara. pyrollbar dopasowuje po DOKŁADNEJ nazwie klucza (nie po sufiksie) i -PODMIENIA domyślną listę, gdy podamy własną — więc lista musi zawierać zarówno -domyślne pola, jak i te specyficzne dla OAuth. +Rollbara. + +Test sprawdza GWARANCJĘ (sekret nie wychodzi w payloadzie), a nie sposób jej +realizacji. Wcześniejsza wersja asertowała obecność nazwy pola na liście +``scrub_fields`` — czyli implementację. Gdy ``"code"`` musiało z tej listy +zniknąć (bo zamazywało też linie kodu w tracebackach — patrz +``bpp.rollbar_config.ScrubKoduAutoryzacyjnego``), test padał, choć sekret nadal +był maskowany. Asercja na gwarancję przeżyje kolejną zmianę mechanizmu. """ -from django.conf import settings - - -def test_rollbar_scrubuje_sekrety_oauth(): - scrub = {f.lower() for f in settings.ROLLBAR.get("scrub_fields", [])} - for field in ( - "refresh_token", - "code", - "code_verifier", - "token", - "access_token", - "client_secret", - "authorization", - "password", - ): - assert field in scrub, f"Rollbar nie scrubuje pola {field!r}" +import pytest + +ATRAPA = "wartosc-do-zamaskowania-w-tescie" + +POLA_WRAZLIWE = ( + "refresh_token", + "code", + "code_verifier", + "token", + "access_token", + "client_secret", + "authorization", + "password", +) + + +@pytest.fixture +def zbuduj_payload(monkeypatch): + """Zwraca funkcję ``data -> payload`` przepuszczony przez pełny pyrollbar.""" + import rollbar + + from bpp.rollbar_config import ustawienia_rollbara + + monkeypatch.setattr(rollbar, "_initialized", False) + monkeypatch.setattr(rollbar, "send_payload", lambda p, t: None) + + ustawienia = ustawienia_rollbara() + ustawienia["access_token"] = "atrapa" + ustawienia["environment"] = "test" + ustawienia["handler"] = "blocking" + ustawienia["suppress_reinit_warning"] = True + rollbar.init(**ustawienia) + + return lambda data: rollbar._build_payload(data)["data"] + + +@pytest.mark.parametrize("pole", POLA_WRAZLIWE) +def test_rollbar_maskuje_sekrety_oauth_w_parametrach_zadania(zbuduj_payload, pole): + """Sekret w POST (wymiana kodu na token) nie może opuścić serwera.""" + out = zbuduj_payload({"request": {"POST": {pole: ATRAPA}}}) + + assert ATRAPA not in str(out["request"]["POST"][pole]), ( + f"Rollbar nie maskuje pola {pole!r} w request.POST" + ) + + +@pytest.mark.parametrize("pole", POLA_WRAZLIWE) +def test_rollbar_maskuje_sekrety_oauth_w_zmiennych_lokalnych(zbuduj_payload, pole): + """django-oauth-toolkit przekazuje te sekrety jako argumenty walidatora. + + Wyjątek w ``validate_code`` / ``save_authorization_code`` wystawiłby je + w ``frames[N].locals`` — to inna ścieżka klucza niż parametry żądania, + a właśnie ją przeoczyła pierwsza wersja ``ScrubKoduAutoryzacyjnego``. + """ + out = zbuduj_payload({"body": {"trace": {"frames": [{"locals": {pole: ATRAPA}}]}}}) + + lokalne = out["body"]["trace"]["frames"][0]["locals"] + assert ATRAPA not in str(lokalne[pole]), ( + f"Rollbar nie maskuje pola {pole!r} w zmiennych lokalnych ramki" + ) From bc36384f442ef8c9b1fa85f58654ac7bcd41fadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sat, 25 Jul 2026 02:25:12 +0200 Subject: [PATCH 4/4] =?UTF-8?q?test(rollbar):=20nie=20wpisuj=20atrapy=20to?= =?UTF-8?q?kena=20=E2=80=94=20GitGuardian=20mia=C5=82=20racj=C4=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skaner sekretów w CI zgłosił "2 secrets uncovered" i wskazał dokładnie dwa moje wiersze: `ustawienia["access_token"] = "atrapa"` w dwóch plikach testowych. To były atrapy, ale skaner ma rację — wzorzec "przypisanie do access_token" jest nieodróżnialny od prawdziwego przecieku, a wyciszanie go przez allowlistę uczyłoby ignorowania właśnie tej klasy alertu. Przypisanie jest zbędne: `ustawienia_rollbara()` wnosi `access_token` z `settings.ROLLBAR` (w testach None), a wysyłka i tak jest zaślepiona podmienionym `send_payload`. Kontrola po zmianie: usunięcie własnego ScrubUrlTransform nadal wywala 4 testy, czyli suite pozostaje load-bearing. 34 passed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NcAqeqyqBzNEkkVnhpHDaH --- src/bpp/tests/test_rollbar_config.py | 6 +++++- src/oauth_mcp/tests/test_rollbar_scrub.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/bpp/tests/test_rollbar_config.py b/src/bpp/tests/test_rollbar_config.py index 8de178213..6127f8008 100644 --- a/src/bpp/tests/test_rollbar_config.py +++ b/src/bpp/tests/test_rollbar_config.py @@ -116,7 +116,11 @@ def zbuduj_payload(monkeypatch): monkeypatch.setattr(rollbar, "send_payload", lambda p, t: None) ustawienia = ustawienia_rollbara() - ustawienia["access_token"] = "atrapa" + # `access_token` NIE jest tu ustawiany: `settings.ROLLBAR` wnosi go + # z konfiguracji (w testach = None), a wysyłka i tak jest zaślepiona + # przez podmieniony `send_payload`. Wpisanie tu atrapy tokena zapalało + # skaner sekretów w CI — słusznie, bo wzorzec jest nieodróżnialny od + # prawdziwego przecieku. ustawienia["environment"] = "test" ustawienia["handler"] = "blocking" ustawienia["suppress_reinit_warning"] = True diff --git a/src/oauth_mcp/tests/test_rollbar_scrub.py b/src/oauth_mcp/tests/test_rollbar_scrub.py index 4150343e3..d7f2c6b02 100644 --- a/src/oauth_mcp/tests/test_rollbar_scrub.py +++ b/src/oauth_mcp/tests/test_rollbar_scrub.py @@ -41,7 +41,11 @@ def zbuduj_payload(monkeypatch): monkeypatch.setattr(rollbar, "send_payload", lambda p, t: None) ustawienia = ustawienia_rollbara() - ustawienia["access_token"] = "atrapa" + # `access_token` NIE jest tu ustawiany: `settings.ROLLBAR` wnosi go + # z konfiguracji (w testach = None), a wysyłka i tak jest zaślepiona + # przez podmieniony `send_payload`. Wpisanie tu atrapy tokena zapalało + # skaner sekretów w CI — słusznie, bo wzorzec jest nieodróżnialny od + # prawdziwego przecieku. ustawienia["environment"] = "test" ustawienia["handler"] = "blocking" ustawienia["suppress_reinit_warning"] = True