Redact denylisted fields before a log line is rendered
A processor in the shared chain masks sensitive field names (credential fragments like password/token/authorization, plus exact names such as card_number and holder_name) at any depth, for structlog and stdlib records alike. It sees field names only: values interpolated into the event string still leak until call sites pass them as fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
455b0970b1
commit
52a6d711a0
@@ -36,6 +36,41 @@ def _add_trace_context(logger, method_name, event_dict):
|
|||||||
return event_dict
|
return event_dict
|
||||||
|
|
||||||
|
|
||||||
|
# Field names whose values never reach a log line. Keys are compared
|
||||||
|
# lowercased with "-" folded to "_", so header names (X-Api-Key) match too.
|
||||||
|
# Fragments match anywhere in the key (db_password, refresh_token); the exact
|
||||||
|
# names are too short or too specific to match as fragments ("pan" would
|
||||||
|
# catch "company").
|
||||||
|
_REDACT_FRAGMENTS = ("password", "passwd", "secret", "token", "authorization", "cookie", "api_key")
|
||||||
|
_REDACT_EXACT = {"pan", "card_number", "cvv", "cvc", "holder_name"}
|
||||||
|
_REDACTED = "[REDACTED]"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_sensitive(key) -> bool:
|
||||||
|
k = str(key).lower().replace("-", "_")
|
||||||
|
return k in _REDACT_EXACT or any(f in k for f in _REDACT_FRAGMENTS)
|
||||||
|
|
||||||
|
|
||||||
|
def _scrub(value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {k: _REDACTED if _is_sensitive(k) else _scrub(v) for k, v in value.items()}
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [_scrub(v) for v in value]
|
||||||
|
if isinstance(value, tuple):
|
||||||
|
# Stays a tuple: format_exc_info only trusts an exc_info that is one.
|
||||||
|
return tuple(_scrub(v) for v in value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _redact(logger, method_name, event_dict):
|
||||||
|
"""Mask denylisted fields, at any depth (a logged headers dict included).
|
||||||
|
|
||||||
|
This works on field names only, so it is only as good as the call sites:
|
||||||
|
a value interpolated into the `event` string is invisible to it.
|
||||||
|
"""
|
||||||
|
return _scrub(event_dict)
|
||||||
|
|
||||||
|
|
||||||
def configure_logging(service_name: str) -> structlog.stdlib.BoundLogger:
|
def configure_logging(service_name: str) -> structlog.stdlib.BoundLogger:
|
||||||
"""One JSON object per line on stdout, for structlog and stdlib loggers alike.
|
"""One JSON object per line on stdout, for structlog and stdlib loggers alike.
|
||||||
|
|
||||||
@@ -50,6 +85,7 @@ def configure_logging(service_name: str) -> structlog.stdlib.BoundLogger:
|
|||||||
structlog.processors.TimeStamper(fmt="iso", utc=True),
|
structlog.processors.TimeStamper(fmt="iso", utc=True),
|
||||||
_add_service(service_name),
|
_add_service(service_name),
|
||||||
_add_trace_context,
|
_add_trace_context,
|
||||||
|
_redact,
|
||||||
]
|
]
|
||||||
|
|
||||||
structlog.configure(
|
structlog.configure(
|
||||||
|
|||||||
Reference in New Issue
Block a user