Skip to content

Commit 68514a9

Browse files
committed
use sanitary instead of clean_data
1 parent de862fc commit 68514a9

File tree

16 files changed

+257
-606
lines changed

16 files changed

+257
-606
lines changed

.readthedocs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ version: 2
99
build:
1010
os: ubuntu-22.04
1111
tools:
12-
python: "3.9"
12+
python: "3.10"
1313

1414
mkdocs:
1515
configuration: mkdocs.yml

docs/index.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,30 @@ The [`context_bind`](reference.md#unclogger.context_bind) function will set valu
146146
}
147147
>>>
148148
```
149+
150+
## Custom Processors
151+
152+
It is possible to add other `structlog` processors into the logger configuration. For example, to hide sensitive information that might be present in the logged data (using the [Sanitary](https://sanitary.readthedocs.io) library as an example):
153+
154+
!!! Example
155+
156+
```python
157+
>>> from sanitary import StructlogSanitizer
158+
>>> from unclogger import add_processors, get_logger
159+
>>>
160+
>>> add_processors(StructlogSanitizer(keys={"password", "email"}))
161+
>>>
162+
>>> logger = get_logger("test logger")
163+
>>> logger.info("test test", foo="abc", email="[email protected]", password="myPa55w0rd")
164+
{
165+
"foo": "abc",
166+
"email": "********",
167+
"password": "********",
168+
"event": "test test",
169+
"logger": "test logger",
170+
"level": "info",
171+
"timestamp": "2021-02-12T22:40:07.600385Z"
172+
}
173+
>>>
174+
```
175+

docs/reference.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@
3636

3737
::: unclogger.set_level
3838

39-
## Processors
39+
## Custom Processors
4040

41-
### Sensitive Data
42-
43-
::: unclogger.processors.clean_data.clean_sensitive_data
41+
::: unclogger.processors.add_processors

justfile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
dotenv-filename := ".env"
2-
31
# List available recipes.
42
help:
53
@just --list --unsorted

pyproject.toml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ docs = [
2020
"pymdown-extensions>=10.7",
2121
"jinja2>=3.1.3",
2222
]
23+
clean = [
24+
"sanitary>=0.1.0",
25+
]
26+
sanitary = [
27+
"sanitary>=0.1.0",
28+
]
2329

2430
[project.urls]
2531
Homepage = "https://unclogger.readthedocs.io"
@@ -50,7 +56,7 @@ dev = [
5056
default-groups = "all"
5157

5258
[tool.pytest.ini_options]
53-
minversion = "6.0"
59+
minversion = "7.0"
5460

5561
[tool.coverage.run]
5662
source = [ "unclogger/", ]
@@ -63,9 +69,6 @@ fail_under = 90
6369
exclude_lines = [ "pragma: no cover", "@abstract",]
6470

6571
[tool.ruff]
66-
extend-exclude = [
67-
"migrations/*",
68-
]
6972
line-length = 96
7073
output-format = "grouped"
7174

@@ -128,3 +131,13 @@ ignore_missing_imports = true
128131
add-ignore = "D105, D107, D212, D401"
129132
convention = "google"
130133
match-dir = "(?!tests).*"
134+
135+
[tool.deptry.per_rule_ignores]
136+
DEP002 = [
137+
"mkdocs",
138+
"mkapi",
139+
"mkdocs-material",
140+
"pymdown-extensions",
141+
"jinja2",
142+
"sanitary",
143+
]
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import re
2+
3+
import pytest
4+
from sanitary import StructlogSanitizer
5+
6+
SENSITIVE_KEYS = [
7+
"password",
8+
"email",
9+
"email_1",
10+
"firstname",
11+
"lastname",
12+
"currentpassword",
13+
"newpassword",
14+
"tmppassword",
15+
"authentication",
16+
"refresh",
17+
"auth",
18+
"http_refresh",
19+
"http_x_forwarded_authorization",
20+
"http_x_endpoint_api_userinfo",
21+
"http_authorization",
22+
"idtoken",
23+
"oauthidtoken",
24+
"publickey",
25+
"privatekey",
26+
]
27+
SENSITIVE_PATTERNS = [
28+
"""'Authentication':""",
29+
""""Authentication":""",
30+
"""'Refresh':""",
31+
""""Refresh":""",
32+
"""'Bearer """,
33+
""""Bearer """,
34+
"Bearer ",
35+
]
36+
REPLACEMENT_MESSAGE = "#### WARNING: Log message replaced due to sensitive keyword: "
37+
38+
39+
@pytest.fixture
40+
def sanitary_replacement():
41+
return StructlogSanitizer(
42+
keys=SENSITIVE_KEYS,
43+
patterns=map(re.compile, SENSITIVE_PATTERNS),
44+
message=REPLACEMENT_MESSAGE,
45+
)
46+
47+
48+
@pytest.fixture
49+
def sanitary_hash():
50+
def get_processor(hash_function):
51+
return StructlogSanitizer(
52+
keys=SENSITIVE_KEYS,
53+
patterns=map(re.compile, SENSITIVE_PATTERNS),
54+
replacement=hash_function,
55+
)
56+
57+
return get_processor
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import json
2+
3+
from unclogger import add_processors, get_logger
4+
5+
message_replacement_part = "Log message replaced"
6+
7+
8+
def test_message_with_password_and_email_is_cleaned_correctly(caplog, sanitary_replacement):
9+
add_processors(sanitary_replacement)
10+
field_replacement = sanitary_replacement.replacement
11+
12+
caplog.set_level("INFO")
13+
14+
message = "Test with request password"
15+
logger_name = "test logger"
16+
17+
logger = get_logger(logger_name)
18+
request = {"Email": "[email protected]", "password": "this is a sensitive value"}
19+
20+
logger.info(message, request=request)
21+
22+
record = json.loads(caplog.messages[0])
23+
assert len(record) == 5
24+
assert record["event"] == "Test with request password"
25+
assert record["level"] == "info"
26+
assert record["logger"] == logger_name
27+
assert record["request"]["password"] == field_replacement
28+
assert record["request"]["Email"] == field_replacement
29+
30+
31+
def test_message_with_password_and_email_in_event_is_cleaned_correctly(
32+
caplog, sanitary_replacement
33+
):
34+
add_processors(sanitary_replacement)
35+
36+
caplog.set_level("INFO")
37+
38+
logger_name = "test logger"
39+
message = {"email": "[email protected]", "password": "this is a sensitive value"}
40+
41+
logger = get_logger(logger_name)
42+
logger.info(message)
43+
44+
record = json.loads(caplog.messages[0])
45+
assert len(record) == 4
46+
assert record["level"] == "info"
47+
assert record["logger"] == logger_name
48+
assert record["event"]["password"] == sanitary_replacement.replacement
49+
assert record["event"]["email"] == sanitary_replacement.replacement
50+
51+
52+
def _test_adding_custom_sensitive_keywords_on_runtime_cleans_output_correctly(
53+
caplog, sanitary_processor
54+
):
55+
add_processors(sanitary_processor)
56+
57+
logger = get_logger("test logger")
58+
logger.config.sensitive_keys = {"illegalkey", "foo"}
59+
60+
logger.info(
61+
"clean sensitive keys with dict",
62+
context_id="cxt_fe76c000000000000000000_0000000000000",
63+
illegalKey="blabla",
64+
)
65+
66+
record = json.loads(caplog.messages[0])
67+
assert record["illegalKey"] == message_replacement_part
68+
assert record["context_id"] == "cxt_fe76c000000000000000000_0000000000000"
69+
70+
logger.config.sensitive_keys = {}
71+
72+
73+
def _test_setting_custom_sensitive_keywords_on_runtime_cleans_output_correctly(caplog):
74+
logger = get_logger("test logger")
75+
logger.config.sensitive_keys = {"illegalkey", "foo"}
76+
77+
logger.info(
78+
"clean sensitive keys with dict",
79+
context_id="cxt_fe76c000000000000000000_0000000000000",
80+
illegalKey="blabla",
81+
)
82+
83+
record = json.loads(caplog.messages[0])
84+
assert message_replacement_part in record["illegalKey"]
85+
assert record["context_id"] == "cxt_fe76c000000000000000000_0000000000000"
86+
87+
logger.config.sensitive_keys = {}
88+
89+
90+
def _test_changing_custom_sensitive_keywords_on_runtime_cleans_output_correctly(caplog):
91+
logger = get_logger("test logger")
92+
payload = {"foo": "1234", "bar": "5678", "fooBar": "9876"}
93+
94+
logger.config.sensitive_keys = {"FOO", "bar"}
95+
logger.info("clean sensitive keys with dict", payload=payload)
96+
record = json.loads(caplog.messages[0])
97+
98+
assert message_replacement_part in record["payload"]["foo"]
99+
assert message_replacement_part in record["payload"]["bar"]
100+
assert message_replacement_part in record["payload"]["fooBar"]
101+
102+
logger.config.sensitive_keys.add("fooBar")
103+
logger.info("clean sensitive keys with dict", payload=payload)
104+
fully_cleaned_record = json.loads(caplog.messages[1])
105+
106+
assert message_replacement_part in fully_cleaned_record["payload"]["foo"]
107+
assert message_replacement_part in fully_cleaned_record["payload"]["bar"]
108+
assert message_replacement_part in fully_cleaned_record["payload"]["fooBar"]
109+
110+
logger.config.sensitive_keys = {}

tests/logger/processors/test_hash_fields.py renamed to tests/logger/clean_data/test_hash_fields.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33

44
import pytest
55

6-
from unclogger import get_logger
6+
from unclogger import add_processors, get_logger, processors
77

88

9-
def _expected(value, hash_algo):
10-
hash_function = getattr(hashlib, hash_algo)
9+
def _expected(value, hash_function):
1110
hashed = hash_function(value.encode())
1211
try:
1312
return hashed.hexdigest()
@@ -16,14 +15,17 @@ def _expected(value, hash_algo):
1615

1716

1817
@pytest.mark.parametrize("hash_algo", hashlib.algorithms_guaranteed)
19-
def test_message_with_password_and_email_is_hashed_correctly(caplog, monkeypatch, hash_algo):
18+
def test_message_with_password_and_email_is_hashed_correctly(caplog, hash_algo, sanitary_hash):
19+
processors.CUSTOM_PROCESSORS = []
20+
hash_function = getattr(hashlib, hash_algo)
21+
22+
add_processors(sanitary_hash(hash_function))
2023
caplog.set_level("INFO")
2124

2225
message = "Test with request password"
23-
logger_name = "test logger"
26+
logger_name = __name__
2427

2528
logger = get_logger(logger_name)
26-
logger.config.replacement = getattr(hashlib, hash_algo)
2729

2830
request = {
2931
"email": "[email protected]",
@@ -38,5 +40,5 @@ def test_message_with_password_and_email_is_hashed_correctly(caplog, monkeypatch
3840
assert record["level"] == "info"
3941
assert record["logger"] == logger_name
4042
assert record["request"]["safe_value"] == request["safe_value"]
41-
assert record["request"]["password"] == _expected(request["password"], hash_algo)
42-
assert record["request"]["email"] == _expected(request["email"], hash_algo)
43+
assert record["request"]["password"] == _expected(request["password"], hash_function)
44+
assert record["request"]["email"] == _expected(request["email"], hash_function)

0 commit comments

Comments
 (0)