"""Output sanitizer -- defence in depth. The *primary* guarantee is architectural: secret values never enter argv/stdout/stderr of adapter calls. This layer additionally scans any text crossing the broker boundary (child output, HTTP bodies/headers, exception text) and replaces occurrences of known secret material with [REDACTED]. If a secret appears in output, that is already an adapter bug -- the sanitizer makes sure it can never reach the agent. """ from __future__ import annotations import re REDACTED = "[REDACTED]" # Header names that must never be echoed back to the agent. SENSITIVE_HEADERS = { "authorization", "proxy-authorization", "set-cookie", "cookie", "x-api-key", "x-auth-token", "x-csrf-token", "x-session-token", "www-authenticate", } # name=value / name: value patterns whose values are suppressed. _SECRET_KV = re.compile( r"(?i)\b(password|passwd|pwd|secret|token|api[_-]?key|access[_-]?token|" r"refresh[_-]?token|session[_-]?(id|token|key)|client[_-]?secret|" r"private[_-]?key|authorization)\b(\s*[:=]\s*)(\"[^\"]*\"|'[^']*'|[^\s,;&}]{4,})" ) _BEARER = re.compile(r"(?i)bearer\s+[A-Za-z0-9._~+/=-]{8,}") _CRED_IN_URL = re.compile(r"(https?|ssh|git)://[^/@\s]+(:[^/@\s]*)?@") def sanitize_text(text: str, secrets: list[str] | tuple[str, ...] = ()) -> str: if not text: return text out = text for secret in secrets: if secret and len(secret) >= 4: out = out.replace(secret, REDACTED) out = _BEARER.sub("Bearer " + REDACTED, out) out = _CRED_IN_URL.sub(r"\1://" + REDACTED + "@", out) out = _SECRET_KV.sub(lambda m: m.group(1) + m.group(3) + REDACTED, out) return out def sanitize_headers(headers: dict, secrets=()) -> dict: """Return a copy of headers with sensitive entries dropped and remaining values scrubbed for accidental secret content.""" out = {} for k, v in headers.items(): if k.lower() in SENSITIVE_HEADERS: out[k] = REDACTED else: out[k] = sanitize_text(str(v), secrets) return out def sanitize_bytes(data: bytes, secrets: list[bytes] | tuple[bytes, ...] = ()) -> bytes: for secret in secrets: if secret: data = data.replace(secret, REDACTED.encode()) return data