121 lines
4.1 KiB
Python
121 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
import io
|
|
import logging
|
|
|
|
import pytest
|
|
|
|
from beaver_gateway.core import redact as redact_mod
|
|
from beaver_gateway.core.redact import (
|
|
RedactFilter,
|
|
RedactingFormatter,
|
|
env_secrets,
|
|
redact,
|
|
)
|
|
|
|
# The two lines the gateway actually printed (httpx at INFO), verbatim
|
|
# apart from the token bodies: the calendar MCP takes the private ical
|
|
# feed as a query parameter, so the feed's secret is in the URL.
|
|
GOOGLE = (
|
|
"HTTP Request: POST https://calendar-mcp.com/api/mcp"
|
|
"?email=someone%40gmail.com&icsUrl=https%3A%2F%2Fcalendar.google.com"
|
|
"%2Fcalendar%2Fical%2Fsomeone%2540gmail.com"
|
|
"%2Fprivate-1f4a9c0de1f4a9c0de1f4a9c0de%2Fbasic.ics"
|
|
' "HTTP/1.1 200 OK"'
|
|
)
|
|
USOS = (
|
|
"HTTP Request: POST https://calendar-mcp.com/api/mcp"
|
|
"?email=someone%40gmail.com&icsUrl=https%3A%2F%2Fusosapps.put.poznan.pl"
|
|
"%2Fservices%2Ftt%2Fupcoming_ical%3Flang%3Dpl%26user_id%3D145744"
|
|
"%26key%3DABCDEF0123456789abcdef"
|
|
' "HTTP/1.1 202 Accepted"'
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _no_env_secrets() -> None:
|
|
redact_mod._ENV_SECRETS[:] = [] # noqa: SLF001
|
|
|
|
|
|
def test_google_private_ical_token_is_masked() -> None:
|
|
out = redact(GOOGLE)
|
|
assert "1f4a9c0de1f4a9c0de1f4a9c0de" not in out
|
|
assert "private-<…>" in out
|
|
# The harmless part of the line survives, so the log stays useful.
|
|
assert out.startswith("HTTP Request: POST https://calendar-mcp.com/api/mcp")
|
|
assert out.endswith('"HTTP/1.1 200 OK"')
|
|
|
|
|
|
def test_usos_key_is_masked_through_percent_encoding() -> None:
|
|
out = redact(USOS)
|
|
assert "ABCDEF0123456789abcdef" not in out
|
|
assert "key%3D<…>" in out
|
|
assert out.endswith('"HTTP/1.1 202 Accepted"')
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("line", "secret"),
|
|
[
|
|
("GET /hooks/komodo?token=s3cret-value&x=1", "s3cret-value"),
|
|
("GET /x?api_key=s3cret-value", "s3cret-value"),
|
|
("GET /x?session_key=s3cret-value", "s3cret-value"),
|
|
("headers={'Authorization': 'Bearer s3cret-value'}", "s3cret-value"),
|
|
("authorization: token s3cret-value", "s3cret-value"),
|
|
("retrying with Bearer s3cret-value", "s3cret-value"),
|
|
],
|
|
)
|
|
def test_credentials_in_urls_and_headers(line: str, secret: str) -> None:
|
|
assert secret not in redact(line)
|
|
|
|
|
|
def test_known_env_values_are_masked() -> None:
|
|
redact_mod._ENV_SECRETS[:] = env_secrets( # noqa: SLF001
|
|
{"FIREFLY_PAT": "pat-0123456789", "POSTGRES_USER": "beaver"}
|
|
)
|
|
out = redact("firefly said no to pat-0123456789 (user beaver)")
|
|
assert "pat-0123456789" not in out
|
|
# Only credential-shaped names contribute; ordinary config survives.
|
|
assert "beaver" in out
|
|
|
|
|
|
def test_env_values_are_masked_percent_encoded_too() -> None:
|
|
raw = "https://cal/private-xyz/basic.ics"
|
|
redact_mod._ENV_SECRETS[:] = env_secrets({"CALENDAR_MCPS": raw}) # noqa: SLF001
|
|
assert raw not in redact(f"fetching {raw}")
|
|
assert "%2Fprivate-xyz%2F" not in redact(
|
|
"fetching https%3A%2F%2Fcal%2Fprivate-xyz%2Fbasic.ics"
|
|
)
|
|
|
|
|
|
def test_formatter_redacts_the_traceback_too() -> None:
|
|
stream = io.StringIO()
|
|
handler = logging.StreamHandler(stream)
|
|
handler.setFormatter(RedactingFormatter(logging.Formatter("%(message)s")))
|
|
log = logging.getLogger("test_redact.traceback")
|
|
log.propagate = False
|
|
log.handlers = [handler]
|
|
try:
|
|
msg = f"connect failed: {GOOGLE}"
|
|
raise ConnectionError(msg) # noqa: TRY301
|
|
except ConnectionError:
|
|
log.exception("failed to list tools for MCP %r", "calendar-personal")
|
|
out = stream.getvalue()
|
|
assert "calendar-personal" in out
|
|
assert "1f4a9c0de1f4a9c0de1f4a9c0de" not in out
|
|
|
|
|
|
def test_filter_masks_a_secret_split_across_template_and_args() -> None:
|
|
record = logging.LogRecord(
|
|
"uvicorn.access", logging.INFO, __file__, 1, "key=%s", ("s3cret-value",), None
|
|
)
|
|
assert RedactFilter().filter(record)
|
|
assert record.getMessage() == "key=<…>"
|
|
|
|
|
|
def test_filter_leaves_a_clean_record_structured() -> None:
|
|
record = logging.LogRecord(
|
|
"uvicorn.access", logging.INFO, __file__, 1, "closing %s", ("conv-1",), None
|
|
)
|
|
assert RedactFilter().filter(record)
|
|
assert record.args == ("conv-1",)
|