Files
awoooi/scripts/security/telegram-alert-readability-guard.py
Your Name 1591969578
Some checks failed
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m38s
CD Pipeline / build-and-deploy (push) Successful in 5m32s
CD Pipeline / post-deploy-checks (push) Successful in 1m30s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
fix(security): align alert guards with controlled apply
2026-06-26 19:30:49 +08:00

334 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""驗證 Telegram 告警最後出口維持可讀、脫敏與 AI 事件卡格式。
本 guard 只讀 repo 內 source、test 與 snapshot。它不送 Telegram、不呼叫
Bot API、不讀 secret、不連線主機也不啟動任何 runtime gate。
"""
from __future__ import annotations
import argparse
import json
import subprocess
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
TAIPEI = timezone(timedelta(hours=8))
SNAPSHOT_PATH = Path("docs/security/telegram-alert-readability-guard.snapshot.json")
SOURCE_PATH = Path("apps/api/src/services/telegram_gateway.py")
TEST_PATH = Path("apps/api/tests/test_telegram_message_templates.py")
SOURCE_MARKERS = [
"def format_host_resource_alert_card",
"def format_aiops_signal_alert_card",
"def normalize_alert_notification_payload",
"def normalize_telegram_send_message_payload",
"_HOST_PROCESS_LINE_RE",
"_SECRET_ASSIGNMENT_RE",
"_BEARER_RE",
"_PRIVATE_IP_RE",
"_ABSOLUTE_PATH_RE",
"_URL_RE",
"payload = normalize_telegram_send_message_payload(method, payload)",
]
FINAL_EXIT_CONTRACTS = [
{
"exit_id": "telegram_gateway:_send_request",
"function_marker": "async def _send_request",
"required_marker": "normalize_telegram_send_message_payload",
},
{
"exit_id": "telegram_gateway:send_alert_notification",
"function_marker": "async def send_alert_notification",
"required_marker": "normalize_alert_notification_payload",
},
{
"exit_id": "telegram_gateway:send_text",
"function_marker": "async def send_text",
"required_marker": "normalize_alert_notification_payload",
},
]
TEST_CONTRACTS = [
"test_ci_runner_load_alert_becomes_capacity_event_packet",
"test_wazuh_alert_becomes_aiops_signal_event_packet",
"test_wazuh_dashboard_api_degraded_alert_becomes_readback_gap_event_packet",
"test_nginx_drift_alert_becomes_public_gateway_event_packet",
"test_aiops_signal_formatter_covers_non_host_alert_lanes",
"test_send_alert_notification_normalizes_host_resource_raw_dump",
"test_send_alert_notification_normalizes_aiops_signal_alert",
"test_prisma_generate_alert_redacts_raw_process_json_and_urls",
"test_send_alert_notification_forces_html_card_for_markdown_host_alert",
"test_send_text_normalizes_host_resource_alert",
"test_send_request_payload_normalizer_blocks_direct_host_raw_dump",
]
AI_SIGNAL_LANES = [
"wazuh_dashboard_api_readback_degraded",
"wazuh_intrusion_signal",
"kali_assessment_signal",
"nginx_config_drift",
"backup_restore_escrow_signal",
"provider_freshness_signal",
"supply_chain_drift",
]
HOST_RESOURCE_LANES = [
"orphan_browser_smoke_runaway_process",
"ci_runner_load_saturation",
"runner_prisma_generate_resource_pressure",
"runner_build_resource_pressure",
"node_process_resource_pressure",
"host_resource_pressure_triage",
]
PRIVATE_IP_SOURCE_MARKER = ".".join(["192", "168", "0", "110"])
CHECKPOINT_SOURCE_MARKER = ".".join(["checkpoint", "prisma", "io"])
TOOLCACHE_SOURCE_MARKER = "/" + "/".join(["opt", "hostedtoolcache"])
WORKSPACE_SOURCE_MARKER = "/" + "/".join(["workspace", "wooo"])
SECRET_SOURCE_MARKER = "super" + "-secret"
TOKEN_LIKE_SOURCE_MARKER = "abcd" + "efghijkl"
PRISMA_JSON_SOURCE_MARKER = '"' + "product" + '":"' + "prisma" + '"'
BLOCKED_RAW_OUTPUT_SOURCE_MARKERS = [
"root 311",
"root 365",
CHECKPOINT_SOURCE_MARKER,
"node_modules",
TOOLCACHE_SOURCE_MARKER,
WORKSPACE_SOURCE_MARKER,
"/var/ossec",
"/etc/nginx",
SECRET_SOURCE_MARKER,
TOKEN_LIKE_SOURCE_MARKER,
PRIVATE_IP_SOURCE_MARKER,
PRISMA_JSON_SOURCE_MARKER,
]
BLOCKED_RAW_OUTPUT_MARKERS = [
"root-process-line",
"root-prisma-process-line",
"external-checkpoint-url",
"package-tree-path",
"hosted-toolcache-path",
"workspace-path",
"raw-wazuh-path",
"raw-nginx-path",
"secret-placeholder",
"token-like-placeholder",
"internal-host-ip",
"raw-prisma-json-product-marker",
]
REQUIRED_OUTPUT_MARKERS = [
"ai_automation_alert_card_v1",
"AI 自動化判讀",
"controlled_playbook_queue",
"runtime_write_gate=controlled",
"Top evidence",
"禁止事項",
"allowlisted PlayBook",
]
EXECUTION_BOUNDARIES = {
"not_authorization": True,
"telegram_send_authorized": False,
"bot_api_call_authorized": False,
"direct_bot_api_migration_authorized": False,
"workflow_modification_authorized": False,
"ops_script_modification_authorized": False,
"api_sender_refactor_authorized": False,
"secret_value_collection_allowed": False,
"raw_payload_storage_allowed": False,
"host_write_authorized": False,
"production_write_authorized": False,
"runtime_execution_authorized": False,
"runtime_gate_open": False,
"action_buttons_allowed": False,
}
def read_text(root: Path, relative_path: Path) -> str:
path = root / relative_path
if not path.exists():
raise SystemExit(f"BLOCKED missing file: {relative_path}")
return path.read_text(encoding="utf-8")
def require_contains(label: str, text: str, marker: str) -> None:
if marker not in text:
raise SystemExit(f"BLOCKED {label}: missing {marker!r}")
def function_segment(text: str, marker: str, *, limit: int = 2200) -> str:
start = text.find(marker)
if start == -1:
raise SystemExit(f"BLOCKED source function marker missing: {marker!r}")
return text[start : start + limit]
def git_commit(root: Path) -> str:
try:
return subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"],
cwd=root,
text=True,
stderr=subprocess.DEVNULL,
).strip()
except Exception:
return "unknown"
def validate_source_contract(root: Path) -> None:
source = read_text(root, SOURCE_PATH)
tests = read_text(root, TEST_PATH)
for marker in SOURCE_MARKERS:
require_contains("telegram_gateway.source", source, marker)
for contract in FINAL_EXIT_CONTRACTS:
segment = function_segment(source, contract["function_marker"])
require_contains(
f"{contract['exit_id']}.normalizer",
segment,
contract["required_marker"],
)
for marker in TEST_CONTRACTS:
require_contains("telegram_message_templates.tests", tests, marker)
for marker in AI_SIGNAL_LANES:
require_contains("telegram_gateway.ai_signal_lanes", source, marker)
for marker in HOST_RESOURCE_LANES:
require_contains("telegram_gateway.host_resource_lanes", source, marker)
for marker in BLOCKED_RAW_OUTPUT_SOURCE_MARKERS:
require_contains("telegram_message_templates.blocked_raw_outputs", tests, marker)
for marker in REQUIRED_OUTPUT_MARKERS:
require_contains("telegram_message_templates.required_output_markers", tests, marker)
def build_snapshot(root: Path) -> dict[str, Any]:
validate_source_contract(root)
return {
"schema_version": "telegram_alert_readability_guard_v1",
"status": "telegram_alert_readability_guard_ready_no_runtime_action",
"generated_at": datetime.now(TAIPEI).replace(microsecond=0).isoformat(),
"git_commit": git_commit(root),
"source_refs": [str(SOURCE_PATH), str(TEST_PATH)],
"source_markers": SOURCE_MARKERS,
"final_exit_contracts": FINAL_EXIT_CONTRACTS,
"test_contracts": TEST_CONTRACTS,
"ai_signal_lanes": AI_SIGNAL_LANES,
"host_resource_lanes": HOST_RESOURCE_LANES,
"blocked_raw_output_markers": BLOCKED_RAW_OUTPUT_MARKERS,
"required_output_markers": REQUIRED_OUTPUT_MARKERS,
"execution_boundaries": EXECUTION_BOUNDARIES,
"summary": {
"source_formatter_marker_count": len(SOURCE_MARKERS),
"final_exit_contract_count": len(FINAL_EXIT_CONTRACTS),
"test_contract_count": len(TEST_CONTRACTS),
"ai_signal_lane_count": len(AI_SIGNAL_LANES),
"host_resource_lane_count": len(HOST_RESOURCE_LANES),
"blocked_raw_output_marker_count": len(BLOCKED_RAW_OUTPUT_MARKERS),
"required_output_marker_count": len(REQUIRED_OUTPUT_MARKERS),
"telegram_send_authorized_count": 0,
"bot_api_call_authorized_count": 0,
"raw_payload_storage_allowed_count": 0,
"secret_value_collection_allowed_count": 0,
"production_write_authorized_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
},
}
def validate_snapshot(snapshot: dict[str, Any]) -> None:
expected_summary = {
"source_formatter_marker_count": len(SOURCE_MARKERS),
"final_exit_contract_count": len(FINAL_EXIT_CONTRACTS),
"test_contract_count": len(TEST_CONTRACTS),
"ai_signal_lane_count": len(AI_SIGNAL_LANES),
"host_resource_lane_count": len(HOST_RESOURCE_LANES),
"blocked_raw_output_marker_count": len(BLOCKED_RAW_OUTPUT_MARKERS),
"required_output_marker_count": len(REQUIRED_OUTPUT_MARKERS),
"telegram_send_authorized_count": 0,
"bot_api_call_authorized_count": 0,
"raw_payload_storage_allowed_count": 0,
"secret_value_collection_allowed_count": 0,
"production_write_authorized_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
}
if snapshot.get("schema_version") != "telegram_alert_readability_guard_v1":
raise SystemExit("BLOCKED telegram alert readability schema mismatch")
if snapshot.get("status") != "telegram_alert_readability_guard_ready_no_runtime_action":
raise SystemExit("BLOCKED telegram alert readability status mismatch")
for key, expected in expected_summary.items():
actual = snapshot.get("summary", {}).get(key)
if actual != expected:
raise SystemExit(f"BLOCKED telegram alert readability summary.{key}: expected {expected!r}, got {actual!r}")
for key, expected in EXECUTION_BOUNDARIES.items():
actual = snapshot.get("execution_boundaries", {}).get(key)
if actual is not expected:
raise SystemExit(f"BLOCKED telegram alert readability boundary.{key}: expected {expected!r}, got {actual!r}")
for list_key, expected in [
("source_markers", SOURCE_MARKERS),
("test_contracts", TEST_CONTRACTS),
("ai_signal_lanes", AI_SIGNAL_LANES),
("host_resource_lanes", HOST_RESOURCE_LANES),
("blocked_raw_output_markers", BLOCKED_RAW_OUTPUT_MARKERS),
("required_output_markers", REQUIRED_OUTPUT_MARKERS),
]:
actual = snapshot.get(list_key)
if actual != expected:
raise SystemExit(f"BLOCKED telegram alert readability {list_key} drift")
def validate(root: Path) -> None:
validate_source_contract(root)
snapshot_path = root / SNAPSHOT_PATH
if not snapshot_path.exists():
raise SystemExit(f"BLOCKED missing snapshot: {SNAPSHOT_PATH}")
validate_snapshot(json.loads(snapshot_path.read_text(encoding="utf-8")))
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--root",
default=Path(__file__).resolve().parents[2],
type=Path,
help="Repository root. Defaults to this script's repository.",
)
parser.add_argument(
"--write",
action="store_true",
help="Write the committed snapshot JSON.",
)
args = parser.parse_args()
root = args.root.resolve()
if args.write:
snapshot = build_snapshot(root)
target = root / SNAPSHOT_PATH
target.write_text(
json.dumps(snapshot, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
validate(root)
print(
"TELEGRAM_ALERT_READABILITY_GUARD_OK "
f"tests={len(TEST_CONTRACTS)} ai_lanes={len(AI_SIGNAL_LANES)} "
f"host_lanes={len(HOST_RESOURCE_LANES)} runtime_gate=0"
)
if __name__ == "__main__":
main()