Skip to content

Commit d206ea9

Browse files
ablaszkiewiczclaude
andcommitted
feat: entropy-based secret detection for exception code variables
Add a last-resort entropy-based detector that redacts high-entropy, secret-looking values (API keys, tokens, strong passwords) sitting in innocuously-named code variables, after the existing name-pattern and URL-credential checks. - Known vendor key formats (OpenAI, Anthropic, AWS, Stripe, GitHub, GitLab, Slack, Google, JWT, PEM private keys) are matched directly. - Structured identifiers (UUIDs, Mongo ObjectIds, hashes), object reprs, file paths and URLs are never flagged. - Exposed as the `code_variables_detect_secrets` option (default True) with a per-context override, threaded through client/contexts. - Tighten the masking size caps to keep capture cost bounded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 637f262 commit d206ea9

7 files changed

Lines changed: 431 additions & 18 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'pypi/posthog': patch
3+
---
4+
5+
Add entropy-based secret detection as a last-resort redaction step when capturing exception code variables. High-entropy, secret-looking values (API keys, tokens, strong passwords) sitting in innocuously-named variables are now redacted after the existing name-pattern and URL-credential checks. Well-known vendor key formats (OpenAI, Anthropic, AWS, Stripe, GitHub, GitLab, Slack, Google, JWT, PEM private keys) are matched directly, while structured identifiers (UUIDs, Mongo ObjectIds, hashes), object reprs, file paths and URLs are left intact. Adds the `code_variables_detect_secrets` option (default `True`) with a per-context override. Also tightens the code-variable size caps so capture stays bounded.

