agent-credential-broker: fork of vaultwarden-agent-broker + OpenBao backend
Mechanical rename (package agent_credential_broker; entry points
agent-broker{,-mcp,-plugin}; env AGENT_BROKER_*; paths agent-broker;
agent-browser credential provider 'broker'; MCP server name agent-broker).
New backend 'openbao': stdlib-only HTTP client for OpenBao/Vault KV v2
(approle login or token file/env; metadata-only ref index; lazy re-login
on 401/403; token renewal). bitwarden-cli backend kept as transition.
This commit is contained in:
commit
be87900794
35 files changed
+5003
No files matched your search
@@ -0,0 +1,71 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user