Repository navigation
fix(sentry): scrub PII and secrets inside object reprs and nested locals, add SENTRY_SEND_DEFAULT_PII opt-in #43123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 7 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
b8a8021
fix(sentry): scrub PII and secrets inside object reprs and nested loc…
mateo-berri 77289bf
fix(sentry): keep the SDK denylist and filter the request headers a v…
mateo-berri e49eaaa
fix(sentry): leave source context lines unscrubbed
mateo-berri 4948e06
fix(sentry): filter bracketed secret values and cap the JSON walk depth
mateo-berri 2e37c28
ci(deps): install sentry-sdk in the proxy-dev group so the unit shard…
mateo-berri 861dd08
fix(sentry): scrub source-context names outside real stack frames
mateo-berri 8169f98
fix(sentry): tie the key pattern floor to the custom key minimum
mateo-berri 28ce1ad
fix(sentry): keep the key pattern floor at or below a generated key's…
mateo-berri 5dbc9fb
Merge remote-tracking branch 'origin/main' into litellm_sentry_scrub_…
mateo-berri a2bfb20
Merge remote-tracking branch 'origin/main' into litellm_sentry_scrub_…
mateo-berri File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import re | ||
| from collections.abc import Callable, Mapping, Sequence | ||
| from functools import reduce | ||
| from typing import TYPE_CHECKING, Final, TypeAlias, cast | ||
|
|
||
| from pydantic import JsonValue | ||
| from sentry_sdk.scrubber import DEFAULT_DENYLIST, DEFAULT_PII_DENYLIST, EventScrubber | ||
| from typing_extensions import ReadOnly, TypedDict | ||
|
|
||
| from litellm.constants import MINIMUM_CUSTOM_KEY_LENGTH, SENTRY_DENYLIST, SENTRY_PII_DENYLIST | ||
| from litellm.secret_managers.main import str_to_bool | ||
|
|
||
| if TYPE_CHECKING: | ||
| from sentry_sdk.types import Event, Hint | ||
|
|
||
| EventScrubFn: TypeAlias = "Callable[[Event, Hint], Event]" | ||
| JsonPath: TypeAlias = tuple[str, ...] | ||
|
|
||
| FILTERED: Final = "[Filtered]" | ||
| SEND_DEFAULT_PII_ENV: Final = "SENTRY_SEND_DEFAULT_PII" | ||
| SECRET_FIELD_NAMES: Final = tuple(DEFAULT_DENYLIST) + tuple(SENTRY_DENYLIST) | ||
| PII_FIELD_NAMES: Final = tuple(DEFAULT_PII_DENYLIST) + tuple(SENTRY_PII_DENYLIST) | ||
|
|
||
| LITELLM_KEY_PATTERN: Final = re.compile(rf"sk-[A-Za-z0-9_-]{{{MINIMUM_CUSTOM_KEY_LENGTH - len('sk-')},}}") | ||
| SOURCE_CONTEXT_KEYS: Final = frozenset({"pre_context", "context_line", "post_context"}) | ||
| STACK_FRAME_PATHS: Final = frozenset( | ||
| { | ||
| ("exception", "values", "*", "stacktrace", "frames", "*"), | ||
| ("threads", "values", "*", "stacktrace", "frames", "*"), | ||
| ("stacktrace", "frames", "*"), | ||
| } | ||
| ) | ||
| MAX_SCRUB_DEPTH: Final = 64 | ||
| EMAIL_PATTERN: Final = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}") | ||
| SHA256_HEX_PATTERN: Final = re.compile(r"(?<![0-9A-Za-z])[0-9a-f]{64}(?![0-9A-Za-z])") | ||
| QUOTED_VALUE: Final = r"'(?:[^'\\]|\\.)*'|\"(?:[^\"\\]|\\.)*\"" | ||
| BRACKET_ATOM: Final = rf"(?:{QUOTED_VALUE})|[^\[\]{{}}()'\"]" | ||
| NESTED_BRACKET_LEVELS: Final = 3 | ||
| BRACKETED_VALUE: Final = reduce( | ||
| lambda inner, _: rf"[\[{{(](?:{BRACKET_ATOM}|{inner})*[\]}})]", | ||
| range(NESTED_BRACKET_LEVELS), | ||
| rf"[\[{{(](?:{BRACKET_ATOM})*[\]}})]", | ||
| ) | ||
| BARE_VALUE: Final = r"(?!None(?![0-9A-Za-z_]))[^,)\]}\s]+" | ||
|
greptile-apps[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
|
|
||
| class SentryInitOptions(TypedDict): | ||
| dsn: ReadOnly[str | None] | ||
| traces_sample_rate: ReadOnly[float] | ||
| sample_rate: ReadOnly[float] | ||
| send_default_pii: ReadOnly[bool] | ||
| event_scrubber: ReadOnly[EventScrubber] | ||
| before_send: ReadOnly[EventScrubFn] | ||
| before_send_transaction: ReadOnly[EventScrubFn] | ||
| environment: ReadOnly[str] | ||
|
|
||
|
|
||
| def build_repr_field_pattern(field_names: Sequence[str]) -> re.Pattern[str]: | ||
| names: Final = "|".join(re.escape(name) for name in field_names) | ||
| return re.compile( | ||
| rf"(?P<field>(?<![0-9A-Za-z_])(?:{names})=|['\"](?:{names})['\"]:\s*)(?P<value>{QUOTED_VALUE}|{BRACKETED_VALUE}|{BARE_VALUE})", | ||
| re.IGNORECASE, | ||
| ) | ||
|
|
||
|
|
||
| def build_string_scrubber(send_default_pii: bool) -> Callable[[str], str]: | ||
| field_names: Final = SECRET_FIELD_NAMES if send_default_pii else SECRET_FIELD_NAMES + PII_FIELD_NAMES | ||
| field_pattern: Final = build_repr_field_pattern(field_names) | ||
| value_patterns: Final = ( | ||
| (LITELLM_KEY_PATTERN,) if send_default_pii else (LITELLM_KEY_PATTERN, EMAIL_PATTERN, SHA256_HEX_PATTERN) | ||
| ) | ||
|
|
||
| def scrub(text: str) -> str: | ||
| fields_scrubbed: Final = field_pattern.sub(_filtered_field, text) | ||
| return _substitute_all(value_patterns, fields_scrubbed) | ||
|
|
||
| return scrub | ||
|
|
||
|
|
||
| def _filtered_field(match: re.Match[str]) -> str: | ||
| quote: Final = '"' if match.group("value").startswith('"') else "'" | ||
| return f"{match.group('field')}{quote}{FILTERED}{quote}" | ||
|
|
||
|
|
||
| def _substitute_all(patterns: Sequence[re.Pattern[str]], text: str) -> str: | ||
| return reduce(lambda scrubbed, pattern: pattern.sub(FILTERED, scrubbed), patterns, text) | ||
|
|
||
|
|
||
| def scrub_json_strings(value: JsonValue, scrub: Callable[[str], str], path: JsonPath = ()) -> JsonValue: | ||
| if len(path) > MAX_SCRUB_DEPTH: | ||
| return FILTERED | ||
| if isinstance(value, str): | ||
| return scrub(value) | ||
| if isinstance(value, dict): | ||
| unscrubbed_keys: Final = SOURCE_CONTEXT_KEYS if path in STACK_FRAME_PATHS else frozenset[str]() | ||
| return { # mutable-ok: JSON object | ||
| key: item if key in unscrubbed_keys else scrub_json_strings(item, scrub, (*path, key)) | ||
| for key, item in value.items() | ||
| } | ||
| if isinstance(value, list): | ||
| return [scrub_json_strings(item, scrub, (*path, "*")) for item in value] # mutable-ok: JSON array | ||
| return value | ||
|
|
||
|
|
||
| def build_event_scrubber(send_default_pii: bool) -> EventScrubFn: | ||
| scrub: Final = build_string_scrubber(send_default_pii) | ||
|
|
||
| def scrub_event(event: Event, _hint: Hint) -> Event: | ||
| json_event: Final = cast("JsonValue", event) # cast-ok: [LIT006] the SDK serialized the event to JSON already | ||
| return cast("Event", scrub_json_strings(json_event, scrub)) # cast-ok: [LIT006] same JSON shape going back | ||
|
|
||
| return scrub_event | ||
|
|
||
|
|
||
| def send_default_pii_from_env(env: Mapping[str, str]) -> bool: | ||
| return str_to_bool(env.get(SEND_DEFAULT_PII_ENV)) is True | ||
|
|
||
|
|
||
| def build_sentry_init_options(env: Mapping[str, str]) -> SentryInitOptions: | ||
| send_default_pii: Final = send_default_pii_from_env(env) | ||
| scrub_event: Final = build_event_scrubber(send_default_pii) | ||
| return SentryInitOptions( | ||
| dsn=env.get("SENTRY_DSN"), | ||
| traces_sample_rate=float(env.get("SENTRY_API_TRACE_RATE") or "1.0"), | ||
| sample_rate=float(env.get("SENTRY_API_SAMPLE_RATE") or "1.0"), | ||
| send_default_pii=send_default_pii, | ||
| event_scrubber=EventScrubber( | ||
| denylist=list(SECRET_FIELD_NAMES), # mutable-ok: EventScrubber appends pii_denylist onto denylist in place | ||
| pii_denylist=list(PII_FIELD_NAMES), # mutable-ok: EventScrubber takes List[str] | ||
| recursive=True, | ||
| send_default_pii=send_default_pii, | ||
| ), | ||
| before_send=scrub_event, | ||
| before_send_transaction=scrub_event, | ||
| environment=env.get("SENTRY_ENVIRONMENT", "production"), | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.