posthog/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
from posthog.contexts import (
3333
set_code_variables_mask_url_credentials_context as inner_set_code_variables_mask_url_credentials_context,
3434
)
35+
from posthog.contexts import (
36+
set_code_variables_detect_secrets_context as inner_set_code_variables_detect_secrets_context,
37+
)
3538
from posthog.contexts import (
3639
set_context_device_id as inner_set_context_device_id,
3740
)
@@ -45,6 +48,7 @@
4548
get_tags as inner_get_tags,
4649
)
4750
from posthog.exception_utils import (
51+
DEFAULT_CODE_VARIABLES_DETECT_SECRETS,
4852
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
4953
DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
5054
DEFAULT_CODE_VARIABLES_MASK_URL_CREDENTIALS,
@@ -244,6 +248,18 @@ def set_code_variables_mask_url_credentials_context(enabled: bool):
244248
return inner_set_code_variables_mask_url_credentials_context(enabled)
245249

246250

251+
def set_code_variables_detect_secrets_context(enabled: bool):
252+
"""
253+
Whether to apply entropy-based secret detection as a last-resort redaction of
254+
high-entropy values (API keys, tokens, strong passwords) in captured code
255+
variables for the current context.
256+
257+
Category:
258+
Contexts
259+
"""
260+
return inner_set_code_variables_detect_secrets_context(enabled)
261+
262+
247263
def tag(name: str, value: Any):
248264
"""
249265
Add a tag to the current context.
@@ -321,6 +337,9 @@ def get_tags() -> Dict[str, Any]:
321337
code variables.
322338
code_variables_ignore_patterns: Variable-name patterns to omit when capturing
323339
code variables.
340+
code_variables_detect_secrets: Last-resort entropy-based redaction of
341+
high-entropy secret-looking values (API keys, tokens, strong passwords)
342+
in captured code variables. Defaults to True.
324343
in_app_modules: Module/package prefixes treated as in-app frames in captured
325344
exceptions.
326345
enable_exception_autocapture_rate_limiting: Rate limit autocaptured
@@ -365,6 +384,7 @@ def get_tags() -> Dict[str, Any]:
365384
code_variables_mask_patterns = DEFAULT_CODE_VARIABLES_MASK_PATTERNS
366385
code_variables_ignore_patterns = DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS
367386
code_variables_mask_url_credentials = DEFAULT_CODE_VARIABLES_MASK_URL_CREDENTIALS
387+
code_variables_detect_secrets = DEFAULT_CODE_VARIABLES_DETECT_SECRETS
368388
in_app_modules = None # type: Optional[list[str]]
369389
enable_exception_autocapture_rate_limiting = False # type: bool
370390
exception_autocapture_bucket_size = ExceptionCapture.DEFAULT_BUCKET_SIZE # type: int
@@ -1149,6 +1169,7 @@ def setup() -> Client:
11491169
code_variables_mask_patterns=code_variables_mask_patterns,
11501170
code_variables_ignore_patterns=code_variables_ignore_patterns,
11511171
code_variables_mask_url_credentials=code_variables_mask_url_credentials,
1172+
code_variables_detect_secrets=code_variables_detect_secrets,
11521173
in_app_modules=in_app_modules,
11531174
enable_exception_autocapture_rate_limiting=enable_exception_autocapture_rate_limiting,
11541175
exception_autocapture_bucket_size=exception_autocapture_bucket_size,

posthog/client.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from posthog.contexts import (
2121
_get_current_context,
2222
get_capture_exception_code_variables_context,
23+
get_code_variables_detect_secrets_context,
2324
get_code_variables_ignore_patterns_context,
2425
get_code_variables_mask_patterns_context,
2526
get_code_variables_mask_url_credentials_context,
@@ -37,6 +38,7 @@
3738
from posthog.exception_capture import ExceptionCapture
3839
from posthog._logging import _configure_posthog_logging
3940
from posthog.exception_utils import (
41+
DEFAULT_CODE_VARIABLES_DETECT_SECRETS,
4042
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
4143
DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
4244
DEFAULT_CODE_VARIABLES_MASK_URL_CREDENTIALS,
@@ -251,6 +253,7 @@ def __init__(
251253
code_variables_mask_patterns=None,
252254
code_variables_ignore_patterns=None,
253255
code_variables_mask_url_credentials=None,
256+
code_variables_detect_secrets=None,
254257
in_app_modules: list[str] | None = None,
255258
enable_exception_autocapture_rate_limiting=False,
256259
exception_autocapture_bucket_size=ExceptionCapture.DEFAULT_BUCKET_SIZE,
@@ -319,6 +322,11 @@ def __init__(
319322
code_variables_mask_url_credentials: Scrub credentials embedded in
320323
URLs/DSNs (e.g. ``user:pass@host``) from captured code variables,
321324
regardless of the surrounding variable name. Defaults to True.
325+
code_variables_detect_secrets: Last-resort entropy-based detection that
326+
redacts high-entropy secret-looking values (API keys, tokens, strong
327+
passwords) sitting in innocuously-named variables, after the name and
328+
URL checks. Skips structured ids (UUIDs, ObjectIds, hashes). Defaults
329+
to True.
322330
in_app_modules: Module/package prefixes treated as in-app frames in
323331
captured exceptions.
324332
enable_exception_autocapture_rate_limiting: Rate limit
@@ -417,6 +425,11 @@ def __init__(
417425
if code_variables_mask_url_credentials is not None
418426
else DEFAULT_CODE_VARIABLES_MASK_URL_CREDENTIALS
419427
)
428+
self.code_variables_detect_secrets = (
429+
code_variables_detect_secrets
430+
if code_variables_detect_secrets is not None
431+
else DEFAULT_CODE_VARIABLES_DETECT_SECRETS
432+
)
420433
self.in_app_modules = in_app_modules
421434

422435
if project_root is None:
@@ -1399,6 +1412,7 @@ def capture_exception(
13991412
context_mask_url_credentials = (
14001413
get_code_variables_mask_url_credentials_context()
14011414
)
1415+
context_detect_secrets = get_code_variables_detect_secrets_context()
14021416

14031417
enabled = (
14041418
context_enabled
@@ -1420,6 +1434,11 @@ def capture_exception(
14201434
if context_mask_url_credentials is not None
14211435
else self.code_variables_mask_url_credentials
14221436
)
1437+
detect_secrets = (
1438+
context_detect_secrets
1439+
if context_detect_secrets is not None
1440+
else self.code_variables_detect_secrets
1441+
)
14231442

14241443
if enabled:
14251444
try_attach_code_variables_to_frames(
@@ -1428,6 +1447,7 @@ def capture_exception(
14281447
mask_patterns=mask_patterns,
14291448
ignore_patterns=ignore_patterns,
14301449
mask_url_credentials=mask_url_credentials,
1450+
detect_secrets=detect_secrets,
14311451
)
14321452

14331453
if self.log_captured_exceptions:

posthog/contexts.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def __init__(
2727
self.code_variables_mask_patterns: Optional[list] = None
2828
self.code_variables_ignore_patterns: Optional[list] = None
2929
self.code_variables_mask_url_credentials: Optional[bool] = None
30+
self.code_variables_detect_secrets: Optional[bool] = None
3031

3132
def set_session_id(self, session_id: str):
3233
self.session_id = session_id
@@ -52,6 +53,9 @@ def set_code_variables_ignore_patterns(self, ignore_patterns: list):
5253
def set_code_variables_mask_url_credentials(self, enabled: bool):
5354
self.code_variables_mask_url_credentials = enabled
5455

56+
def set_code_variables_detect_secrets(self, enabled: bool):
57+
self.code_variables_detect_secrets = enabled
58+
5559
def get_parent(self):
5660
return self.parent
5761

@@ -113,6 +117,13 @@ def get_code_variables_mask_url_credentials(self) -> Optional[bool]:
113117
return self.parent.get_code_variables_mask_url_credentials()
114118
return None
115119

120+
def get_code_variables_detect_secrets(self) -> Optional[bool]:
121+
if self.code_variables_detect_secrets is not None:
122+
return self.code_variables_detect_secrets
123+
if self.parent is not None and not self.fresh:
124+
return self.parent.get_code_variables_detect_secrets()
125+
return None
126+
116127

117128
_context_stack: contextvars.ContextVar[Optional[ContextScope]] = contextvars.ContextVar(
118129
"posthog_context_stack", default=None
@@ -390,6 +401,17 @@ def set_code_variables_mask_url_credentials_context(enabled: bool) -> None:
390401
current_context.set_code_variables_mask_url_credentials(enabled)
391402

392403

404+
def set_code_variables_detect_secrets_context(enabled: bool) -> None:
405+
"""
406+
Whether to apply entropy-based secret detection as a last-resort redaction of
407+
high-entropy values (API keys, tokens, strong passwords) in captured code
408+
variables for the current context.
409+
"""
410+
current_context = _get_current_context()
411+
if current_context:
412+
current_context.set_code_variables_detect_secrets(enabled)
413+
414+
393415
def get_capture_exception_code_variables_context() -> Optional[bool]:
394416
current_context = _get_current_context()
395417
if current_context:
@@ -418,6 +440,13 @@ def get_code_variables_mask_url_credentials_context() -> Optional[bool]:
418440
return None
419441

420442

443+
def get_code_variables_detect_secrets_context() -> Optional[bool]:
444+
current_context = _get_current_context()
445+
if current_context:
446+
return current_context.get_code_variables_detect_secrets()
447+
return None
448+
449+
421450
F = TypeVar("F", bound=Callable[..., Any])
422451

423452

0 commit comments

Comments
 (0)