fix(telegram): fail closed on unverified delivery

This commit is contained in:
ogt
2026-07-14 20:49:34 +08:00
parent 180e708444
commit 849f06ad11
15 changed files with 2751 additions and 597 deletions

View File

@@ -96,7 +96,7 @@ def test_agent99_v5_formatter_synthetic_gate_loads_model_dependencies() -> None:
assert 'C:\\Wooo' in synthetic
def test_agent99_telegram_threads_source_alert_and_persists_lifecycle() -> None:
def test_agent99_telegram_lifecycle_fails_closed_without_direct_sender() -> None:
source = CONTROL.read_text(encoding="utf-8")
inbox = SRE_INBOX.read_text(encoding="utf-8")
@@ -104,8 +104,6 @@ def test_agent99_telegram_threads_source_alert_and_persists_lifecycle() -> None:
assert "replyMessageId = $replyMessageId" in inbox
assert 'PSObject.Properties["replyMessageId"]' in source
assert 'PSObject.Properties["correlationKey"]' in source
assert '"reply_parameters"' in source
assert 'telegram_receipt_b64=' in source
assert 'schemaVersion = "agent99_telegram_incident_state_v1"' in source
assert 'rootMessageId = $rootMessageId' in source
assert 'reason = "same_lifecycle_state"' in source
@@ -113,6 +111,12 @@ def test_agent99_telegram_threads_source_alert_and_persists_lifecycle() -> None:
assert "lastRunKey = $Card.runKey" in source
assert "$previousRunKey -eq [string]$incidentCard.runKey" in source
assert '$dedupeParts += "run=$($incidentCard.runKey)"' in source
assert 'error = "canonical_telegram_gateway_transport_required"' in source
assert 'providerSendPerformed = $false' in source
assert 'routeStatus = "blocked_no_egress"' in source
assert "function Send-AgentTelegramRelay" not in source
assert "function Send-AgentTelegramPhotoDirect" not in source
assert "telegram_receipt_b64=" not in source
def test_agent99_records_deduped_telegram_as_an_explicit_attempt() -> None:
@@ -495,7 +499,7 @@ def test_agent99_bounds_stale_ssh_processes_without_touching_tunnels() -> None:
source = CONTROL.read_text(encoding="utf-8")
config = json.loads((ROOT / "agent99.config.99.example.json").read_text())
guard = source[source.index("function Invoke-AgentStaleSshProcessGuard") :]
guard = guard[: guard.index("function Send-AgentTelegramRelay")]
guard = guard[: guard.index("function Get-AgentObjectValue")]
assert 'Get-CimInstance Win32_Process -Filter "Name=\'ssh.exe\'"' in guard
assert 'CommandLine -cmatch "(^|\\s)-[NLRD](\\s|$)"' in guard

View File

@@ -14,6 +14,37 @@ from unittest.mock import AsyncMock, patch
import pytest
import src.services.telegram_gateway as gateway_module
_SENT_CHAT_ID = -1001
_SENT_DESTINATION_BINDING = gateway_module._telegram_destination_binding(
_SENT_CHAT_ID
)
_SENT_RECEIPT = {
"ok": True,
"_awooop_delivery_status": "sent",
"_awooop_provider_send_performed": True,
"result": {"message_id": 42, "chat": {"id": _SENT_CHAT_ID}},
"_awoooi_canonical_route_receipt": {
"schema_version": "telegram_canonical_egress_receipt_v1",
"decision": "allowed",
"destination_alias": "awoooi_sre_war_room",
"destination_binding": _SENT_DESTINATION_BINDING,
"sender_bot_alias": "tsenyang_bot",
"provider_send_performed": True,
},
gateway_module._DELIVERY_CONTEXT_KEY: {
"schema_version": "telegram_delivery_context_v1",
"method": "sendMessage",
"sender_bot_alias": "tsenyang_bot",
"destination_alias": "awoooi_sre_war_room",
"destination_binding": _SENT_DESTINATION_BINDING,
"payload_destination_binding": _SENT_DESTINATION_BINDING,
"provider_destination_binding": _SENT_DESTINATION_BINDING,
"destination_binding_verified": True,
},
}
class FakeRedis:
"""模擬 Redis 行為,記錄 set/get/delete 呼叫"""
@@ -78,7 +109,7 @@ def gateway_with_fake_redis():
gw = TelegramGateway.__new__(TelegramGateway) # 跳過 __init__
gw._initialized = True
gw._last_message_time = None
gw.send_to_group = AsyncMock()
gw.send_to_group = AsyncMock(return_value=dict(_SENT_RECEIPT))
gw.send_notification = AsyncMock()
fake_redis = FakeRedis()

View File

