Skip to content

Commit c885a0d

Browse files
committed
tighten up sensitive field handling in log
1 parent 435eb40 commit c885a0d

2 files changed

Lines changed: 117 additions & 8 deletions

File tree

python/lib/sift_client/_internal/pytest_plugin/audit_log.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,38 @@
4040
EVENT_WIDTH = 16
4141
# Tag so a re-entered pytest_configure (or both processes) doesn't double-attach.
4242
HANDLER_TAG = "sift_audit"
43-
44-
45-
def _fmt_value(value: object) -> str:
46-
"""Render one field value: bare when safe, quoted when it would break tokenizing."""
47-
if isinstance(value, str):
48-
return repr(value) if value == "" or any(c in value for c in " \t=") else value
49-
return str(value)
43+
# Field names whose values should be redacted in audit logs to avoid logging
44+
# sensitive data. This is a best-effort blacklist; callers should avoid passing
45+
# sensitive values in the first place.
46+
SENSITIVE_FIELD_NAMES = frozenset(
47+
{
48+
"password",
49+
"passwd",
50+
"pwd",
51+
"token",
52+
"secret",
53+
"api_key",
54+
"apikey",
55+
"api-key",
56+
"access_token",
57+
"refresh_token",
58+
"private_key",
59+
"auth",
60+
"credential",
61+
"credentials",
62+
}
63+
)
64+
65+
66+
def _fmt_kv(key: str, value: object) -> str:
67+
"""Format a key-value pair for audit log output.
68+
69+
If ``key`` is in ``SENSITIVE_FIELD_NAMES``, the value is redacted as
70+
``[REDACTED]`` to avoid logging sensitive data.
71+
"""
72+
if key.lower() in SENSITIVE_FIELD_NAMES:
73+
value = "[REDACTED]"
74+
return f"{key}={value}"
5075

5176

5277
def log_event(logger: logging.Logger, level: int, event: str, **fields: object) -> None:
@@ -55,10 +80,13 @@ def log_event(logger: logging.Logger, level: int, event: str, **fields: object)
5580
Centralizes the event-token padding and value quoting so call sites read as
5681
``log_event(logger, logging.DEBUG, "step.open", name=…, path=…)``. Guarded by
5782
``isEnabledFor`` so nothing is formatted when audit logging is off.
83+
84+
Values for field names in ``SENSITIVE_FIELD_NAMES`` are redacted as
85+
``[REDACTED]`` to avoid logging sensitive data.
5886
"""
5987
if not logger.isEnabledFor(level):
6088
return
61-
body = " ".join(f"{key}={_fmt_value(value)}" for key, value in fields.items())
89+
body = " ".join(_fmt_kv(key, value) for key, value in fields.items())
6290
logger.log(level, "%-*s %s", EVENT_WIDTH, event, body)
6391

6492

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Tests for audit log redaction of sensitive field values."""
2+
3+
import logging
4+
from io import StringIO
5+
6+
from sift_client._internal.pytest_plugin.audit_log import (
7+
_fmt_kv,
8+
log_event,
9+
)
10+
11+
12+
def test_fmt_kv_redacts_sensitive_field_names():
13+
"""Field names in SENSITIVE_FIELD_NAMES should be redacted."""
14+
assert _fmt_kv("password", "secret123") == "password=[REDACTED]"
15+
assert _fmt_kv("token", "token123") == "token=[REDACTED]"
16+
assert _fmt_kv("api_key", "key1") == "api_key=[REDACTED]"
17+
assert _fmt_kv("apikey", "key2") == "apikey=[REDACTED]"
18+
assert _fmt_kv("api-key", "key3") == "api-key=[REDACTED]"
19+
assert _fmt_kv("name", "secret123") == "name=secret123" # non-sensitive
20+
21+
22+
def test_log_event_redacts_sensitive_fields():
23+
"""Sensitive field values are redacted in log output."""
24+
logger = logging.getLogger("sift_client.test")
25+
logger.setLevel(logging.DEBUG)
26+
27+
stream = StringIO()
28+
handler = logging.StreamHandler(stream)
29+
handler.setLevel(logging.DEBUG)
30+
logger.addHandler(handler)
31+
32+
try:
33+
log_event(
34+
logger,
35+
logging.DEBUG,
36+
"test.event",
37+
password="secret123",
38+
token="abc123",
39+
name="test",
40+
)
41+
output = stream.getvalue()
42+
assert "password=[REDACTED]" in output
43+
assert "token=[REDACTED]" in output
44+
assert "secret123" not in output
45+
assert "abc123" not in output
46+
assert "name=test" in output
47+
finally:
48+
logger.removeHandler(handler)
49+
50+
51+
def test_log_event_case_insensitive_redaction():
52+
"""Redaction works regardless of field name case."""
53+
logger = logging.getLogger("sift_client.test2")
54+
logger.setLevel(logging.DEBUG)
55+
56+
stream = StringIO()
57+
handler = logging.StreamHandler(stream)
58+
handler.setLevel(logging.DEBUG)
59+
logger.addHandler(handler)
60+
61+
try:
62+
log_event(
63+
logger,
64+
logging.DEBUG,
65+
"test.event",
66+
Password="pass1",
67+
TOKEN="token1",
68+
apiKey="key1",
69+
api_key="key2",
70+
)
71+
output = stream.getvalue()
72+
assert "Password=[REDACTED]" in output
73+
assert "TOKEN=[REDACTED]" in output
74+
assert "apiKey=[REDACTED]" in output
75+
assert "api_key=[REDACTED]" in output
76+
assert "pass1" not in output
77+
assert "token1" not in output
78+
assert "key1" not in output
79+
assert "key2" not in output
80+
finally:
81+
logger.removeHandler(handler)

0 commit comments

Comments
 (0)