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
16 changes: 12 additions & 4 deletions packages/helpermodules/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
(r'"{field}":\s*"(.*?)"', r'"{field}": "***REDACTED***"'), # "field": "value", JSON formatted data
(r'\'{field}\':\s*\'(.*?)\'', r"'{field}': '***REDACTED***'") # 'field': 'value', JSON formatted data
]
# Credentials in a URL (scheme://user:password@host/) carry no field name for REDACTION_PATTERNS
# to key on. The user name is kept, it is useful when diagnosing authentication problems.
URL_CREDENTIALS_PATTERN = (r'(\w+://[^/\s:@]*):[^/\s]+@', r'\1:***REDACTED***@')


def redact_sensitive_info(message: str, additional_fields: list = None) -> str:
Expand All @@ -37,13 +40,17 @@ def redact_sensitive_info(message: str, additional_fields: list = None) -> str:
redacted are defined in the KNOWN_SENSITIVE_FIELDS list. The function uses
predefined patterns to identify and replace the sensitive information.

Passwords given as URL credentials (scheme://user:password@host/) are redacted as well,
those carry no field name to key on.

Args:
message (str): The log message to be redacted.

Returns:
str: The redacted log message.
"""
fields_to_redact = KNOWN_SENSITIVE_FIELDS + (additional_fields or [])
message = re.sub(URL_CREDENTIALS_PATTERN[0], URL_CREDENTIALS_PATTERN[1], message)
for field in fields_to_redact:
for pattern, replacement in REDACTION_PATTERNS:
pattern = pattern.replace('{field}', field)
Expand Down Expand Up @@ -344,9 +351,9 @@ def threading_excepthook(args):
with open(thread_errors_path, "a") as f:
f.write("Uncaught exception in thread:\n")
f.write(f"Type: {args.exc_type}\n")
f.write(f"Value: {args.exc_value}\n")
f.write(redact_sensitive_info(f"Value: {args.exc_value}\n"))
import traceback
traceback.print_tb(args.exc_traceback, file=f)
f.write(redact_sensitive_info("".join(traceback.format_tb(args.exc_traceback))))
threading.excepthook = threading_excepthook

def handle_unhandled_exception(exc_type, exc_value, exc_traceback):
Expand All @@ -356,8 +363,9 @@ def handle_unhandled_exception(exc_type, exc_value, exc_traceback):
with open(thread_errors_path, "a") as f:
f.write("Uncaught exception:\n")
f.write(f"Type: {exc_type}\n")
f.write(f"Value: {exc_value}\n")
f.write(f"Traceback:{exc_traceback}\n")
f.write(redact_sensitive_info(f"Value: {exc_value}\n"))
import traceback
f.write(redact_sensitive_info("".join(traceback.format_tb(exc_traceback))))
sys.excepthook = handle_unhandled_exception


Expand Down
46 changes: 46 additions & 0 deletions packages/helpermodules/logger_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import pytest

from helpermodules.logger import redact_sensitive_info


@pytest.mark.parametrize("message, expected", [
pytest.param("connection to ws://openwb:secret@ocpp.example.com/v16/ established",
"connection to ws://openwb:***REDACTED***@ocpp.example.com/v16/ established",
id="websocket url"),
pytest.param("wss://openwb:secret@ocpp.example.com:443/v16/",
"wss://openwb:***REDACTED***@ocpp.example.com:443/v16/",
id="port is kept"),
pytest.param('{"url": "ws://openwb:secret@ocpp.example.com/v16/", "version": "1.6"}',
'{"url": "ws://openwb:***REDACTED***@ocpp.example.com/v16/", "version": "1.6"}',
id="url embedded in json"),
pytest.param("ws://user:p@ssword@host/",
"ws://user:***REDACTED***@host/",
id="at sign within password"),
pytest.param("ws://openwb:***REDACTED***@ocpp.example.com/v16/",
"ws://openwb:***REDACTED***@ocpp.example.com/v16/",
id="already redacted"),
])
def test_redact_url_credentials(message, expected):
assert redact_sensitive_info(message) == expected


@pytest.mark.parametrize("message", [
pytest.param("http://192.168.1.5:8080/api?value=1", id="url without credentials"),
pytest.param("https://api.example.com/mail?to=someone@example.com", id="at sign in query"),
pytest.param("http://host:8080 unreachable, contact someone@example.com", id="mail address after url"),
])
def test_keep_url_without_credentials(message):
assert redact_sensitive_info(message) == message


def test_redact_url_credentials_and_known_field():
message = '{"url": "ws://openwb:secret@ocpp.example.com/v16/", "password": "abc123"}'
expected = '{"url": "ws://openwb:***REDACTED***@ocpp.example.com/v16/", "password": "***REDACTED***"}'

assert redact_sensitive_info(message) == expected


def test_redact_url_credentials_with_sensitive_field_as_user_name():
# If the user name matches an entry of KNOWN_SENSITIVE_FIELDS, the field pattern applies on
# top and truncates the url. The password is removed in that case as well.
assert redact_sensitive_info("ws://token:secret@ocpp.example.com/v16/") == "ws://token=***REDACTED***"