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
25 changes: 23 additions & 2 deletions src/use_notify/redaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@
re.compile(r"(?i)(/open-apis/bot/v2/hook/)[^/?\s)]+"),
re.compile(r"(?i)(ntfy\.sh/)[^/?\s)]+"),
)
_SINGLE_SEGMENT_URL_SECRET_RE = re.compile(r"(?i)(https?://[^/?#\s)]+/)[^/?#\s)]+(?=([?#\s)]|$))")
_SINGLE_SEGMENT_URL_RE = re.compile(
r"(?i)(?P<prefix>https?://[^/?#\s)]+/)(?P<segment>[^/?#\s)]+)(?=(?:[?#\s)]|$))"
)
_SENSITIVE_SEGMENT_RE = re.compile(
r"(?i)(?:^|[-_.])(?:token|secret|pushkey|topic|api[-_.]?key|access[-_.]?key|key)(?:$|[-_.]|\d)"
)


def redact_text(value: str) -> str:
"""Redact common notification provider secrets from text."""
redacted = _QUERY_SECRET_RE.sub(rf"\1{SECRET_REPLACEMENT}", value)
for pattern in _PATH_SECRET_PATTERNS:
redacted = pattern.sub(rf"\1{SECRET_REPLACEMENT}", redacted)
redacted = _SINGLE_SEGMENT_URL_SECRET_RE.sub(rf"\1{SECRET_REPLACEMENT}", redacted)
redacted = _SINGLE_SEGMENT_URL_RE.sub(_redact_sensitive_single_segment_url, redacted)
return redacted


Expand All @@ -28,3 +33,19 @@ def redact_exception_message(error: Exception) -> Exception:

error.args = tuple(redact_text(arg) if isinstance(arg, str) else arg for arg in error.args)
return error


def _redact_sensitive_single_segment_url(match: re.Match) -> str:
segment = match.group("segment")
if _looks_sensitive_path_segment(segment):
return f"{match.group('prefix')}{SECRET_REPLACEMENT}"
return match.group(0)


def _looks_sensitive_path_segment(segment: str) -> bool:
if _SENSITIVE_SEGMENT_RE.search(segment):
return True

has_alpha = any(character.isalpha() for character in segment)
has_digit = any(character.isdigit() for character in segment)
return len(segment) >= 16 and has_alpha and has_digit
13 changes: 13 additions & 0 deletions tests/test_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,17 @@ def test_redacts_custom_base_url_path_secrets():
message = (
"failed https://bark.example.com/bark-secret-token "
"and https://ntfy.example.com/my-secret-topic"
" and https://notify.example.com/abc123def456ghi7"
)

redacted = redact_text(message)

assert "bark-secret-token" not in redacted
assert "my-secret-topic" not in redacted
assert "abc123def456ghi7" not in redacted
assert "https://bark.example.com/<redacted>" in redacted
assert "https://ntfy.example.com/<redacted>" in redacted
assert "https://notify.example.com/<redacted>" in redacted


def test_redaction_keeps_multi_segment_urls_visible():
Expand All @@ -199,6 +202,16 @@ def test_redaction_keeps_multi_segment_urls_visible():
assert redact_text(message) == message


def test_redaction_keeps_ordinary_single_segment_urls_visible():
message = (
"docs https://example.com/docs "
"and status https://status.example.com/health "
"and keyboard https://example.com/keyboard"
)

assert redact_text(message) == message


def test_publisher_copies_initial_channel_collection():
initial_channels = [RecordingChannel()]
publisher = Publisher(initial_channels)
Expand Down