from __future__ import annotations import importlib.util import json from pathlib import Path from types import ModuleType import pytest ROOT = Path(__file__).resolve().parents[3] def _load_scanner(filename: str, module_name: str) -> ModuleType: path = ROOT / "scripts/security" / filename spec = importlib.util.spec_from_file_location(module_name, path) assert spec is not None and spec.loader is not None module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module INVENTORY = _load_scanner( "telegram-notification-egress-inventory.py", "telegram_notification_egress_inventory_test", ) NO_NEW_BYPASS_GUARD = _load_scanner( "telegram-notification-egress-no-new-bypass-guard.py", "telegram_notification_egress_no_new_bypass_guard_test", ) SCANNERS = (INVENTORY, NO_NEW_BYPASS_GUARD) def _write_zero_baseline(root: Path) -> None: snapshot = ( root / "docs/security/telegram-notification-egress-inventory.snapshot.json" ) snapshot.parent.mkdir(parents=True, exist_ok=True) snapshot.write_text( json.dumps( { "summary": { "direct_bot_api_call_count": 0, "direct_bot_api_file_count": 0, }, "direct_bot_api_calls": [], } ), encoding="utf-8", ) def _write_minimal_gateway(root: Path) -> None: gateway = root / "apps/api/src/services/telegram_gateway.py" gateway.parent.mkdir(parents=True, exist_ok=True) gateway.write_text( "normalize_telegram_send_message_payload = object()\n", encoding="utf-8", ) @pytest.mark.parametrize("scanner", SCANNERS) def test_scanner_detects_multiline_split_bot_api_url(scanner: ModuleType) -> None: source = """ import requests def send_telegram_alert(token: str) -> object: base = "https://api.telegram.org/" endpoint = ( base + f"bot{token}/" + "sendMessage" ) return requests.post(endpoint, json={"text": "bounded"}) """ findings = scanner.scan_direct_bot_api_surfaces("scripts/ops/split.py", source) assert len(findings) == 1 assert findings[0]["method"] == "sendMessage" assert findings[0]["detection_kind"] == "direct_bot_api_endpoint" @pytest.mark.parametrize("scanner", SCANNERS) @pytest.mark.parametrize("transport", ["Invoke-WebRequest", "Invoke-RestMethod"]) def test_scanner_detects_powershell_direct_transport( scanner: ModuleType, transport: str, ) -> None: source = f""" function Send-AgentTelegramLegacy {{ $base = "https://api.telegram.org/" $uri = $base + "bot" + $Token + "/sendPhoto" {transport} -Method Post -Uri $uri }} """ findings = scanner.scan_direct_bot_api_surfaces( "agent99-control-plane.ps1", source, ) assert len(findings) == 1 assert findings[0]["method"] == "sendPhoto" assert findings[0]["detection_kind"] == "direct_bot_api_endpoint" @pytest.mark.parametrize("scanner", SCANNERS) @pytest.mark.parametrize( "transport", [ "requests.post(target, json=payload)", "urllib.request.urlopen(target, data=payload)", "client.PostAsync(target, content)", ], ) def test_scanner_detects_custom_sender_without_literal_endpoint( scanner: ModuleType, transport: str, ) -> None: source = f""" function Send-AgentTelegramLegacy {{ $target = $ConfiguredProviderUrl + "/sendMessage" {transport} }} """ findings = scanner.scan_direct_bot_api_surfaces( "agent99-control-plane.ps1", source, ) assert len(findings) == 1 assert findings[0]["method"] == "sendMessage" assert findings[0]["detection_kind"] == "custom_direct_sender" @pytest.mark.parametrize("scanner", SCANNERS) @pytest.mark.parametrize( "transport", [ "httpx.Client().post(endpoint, json=payload)", "requests.Session().post(endpoint, json=payload)", "urllib.request.build_opener().open(endpoint, data=payload)", "opener.open(endpoint, data=payload)", ], ) def test_scanner_detects_factory_clients_and_urllib_openers_with_dynamic_endpoint( scanner: ModuleType, transport: str, ) -> None: source = f""" def dispatch(endpoint: str, bot_token: str, chat_id: str, payload: bytes) -> object: return {transport} """ findings = scanner.scan_direct_bot_api_surfaces( "scripts/ops/provider_dispatch.py", source, ) assert len(findings) == 1 assert findings[0]["method"] == "dynamic" assert findings[0]["detection_kind"] == "custom_direct_sender" assert findings[0]["function"] == "dispatch" @pytest.mark.parametrize("scanner", SCANNERS) @pytest.mark.parametrize( ("imports", "transport"), [ ("import requests as rq", "rq.post(endpoint, json=payload)"), ("import httpx as hx", "hx.post(endpoint, json=payload)"), ( "import httpx as hx", "hx.Client().post(endpoint, json=payload)", ), ( "from httpx import Client as HC", "HC().post(endpoint, json=payload)", ), ( "from requests import Session as S", "S().post(endpoint, json=payload)", ), ( "from urllib.request import build_opener as make_opener", "make_opener().open(endpoint, data=payload)", ), ], ) def test_scanner_resolves_python_import_alias_transport_calls( scanner: ModuleType, imports: str, transport: str, ) -> None: source = f""" {imports} def dispatch(endpoint: str, bot_token: str, chat_id: str, payload: bytes) -> object: return {transport} """ findings = scanner.scan_direct_bot_api_surfaces( "scripts/ops/aliased_provider_dispatch.py", source, ) assert len(findings) == 1 assert findings[0]["method"] == "dynamic" assert findings[0]["detection_kind"] == "custom_direct_sender" assert findings[0]["function"] == "dispatch" @pytest.mark.parametrize("scanner", SCANNERS) @pytest.mark.parametrize( ("imports", "transport"), [ ("import requests as rq", "rq.post(endpoint, json=payload)"), ( "import httpx as hx", "hx.Client().post(endpoint, json=payload)", ), ( "from httpx import Client as HC", "HC().post(endpoint, json=payload)", ), ( "from requests import Session as S", "S().post(endpoint, json=payload)", ), ( "from urllib.request import build_opener as make_opener", "make_opener().open(endpoint, data=payload)", ), ], ) def test_scanner_resolves_module_scope_import_alias_transport_calls( scanner: ModuleType, imports: str, transport: str, ) -> None: source = f""" {imports} bot_token = object() chat_id = object() endpoint = object() payload = b"" result = {transport} """ findings = scanner.scan_direct_bot_api_surfaces( "scripts/ops/module_alias_provider.py", source, ) assert len(findings) == 1 assert findings[0]["method"] == "dynamic" assert findings[0]["detection_kind"] == "custom_direct_sender" assert findings[0]["function"] == "" assert findings[0]["function_qualified"] == "" @pytest.mark.parametrize("scanner", SCANNERS) def test_scanner_resolves_imported_factory_assigned_to_local_client( scanner: ModuleType, ) -> None: source = """ from httpx import Client as HC def dispatch(endpoint: str, bot_token: str, chat_id: str, payload: bytes) -> object: sender = HC() return sender.post(endpoint, content=payload) """ findings = scanner.scan_direct_bot_api_surfaces( "scripts/ops/assigned_alias_provider.py", source, ) assert len(findings) == 1 assert findings[0]["method"] == "dynamic" assert findings[0]["function"] == "dispatch" @pytest.mark.parametrize("scanner", SCANNERS) @pytest.mark.parametrize("upload_method", ["UploadString", "UploadData", "UploadFile"]) def test_scanner_detects_powershell_webclient_uploads_with_dynamic_endpoint( scanner: ModuleType, upload_method: str, ) -> None: source = f""" function Publish-LegacyPayload {{ param($BotToken, $ChatId, $Uri) $webClient = New-Object System.Net.WebClient $webClient.{upload_method}($Uri, $Payload) }} """ findings = scanner.scan_direct_bot_api_surfaces( "scripts/ops/legacy-provider.ps1", source, ) assert len(findings) == 1 assert findings[0]["method"] == "dynamic" assert findings[0]["detection_kind"] == "custom_direct_sender" assert findings[0]["function"] == "Publish-LegacyPayload" @pytest.mark.parametrize("scanner", SCANNERS) def test_scanner_excludes_only_canonical_gateway_final_exit( scanner: ModuleType, ) -> None: source = """ class TelegramGateway: async def _send_request(self, token: str, payload: dict) -> object: url = f"https://api.telegram.org/bot{token}/sendMessage" return await self._http_client.post(url, json=payload) """ findings = scanner.scan_direct_bot_api_surfaces( "apps/api/src/services/telegram_gateway.py", source, ) assert findings == [] @pytest.mark.parametrize("scanner", SCANNERS) def test_scanner_does_not_allowlist_same_function_name_outside_gateway( scanner: ModuleType, ) -> None: source = """ import requests as rq async def _send_request( endpoint: str, bot_token: str, chat_id: str, payload: dict, ) -> object: return rq.post(endpoint, json=payload) """ findings = scanner.scan_direct_bot_api_surfaces( "scripts/ops/telegram_gateway.py", source, ) assert len(findings) == 1 assert findings[0]["function"] == "_send_request" assert findings[0]["detection_kind"] == "custom_direct_sender" @pytest.mark.parametrize("scanner", SCANNERS) def test_scanner_allowlists_only_class_qualified_gateway_final_exit( scanner: ModuleType, ) -> None: source = """ import requests as rq async def _send_request( endpoint: str, bot_token: str, chat_id: str, payload: dict, ) -> object: return rq.post(endpoint, json=payload) class RogueGateway: async def _send_request( self, endpoint: str, bot_token: str, chat_id: str, payload: dict, ) -> object: return rq.post(endpoint, json=payload) class TelegramGateway: async def _send_request( self, endpoint: str, bot_token: str, chat_id: str, payload: dict, ) -> object: return rq.post(endpoint, json=payload) """ findings = scanner.scan_direct_bot_api_surfaces( "apps/api/src/services/telegram_gateway.py", source, ) assert {item["function_qualified"] for item in findings} == { "_send_request", "RogueGateway._send_request", } def test_no_new_bypass_guard_fails_closed_on_split_endpoint(tmp_path: Path) -> None: _write_zero_baseline(tmp_path) _write_minimal_gateway(tmp_path) sender = tmp_path / "scripts/ops/split_sender.py" sender.parent.mkdir(parents=True, exist_ok=True) sender.write_text( """ import requests def send_telegram(token: str) -> object: base = "https://api.telegram.org/" endpoint = base + f"bot{token}/" + "sendDocument" return requests.post(endpoint, files={}) """, encoding="utf-8", ) report = NO_NEW_BYPASS_GUARD.build_report(tmp_path) assert report["summary"]["current_direct_bot_api_call_count"] == 1 assert report["summary"]["new_bypass_count"] == 1 with pytest.raises(SystemExit, match="direct/custom bypass"): NO_NEW_BYPASS_GUARD.validate(tmp_path) def test_local_github_workflow_is_frozen_legacy_not_active_bypass( tmp_path: Path, ) -> None: _write_zero_baseline(tmp_path) _write_minimal_gateway(tmp_path) workflow = tmp_path / ".github/workflows/legacy-alert.yml" workflow.parent.mkdir(parents=True, exist_ok=True) workflow.write_text( """ steps: - name: frozen legacy telegram sender run: | endpoint="https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" curl -X POST "$endpoint" -d "chat_id=${CHAT_ID}" """, encoding="utf-8", ) inventory_report = INVENTORY.build_report(tmp_path) inventory_item = inventory_report["direct_bot_api_calls"][0] assert inventory_report["summary"]["direct_bot_api_call_count"] == 1 assert inventory_report["summary"]["active_direct_bot_api_call_count"] == 0 assert ( inventory_report["summary"]["github_frozen_legacy_direct_bot_api_call_count"] == 1 ) assert inventory_item["source_truth_classification"] == ( "frozen_legacy_source_truth" ) assert inventory_item["runtime_execution_authorized"] is False assert inventory_item["workflow_execution_authorized"] is False assert ( inventory_report["execution_boundaries"]["github_workflow_execution_authorized"] is False ) guard_report = NO_NEW_BYPASS_GUARD.build_report(tmp_path) assert guard_report["status"] == "pass_no_direct_or_custom_bypass" assert guard_report["summary"]["detected_direct_bot_api_call_count"] == 1 assert guard_report["summary"]["current_direct_bot_api_call_count"] == 0 assert guard_report["summary"]["new_bypass_count"] == 0 assert ( guard_report["summary"]["github_frozen_legacy_direct_bot_api_call_count"] == 1 ) frozen_item = guard_report["frozen_legacy_direct_bot_api_calls"][0] assert frozen_item["source_truth_classification"] == ("frozen_legacy_source_truth") assert frozen_item["runtime_execution_authorized"] is False assert frozen_item["workflow_execution_authorized"] is False assert ( guard_report["execution_boundaries"]["github_workflow_execution_authorized"] is False ) NO_NEW_BYPASS_GUARD.validate(tmp_path) def test_agent99_has_no_direct_or_custom_telegram_sender_source() -> None: source = (ROOT / "agent99-control-plane.ps1").read_text(encoding="utf-8") for scanner in SCANNERS: assert ( scanner.scan_direct_bot_api_surfaces("agent99-control-plane.ps1", source) == [] ) assert "function Send-AgentTelegramRelay" not in source assert "function Send-AgentTelegramPhotoDirect" not in source assert "canonical_telegram_gateway_transport_required" in source assert "providerSendPerformed = $false" in source assert 'routeStatus = "blocked_no_egress"' in source