Harden URL redaction in inventory/review failure output (Issue #171)
This commit is contained in:
+82
-2
@@ -19,6 +19,8 @@ Design constraints:
|
||||
import os
|
||||
import json
|
||||
import datetime
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
# Result states for an audited action.
|
||||
ALLOWED = "allowed"
|
||||
@@ -33,9 +35,87 @@ _SECRET_KEY_HINTS = ("token", "password", "secret", "authorization", "auth")
|
||||
# A string value starting with one of these has the following run redacted.
|
||||
_SECRET_VALUE_PREFIXES = ("token ", "Basic ", "Bearer ")
|
||||
|
||||
# Known synthetic test-only domains/hostnames to preserve
|
||||
_SYNTHETIC_HOSTS = {
|
||||
"example.com",
|
||||
"example.test",
|
||||
"example.invalid",
|
||||
"internal.example",
|
||||
"localhost",
|
||||
"gitea.example.com",
|
||||
"x"
|
||||
}
|
||||
|
||||
# Known real service hostnames to redact even if not part of a full URL
|
||||
_REAL_HOSTS = {"gitea.prgs.cc", "gitea.dadeschools.net"}
|
||||
|
||||
|
||||
def redact_urls(text: str) -> str:
|
||||
"""Redact raw URLs, query-string secrets, and URL credentials.
|
||||
|
||||
Synthetic example/test-only domains are preserved to keep test fixtures functional
|
||||
except for any embedded credentials or query parameters containing secrets.
|
||||
"""
|
||||
if not isinstance(text, str) or not text:
|
||||
return text
|
||||
|
||||
url_pattern = re.compile(r'(https?://[^\s)>\]}]+)', re.IGNORECASE)
|
||||
|
||||
def replace_url(match):
|
||||
url_str = match.group(1)
|
||||
try:
|
||||
parsed = urllib.parse.urlsplit(url_str)
|
||||
host = (parsed.hostname or "").lower()
|
||||
|
||||
is_synthetic = False
|
||||
for sh in _SYNTHETIC_HOSTS:
|
||||
if host == sh or host.endswith("." + sh):
|
||||
is_synthetic = True
|
||||
break
|
||||
|
||||
if is_synthetic:
|
||||
# Rebuild synthetic URL to redact any credentials or query secrets
|
||||
new_netloc = parsed.netloc
|
||||
if parsed.username or parsed.password:
|
||||
netloc_clean = parsed.hostname
|
||||
if parsed.port:
|
||||
netloc_clean = f"{netloc_clean}:{parsed.port}"
|
||||
new_netloc = f"[REDACTED_USER]:[REDACTED_PASS]@{netloc_clean}"
|
||||
|
||||
new_query = parsed.query
|
||||
if parsed.query:
|
||||
params = urllib.parse.parse_qsl(parsed.query)
|
||||
clean_params = []
|
||||
for k, v in params:
|
||||
if any(hint in k.lower() for hint in ("token", "password", "secret", "auth", "key")):
|
||||
clean_params.append((k, "[REDACTED]"))
|
||||
else:
|
||||
clean_params.append((k, v))
|
||||
new_query = urllib.parse.urlencode(clean_params)
|
||||
|
||||
return urllib.parse.urlunsplit((
|
||||
parsed.scheme,
|
||||
new_netloc,
|
||||
parsed.path,
|
||||
new_query,
|
||||
parsed.fragment
|
||||
))
|
||||
else:
|
||||
return "[REDACTED_URL]"
|
||||
except Exception:
|
||||
return "[REDACTED_URL]"
|
||||
|
||||
out = url_pattern.sub(replace_url, text)
|
||||
|
||||
# Redact raw occurrences of real service hostnames afterward
|
||||
for host in _REAL_HOSTS:
|
||||
out = out.replace(host, "[REDACTED_HOST]")
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _redact_str(text):
|
||||
"""Redact anything that looks like an Authorization credential in *text*."""
|
||||
"""Redact anything that looks like an Authorization credential or raw URL in *text*."""
|
||||
if not isinstance(text, str) or not text:
|
||||
return text
|
||||
out = text
|
||||
@@ -50,7 +130,7 @@ def _redact_str(text):
|
||||
j += 1
|
||||
out = out[:i] + prefix + REDACTED + out[j:]
|
||||
idx = i + len(prefix) + len(REDACTED)
|
||||
return out
|
||||
return redact_urls(out)
|
||||
|
||||
|
||||
def redact(value):
|
||||
|
||||
Reference in New Issue
Block a user