@@ -20,6 +20,31 @@ from src.services.telegram_gateway import TelegramGateway
_CHAT_ID = "verified-chat"
_MESSAGE_ID = 77
_DESTINATION_BINDING = gateway_module._telegram_destination_binding(_CHAT_ID)
_SENT_RECEIPT = {
"ok": True,
"_awooop_delivery_status": "sent",
"_awooop_provider_send_performed": True,
"result": {"message_id": _MESSAGE_ID, "chat": {"id": _CHAT_ID}},
"_awoooi_canonical_route_receipt": {
"schema_version": "telegram_canonical_egress_receipt_v1",
"decision": "allowed",
"destination_alias": "inbound_interaction_reply_target",
"destination_binding": _DESTINATION_BINDING,
"sender_bot_alias": "tsenyang_bot",
"provider_send_performed": True,
},
gateway_module._DELIVERY_CONTEXT_KEY: {
"schema_version": "telegram_delivery_context_v1",
"method": "sendMessage",
"sender_bot_alias": "tsenyang_bot",
"destination_alias": "inbound_interaction_reply_target",
"destination_binding": _DESTINATION_BINDING,
"payload_destination_binding": _DESTINATION_BINDING,
"provider_destination_binding": _DESTINATION_BINDING,
"destination_binding_verified": True,
},
}
def _assert_verified_callback_payload(payload: dict) -> None:
@@ -174,13 +199,15 @@ async def test_llm_and_category_results_keep_verified_callback_context(
class _Redis:
async def get(self, _key: str) -> str:
return json.dumps({
"name": "inspect",
"provider": "internal",
"tool": "status",
"risk": "low",
"incident_id": "INC-1",
})
return json.dumps(
{
"name": "inspect",
"provider": "internal",
"tool": "status",
"risk": "low",
"incident_id": "INC-1",
}
)
monkeypatch.setattr(gateway_module, "get_redis", lambda: _Redis())
monkeypatch.setattr(
@@ -213,13 +240,15 @@ async def test_llm_and_category_results_keep_verified_callback_context(
monkeypatch.setattr(
callback_dispatcher,
"dispatch_action",
AsyncMock(return_value=DispatchResult(
success=True,
action="inspect",
incident_id="INC-1",
user_id=42,
result_text="result",
)),
AsyncMock(
return_value=DispatchResult(
success=True,
action="inspect",
incident_id="INC-1",
user_id=42,
result_text="result",
)
),
)
class _IncidentRepo:
@@ -264,7 +293,9 @@ async def test_write_category_callback_keeps_verified_context(
"verify_callback",
AsyncMock(return_value={"id": 42}),
)
monkeypatch.setattr(gateway, "_check_incident_state_guard", AsyncMock(return_value=None))
monkeypatch.setattr(
gateway, "_check_incident_state_guard", AsyncMock(return_value=None)
)
monkeypatch.setattr(gateway, "_answer_callback", AsyncMock())
spec = SimpleNamespace(
requires_multi_sig=False,
@@ -278,13 +309,15 @@ async def test_write_category_callback_keeps_verified_context(
monkeypatch.setattr(
callback_dispatcher,
"dispatch_action",
AsyncMock(return_value=DispatchResult(
success=True,
action="restart_safe",
incident_id="INC-1",
user_id=42,
result_text="done",
)),
AsyncMock(
return_value=DispatchResult(
success=True,
action="restart_safe",
incident_id="INC-1",
user_id=42,
result_text="done",
)
),
)
class _IncidentRepo:
@@ -335,9 +368,7 @@ async def test_approval_replies_keep_verified_callback_context(
_assert_verified_callback_payload(status_payload)
source_extra = status_payload[gateway_module._AWOOOP_SOURCE_ENVELOPE_EXTRA_KEY]
merged = gateway_module._merge_outbound_source_envelope_extra({}, source_extra)
assert merged["callback_reply"]["parent_provider_message_id"] == str(
_MESSAGE_ID
)
assert merged["callback_reply"]["parent_provider_message_id"] == str(_MESSAGE_ID)
send_request.reset_mock()
approval_id = UUID("44444444-4444-4444-4444-444444444444")
@@ -367,8 +398,9 @@ async def test_approval_replies_keep_verified_callback_context(
@pytest.mark.parametrize(
("result", "expected"),
[
({"ok": True}, True),
({"ok": True, "_awooop_delivery_status": "sent_reused"}, True),
(_SENT_RECEIPT, True),
({"ok": True}, False),
({"ok": True, "_awooop_delivery_status": "sent_reused"}, False),
({"ok": False}, False),
(
{"ok": True, "_awooop_delivery_status": "blocked_no_egress"},
@@ -414,7 +446,7 @@ async def test_approval_structured_no_send_retries_plain_on_exact_parent(
) -> None:
gateway = TelegramGateway()
attempts: list[tuple[str, dict]] = []
send_results = iter([primary_failure, {"ok": True}])
send_results = iter([primary_failure, dict(_SENT_RECEIPT)])
async def structured_primary_failure(method: str, payload: dict) -> dict:
attempts.append((method, payload))
@@ -423,7 +455,7 @@ async def test_approval_structured_no_send_retries_plain_on_exact_parent(
return {"ok": True}
record_failure = AsyncMock(return_value=None)
shared_fallback = AsyncMock(return_value={"ok": True})
shared_fallback = AsyncMock(return_value=dict(_SENT_RECEIPT))
monkeypatch.setattr(gateway, "_send_request", structured_primary_failure)
monkeypatch.setattr(gateway, "_record_callback_reply_failure", record_failure)
monkeypatch.setattr(gateway, "send_alert_notification", shared_fallback)
@@ -454,10 +486,12 @@ async def test_approval_structured_double_no_send_records_durable_failure(
) -> None:
gateway = TelegramGateway()
attempts: list[tuple[str, dict]] = []
send_results = iter([
{"ok": True, "_awooop_delivery_status": "blocked_no_egress"},
{"ok": True, "_awooop_provider_send_performed": False},
])
send_results = iter(
[
{"ok": True, "_awooop_delivery_status": "blocked_no_egress"},
{"ok": True, "_awooop_provider_send_performed": False},
]
)
async def structured_no_send(method: str, payload: dict) -> dict:
attempts.append((method, payload))
@@ -501,7 +535,7 @@ async def test_callback_card_edits_use_only_verified_inbound_identity(
action: str,
) -> None:
gateway = TelegramGateway()
send_request = AsyncMock(return_value={"ok": True})
send_request = AsyncMock(return_value=dict(_SENT_RECEIPT))
monkeypatch.setattr(gateway, "_send_request", send_request)
await gateway._update_message_after_action(
@@ -658,17 +692,20 @@ async def test_autonomous_lifecycle_threads_use_canonical_route_and_parent_recei
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway = TelegramGateway()
send_request = AsyncMock(return_value={"ok": True})
send_request = AsyncMock(return_value=dict(_SENT_RECEIPT))
monkeypatch.setattr(gateway, "_send_request", send_request)
monkeypatch.setattr(gateway_module, "get_redis", lambda: _LifecycleRedis())
assert await gateway.mark_auto_repaired("INC-1", "playbook", 10) is True
assert await gateway.append_incident_update("INC-2", "healthy") is True
assert await gateway.append_grouped_alert_digest(
incident_id="INC-3",
group_key="group-1",
digest_text="digest",
) is True
assert (
await gateway.append_grouped_alert_digest(
incident_id="INC-3",
group_key="group-1",
digest_text="digest",
)
is True
)
send_payloads = [
call.args[1]

View File

@@ -1,6 +1,8 @@
from __future__ import annotations
import runpy
from contextlib import asynccontextmanager
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock
@@ -11,13 +13,24 @@ import src.services.approval_db as approval_db
import src.services.telegram_gateway as gateway_module
from src.services.telegram_gateway import TelegramGateway
_REPO_ROOT = Path(__file__).resolve().parents[3]
class _FakeResponse:
def __init__(self, payload: dict) -> None:
self._payload = payload
def raise_for_status(self) -> None:
return None
def json(self) -> dict:
return {"ok": True, "result": {"message_id": 11}}
return {
"ok": True,
"result": {
"message_id": 11,
"chat": {"id": self._payload.get("chat_id")},
},
}
class _FakeHttpClient:
@@ -26,10 +39,12 @@ class _FakeHttpClient:
async def post(self, url: str, json: dict) -> _FakeResponse:
self.posts.append((url, json))
return _FakeResponse()
return _FakeResponse(json)
def _prepared_gateway(monkeypatch: pytest.MonkeyPatch) -> tuple[TelegramGateway, _FakeHttpClient]:
def _prepared_gateway(
monkeypatch: pytest.MonkeyPatch,
) -> tuple[TelegramGateway, _FakeHttpClient]:
gateway = TelegramGateway()
client = _FakeHttpClient()
gateway._initialized = True
@@ -115,6 +130,77 @@ async def test_notification_provider_requires_affirmative_delivery_ack(
assert result.error == "delivery_not_acknowledged"
@pytest.mark.asyncio
async def test_notification_connection_probe_rejects_structured_no_provider_send(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.services.notifications import telegram as provider_module
monkeypatch.setattr(provider_module.settings, "OPENCLAW_TG_BOT_TOKEN", "configured")
monkeypatch.setattr(provider_module.settings, "SRE_GROUP_CHAT_ID", "test-room")
gateway = SimpleNamespace(
send_alert_notification=AsyncMock(
return_value={
"ok": True,
"_awooop_delivery_status": "blocked_no_egress",
"_awooop_provider_send_performed": False,
"_awoooi_canonical_route_receipt": {
"decision": "blocked_no_egress",
"provider_send_performed": False,
},
}
)
)
monkeypatch.setattr(gateway_module, "get_telegram_gateway", lambda: gateway)
assert await provider_module.TelegramWebhookProvider().test_connection() is False
gateway.send_alert_notification.assert_awaited_once()
@pytest.mark.asyncio
async def test_notification_connection_probe_accepts_complete_canonical_delivery(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.services.notifications import telegram as provider_module
monkeypatch.setattr(provider_module.settings, "OPENCLAW_TG_BOT_TOKEN", "configured")
monkeypatch.setattr(provider_module.settings, "SRE_GROUP_CHAT_ID", "test-room")
chat_id = -1001
destination_binding = gateway_module._telegram_destination_binding(chat_id)
gateway = SimpleNamespace(
send_alert_notification=AsyncMock(
return_value={
"ok": True,
"_awooop_delivery_status": "sent",
"_awooop_provider_send_performed": True,
"result": {"message_id": 42, "chat": {"id": chat_id}},
"_awoooi_canonical_route_receipt": {
"schema_version": "telegram_canonical_egress_receipt_v1",
"decision": "allowed",
"destination_alias": "awoooi_sre_war_room",
"destination_binding": destination_binding,
"sender_bot_alias": "tsenyang_bot",
"provider_send_performed": True,
},
gateway_module._DELIVERY_CONTEXT_KEY: {
"schema_version": "telegram_delivery_context_v1",
"method": "sendMessage",
"sender_bot_alias": "tsenyang_bot",
"destination_alias": "awoooi_sre_war_room",
"destination_binding": destination_binding,
"payload_destination_binding": destination_binding,
"provider_destination_binding": destination_binding,
"destination_binding_verified": True,
},
}
)
)
monkeypatch.setattr(gateway_module, "get_telegram_gateway", lambda: gateway)
assert await provider_module.TelegramWebhookProvider().test_connection() is True
gateway.send_alert_notification.assert_awaited_once()
@pytest.mark.asyncio
async def test_unclassified_send_message_is_blocked_before_provider(
monkeypatch: pytest.MonkeyPatch,
@@ -131,6 +217,92 @@ async def test_unclassified_send_message_is_blocked_before_provider(
assert client.posts == []
@pytest.mark.asyncio
@pytest.mark.parametrize(
"method",
sorted(gateway_module._GUARDED_TELEGRAM_BOT_METHODS),
)
async def test_every_guarded_method_without_context_is_blocked_before_provider(
monkeypatch: pytest.MonkeyPatch,
method: str,
) -> None:
gateway, client = _prepared_gateway(monkeypatch)
result = await gateway._send_request(
method,
{
"chat_id": "test-room",
"text": "unclassified",
"caption": "unclassified",
"photo": "file-placeholder",
"document": "file-placeholder",
"media": [],
"message_id": 42,
},
)
assert result["_awooop_delivery_status"] == "blocked_no_egress"
assert result["_awooop_provider_send_performed"] is False
assert client.posts == []
@pytest.mark.asyncio
async def test_guarded_photo_accepts_only_a_valid_canonical_route(
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway, client = _prepared_gateway(monkeypatch)
result = await gateway._send_request(
"sendPhoto",
{
"chat_id": "test-room",
"photo": "file-placeholder",
gateway_module._CANONICAL_ROUTE_CONTEXT_KEY: {
"product_id": "awoooi",
"signal_family": "security_incident",
"severity": "P1",
},
},
)
assert result["_awooop_delivery_status"] == "sent"
assert gateway_module._telegram_send_delivery_succeeded(result) is True
assert len(client.posts) == 1
@pytest.mark.asyncio
@pytest.mark.parametrize(
("method", "method_payload"),
[
("sendDocument", {"document": "file-placeholder", "reply_to_message_id": 42}),
("editMessageText", {"text": "updated", "message_id": 42}),
],
)
async def test_guarded_interaction_method_requires_exact_inbound_binding(
monkeypatch: pytest.MonkeyPatch,
method: str,
method_payload: dict,
) -> None:
gateway, client = _prepared_gateway(monkeypatch)
result = await gateway._send_request(
method,
{
"chat_id": "operator-room",
**method_payload,
gateway_module._NON_MONITORING_INTERACTION_KEY: {
"interaction_kind": "callback_reply",
"inbound_chat_id": "operator-room",
"inbound_message_id": 42,
},
},
)
assert result["_awooop_delivery_status"] == "sent"
assert gateway_module._telegram_send_delivery_succeeded(result) is True
assert len(client.posts) == 1
@pytest.mark.asyncio
async def test_info_and_business_methods_are_blocked_before_provider(
monkeypatch: pytest.MonkeyPatch,
@@ -209,15 +381,77 @@ async def test_allowed_legacy_monitoring_methods_use_canonical_provider_once(
assert result["ok"] is True
assert result["_awoooi_canonical_route_receipt"]["decision"] == "allowed"
assert result["_awoooi_canonical_route_receipt"]["provider_send_performed"] is True
assert gateway_module._telegram_send_delivery_succeeded(result) is True
assert len(client.posts) == 1
assert client.posts[0][1]["chat_id"] == "test-room"
@pytest.mark.asyncio
async def test_provider_ok_without_message_id_is_non_terminal(
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway, client = _prepared_gateway(monkeypatch)
monkeypatch.setattr(
_FakeResponse,
"json",
lambda _self: {"ok": True, "result": {}},
)
result = await gateway.send_escalation_card(
incident_id="INC-TEST",
original_alertname="Outage",
duration_min=15,
)
assert result["_awooop_delivery_status"] == "provider_message_id_missing"
assert result["_awooop_provider_send_performed"] is True
assert result["_awoooi_canonical_route_receipt"]["provider_send_performed"] is True
assert gateway_module._telegram_send_delivery_succeeded(result) is False
assert len(client.posts) == 1
@pytest.mark.asyncio
async def test_provider_chat_mismatch_is_non_terminal_delivery(
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway, client = _prepared_gateway(monkeypatch)
monkeypatch.setattr(
_FakeResponse,
"json",
lambda _self: {
"ok": True,
"result": {"message_id": 11, "chat": {"id": "wrong-room"}},
},
)
result = await gateway.send_escalation_card(
incident_id="INC-TEST",
original_alertname="Outage",
duration_min=15,
)
assert result["_awooop_delivery_status"] == "provider_destination_mismatch"
assert result["_awooop_provider_send_performed"] is True
assert (
result[gateway_module._DELIVERY_CONTEXT_KEY][
"destination_binding_verified"
]
is False
)
assert gateway_module._telegram_send_delivery_succeeded(result) is False
assert len(client.posts) == 1
@pytest.mark.asyncio
async def test_openclaw_cannot_own_tsenyang_monitoring_route(
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway, client = _prepared_gateway(monkeypatch)
monkeypatch.setattr(
gateway_module.settings,
"OPENCLAW_BOT_TOKEN",
"test-token-placeholder",
)
result = await gateway._send_as_bot(
token="test-token-placeholder",
@@ -235,6 +469,100 @@ async def test_openclaw_cannot_own_tsenyang_monitoring_route(
assert client.posts == []
@pytest.mark.asyncio
async def test_alternate_bot_uses_only_canonical_send_request(
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway = TelegramGateway()
canonical_send = AsyncMock(
return_value={
"ok": True,
"_awooop_delivery_status": "sent",
"_awooop_provider_send_performed": True,
"result": {"message_id": 11},
"_awoooi_canonical_route_receipt": {
"schema_version": "telegram_canonical_egress_receipt_v1",
"decision": "allowed",
"provider_send_performed": True,
},
}
)
monkeypatch.setattr(gateway, "_send_request", canonical_send)
result = await gateway._send_as_bot(
token="test-token-placeholder",
text="bounded reply",
reply_to_message_id=11,
actual_bot_alias="openclaw_bot",
inbound_chat_id="verified-room",
)
assert result["_awooop_delivery_status"] == "sent"
canonical_send.assert_awaited_once()
method, payload = canonical_send.await_args.args
assert method == "sendMessage"
assert payload[gateway_module._ACTUAL_BOT_ALIAS_KEY] == "openclaw_bot"
assert payload[gateway_module._BOT_TOKEN_OVERRIDE_KEY] == ("test-token-placeholder")
assert payload[gateway_module._NON_MONITORING_INTERACTION_KEY] == {
"interaction_kind": "bot_discussion_reply",
"inbound_chat_id": "verified-room",
"inbound_message_id": 11,
}
@pytest.mark.asyncio
async def test_alternate_bot_binding_mismatch_blocks_before_http(
monkeypatch: pytest.MonkeyPatch,
) -> None:
gateway, client = _prepared_gateway(monkeypatch)
monkeypatch.setattr(
gateway_module.settings,
"OPENCLAW_BOT_TOKEN",
"expected-placeholder",
)
result = await gateway._send_as_bot(
token="mismatched-placeholder",
text="must not send",
reply_to_message_id=11,
actual_bot_alias="openclaw_bot",
inbound_chat_id="test-room",
)
assert result["_awooop_delivery_status"] == "blocked_no_egress"
assert result["_awoooi_canonical_route_receipt"]["classifier"] == (
"sender_bot_binding_mismatch"
)
assert client.posts == []
def test_readability_guard_uses_complete_ast_function_boundary() -> None:
guard = runpy.run_path(
str(_REPO_ROOT / "scripts" / "security" / "telegram-alert-readability-guard.py")
)
long_body = "\n".join(f" value_{index} = {index}" for index in range(300))
source = (
'DECOY = "async def _send_request"\n'
"class Gateway:\n"
" async def _send_request(self, payload):\n"
f"{long_body}\n"
" return normalize_telegram_send_message_payload(\n"
' "sendMessage", payload\n'
" )\n\n"
" async def unrelated(self):\n"
" return False\n"
)
segment = guard["function_segment"](
source,
"async def _send_request",
)
assert len(segment) > 2200
assert "normalize_telegram_send_message_payload" in segment
assert "async def unrelated" not in segment
@pytest.mark.asyncio
async def test_operator_reply_lane_and_runtime_edit_binding_remain_available(
monkeypatch: pytest.MonkeyPatch,

View File

@@ -13,6 +13,7 @@ from src.models.incident import Incident, Severity, Signal
from src.repositories.alert_operation_log_repository import ALERT_EVENT_TYPES
from src.services import decision_manager as decision_module
from src.services import report_generation_service as report_module
from src.services import telegram_gateway as gateway_module
from src.services.ai_rate_limiter import AIRateLimiter
from src.services.approval_execution import ApprovalExecutionService
from src.services.report_generation_service import ReportGenerationService
@@ -27,11 +28,33 @@ NO_SEND_RECEIPT = {
},
}
_SENT_CHAT_ID = -1001
_SENT_DESTINATION_BINDING = gateway_module._telegram_destination_binding(
_SENT_CHAT_ID
)
SENT_RECEIPT = {
"ok": True,
"_awooop_delivery_status": "sent",
"_awooop_provider_send_performed": True,
"result": {"message_id": 42, "chat": {"id": -1001}},
"_awoooi_canonical_route_receipt": {
"schema_version": "telegram_canonical_egress_receipt_v1",
"decision": "allowed",
"destination_alias": "awoooi_sre_war_room",
"destination_binding": _SENT_DESTINATION_BINDING,
"sender_bot_alias": "tsenyang_bot",
"provider_send_performed": True,
},
gateway_module._DELIVERY_CONTEXT_KEY: {
"schema_version": "telegram_delivery_context_v1",
"method": "sendMessage",
"sender_bot_alias": "tsenyang_bot",
"destination_alias": "awoooi_sre_war_room",
"destination_binding": _SENT_DESTINATION_BINDING,
"payload_destination_binding": _SENT_DESTINATION_BINDING,
"provider_destination_binding": _SENT_DESTINATION_BINDING,
"destination_binding_verified": True,
},
}
@@ -260,9 +283,7 @@ async def test_approval_blocked_receipt_writes_no_send_not_sent(monkeypatch) ->
repair_attempted=True,
)
assert [row["event_type"] for row in operations] == [
"NOTIFICATION_CLASSIFIED"
]
assert [row["event_type"] for row in operations] == ["NOTIFICATION_CLASSIFIED"]
assert operations[0]["event_type"] in ALERT_EVENT_TYPES
assert operations[0]["action_detail"] == "telegram_execution_result_no_send"
assert operations[0]["success"] is False

View File

@@ -24,6 +24,7 @@ from src.services import converged_alert_recurrence_notifier as recurrence_modul
from src.services import decision_manager as decision_module
from src.services import failover_alerter as failover_module
from src.services import k3s_monitor_service as k3s_module
from src.services import telegram_gateway as gateway_module
from src.services import weekly_report_service as weekly_module
NO_PROVIDER_SEND_RECEIPT = {
@@ -36,11 +37,33 @@ NO_PROVIDER_SEND_RECEIPT = {
},
}
_SENT_CHAT_ID = -1001
_SENT_DESTINATION_BINDING = gateway_module._telegram_destination_binding(
_SENT_CHAT_ID
)
SENT_RECEIPT = {
"ok": True,
"_awooop_delivery_status": "sent",
"_awooop_provider_send_performed": True,
"result": {"message_id": 42, "chat": {"id": -1001}},
"_awoooi_canonical_route_receipt": {
"schema_version": "telegram_canonical_egress_receipt_v1",
"decision": "allowed",
"destination_alias": "awoooi_sre_war_room",
"destination_binding": _SENT_DESTINATION_BINDING,
"sender_bot_alias": "tsenyang_bot",
"provider_send_performed": True,
},
gateway_module._DELIVERY_CONTEXT_KEY: {
"schema_version": "telegram_delivery_context_v1",
"method": "sendMessage",
"sender_bot_alias": "tsenyang_bot",
"destination_alias": "awoooi_sre_war_room",
"destination_binding": _SENT_DESTINATION_BINDING,
"payload_destination_binding": _SENT_DESTINATION_BINDING,
"provider_destination_binding": _SENT_DESTINATION_BINDING,
"destination_binding_verified": True,
},
}
@@ -170,7 +193,9 @@ async def test_watchdog_releases_dedup_and_does_not_log_sent_without_provider_ac
logger = _Logger()
monkeypatch.setattr(watchdog_module, "logger", logger)
monkeypatch.setattr(watchdog_module, "get_redis", lambda: redis)
monkeypatch.setattr(watchdog_module, "_is_grace_active", AsyncMock(return_value=True))
monkeypatch.setattr(
watchdog_module, "_is_grace_active", AsyncMock(return_value=True)
)
monkeypatch.setattr(
watchdog_module,
"_count_pending_no_tg_sent",

View File

@@ -25,11 +25,33 @@ NO_SEND_RECEIPT = {
},
}
_SENT_CHAT_ID = -1001
_SENT_DESTINATION_BINDING = gateway_module._telegram_destination_binding(
_SENT_CHAT_ID
)
SENT_RECEIPT = {
"ok": True,
"_awooop_delivery_status": "sent",
"_awooop_provider_send_performed": True,
"result": {"message_id": 42},
"result": {"message_id": 42, "chat": {"id": _SENT_CHAT_ID}},
"_awoooi_canonical_route_receipt": {
"schema_version": "telegram_canonical_egress_receipt_v1",
"decision": "allowed",
"destination_alias": "awoooi_sre_war_room",
"destination_binding": _SENT_DESTINATION_BINDING,
"sender_bot_alias": "tsenyang_bot",
"provider_send_performed": True,
},
gateway_module._DELIVERY_CONTEXT_KEY: {
"schema_version": "telegram_delivery_context_v1",
"method": "sendMessage",
"sender_bot_alias": "tsenyang_bot",
"destination_alias": "awoooi_sre_war_room",
"destination_binding": _SENT_DESTINATION_BINDING,
"payload_destination_binding": _SENT_DESTINATION_BINDING,
"provider_destination_binding": _SENT_DESTINATION_BINDING,
"destination_binding_verified": True,
},
}
@@ -73,9 +95,86 @@ class FakeRedis:
return True
def test_delivery_helper_rejects_truthy_structured_no_send() -> None:
assert _telegram_send_delivery_succeeded(dict(NO_SEND_RECEIPT)) is False
assert _telegram_send_delivery_succeeded({"ok": True}) is True
@pytest.mark.parametrize(
("receipt", "expected"),
[
(SENT_RECEIPT, True),
(NO_SEND_RECEIPT, False),
({"ok": True}, False),
({**SENT_RECEIPT, "ok": 1}, False),
({**SENT_RECEIPT, "_awooop_delivery_status": ""}, False),
({**SENT_RECEIPT, "_awooop_provider_send_performed": None}, False),
({**SENT_RECEIPT, "result": {}}, False),
({**SENT_RECEIPT, "result": {"message_id": 0}}, False),
({**SENT_RECEIPT, "result": {"message_id": True}}, False),
({**SENT_RECEIPT, "result": {"message_id": "42"}}, False),
({**SENT_RECEIPT, "_awoooi_canonical_route_receipt": {}}, False),
({**SENT_RECEIPT, gateway_module._DELIVERY_CONTEXT_KEY: {}}, False),
(
{
**SENT_RECEIPT,
"_awoooi_canonical_route_receipt": {
"schema_version": "telegram_canonical_egress_receipt_v1",
"decision": "blocked_no_egress",
"provider_send_performed": True,
},
},
False,
),
(
{
**SENT_RECEIPT,
"_awoooi_canonical_route_receipt": {
**SENT_RECEIPT["_awoooi_canonical_route_receipt"],
"sender_bot_alias": "",
},
},
False,
),
(
{
**SENT_RECEIPT,
gateway_module._DELIVERY_CONTEXT_KEY: {
**SENT_RECEIPT[gateway_module._DELIVERY_CONTEXT_KEY],
"sender_bot_alias": "openclaw_bot",
},
},
False,
),
(
{
**SENT_RECEIPT,
"_awoooi_canonical_route_receipt": {
**SENT_RECEIPT["_awoooi_canonical_route_receipt"],
"destination_alias": "other_destination",
},
},
False,
),
(
{
**SENT_RECEIPT,
"_awoooi_canonical_route_receipt": {
**SENT_RECEIPT["_awoooi_canonical_route_receipt"],
"destination_binding": "",
},
},
False,
),
(
{
**SENT_RECEIPT,
"result": {"message_id": 42, "chat": {"id": -2002}},
},
False,
),
],
)
def test_delivery_helper_fails_closed_without_complete_canonical_receipt(
receipt: object,
expected: bool,
) -> None:
assert _telegram_send_delivery_succeeded(receipt) is expected
@pytest.mark.asyncio
@@ -244,7 +343,8 @@ async def test_html_callback_no_send_exhausts_fallback_and_records_failure(
assert send_request.await_count == 3
record_failure.assert_awaited_once()
statuses = [
call.args[1].get(gateway_module._AWOOOP_SOURCE_ENVELOPE_EXTRA_KEY, {})
call.args[1]
.get(gateway_module._AWOOOP_SOURCE_ENVELOPE_EXTRA_KEY, {})
.get("callback_reply", {})
.get("status")
for call in send_request.await_args_list
@@ -298,6 +398,7 @@ async def test_interactive_reply_no_send_logs_failure_not_sent(
failed_event = "ai_advisory_group_reply_failed"
sent_event = "ai_advisory_group_reply_sent"
else:
class Repo:
async def get_by_id(self, _incident_id: str):
return None
@@ -451,9 +552,7 @@ async def test_dev_test_push_returns_false_for_structured_no_send(
monkeypatch.setattr(telegram.settings, "ENVIRONMENT", "dev")
monkeypatch.setattr(telegram, "get_telegram_gateway", lambda: Gateway())
response = await telegram.test_push(
telegram.TestPushRequest(approval_id="APR-1")
)
response = await telegram.test_push(telegram.TestPushRequest(approval_id="APR-1"))
assert response["ok"] is False
assert response["message"] == "Test push no-send"

View File

@@ -26,6 +26,35 @@ from src.services.telegram_gateway import (
normalize_telegram_send_message_payload,
)
_SENT_CHAT_ID = -1001
_SENT_DESTINATION_BINDING = (
telegram_gateway_module._telegram_destination_binding(_SENT_CHAT_ID)
)
_CANONICAL_SENT_RECEIPT = {
"ok": True,
"_awooop_delivery_status": "sent",
"_awooop_provider_send_performed": True,
"result": {"message_id": 77, "chat": {"id": _SENT_CHAT_ID}},
"_awoooi_canonical_route_receipt": {
"schema_version": "telegram_canonical_egress_receipt_v1",
"decision": "allowed",
"destination_alias": "awoooi_sre_war_room",
"destination_binding": _SENT_DESTINATION_BINDING,
"sender_bot_alias": "tsenyang_bot",
"provider_send_performed": True,
},
telegram_gateway_module._DELIVERY_CONTEXT_KEY: {
"schema_version": "telegram_delivery_context_v1",
"method": "sendMessage",
"sender_bot_alias": "tsenyang_bot",
"destination_alias": "awoooi_sre_war_room",
"destination_binding": _SENT_DESTINATION_BINDING,
"payload_destination_binding": _SENT_DESTINATION_BINDING,
"provider_destination_binding": _SENT_DESTINATION_BINDING,
"destination_binding_verified": True,
},
}
def test_auto_repair_status_line_distinguishes_ai_retry_queued() -> None:
"""自動化失敗 reply 必須明確標示 AI 續跑,且不把 raw error 當純文字噴出。"""
@@ -289,7 +318,7 @@ async def test_send_alert_notification_normalizes_host_resource_raw_dump(monkeyp
async def fake_send_request(method, payload):
sent_requests.append((method, payload))
return {"ok": True}
return dict(_CANONICAL_SENT_RECEIPT)
monkeypatch.setattr(TelegramGateway, "alert_chat_id", property(lambda _self: "chat"))
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
@@ -329,7 +358,7 @@ async def test_send_alert_notification_normalizes_aiops_signal_alert(monkeypatch
async def fake_send_request(method, payload):
sent_requests.append((method, payload))
return {"ok": True}
return dict(_CANONICAL_SENT_RECEIPT)
monkeypatch.setattr(TelegramGateway, "alert_chat_id", property(lambda _self: "chat"))
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
@@ -384,7 +413,7 @@ async def test_send_alert_notification_forces_html_card_for_markdown_host_alert(
async def fake_send_request(method, payload):
sent_requests.append((method, payload))
return {"ok": True}
return dict(_CANONICAL_SENT_RECEIPT)
monkeypatch.setattr(TelegramGateway, "alert_chat_id", property(lambda _self: "chat"))
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
@@ -417,7 +446,7 @@ async def test_send_text_normalizes_host_resource_alert(monkeypatch) -> None:
async def fake_send_request(method, payload):
sent_requests.append((method, payload))
return {"ok": True}
return dict(_CANONICAL_SENT_RECEIPT)
monkeypatch.setattr(TelegramGateway, "alert_chat_id", property(lambda _self: "chat"))
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
@@ -482,7 +511,10 @@ async def test_send_request_mirrors_direct_ai_alert_card_to_agent99(monkeypatch)
return None
def json(self) -> dict:
return {"ok": True, "result": {"message_id": 991}}
return {
"ok": True,
"result": {"message_id": 991, "chat": {"id": "chat"}},
}
class FakeHttpClient:
async def post(self, url, json): # type: ignore[no-untyped-def]
@@ -541,7 +573,10 @@ async def test_send_request_mirrors_direct_ai_alert_card_to_agent99(monkeypatch)
await asyncio.sleep(0)
assert result["ok"] is True
assert result["result"] == {"message_id": 991}
assert result["result"] == {
"message_id": 991,
"chat": {"id": "chat"},
}
assert result["_awoooi_canonical_route_receipt"]["decision"] == "allowed"
assert mirrored
assert mirrored[0]["source"] == "send_request"
@@ -1789,7 +1824,10 @@ async def test_send_request_strips_awooop_callback_metadata_before_telegram_api(
return None
def json(self):
return {"ok": True, "result": {"message_id": 456}}
return {
"ok": True,
"result": {"message_id": 456, "chat": {"id": "chat"}},
}
class FakeClient:
async def post(self, url, json):
@@ -1878,7 +1916,7 @@ async def test_send_html_line_message_falls_back_to_plain_text_on_parse_error(mo
sent_requests.append((method, payload))
if payload.get("parse_mode") == "HTML":
raise telegram_gateway_module.TelegramGatewayError("HTTP error: 400")
return {"ok": True}
return dict(_CANONICAL_SENT_RECEIPT)
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
@@ -1910,7 +1948,7 @@ async def test_send_html_line_message_marks_callback_reply_evidence(monkeypatch)
sent_requests.append((method, payload))
if payload.get("parse_mode") == "HTML":
raise telegram_gateway_module.TelegramGatewayError("HTTP error: 400")
return {"ok": True}
return dict(_CANONICAL_SENT_RECEIPT)
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
@@ -2002,7 +2040,13 @@ async def test_send_html_line_message_marks_callback_reply_evidence(monkeypatch)
acknowledged = (
telegram_gateway_module._acknowledge_callback_reply_source_envelope(
fallback_source_extra,
delivery_result={"ok": True},
delivery_result={
**_CANONICAL_SENT_RECEIPT,
"result": {
"message_id": 88,
"chat": {"id": _SENT_CHAT_ID},
},
},
provider_message_id="88",
)
)
@@ -2042,7 +2086,7 @@ async def test_send_html_line_message_uses_rescue_when_markup_fallback_fails(mon
sent_requests.append((method, payload))
if len(sent_requests) < 3:
raise telegram_gateway_module.TelegramGatewayError("HTTP error: 400")
return {"ok": True}
return dict(_CANONICAL_SENT_RECEIPT)
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
@@ -2070,7 +2114,7 @@ async def test_send_html_line_message_attaches_awooop_markup_to_first_chunk(monk
async def fake_send_request(method, payload):
sent_requests.append((method, payload))
return {"ok": True}
return dict(_CANONICAL_SENT_RECEIPT)
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
@@ -2160,7 +2204,7 @@ async def test_send_notification_long_html_uses_plain_text_without_cutting_tags(
async def fake_send_request(method, payload):
sent_requests.append((method, payload))
return {"ok": True}
return dict(_CANONICAL_SENT_RECEIPT)
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
long_text = "📊 <b>事件歷史統計</b>\n告警鍵: <code>" + ("node<188>&" * 90) + "</code>"
@@ -2446,7 +2490,7 @@ async def test_append_incident_update_deduplicates_same_status(monkeypatch):
async def fake_send_request(method, payload):
sent_requests.append((method, payload))
return {"ok": True}
return dict(_CANONICAL_SENT_RECEIPT)
monkeypatch.setattr(telegram_gateway_module, "get_redis", lambda: fake_redis)
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
@@ -2491,7 +2535,7 @@ async def test_append_incident_update_suppresses_duplicate_failure_across_incide
async def fake_send_request(method, payload):
sent_requests.append((method, payload))
return {"ok": True}
return dict(_CANONICAL_SENT_RECEIPT)
monkeypatch.setattr(telegram_gateway_module, "get_redis", lambda: fake_redis)
monkeypatch.setattr(gateway, "_send_request", fake_send_request)

View File

@@ -0,0 +1,495 @@
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"] == "<module>"
assert findings[0]["function_qualified"] == "<module>"
@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