Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .sampo/changesets/entropy-secret-detection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'pypi/posthog': patch
---

Detect and redact high-entropy secrets (API keys, tokens, passwords) in exception code variables. Adds the `code_variables_detect_secrets` option (default `True`).
21 changes: 21 additions & 0 deletions posthog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
from posthog.contexts import (
set_code_variables_mask_url_credentials_context as inner_set_code_variables_mask_url_credentials_context,
)
from posthog.contexts import (
set_code_variables_detect_secrets_context as inner_set_code_variables_detect_secrets_context,
)
from posthog.contexts import (
set_context_device_id as inner_set_context_device_id,
)
Expand All @@ -45,6 +48,7 @@
get_tags as inner_get_tags,
)
from posthog.exception_utils import (
DEFAULT_CODE_VARIABLES_DETECT_SECRETS,
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
DEFAULT_CODE_VARIABLES_MASK_URL_CREDENTIALS,
Expand Down Expand Up @@ -244,6 +248,18 @@ def set_code_variables_mask_url_credentials_context(enabled: bool):
return inner_set_code_variables_mask_url_credentials_context(enabled)


def set_code_variables_detect_secrets_context(enabled: bool):
"""
Whether to apply entropy-based secret detection as a last-resort redaction of
high-entropy values (API keys, tokens, strong passwords) in captured code
variables for the current context.

Category:
Contexts
"""
return inner_set_code_variables_detect_secrets_context(enabled)


def tag(name: str, value: Any):
"""
Add a tag to the current context.
Expand Down Expand Up @@ -321,6 +337,9 @@ def get_tags() -> Dict[str, Any]:
code variables.
code_variables_ignore_patterns: Variable-name patterns to omit when capturing
code variables.
code_variables_detect_secrets: Last-resort entropy-based redaction of
high-entropy secret-looking values (API keys, tokens, strong passwords)
in captured code variables. Defaults to True.
in_app_modules: Module/package prefixes treated as in-app frames in captured
exceptions.
enable_exception_autocapture_rate_limiting: Rate limit autocaptured
Expand Down Expand Up @@ -365,6 +384,7 @@ def get_tags() -> Dict[str, Any]:
code_variables_mask_patterns = DEFAULT_CODE_VARIABLES_MASK_PATTERNS
code_variables_ignore_patterns = DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS
code_variables_mask_url_credentials = DEFAULT_CODE_VARIABLES_MASK_URL_CREDENTIALS
code_variables_detect_secrets = DEFAULT_CODE_VARIABLES_DETECT_SECRETS
in_app_modules = None # type: Optional[list[str]]
enable_exception_autocapture_rate_limiting = False # type: bool
exception_autocapture_bucket_size = ExceptionCapture.DEFAULT_BUCKET_SIZE # type: int
Expand Down Expand Up @@ -1149,6 +1169,7 @@ def setup() -> Client:
code_variables_mask_patterns=code_variables_mask_patterns,
code_variables_ignore_patterns=code_variables_ignore_patterns,
code_variables_mask_url_credentials=code_variables_mask_url_credentials,
code_variables_detect_secrets=code_variables_detect_secrets,
in_app_modules=in_app_modules,
enable_exception_autocapture_rate_limiting=enable_exception_autocapture_rate_limiting,
exception_autocapture_bucket_size=exception_autocapture_bucket_size,
Expand Down
20 changes: 20 additions & 0 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from posthog.contexts import (
_get_current_context,
get_capture_exception_code_variables_context,
get_code_variables_detect_secrets_context,
get_code_variables_ignore_patterns_context,
get_code_variables_mask_patterns_context,
get_code_variables_mask_url_credentials_context,
Expand All @@ -37,6 +38,7 @@
from posthog.exception_capture import ExceptionCapture
from posthog._logging import _configure_posthog_logging
from posthog.exception_utils import (
DEFAULT_CODE_VARIABLES_DETECT_SECRETS,
DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
DEFAULT_CODE_VARIABLES_MASK_URL_CREDENTIALS,
Expand Down Expand Up @@ -251,6 +253,7 @@ def __init__(
code_variables_mask_patterns=None,
code_variables_ignore_patterns=None,
code_variables_mask_url_credentials=None,
code_variables_detect_secrets=None,
in_app_modules: list[str] | None = None,
enable_exception_autocapture_rate_limiting=False,
exception_autocapture_bucket_size=ExceptionCapture.DEFAULT_BUCKET_SIZE,
Expand Down Expand Up @@ -319,6 +322,11 @@ def __init__(
code_variables_mask_url_credentials: Scrub credentials embedded in
URLs/DSNs (e.g. ``user:pass@host``) from captured code variables,
regardless of the surrounding variable name. Defaults to True.
code_variables_detect_secrets: Last-resort entropy-based detection that
redacts high-entropy secret-looking values (API keys, tokens, strong
passwords) sitting in innocuously-named variables, after the name and
URL checks. Skips structured ids (UUIDs, ObjectIds, hashes). Defaults
to True.
in_app_modules: Module/package prefixes treated as in-app frames in
captured exceptions.
enable_exception_autocapture_rate_limiting: Rate limit
Expand Down Expand Up @@ -417,6 +425,11 @@ def __init__(
if code_variables_mask_url_credentials is not None
else DEFAULT_CODE_VARIABLES_MASK_URL_CREDENTIALS
)
self.code_variables_detect_secrets = (
code_variables_detect_secrets
if code_variables_detect_secrets is not None
else DEFAULT_CODE_VARIABLES_DETECT_SECRETS
)
self.in_app_modules = in_app_modules

if project_root is None:
Expand Down Expand Up @@ -1399,6 +1412,7 @@ def capture_exception(
context_mask_url_credentials = (
get_code_variables_mask_url_credentials_context()
)
context_detect_secrets = get_code_variables_detect_secrets_context()

enabled = (
context_enabled
Expand All @@ -1420,6 +1434,11 @@ def capture_exception(
if context_mask_url_credentials is not None
else self.code_variables_mask_url_credentials
)
detect_secrets = (
context_detect_secrets
if context_detect_secrets is not None
else self.code_variables_detect_secrets
)

if enabled:
try_attach_code_variables_to_frames(
Expand All @@ -1428,6 +1447,7 @@ def capture_exception(
mask_patterns=mask_patterns,
ignore_patterns=ignore_patterns,
mask_url_credentials=mask_url_credentials,
detect_secrets=detect_secrets,
)

if self.log_captured_exceptions:
Expand Down
29 changes: 29 additions & 0 deletions posthog/contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__(
self.code_variables_mask_patterns: Optional[list] = None
self.code_variables_ignore_patterns: Optional[list] = None
self.code_variables_mask_url_credentials: Optional[bool] = None
self.code_variables_detect_secrets: Optional[bool] = None

def set_session_id(self, session_id: str):
self.session_id = session_id
Expand All @@ -52,6 +53,9 @@ def set_code_variables_ignore_patterns(self, ignore_patterns: list):
def set_code_variables_mask_url_credentials(self, enabled: bool):
self.code_variables_mask_url_credentials = enabled

def set_code_variables_detect_secrets(self, enabled: bool):
self.code_variables_detect_secrets = enabled

def get_parent(self):
return self.parent

Expand Down Expand Up @@ -113,6 +117,13 @@ def get_code_variables_mask_url_credentials(self) -> Optional[bool]:
return self.parent.get_code_variables_mask_url_credentials()
return None

def get_code_variables_detect_secrets(self) -> Optional[bool]:
if self.code_variables_detect_secrets is not None:
return self.code_variables_detect_secrets
if self.parent is not None and not self.fresh:
return self.parent.get_code_variables_detect_secrets()
return None


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


def set_code_variables_detect_secrets_context(enabled: bool) -> None:
"""
Whether to apply entropy-based secret detection as a last-resort redaction of
high-entropy values (API keys, tokens, strong passwords) in captured code
variables for the current context.
"""
current_context = _get_current_context()
if current_context:
current_context.set_code_variables_detect_secrets(enabled)


def get_capture_exception_code_variables_context() -> Optional[bool]:
current_context = _get_current_context()
if current_context:
Expand Down Expand Up @@ -418,6 +440,13 @@ def get_code_variables_mask_url_credentials_context() -> Optional[bool]:
return None


def get_code_variables_detect_secrets_context() -> Optional[bool]:
current_context = _get_current_context()
if current_context:
return current_context.get_code_variables_detect_secrets()
return None


F = TypeVar("F", bound=Callable[..., Any])


Expand Down
Loading
Loading