"""Unit: LOG_FULL credential redaction in _log_request / _log_response (issue #257). egress_addon.py is gateway-only code that depends on mitmproxy, which is not installed on the host. This file pre-populates sys.modules with the minimum mocks needed so EgressAddon can be imported and tested without the real mitmproxy package.""" from __future__ import annotations import json import os import sys import types import unittest from io import StringIO from typing import Any from unittest.mock import patch # --------------------------------------------------------------------------- # mitmproxy stub — must run before importing egress_addon # --------------------------------------------------------------------------- def _ensure_shims() -> None: # Resolver-only egress: importing the module builds the `addons` singleton, # which requires an orchestrator URL. These tests exercise the log helpers # on a __new__-built addon, so the value is never dialed. os.environ.setdefault("BOT_BOTTLE_ORCHESTRATOR_URL", "http://127.0.0.1:0") if "mitmproxy" not in sys.modules: _mm = types.ModuleType("mitmproxy") _mh = types.ModuleType("mitmproxy.http") setattr(_mm, "http", _mh) sys.modules["mitmproxy"] = _mm sys.modules["mitmproxy.http"] = _mh _ensure_shims() from bot_bottle.egress_addon import EgressAddon # noqa: E402 (import after shims) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _addon() -> EgressAddon: """A bare EgressAddon for exercising the log helpers directly. The redaction log methods take their env explicitly, so no resolver/config wiring is needed here.""" return EgressAddon.__new__(EgressAddon) class _Headers: def __init__(self, d: dict[str, str]) -> None: self._d = d def items(self) -> list[tuple[str, str]]: return list(self._d.items()) class _Request: def __init__( self, host: str = "api.example.com", method: str = "POST", path: str = "/v1/messages", headers: dict[str, str] | None = None, body: str = "", ) -> None: self.pretty_host = host self.method = method self.path = path self.headers = _Headers(headers or {}) self._body = body def get_text(self, *, strict: bool = True) -> str: return self._body class _Response: def __init__( self, status_code: int = 200, headers: dict[str, str] | None = None, body: str = "", ) -> None: self.status_code = status_code self.headers = _Headers(headers or {}) self._body = body def get_text(self, *, strict: bool = True) -> str: return self._body class _Flow: def __init__( self, request: _Request | None = None, response: _Response | None = None, ) -> None: self.request = request or _Request() self.response = response or _Response() def _log_request(addon: EgressAddon, flow: _Flow) -> dict[str, Any]: buf = StringIO() with patch("sys.stderr", buf): addon._log_request(flow, os.environ) # type: ignore[arg-type] return json.loads(buf.getvalue()) def _log_response(addon: EgressAddon, flow: _Flow) -> dict[str, Any]: buf = StringIO() with patch("sys.stderr", buf): addon._log_response(flow, os.environ) # type: ignore[arg-type] return json.loads(buf.getvalue()) # --------------------------------------------------------------------------- # _log_request — authorization header stripped # --------------------------------------------------------------------------- class TestLogRequestAuthorizationStripped(unittest.TestCase): def test_lowercase_authorization_excluded(self) -> None: addon = _addon() flow = _Flow(request=_Request(headers={"authorization": "Bearer sk-real-secret"})) entry = _log_request(addon, flow) self.assertNotIn("authorization", entry["headers"]) def test_titlecase_authorization_excluded(self) -> None: addon = _addon() flow = _Flow(request=_Request(headers={"Authorization": "Bearer sk-real-secret"})) entry = _log_request(addon, flow) self.assertNotIn("Authorization", entry["headers"]) self.assertNotIn("authorization", entry["headers"]) def test_non_auth_headers_retained(self) -> None: addon = _addon() flow = _Flow(request=_Request(headers={ "authorization": "Bearer sk-real-secret", "content-type": "application/json", })) entry = _log_request(addon, flow) self.assertIn("content-type", entry["headers"]) self.assertEqual("application/json", entry["headers"]["content-type"]) def test_no_authorization_header_logs_all_others(self) -> None: addon = _addon() flow = _Flow(request=_Request(headers={"x-request-id": "abc"})) entry = _log_request(addon, flow) self.assertEqual({"x-request-id": "abc"}, entry["headers"]) # --------------------------------------------------------------------------- # _log_request — body redaction # --------------------------------------------------------------------------- _OPENAI_KEY = "sk-" + "A" * 48 class TestLogRequestBodyRedacted(unittest.TestCase): def test_token_pattern_in_body_scrubbed(self) -> None: addon = _addon() flow = _Flow(request=_Request(body=f"key={_OPENAI_KEY}")) entry = _log_request(addon, flow) self.assertNotIn(_OPENAI_KEY, entry["body"]) self.assertIn("********", entry["body"]) def test_provisioned_secret_in_body_scrubbed(self) -> None: addon = _addon() secret = "provisioned-egress-secret-xyz" flow = _Flow(request=_Request(body=f"token={secret}")) with patch.dict("os.environ", {"EGRESS_TOKEN_0": secret}): entry = _log_request(addon, flow) self.assertNotIn(secret, entry["body"]) self.assertIn("********", entry["body"]) def test_clean_body_preserved(self) -> None: addon = _addon() payload = '{"model": "claude-3", "max_tokens": 1024}' flow = _Flow(request=_Request(body=payload)) entry = _log_request(addon, flow) self.assertEqual(payload, entry["body"]) # --------------------------------------------------------------------------- # _log_request — non-authorization header value redaction # --------------------------------------------------------------------------- class TestLogRequestHeaderValuesRedacted(unittest.TestCase): def test_token_in_custom_header_scrubbed(self) -> None: addon = _addon() flow = _Flow(request=_Request(headers={"x-api-key": _OPENAI_KEY})) entry = _log_request(addon, flow) self.assertNotIn(_OPENAI_KEY, entry["headers"].get("x-api-key", "")) self.assertIn("********", entry["headers"].get("x-api-key", "")) def test_clean_header_value_preserved(self) -> None: addon = _addon() flow = _Flow(request=_Request(headers={"accept": "application/json"})) entry = _log_request(addon, flow) self.assertEqual("application/json", entry["headers"]["accept"]) # --------------------------------------------------------------------------- # _log_response — body redaction # --------------------------------------------------------------------------- class TestLogResponseBodyRedacted(unittest.TestCase): def test_token_pattern_in_response_body_scrubbed(self) -> None: addon = _addon() flow = _Flow( request=_Request(), response=_Response(body=f'{{"key": "{_OPENAI_KEY}"}}'), ) entry = _log_response(addon, flow) self.assertNotIn(_OPENAI_KEY, entry["body"]) self.assertIn("********", entry["body"]) def test_provisioned_secret_in_response_body_scrubbed(self) -> None: addon = _addon() secret = "provisioned-egress-secret-xyz" flow = _Flow( request=_Request(), response=_Response(body=f'{{"token": "{secret}"}}'), ) with patch.dict("os.environ", {"EGRESS_TOKEN_0": secret}): entry = _log_response(addon, flow) self.assertNotIn(secret, entry["body"]) self.assertIn("********", entry["body"]) def test_clean_response_body_preserved(self) -> None: addon = _addon() flow = _Flow(request=_Request(), response=_Response(body='{"result": "ok"}')) entry = _log_response(addon, flow) self.assertEqual('{"result": "ok"}', entry["body"]) # --------------------------------------------------------------------------- # _log_response — response header value redaction # --------------------------------------------------------------------------- class TestLogResponseHeaderValuesRedacted(unittest.TestCase): def test_token_in_response_header_scrubbed(self) -> None: addon = _addon() flow = _Flow( request=_Request(), response=_Response(headers={"set-cookie": f"token={_OPENAI_KEY}"}), ) entry = _log_response(addon, flow) cookie_val = entry["headers"].get("set-cookie", "") self.assertNotIn(_OPENAI_KEY, cookie_val) self.assertIn("********", cookie_val) def test_clean_response_header_preserved(self) -> None: addon = _addon() flow = _Flow( request=_Request(), response=_Response(headers={"content-type": "application/json"}), ) entry = _log_response(addon, flow) self.assertEqual("application/json", entry["headers"]["content-type"]) if __name__ == "__main__": unittest.main()