From 1eaa51e6456af5dc922e38353546e493550e35ae Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 19 Jun 2026 02:02:13 +0800 Subject: [PATCH] =?UTF-8?q?docs(security):=20=E6=96=B0=E5=A2=9E=20Telegram?= =?UTF-8?q?=20=E5=91=8A=E8=AD=A6=E5=8F=AF=E8=AE=80=E6=80=A7=20guard=20[ski?= =?UTF-8?q?p=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/LOGBOOK.md | 38 ++ .../HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md | 2 + .../IWOOOS-CONFIG-CONTROL-INVENTORY.md | 2 + .../SECURITY-SUPPLY-CHAIN-PROGRESS.md | 8 + .../TELEGRAM-ALERT-READABILITY-GUARD.md | 67 ++++ ...gram-alert-readability-guard.snapshot.json | 122 +++++++ ...026-06-04-iwooos-security-governance-p0.md | 2 +- .../security/iwooos-config-control-guard.py | 32 ++ .../security-mirror-progress-guard.py | 4 + .../telegram-alert-readability-guard.py | 330 ++++++++++++++++++ 10 files changed, 606 insertions(+), 1 deletion(-) create mode 100644 docs/security/TELEGRAM-ALERT-READABILITY-GUARD.md create mode 100644 docs/security/telegram-alert-readability-guard.snapshot.json create mode 100644 scripts/security/telegram-alert-readability-guard.py diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 5caa925b3..6722914b8 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,41 @@ +## 2026-06-19|Telegram 告警可讀性防退化 Guard 完成 + +**背景**:Telegram 群組曾收到 Host CPU / root Node.js / Prisma generate 類告警時,訊息直接包含 process list、完整路徑、package JSON、URL 與過長命令列,值班者難以判讀,也容易把內部路徑、raw payload 或敏感片段帶到通知通道。本輪把既有 `TelegramGateway` AI 事件卡 formatter 補成可重跑 guard,避免未來退化。 + +**完成內容**: +- 新增 `scripts/security/telegram-alert-readability-guard.py`。 +- 新增 `docs/security/telegram-alert-readability-guard.snapshot.json`。 +- 新增 `docs/security/TELEGRAM-ALERT-READABILITY-GUARD.md`。 +- `security-mirror-progress-guard.py` 已直接呼叫 readability guard。 +- `iwooos-config-control-guard.py` 已把 readability guard 納入高價值配置控管文件與 snapshot 檢查。 +- P0 主控板、`IWOOOS-CONFIG-CONTROL-INVENTORY.md`、`HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md` 與 `SECURITY-SUPPLY-CHAIN-PROGRESS.md` 已同步。 + +**固定數字**: +- source formatter marker `11`。 +- final exit contract `3`:`_send_request()`、`send_alert_notification()`、`send_text()`。 +- test contract `10`。 +- AI signal lane `6`:Wazuh、Kali、Nginx drift、backup / restore、provider freshness、supply-chain。 +- Host resource lane `6`:orphan browser smoke、CI runner saturation、Prisma generate、build pressure、Node process、generic host pressure。 +- blocked raw output marker `12`,包含 process list、`checkpoint.prisma.io`、`node_modules`、`/workspace/wooo`、`/opt/hostedtoolcache`、raw Wazuh / Nginx path、內網 IP、token 與 raw Prisma JSON。 +- runtime gate / action button / Telegram send / Bot API call 全部 `0`。 + +**驗證**: +- `TELEGRAM_ALERT_READABILITY_GUARD_OK tests=10 ai_lanes=6 host_lanes=6 runtime_gate=0`。 +- `apps/api/tests/test_telegram_message_templates.py`:`72 passed`。 +- `SECURITY_MIRROR_PROGRESS_GUARD_OK`。 +- `IWOOOS_CONFIG_CONTROL_GUARD_OK`。 +- `SOC_SIEM_KALI_WAZUH_INTEGRATION_CONTROL_OK frameworks=7 domains=16 signals=12 candidates=20 runtime_gate=0`。 + +**完成度同步**: +- Telegram 告警可讀性防退化 guard:`100%`。 +- P0-6 Telegram 監控告警 / 通知出口治理:新增 readability guard slice `100%`。 +- Direct Bot API convergence:仍 `0%`。 +- Delivery receipt accepted:仍 `0%`。 +- IwoooS headline:仍維持 `64%`。 +- Active runtime gate:仍 `0`。 + +**邊界**:本段只新增 repo-only guard、snapshot 與文件;沒有送 Telegram、沒有呼叫 Bot API、沒有 Alertmanager reload、沒有 receiver route change、沒有 live alert fire、沒有 workflow / script / API sender 修改、沒有 owner response accepted、沒有 delivery receipt accepted、沒有 secret read、沒有 production write、沒有 host write、沒有 runtime worker、沒有 action button。 + ## 2026-06-19|P2-410 治理頁最終部署標記讀回完成 **背景**:P2-410 治理頁補齊後,後續 `bd1021e7 fix(web): 遮罩治理頁 raw blocked 狀態` 與 `5612526b fix(web): 修正治理頁 tab 深連結` 又推進了治理頁安全顯示與深連結修正;因此需以最新正式部署標記重新讀回 `/zh-TW/governance?tab=automation-inventory`,避免只停留在先前 `7a9e1cfd` 的 UI smoke。 diff --git a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md index 399b0b3b7..900a806c2 100644 --- a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md +++ b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md @@ -58,6 +58,8 @@ 同日再新增 `docs/security/TELEGRAM-NOTIFICATION-EGRESS-OWNER-RESPONSE-ACCEPTANCE.md` 與 `docs/security/telegram-notification-egress-owner-response-acceptance.snapshot.json`,把 11 份 direct egress 檔案轉成 owner response acceptance 候選。固定 `acceptance_candidate_count=11`、workflow `6`、ops script `4`、API direct `1`、`acceptance_field_count=32`、`required_owner_field_count=19`、`reviewer_check_count=22`、`outcome_lane_count=10`、`blocked_action_count=35`;owner response received / accepted、formatter convergence accepted、redaction contract accepted、delivery receipt accepted、migration authorized、workflow / script / API sender modification、Telegram send、Bot API call、secret collection、production write、runtime gate 與 action button 仍全部為 `0 / false`。 +同日再新增 `docs/security/TELEGRAM-ALERT-READABILITY-GUARD.md`、`docs/security/telegram-alert-readability-guard.snapshot.json` 與 `scripts/security/telegram-alert-readability-guard.py`,把 Telegram 告警最後出口可讀性固定成可重跑 guard。固定 `source_formatter_marker_count=11`、`final_exit_contract_count=3`、`test_contract_count=10`、`ai_signal_lane_count=6`、`host_resource_lane_count=6`、`blocked_raw_output_marker_count=12`、`required_output_marker_count=6`。此更新鎖住 Host CPU / root Node.js / Prisma / Next build、Wazuh、Kali、Nginx drift、backup / restore、provider freshness 與 supply-chain 類訊號必須轉成 AI 事件卡,且不得把 process list、raw JSON、內網 IP、完整路徑、URL、token 或 raw Wazuh / Nginx path 直接送進 Telegram;但 Telegram send、Bot API call、delivery receipt、direct egress migration、workflow / script / API sender 修改、production write、runtime gate 與 action button 仍全部為 `0 / false`。 + ## 1.2c 2026-06-18 Backup / Restore / Escrow 事故後回讀計畫 已新增 `docs/security/BACKUP-RESTORE-POST-INCIDENT-READBACK-PLAN.md` 與 `docs/security/backup-restore-post-incident-readback-plan.snapshot.json`,將 38 個 backup / restore / escrow / retention surface 轉成事故後回讀計畫。固定 `readback_candidate_count=38`、`write_capable_readback_candidate_count=27`、`live_evidence_required_readback_candidate_count=38`、`restore_drill_readback_required_candidate_count=38`、`offsite_or_escrow_readback_required_candidate_count=20`、`retention_or_remote_delete_readback_required_candidate_count=17`、`required_readback_field_count=34`、`reviewer_check_count=32`、`outcome_lane_count=11`、`blocked_action_count=51`,讓 `backup_restore_credential` 從 `64%` 推進到 `66%`,高價值配置平均維持 `71%`,需 live evidence 類別仍為 `9`。 diff --git a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md index 7d7766263..d9ee0b4d2 100644 --- a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md +++ b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md @@ -97,6 +97,8 @@ 同日再新增 `telegram_notification_egress_owner_response_acceptance_v1`,把 11 份 owner request draft 與 11 份 migration candidate 轉成 owner response acceptance 帳本。固定 `acceptance_candidate_count=11`、workflow `6`、ops script `4`、API direct `1`、`acceptance_field_count=32`、`required_owner_field_count=19`、`reviewer_check_count=22`、`outcome_lane_count=10`、`forbidden_payload_count=14`、`blocked_action_count=35`。owner response received / accepted / rejected / quarantined、supplement requested、formatter convergence accepted、redaction contract accepted、delivery receipt accepted、break-glass fallback accepted、maintenance / rollback / postcheck accepted、migration authorized、workflow / script / API sender modification、Telegram send、Bot API call、workflow dispatch、production deploy、secret collection、raw payload storage、runtime gate 仍全部為 `0 / false`。 +同日再新增 `telegram_alert_readability_guard_v1`,把 Telegram 告警最後出口可讀性納入高價值配置控管。固定 `source_formatter_marker_count=11`、`final_exit_contract_count=3`、`test_contract_count=10`、`ai_signal_lane_count=6`、`host_resource_lane_count=6`、`blocked_raw_output_marker_count=12`、`required_output_marker_count=6`,並由 `security-mirror-progress-guard.py` 直接呼叫。此 guard 確認 `_send_request()`、`send_alert_notification()` 與 `send_text()` 都會套用 normalizer,且 Prisma / root Node.js / Next build / Wazuh / Kali / Nginx drift 等告警只能變成脫敏 AI 事件卡;Telegram 實發、Bot API call、delivery receipt、direct egress migration、workflow / script / API sender 修改、production write、runtime gate 仍全部為 `0 / false`。 + ### 0.3d 2026-06-15 Public / Admin / API runtime config 變更證據驗收 `public_runtime_config_change_evidence_acceptance_v1` 已把公開產品頁、AwoooP 後台、API / CORS、frontend env、Sentry tunnel、webhook / callback 與跨產品 runtime route 轉成 metadata-only 變更證據驗收只讀帳本。固定 `candidates=6`、`c0=5`、`c1=1`、`write_capable=6`、`source_refs=20`、`required_evidence_fields=21`、`reviewer_checks=21`、`outcome_lanes=8`、`blocked_actions=32`。 diff --git a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md index 9d13dac4f..d0741da0a 100644 --- a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md +++ b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md @@ -14,6 +14,14 @@ | 原則 | 低摩擦分階段;文件、schema、read-only evidence 優先;不做 runtime enforcement、不切 primary | | P0 主控板 | `docs/workplans/2026-06-04-iwooos-security-governance-p0.md` | +## 0.0 2026-06-19 Telegram 告警可讀性防退化 Guard + +本輪把 Telegram 告警從「最後出口 formatter 已存在」再補成可重跑 guard:`telegram_alert_readability_guard_v1` 固定 `source_formatter_marker_count=11`、`final_exit_contract_count=3`、`test_contract_count=10`、`ai_signal_lane_count=6`、`host_resource_lane_count=6`、`blocked_raw_output_marker_count=12`、`required_output_marker_count=6`。 + +此 guard 明確鎖住 `_send_request()`、`send_alert_notification()` 與 `send_text()` 都必須套用告警 normalizer;Host CPU / root Node.js / Prisma / Next build、Wazuh、Kali、Nginx drift、backup / restore、provider freshness 與 supply-chain 類訊號必須轉成脫敏 AI 事件卡,不得把 process list、raw JSON、內網 IP、完整路徑、URL、token 或 raw Wazuh / Nginx path 直接送進 Telegram。 + +同步邊界:IwoooS headline 維持 `64%`,active runtime gate 維持 `0`;Telegram 實發、Bot API call、delivery receipt、owner response accepted、direct egress migration、workflow / script / API sender 修改、secret collection、raw payload storage、production write、runtime gate 與 action buttons 全部仍為 `0 / false`。本段只更新文件、snapshot 與 guard,不送 Telegram、不 reload Alertmanager、不改 receiver route、不打 live alert。 + ## 0.0a 2026-06-18 Backup / Restore / Escrow 事故後回讀計畫 本輪把 Backup / Restore / Escrow 從 owner response acceptance 再補一層 post-incident readback plan:`backup_restore_post_incident_readback_plan_v1` 固定 `candidates=38`、`write_capable=27`、`live_evidence_required=38`、`restore_drill_required=38`、`offsite_or_escrow_required=20`、`retention_or_remote_delete_required=17`、`required_readback_fields=34`、`reviewer_checks=32`、`outcome_lanes=11`、`blocked_actions=51`,並讓 `backup_restore_credential` 只讀治理成熟度 `64% -> 66%`,高價值配置平均成熟度維持 `71%`。新增要求包含 actor、時間窗、改前改後 freshness、backup status readback、restore drill、隔離 restore target、offsite sync、remote delete guard、credential escrow non-secret proof、retention runway、retention / prune decision、dependency map、data classification、restore observer、alert textfile、cold-start scorecard、cross-project sync、rollback、post-change monitoring、防再發與 no-false-green attestation。 diff --git a/docs/security/TELEGRAM-ALERT-READABILITY-GUARD.md b/docs/security/TELEGRAM-ALERT-READABILITY-GUARD.md new file mode 100644 index 000000000..1c7b1348e --- /dev/null +++ b/docs/security/TELEGRAM-ALERT-READABILITY-GUARD.md @@ -0,0 +1,67 @@ +# Telegram 告警可讀性防退化 Guard + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-06-19 | +| 狀態 | `telegram_alert_readability_guard_ready_no_runtime_action` | +| 工具 | `scripts/security/telegram-alert-readability-guard.py` | +| Snapshot | `docs/security/telegram-alert-readability-guard.snapshot.json` | +| runtime gate | `0` | + +## 1. 目的 + +這個 guard 鎖住 Telegram 告警的最後出口格式,避免監控訊息又退回「整段 process list、raw JSON、內網 IP、完整路徑、URL、token 或外部掃描原文直接丟到群組」的狀態。 + +告警不是送出就算完成。IwoooS 的告警必須能讓值班者一眼看懂: + +1. 事件嚴重度與 target。 +2. AI 自動化 lane 與 gate。 +3. top evidence 的脫敏摘要。 +4. 建議下一步。 +5. 禁止事項與 `runtime_write_gate=0` 邊界。 + +## 2. 固定合約 + +| 合約 | 固定值 | +|------|--------| +| source formatter markers | `11` | +| 最後出口合約 | `3` | +| 測試合約 | `10` | +| AIOps signal lanes | `6` | +| Host resource lanes | `6` | +| raw output 阻擋 marker | `12` | +| required output marker | `6` | + +目前 guard 會確認: + +- `TelegramGateway._send_request()` 必須套用 `normalize_telegram_send_message_payload()`。 +- `send_alert_notification()` 與 `send_text()` 必須先走 `normalize_alert_notification_payload()`。 +- Host CPU / load / root Node.js / Prisma / Next build 只能轉成 `主機資源壓力` AI 事件卡。 +- Wazuh、Kali、Nginx drift、backup / restore、provider freshness、supply-chain 類訊號只能轉成 AIOps 候選事件卡。 +- 外部版本檢查 URL、套件樹路徑、workspace 路徑、runner toolcache 路徑、raw Wazuh path、raw Nginx path、內網 IP、token-like 字串、raw Prisma JSON 不得出現在格式化後訊息。 + +## 3. 驗收邊界 + +這是 repo-only guard,不是 Telegram 實發、不代表 delivery receipt、不是 owner response accepted,也不是 runtime 修復授權。 + +仍維持: + +```text +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 +``` + +## 4. 下一步 + +下一步不是直接實發測試訊息,而是把這個 guard 併入 IwoooS 例行 security progress guard,並在 owner response 之後分批收斂 direct Bot API 旁路與 delivery receipt。正式實發、Alertmanager route 變更、receiver reload、workflow/script/API sender 修改都仍需獨立批准、維護窗口、rollback owner 與 post-check。 diff --git a/docs/security/telegram-alert-readability-guard.snapshot.json b/docs/security/telegram-alert-readability-guard.snapshot.json new file mode 100644 index 000000000..2f97ffebc --- /dev/null +++ b/docs/security/telegram-alert-readability-guard.snapshot.json @@ -0,0 +1,122 @@ +{ + "ai_signal_lanes": [ + "wazuh_intrusion_signal", + "kali_assessment_signal", + "nginx_config_drift", + "backup_restore_escrow_signal", + "provider_freshness_signal", + "supply_chain_drift" + ], + "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" + ], + "execution_boundaries": { + "action_buttons_allowed": false, + "api_sender_refactor_authorized": false, + "bot_api_call_authorized": false, + "direct_bot_api_migration_authorized": false, + "host_write_authorized": false, + "not_authorization": true, + "ops_script_modification_authorized": false, + "production_write_authorized": false, + "raw_payload_storage_allowed": false, + "runtime_execution_authorized": false, + "runtime_gate_open": false, + "secret_value_collection_allowed": false, + "telegram_send_authorized": false, + "workflow_modification_authorized": false + }, + "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" + } + ], + "generated_at": "2026-06-19T02:01:35+08:00", + "git_commit": "7d032eab", + "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" + ], + "required_output_markers": [ + "ai_automation_alert_card_v1", + "AI 自動化判讀", + "runtime_write_gate=0", + "candidate_only", + "Top evidence", + "禁止事項" + ], + "schema_version": "telegram_alert_readability_guard_v1", + "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)" + ], + "source_refs": [ + "apps/api/src/services/telegram_gateway.py", + "apps/api/tests/test_telegram_message_templates.py" + ], + "status": "telegram_alert_readability_guard_ready_no_runtime_action", + "summary": { + "action_button_count": 0, + "ai_signal_lane_count": 6, + "blocked_raw_output_marker_count": 12, + "bot_api_call_authorized_count": 0, + "final_exit_contract_count": 3, + "host_resource_lane_count": 6, + "production_write_authorized_count": 0, + "raw_payload_storage_allowed_count": 0, + "required_output_marker_count": 6, + "runtime_gate_count": 0, + "secret_value_collection_allowed_count": 0, + "source_formatter_marker_count": 11, + "telegram_send_authorized_count": 0, + "test_contract_count": 10 + }, + "test_contracts": [ + "test_ci_runner_load_alert_becomes_capacity_event_packet", + "test_wazuh_alert_becomes_aiops_signal_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" + ] +} diff --git a/docs/workplans/2026-06-04-iwooos-security-governance-p0.md b/docs/workplans/2026-06-04-iwooos-security-governance-p0.md index ae2a3b30a..16a000ed5 100644 --- a/docs/workplans/2026-06-04-iwooos-security-governance-p0.md +++ b/docs/workplans/2026-06-04-iwooos-security-governance-p0.md @@ -84,7 +84,7 @@ | P0-3 | AwoooP 同步封包 | 100% | 已送至 AwoooP 平行工作 thread `019e9154-7d5e-7b72-85be-c9d97e43ecc9`;後續仍需每次推版前重新 fetch / fast-forward | 本文件、thread send readback、mirror checklist readback | | P0-4 | production live sanity 節點 | 100% | desktop / mobile / 展開區塊 / overflow / action href 檢查已完成 | Playwright production sanity 通過 | | P0-5 | LOGBOOK 與完成度更新 | 100% | D2 comments-only、D2 AIOps sample、D2 Code Review 候選分類與 D2 AwoooP Runs fallback 皆已回填;可見 / bundle 變更皆已補 local / production desktop + mobile smoke | `docs/LOGBOOK.md` readback | -| P0-6 | Telegram 監控告警 / 通知出口治理 | outbound 主鏈路 100%;靜音 / recurrence slice 88%;通知出口清冊 100%;owner request draft 100%;migration plan draft 100%;防新增旁路 guard 100%;owner response acceptance 100%;direct Bot API convergence 0% | Alertmanager 缺 project context、既有 approval 收斂告警靜音、AI 分析中重複告警靜音皆已修復並正式 smoke;`TelegramGateway` final-exit formatter 已完成 host / multi-signal 卡片化;direct Bot API egress inventory 固定 workflow 13、ops script 4、API direct 1,並聚成 11 份 owner request 草稿與 3 個遷移波次;本輪新增 no-new-bypass guard,覆蓋 `sendMessage` / `sendDocument` / `sendPhoto` / `sendMediaGroup` / `editMessageText` 等 9 類 Bot API method,並新增 11 份 owner response acceptance 候選;後續需 owner response 後分批收斂,不得把 API formatter 或防新增 guard 完成誤判成既有旁路已收斂 | API health、Telegram health、API pod Alertmanager smoke、production logs `converged_alert_recurrence_sent`、`telegram-notification-egress-inventory.snapshot.json`、`telegram-notification-egress-owner-request-draft.snapshot.json`、`telegram-notification-egress-migration-plan-draft.snapshot.json`、`telegram-notification-egress-no-new-bypass-guard.snapshot.json`、`telegram-notification-egress-owner-response-acceptance.snapshot.json` | +| P0-6 | Telegram 監控告警 / 通知出口治理 | outbound 主鏈路 100%;靜音 / recurrence slice 88%;通知出口清冊 100%;owner request draft 100%;migration plan draft 100%;防新增旁路 guard 100%;owner response acceptance 100%;告警可讀性防退化 guard 100%;direct Bot API convergence 0% | Alertmanager 缺 project context、既有 approval 收斂告警靜音、AI 分析中重複告警靜音皆已修復並正式 smoke;`TelegramGateway` final-exit formatter 已完成 host / multi-signal 卡片化;direct Bot API egress inventory 固定 workflow 13、ops script 4、API direct 1,並聚成 11 份 owner request 草稿與 3 個遷移波次;no-new-bypass guard 已覆蓋 `sendMessage` / `sendDocument` / `sendPhoto` / `sendMediaGroup` / `editMessageText` 等 9 類 Bot API method;本輪新增 readability guard,鎖住 Prisma / root Node.js / Next build / Wazuh / Kali / Nginx drift 等告警必須轉成脫敏 AI 事件卡,不能把 process list、raw JSON、URL、內網 IP、完整路徑或 token 直接送到 Telegram;後續需 owner response 後分批收斂,不得把 API formatter、防新增 guard 或 readability guard 完成誤判成既有旁路已收斂或 delivery receipt 已完成 | API health、Telegram health、API pod Alertmanager smoke、production logs `converged_alert_recurrence_sent`、`telegram-notification-egress-inventory.snapshot.json`、`telegram-notification-egress-owner-request-draft.snapshot.json`、`telegram-notification-egress-migration-plan-draft.snapshot.json`、`telegram-notification-egress-no-new-bypass-guard.snapshot.json`、`telegram-notification-egress-owner-response-acceptance.snapshot.json`、`telegram-alert-readability-guard.snapshot.json` | | P0-7 | Telegram 批准後執行真相鏈止血 | 100% | no-action approval 不再顯示批准 / 執行中;可執行修復 approval 會寫入 `auto_repair_executions`、KM 與 verifier;下一步補 MCP evidence / PlayBook trust 產生真正修復候選 | 目標 pytest `125 passed`、py_compile、guard、production health、API / worker rollout、production pod classifier readback | | P0-8 | Telegram no-action 人工處置包與操作入口 | 100% | no-action 卡片已新增人工處置包、證據補齊清單、AwoooP 修復候選建立步驟、verifier / KM / PlayBook 回寫提醒,並改成 `處置包`、`重診`、`歷史`、`靜默`、`真相鏈`、`Runs` 鍵盤;舊訊息不 retroactive 改寫 | 目標 pytest `64 passed + 44 passed`、py_compile、guard、production health、API / worker rollout、production pod render / keyboard smoke | | P0-9 | MCP evidence -> PlayBook 修復候選產生 | D5 `88%`;Approvals ledger `100%`;Runs ledger desktop `100%`;Alerts ledger desktop / mobile `100%` | 已補 webhook fallback 先建立 incident,再收 MCP evidence、查 approved PlayBook、檢查 trust / command safety、產生 medium approval candidate 與 verifier plan;D1 追加通用兜底 PlayBook / 診斷型命令不可誤當修復、阻擋理由繁中化;D2 在缺候選時產生 `repair_candidate_draft_package_v1`、`playbook_draft_required`、下一步與必填欄位;D3 新增 `awooop_repair_candidate_draft_work_item_v1` read-only projection 與 Telegram `工作項目` deeplink;D4 讓 AwoooP Work Items 詳細呈現 PlayBook 草案處置板、必填欄位、阻擋原因、下一步、Runs / 審批連結;D5 新增 `repair_candidate_coverage_gap_v1`,讓 blocked result 帶出 coverage key、target kind、blocking stage、必收 MCP evidence refs、PlayBook template fields 與 runtime 0 / false 邊界;Approvals / Runs / Alerts 已新增 `資產沉澱` 欄或焦點矩陣,可直接看到 KM / PlayBook / 腳本 / 排程 / Verifier 的完成與卡點;下一步要補 Runs mobile smoke,並把同一總帳接到正式 Telegram 告警卡、Observability 與 Tenants,用真實告警驗證 approval -> execution -> verifier -> KM / PlayBook 回寫 | Approvals code `dafe5342` 已隨 deploy marker `42c08ece` 正式站 desktop / mobile smoke;Runs code `11c2b5d4` 已隨 deploy marker `8b6ab87c` 正式站 desktop DOM smoke;Alerts code `10cd6167` 已隨 deploy marker `d36d764a` 正式站 desktop / mobile DOM smoke;P2-407 API production readback `overall_completion_percent=100`;status-chain 後續仍必須看到 tool call、PlayBook id、risk gate、repair candidate、verifier plan | diff --git a/scripts/security/iwooos-config-control-guard.py b/scripts/security/iwooos-config-control-guard.py index c6383d954..be4db4318 100644 --- a/scripts/security/iwooos-config-control-guard.py +++ b/scripts/security/iwooos-config-control-guard.py @@ -76,6 +76,7 @@ REQUIRED_CONTROL_DOCS = [ "docs/security/AGENT-BOUNTY-OWNER-REQUEST-DRAFT.md", "docs/security/TELEGRAM-NOTIFICATION-EGRESS-NO-NEW-BYPASS-GUARD.md", "docs/security/TELEGRAM-NOTIFICATION-EGRESS-OWNER-RESPONSE-ACCEPTANCE.md", + "docs/security/TELEGRAM-ALERT-READABILITY-GUARD.md", "docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md", ] @@ -986,6 +987,37 @@ ARTIFACT_SPECS = [ "action_button_count": 0, }, }, + { + "label": "telegram alert readability guard", + "path": "docs/security/telegram-alert-readability-guard.snapshot.json", + "schema": "telegram_alert_readability_guard_v1", + "status": "telegram_alert_readability_guard_ready_no_runtime_action", + "list_counts": { + "source_markers": 11, + "final_exit_contracts": 3, + "test_contracts": 10, + "ai_signal_lanes": 6, + "host_resource_lanes": 6, + "blocked_raw_output_markers": 12, + "required_output_markers": 6, + }, + "summary_counts": { + "source_formatter_marker_count": 11, + "final_exit_contract_count": 3, + "test_contract_count": 10, + "ai_signal_lane_count": 6, + "host_resource_lane_count": 6, + "blocked_raw_output_marker_count": 12, + "required_output_marker_count": 6, + "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, + }, + }, { "label": "security asset control ledger", "path": "docs/security/security-asset-control-ledger.snapshot.json", diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 3b4fe79ab..d9456cb3d 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -87,6 +87,10 @@ def validate(root: Path) -> None: str(root / "scripts" / "security" / "public-frontend-env-guard.py") ) public_frontend_env_guard["validate"](root) + telegram_alert_readability_guard = runpy.run_path( + str(root / "scripts" / "security" / "telegram-alert-readability-guard.py") + ) + telegram_alert_readability_guard["validate"](root) public_frontend_sensitive_surface = load_json( security_dir / "public-frontend-sensitive-surface-guard.snapshot.json" ) diff --git a/scripts/security/telegram-alert-readability-guard.py b/scripts/security/telegram-alert-readability-guard.py new file mode 100644 index 000000000..bc63f5532 --- /dev/null +++ b/scripts/security/telegram-alert-readability-guard.py @@ -0,0 +1,330 @@ +#!/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_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_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 自動化判讀", + "runtime_write_gate=0", + "candidate_only", + "Top evidence", + "禁止事項", +] + +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()