feat(security): 鎖住 Telegram 通知出口新增旁路
This commit is contained in:
@@ -1,3 +1,41 @@
|
||||
## 2026-06-19|Telegram 通知出口 owner response 驗收帳本與 no-new-bypass guard
|
||||
|
||||
**背景**:Telegram 通知出口清冊已固定 repo 內 `18` 個 direct Bot API `sendMessage` call site,並完成 11 份 owner request 草稿與三個 no-runtime 遷移波次。但若只有清冊與遷移草稿,仍無法阻止新 direct Bot API 旁路繼續出現,也無法讓 reviewer 判斷 owner response 是否合格、是否夾帶 secret / raw payload、是否把 CD success / route 200 / UI 可見誤當 delivery receipt。
|
||||
|
||||
**完成內容**:
|
||||
- 新增 `scripts/security/telegram-notification-egress-owner-response-acceptance.py`。
|
||||
- 新增 `docs/security/telegram-notification-egress-owner-response-acceptance.snapshot.json`。
|
||||
- 新增 `docs/security/TELEGRAM-NOTIFICATION-EGRESS-OWNER-RESPONSE-ACCEPTANCE.md`。
|
||||
- 新增 `scripts/security/telegram-notification-egress-no-new-bypass-guard.py`。
|
||||
- 新增 `docs/security/telegram-notification-egress-no-new-bypass-guard.snapshot.json`。
|
||||
- 新增 `docs/security/TELEGRAM-NOTIFICATION-EGRESS-NO-NEW-BYPASS-GUARD.md`。
|
||||
- `security-mirror-progress-guard.py` 已鎖住兩個 snapshot 的 schema、summary、false flags、source paths、blocked actions 與 runtime gate。
|
||||
- `iwooos-config-control-guard.py` 已把兩份新 Markdown 納入高價值配置控管文件清單。
|
||||
- `docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md`、`docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md`、`docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md` 與 P0 工作表已同步更新。
|
||||
- Owner response acceptance 固定 `candidates=11`、workflow `6`、ops script `4`、API direct `1`、`acceptance_fields=32`、`owner_fields=19`、`reviewer_checks=22`、`outcome_lanes=10`、`forbidden_payloads=14`、`blocked_actions=35`。
|
||||
- No-new-bypass guard 固定 baseline `18`、current direct call `18`、current files `11`、guarded methods `9`、`sendMessage=18`、`sendDocument/sendPhoto/sendMediaGroup/editMessageText=0`、`new_bypass=0`。
|
||||
|
||||
**驗證**:
|
||||
- `python3 -m json.tool docs/security/telegram-notification-egress-owner-response-acceptance.snapshot.json` 通過。
|
||||
- `python3 -m json.tool docs/security/telegram-notification-egress-no-new-bypass-guard.snapshot.json` 通過。
|
||||
- `python3 -m py_compile scripts/security/telegram-notification-egress-owner-response-acceptance.py scripts/security/telegram-notification-egress-no-new-bypass-guard.py scripts/security/security-mirror-progress-guard.py` 通過。
|
||||
- `python3 scripts/security/telegram-notification-egress-owner-response-acceptance.py --root .` 通過,輸出 `TELEGRAM_NOTIFICATION_EGRESS_OWNER_RESPONSE_ACCEPTANCE_OK candidates=11 workflow=6 ops=4 api=1 accepted=0 runtime_gate=0`。
|
||||
- `python3 scripts/security/telegram-notification-egress-no-new-bypass-guard.py --root .` 通過,輸出 `TELEGRAM_NOTIFICATION_EGRESS_NO_NEW_BYPASS_GUARD_OK current=18 baseline=18 new=0 sendDocument=0 runtime_gate=0`。
|
||||
- `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。
|
||||
- `python3 scripts/security/source-control-owner-response-guard.py --root .`:`SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`。
|
||||
- `python3 scripts/security/iwooos-config-control-guard.py --root .`:`IWOOOS_CONFIG_CONTROL_GUARD_OK`。
|
||||
- `python3 scripts/ops/doc-secrets-sanity-check.py docs .gitea scripts/security`:`DOC_SECRET_SANITY_OK scanned_files=924`。
|
||||
- `git diff --check`:通過。
|
||||
|
||||
**完成度同步**:
|
||||
- Telegram notification egress owner response acceptance artifact:`100%`。
|
||||
- Telegram notification egress no-new-bypass guard:`100%`。
|
||||
- Direct Bot API convergence:仍 `0%`,尚未修改 workflow / script / API sender。
|
||||
- owner response dispatch / received / accepted:仍 `0%`。
|
||||
- IwoooS headline 仍維持 `64%`,active runtime gate 仍 `0`。
|
||||
|
||||
**邊界**:本段是 repo-only metadata / guard / snapshot / 文件收斂,沒有送 Telegram、沒有呼叫 Bot API、沒有 workflow dispatch、沒有觸發 CD、沒有修改 workflow / ops script / API sender、沒有讀 secret store、沒有收 secret value / hash / partial token、沒有保存 raw payload、沒有 production write、沒有 runtime gate、沒有 action button。不能把 no-new-bypass `pass` 誤判成既有 18 個 direct send 已批准或已收斂。
|
||||
|
||||
## 2026-06-19|P2-110E 報表資料源缺口接入 AwoooP Work Items owner review
|
||||
|
||||
**背景**:P2-110D 已讓 Reports 頁顯示三個 `report-source-gap:*` 的 PlayBook 草案與 Verifier 計畫,但操作員仍需要離開 AwoooP 工作鏈路才能看到報表資料源缺口;這會讓 KM / PlayBook / 腳本 / 排程 / Verifier 的沉澱結果看起來像「只存在 API 或文件裡」,沒有形成可追蹤工作項。
|
||||
|
||||
@@ -54,6 +54,10 @@
|
||||
|
||||
同日再新增 `docs/security/TELEGRAM-NOTIFICATION-EGRESS-MIGRATION-PLAN-DRAFT.md` 與 `docs/security/telegram-notification-egress-migration-plan-draft.snapshot.json`,將 11 份草稿排成三個 no-runtime 遷移波次。固定 `migration_candidate_count=11`、workflow `6`、ops script `4`、API direct `1`、`proposed_wave_count=3`、`reviewer_check_count=15`、`blocked_action_count=21`;owner response、migration authorized、workflow / script modification、API sender refactor、Telegram send、Bot API call、secret collection、production write、runtime gate 與 action button 仍全部為 `0 / false`。
|
||||
|
||||
2026-06-19 再新增 `docs/security/TELEGRAM-NOTIFICATION-EGRESS-NO-NEW-BYPASS-GUARD.md` 與 `docs/security/telegram-notification-egress-no-new-bypass-guard.snapshot.json`,把既有 18 個 direct send 固定為 no-new-bypass baseline。固定 `guarded_method_count=9`、`current_direct_bot_api_call_count=18`、`new_bypass_count=0`、`sendDocument_call_count=0`、`sendPhoto_call_count=0`、`sendMediaGroup_call_count=0`、`runtime_gate_count=0`。這是 repo source 防新增旁路 guard,不代表既有 direct send 已收斂。
|
||||
|
||||
同日再新增 `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`。
|
||||
|
||||
## 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`。
|
||||
|
||||
@@ -93,6 +93,10 @@
|
||||
|
||||
同日再新增 `telegram_notification_egress_migration_plan_draft_v1`,將 11 份 owner request 草稿排成 workflow notification wrapper、ops notification wrapper、API sender gateway 三個遷移波次。固定 `migration_candidate_count=11`、`workflow_migration_candidate_count=6`、`ops_script_migration_candidate_count=4`、`api_direct_migration_candidate_count=1`、`proposed_wave_count=3`、`owner_response_required_count=11`、`maintenance_window_required_count=11`、`rollback_owner_required_count=11`。owner response、migration authorized、workflow / script modification、API sender refactor、Telegram send、Bot API call、secret collection、raw payload storage、production write、runtime gate 仍全部為 `0 / false`。
|
||||
|
||||
2026-06-19 再新增 `telegram_notification_egress_no_new_bypass_guard_v1`,將既有 18 個 direct send 固定成 baseline signature,並掃描 `sendMessage`、`sendDocument`、`sendPhoto`、`sendMediaGroup`、`editMessageText`、`sendAnimation`、`sendVideo`、`sendAudio`、`sendVoice` 等 9 類 Bot API method。固定 `baseline_signature_count=18`、`current_direct_bot_api_call_count=18`、`new_bypass_count=0`、`sendDocument_call_count=0`、`runtime_gate_count=0`。此更新只代表 repo source 目前沒有新增未登記 Telegram 直送旁路;既有 18 個 direct send 仍未遷移,owner response、migration authorized、workflow / script modification、API sender refactor、Telegram send、Bot API call、secret collection、raw payload storage、production write、runtime gate 仍全部為 `0 / false`。
|
||||
|
||||
同日再新增 `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`。
|
||||
|
||||
### 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`。
|
||||
|
||||
@@ -98,6 +98,14 @@
|
||||
|
||||
同日再新增 `telegram_notification_egress_migration_plan_draft_v1`,把 11 份草稿排成 workflow notification wrapper、ops notification wrapper、API sender gateway 三個 no-runtime 遷移波次。固定 `migration_candidate_count=11`、workflow `6`、ops script `4`、API direct `1`、`proposed_wave_count=3`、`owner_response_required_count=11`、`maintenance_window_required_count=11`、`rollback_owner_required_count=11`、`migration_authorized_count=0`、`runtime_gate_count=0`。這是遷移計畫草稿,不是 workflow / script / API sender 變更,也不是 Telegram send、Bot API call、secret collection 或 production write。
|
||||
|
||||
## 0.00aaaa2 2026-06-19 Telegram 通知出口防新增旁路與 owner response acceptance
|
||||
|
||||
本輪新增 `telegram_notification_egress_no_new_bypass_guard_v1`,把既有 18 個 direct send 固定成 no-new-bypass baseline,並把 `sendDocument`、`sendPhoto`、`sendMediaGroup`、`editMessageText` 等附件 / 編輯型 Bot API method 一併納入 repo source guard。固定 `current_direct_bot_api_call_count=18`、`guarded_method_count=9`、`new_bypass_count=0`、`sendDocument_call_count=0`、`removed_baseline_call_count=0`、`runtime_gate_count=0`。
|
||||
|
||||
同步新增 `telegram_notification_egress_owner_response_acceptance_v1`,把 11 個 direct egress 檔案轉成 reviewer 可驗收的 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`。
|
||||
|
||||
同步邊界:IwoooS headline 維持 `64%`,active runtime gate 維持 `0`;既有 direct Bot API 收斂仍為 `0%`,owner response received / accepted、migration authorized、workflow / script / API sender modification、Telegram send、Bot API call、workflow dispatch、production deploy、secret value collection、raw payload storage、runtime gate 與 action buttons 全部仍為 `0 / false`。本段只更新文件、snapshot 與 guard,不送 Telegram、不讀 Bot token、不改 workflow、不改 host、不 dispatch workflow、不觸發部署。
|
||||
|
||||
## 0.00aaa 2026-06-15 K8s / ArgoCD GitOps 變更證據驗收
|
||||
|
||||
本輪把 K8s / ArgoCD 從 owner response acceptance 推進到 GitOps 變更證據驗收只讀帳本:`k8s_argocd_change_evidence_acceptance_v1` 固定 `candidates=4`、`c0=3`、`write_capable=4`、`required_evidence_fields=18`、`reviewer_checks=18`、`outcome_lanes=8`、`blocked_actions=28`,並讓 `k8s_production_gitops` 只讀治理成熟度 `62% -> 64%`,高價值配置平均只讀成熟度仍維持 `68%`。這是 metadata-only 收件驗收,不是 change evidence received / accepted、runtime approval package、ArgoCD API read、ArgoCD sync、kubectl action、Helm upgrade、NetworkPolicy apply、NodePort change、RBAC change、live cluster read、production write 或 runtime gate。
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Telegram 通知出口 No-New-Bypass Guard
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 日期 | 2026-06-19 |
|
||||
| 狀態 | `pass_no_new_bypass` |
|
||||
| 工具 | `scripts/security/telegram-notification-egress-no-new-bypass-guard.py` |
|
||||
| Snapshot | `docs/security/telegram-notification-egress-no-new-bypass-guard.snapshot.json` |
|
||||
| Source snapshot | `docs/security/telegram-notification-egress-inventory.snapshot.json` |
|
||||
| 模式 | repo source scan;不讀 secret、不送 Telegram、不修改 workflow / script / API sender |
|
||||
| runtime gate | `0` |
|
||||
|
||||
## 1. 目的
|
||||
|
||||
`telegram_notification_egress_no_new_bypass_guard_v1` 用 committed inventory 當 baseline,掃描 repo source 是否新增未登記的 Telegram Bot API direct endpoint。既有 `18` 個 `sendMessage` call site 仍是待 owner response 與 migration review 的基線;本 guard 的目的不是批准它們,而是防止新旁路在治理收斂期間繼續增加。
|
||||
|
||||
## 2. 掃描範圍
|
||||
|
||||
| 範圍 | 說明 |
|
||||
|------|------|
|
||||
| `.gitea/workflows` | Gitea Actions workflow 中的 direct Bot API |
|
||||
| `scripts/ops` | 主機 / 備份 / DR 類 ops script |
|
||||
| `scripts/ci` | CI helper script |
|
||||
| `apps/api/src` | API sender 與 notification 相關 source |
|
||||
|
||||
Guard 目前保護的方法包含 `sendMessage`、`sendDocument`、`sendPhoto`、`sendMediaGroup`、`editMessageText`、`sendAnimation`、`sendVideo`、`sendAudio`、`sendVoice`。
|
||||
|
||||
## 3. 固定數字
|
||||
|
||||
| 指標 | 數值 | 解讀 |
|
||||
|------|------|------|
|
||||
| `source_direct_bot_api_call_count` | `18` | committed inventory 的 direct call baseline |
|
||||
| `source_direct_bot_api_file_count` | `11` | committed inventory 的 direct file baseline |
|
||||
| `baseline_signature_count` | `18` | baseline signature 數 |
|
||||
| `current_direct_bot_api_call_count` | `18` | 目前掃描到的 direct call 數 |
|
||||
| `current_direct_bot_api_file_count` | `11` | 目前掃描到的 direct file 數 |
|
||||
| `guarded_method_count` | `9` | 受保護 Bot API method 數 |
|
||||
| `sendMessage_call_count` | `18` | 目前全部都是既有 `sendMessage` |
|
||||
| `sendDocument_call_count` | `0` | 不允許新增附件型 direct send |
|
||||
| `sendPhoto_call_count` | `0` | 不允許新增圖片型 direct send |
|
||||
| `sendMediaGroup_call_count` | `0` | 不允許新增 media group direct send |
|
||||
| `editMessageText_call_count` | `0` | 不允許新增 edit direct call |
|
||||
| `new_bypass_count` | `0` | 新增未登記旁路必須維持 0 |
|
||||
| `removed_baseline_call_count` | `0` | baseline 移除需另外走 migration evidence |
|
||||
| `runtime_gate_count` | `0` | 不提供 runtime 執行授權 |
|
||||
|
||||
## 4. 判讀規則
|
||||
|
||||
- `new_bypass_count=0` 才代表沒有新增未登記 direct Bot API 旁路。
|
||||
- 既有 `18` 個 direct `sendMessage` 只是治理基線,不代表已批准保留。
|
||||
- 若新增 `sendDocument`、`sendPhoto`、`sendMediaGroup` 等附件型出口,guard 會阻擋,必須先進 inventory、owner request、migration plan 與 acceptance ledger。
|
||||
- 若移除既有 baseline,也不能只靠 guard 結果宣稱完成;仍需 migration evidence、owner response、delivery receipt 與 postcheck。
|
||||
|
||||
## 5. 禁止事項
|
||||
|
||||
本 guard 不得被解讀成以下授權:
|
||||
|
||||
- 送 Telegram 或呼叫 Bot API。
|
||||
- 修改 workflow、ops script、API sender、chat route 或 Bot token。
|
||||
- 讀 secret store、收 secret value / hash / partial token。
|
||||
- 保存 raw payload、raw log 或未脫敏截圖。
|
||||
- 開 runtime gate 或新增 action button。
|
||||
|
||||
## 6. 驗證指令
|
||||
|
||||
```bash
|
||||
python3 scripts/security/telegram-notification-egress-no-new-bypass-guard.py --root .
|
||||
python3 -m json.tool docs/security/telegram-notification-egress-no-new-bypass-guard.snapshot.json >/dev/null
|
||||
python3 -m py_compile scripts/security/telegram-notification-egress-no-new-bypass-guard.py
|
||||
python3 scripts/security/security-mirror-progress-guard.py --root .
|
||||
```
|
||||
|
||||
## 7. 完成度
|
||||
|
||||
| 項目 | 完成度 | 邊界 |
|
||||
|------|--------|------|
|
||||
| No-new-bypass source guard | `100%` | 目前 `new_bypass_count=0` |
|
||||
| Existing direct Bot API convergence | `0%` | 18 個既有 direct send 尚未 migration |
|
||||
| Runtime execution | `0%` | 不送 Telegram、不呼叫 Bot API、不改 live route |
|
||||
@@ -0,0 +1,95 @@
|
||||
# Telegram 通知出口 Owner Response 驗收帳本
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 日期 | 2026-06-19 |
|
||||
| 狀態 | `owner_response_acceptance_ledger_ready_no_runtime_action` |
|
||||
| 工具 | `scripts/security/telegram-notification-egress-owner-response-acceptance.py` |
|
||||
| Snapshot | `docs/security/telegram-notification-egress-owner-response-acceptance.snapshot.json` |
|
||||
| Source snapshots | `docs/security/telegram-notification-egress-owner-request-draft.snapshot.json`、`docs/security/telegram-notification-egress-migration-plan-draft.snapshot.json` |
|
||||
| 模式 | metadata-only;不讀 secret、不送 Telegram、不呼叫 Bot API、不改 workflow / script / API sender |
|
||||
| runtime gate | `0` |
|
||||
|
||||
## 1. 目的
|
||||
|
||||
`telegram_notification_egress_owner_response_acceptance_v1` 把 Telegram 通知出口 owner request 草稿與 migration plan 草稿轉成 reviewer 可驗收的帳本。它處理的是既有 `18` 個 direct Bot API `sendMessage` call site 的 owner response 收件規則,避免把「CD success、route 200、UI 可見或 Telegram sent」誤判為 delivery receipt 或自動化閉環完成。
|
||||
|
||||
這份帳本不是 owner response received,也不是 migration authorized。workflow、ops script 或 API sender 要真的改走 wrapper / gateway 時,仍需要獨立 change evidence、維護窗口、rollback owner 與 runtime approval。
|
||||
|
||||
## 2. 固定數字
|
||||
|
||||
| 指標 | 數值 | 解讀 |
|
||||
|------|------|------|
|
||||
| `source_request_draft_count` | `11` | 來源 owner request 草稿 |
|
||||
| `source_migration_candidate_count` | `11` | 來源 migration candidate |
|
||||
| `source_direct_bot_api_call_count` | `18` | 既有 direct Bot API call site |
|
||||
| `acceptance_candidate_count` | `11` | 每個 direct egress 檔案一個驗收候選 |
|
||||
| `workflow_acceptance_candidate_count` | `6` | Gitea workflow 類 |
|
||||
| `ops_script_acceptance_candidate_count` | `4` | Ops script 類 |
|
||||
| `api_direct_acceptance_candidate_count` | `1` | API direct sender 類 |
|
||||
| `acceptance_field_count` | `32` | 每個 candidate 的驗收欄位 |
|
||||
| `required_owner_field_count` | `19` | owner response 必填欄位 |
|
||||
| `reviewer_check_count` | `22` | reviewer 必檢規則 |
|
||||
| `outcome_lane_count` | `10` | 收件結果分流 |
|
||||
| `forbidden_payload_count` | `14` | 禁止出現在回覆中的 payload 類型 |
|
||||
| `blocked_action_count` | `35` | 帳本階段禁止動作 |
|
||||
|
||||
所有 request sent、recipient confirmed、audit event emitted、owner response received / accepted / rejected / quarantined、formatter convergence accepted、redaction contract accepted、delivery receipt accepted、break-glass fallback accepted、maintenance window accepted、rollback owner accepted、postcheck evidence accepted、dedup / fingerprint accepted、no-false-green accepted、migration authorized、workflow / script / API sender modification、Telegram send、Bot API call、workflow dispatch、production deploy、secret collection、raw payload storage、production write、runtime gate、action button 全部維持 `0 / false`。
|
||||
|
||||
## 3. Reviewer checks
|
||||
|
||||
Reviewer 必須確認:
|
||||
|
||||
- source owner request draft 與 migration plan 都是目前版本。
|
||||
- owner identity、decision、decision reason、affected scope 與 followup owner 完整。
|
||||
- 所有 evidence refs 都是脫敏 metadata,不包含 secret value、hash、partial token、raw message payload 或 raw workflow log。
|
||||
- message shape contract、redaction contract、formatter convergence、gateway / Alertmanager target、break-glass fallback、delivery receipt、dedup / fingerprint、maintenance window、rollback owner 與 postcheck evidence 都明確存在。
|
||||
- `no_secret_value_attestation`、`no_raw_payload_attestation`、`no_false_green_attestation` 都存在。
|
||||
- migration authorization 與 runtime approval 必須分離;此帳本不能直接批准 workflow / script / API sender 修改。
|
||||
- runtime gate 維持 `0`。
|
||||
|
||||
## 4. Outcome lanes
|
||||
|
||||
| Lane | 說明 |
|
||||
|------|------|
|
||||
| `waiting_owner_response` | 等待合格 owner response |
|
||||
| `quarantine_secret_or_raw_payload` | 收到 secret、raw payload 或 raw log 時隔離 |
|
||||
| `reject_execution_request` | 回覆夾帶立即執行要求時拒收 |
|
||||
| `request_owner_route_supplement` | owner、route 或 scope 不完整 |
|
||||
| `request_formatter_convergence_supplement` | formatter convergence 或 target 不完整 |
|
||||
| `request_redaction_or_receipt_supplement` | redaction contract、delivery receipt 或 dedup 不完整 |
|
||||
| `request_maintenance_or_rollback_supplement` | maintenance window 或 rollback owner 不完整 |
|
||||
| `ready_for_migration_review` | metadata 完整,可另開 migration review |
|
||||
| `owner_review_only_update` | 只更新 owner review metadata |
|
||||
| `waiting_runtime_gate` | owner review 完成後仍等待 runtime gate |
|
||||
|
||||
## 5. 禁止動作
|
||||
|
||||
此帳本階段禁止:
|
||||
|
||||
- 標記 owner response received / accepted。
|
||||
- 送 Telegram、呼叫 Bot API、dispatch workflow、觸發 CD 或 production deploy。
|
||||
- 修改 workflow、ops script、API sender、chat route、Bot token 或 secret。
|
||||
- 讀 secret store、收 secret value / hash / partial token / chat id secret。
|
||||
- 保存 raw message payload、raw workflow log、raw action log、內部協作逐字稿或未脫敏截圖。
|
||||
- 把 CD success、route `200`、UI 可見或 Telegram sent 當作 delivery receipt。
|
||||
- 跳過 formatter convergence、redaction contract、dedup / fingerprint、break-glass fallback 或 no-false-green review。
|
||||
- 開 runtime gate 或新增 action button。
|
||||
|
||||
## 6. 驗證指令
|
||||
|
||||
```bash
|
||||
python3 scripts/security/telegram-notification-egress-owner-response-acceptance.py --root .
|
||||
python3 -m json.tool docs/security/telegram-notification-egress-owner-response-acceptance.snapshot.json >/dev/null
|
||||
python3 -m py_compile scripts/security/telegram-notification-egress-owner-response-acceptance.py
|
||||
python3 scripts/security/security-mirror-progress-guard.py --root .
|
||||
```
|
||||
|
||||
## 7. 完成度
|
||||
|
||||
| 項目 | 完成度 | 邊界 |
|
||||
|------|--------|------|
|
||||
| Owner response acceptance artifact | `100%` | 11 個 candidate 已可 reviewer 驗收 |
|
||||
| Owner response dispatch / received / accepted | `0%` | 尚未送件、尚未收件、尚未接受 |
|
||||
| Direct Bot API convergence | `0%` | 尚未修改 workflow / script / API sender |
|
||||
| Runtime execution | `0%` | 不送 Telegram、不呼叫 Bot API、不開 runtime gate |
|
||||
@@ -0,0 +1,197 @@
|
||||
{
|
||||
"current_direct_bot_api_calls": [
|
||||
{
|
||||
"line": 54,
|
||||
"method": "sendMessage",
|
||||
"path": ".gitea/workflows/cd-dev.yaml",
|
||||
"sanitized_excerpt": "printf '%b' \"$MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": ".gitea/workflows/cd-dev.yaml::sendmessage::printf '%b' \"$MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 241,
|
||||
"method": "sendMessage",
|
||||
"path": ".gitea/workflows/cd-dev.yaml",
|
||||
"sanitized_excerpt": "printf '%b' \"$MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": ".gitea/workflows/cd-dev.yaml::sendmessage::printf '%b' \"$MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 262,
|
||||
"method": "sendMessage",
|
||||
"path": ".gitea/workflows/cd-dev.yaml",
|
||||
"sanitized_excerpt": "printf '%b' \"$MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": ".gitea/workflows/cd-dev.yaml::sendmessage::printf '%b' \"$MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 113,
|
||||
"method": "sendMessage",
|
||||
"path": ".gitea/workflows/cd.yaml",
|
||||
"sanitized_excerpt": "curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": ".gitea/workflows/cd.yaml::sendmessage::curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 305,
|
||||
"method": "sendMessage",
|
||||
"path": ".gitea/workflows/cd.yaml",
|
||||
"sanitized_excerpt": "curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": ".gitea/workflows/cd.yaml::sendmessage::curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 1203,
|
||||
"method": "sendMessage",
|
||||
"path": ".gitea/workflows/cd.yaml",
|
||||
"sanitized_excerpt": "curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": ".gitea/workflows/cd.yaml::sendmessage::curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 1552,
|
||||
"method": "sendMessage",
|
||||
"path": ".gitea/workflows/cd.yaml",
|
||||
"sanitized_excerpt": "printf '%b' \"$TG_MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": ".gitea/workflows/cd.yaml::sendmessage::printf '%b' \"$TG_MSG\" | curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 1575,
|
||||
"method": "sendMessage",
|
||||
"path": ".gitea/workflows/cd.yaml",
|
||||
"sanitized_excerpt": "curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": ".gitea/workflows/cd.yaml::sendmessage::curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 137,
|
||||
"method": "sendMessage",
|
||||
"path": ".gitea/workflows/code-review.yaml",
|
||||
"sanitized_excerpt": "curl -fsS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": ".gitea/workflows/code-review.yaml::sendmessage::curl -fsS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 216,
|
||||
"method": "sendMessage",
|
||||
"path": ".gitea/workflows/code-review.yaml",
|
||||
"sanitized_excerpt": "curl -fsS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": ".gitea/workflows/code-review.yaml::sendmessage::curl -fsS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 69,
|
||||
"method": "sendMessage",
|
||||
"path": ".gitea/workflows/deploy-alerts.yaml",
|
||||
"sanitized_excerpt": "curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": ".gitea/workflows/deploy-alerts.yaml::sendmessage::curl -fS -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 98,
|
||||
"method": "sendMessage",
|
||||
"path": ".gitea/workflows/e2e-health.yaml",
|
||||
"sanitized_excerpt": "curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": ".gitea/workflows/e2e-health.yaml::sendmessage::curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 210,
|
||||
"method": "sendMessage",
|
||||
"path": ".gitea/workflows/run-migration.yml",
|
||||
"sanitized_excerpt": "curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": ".gitea/workflows/run-migration.yml::sendmessage::curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 1138,
|
||||
"method": "sendMessage",
|
||||
"path": "apps/api/src/services/channel_hub.py",
|
||||
"sanitized_excerpt": "f\"https://api.telegram.org/bot<redacted>/sendMessage\",",
|
||||
"signature": "apps/api/src/services/channel_hub.py::sendmessage::f\"https://api.telegram.org/bot<redacted>/sendMessage\","
|
||||
},
|
||||
{
|
||||
"line": 64,
|
||||
"method": "sendMessage",
|
||||
"path": "scripts/ops/backup-from-110.sh",
|
||||
"sanitized_excerpt": "curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": "scripts/ops/backup-from-110.sh::sendmessage::curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 90,
|
||||
"method": "sendMessage",
|
||||
"path": "scripts/ops/docker-health-monitor.sh",
|
||||
"sanitized_excerpt": "curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": "scripts/ops/docker-health-monitor.sh::sendmessage::curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 63,
|
||||
"method": "sendMessage",
|
||||
"path": "scripts/ops/dr-drill.sh",
|
||||
"sanitized_excerpt": "curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": "scripts/ops/dr-drill.sh::sendmessage::curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
},
|
||||
{
|
||||
"line": 52,
|
||||
"method": "sendMessage",
|
||||
"path": "scripts/ops/pg-backup.sh",
|
||||
"sanitized_excerpt": "curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\",
|
||||
"signature": "scripts/ops/pg-backup.sh::sendmessage::curl -s -X POST \"https://api.telegram.org/bot<redacted>/sendMessage\" \\"
|
||||
}
|
||||
],
|
||||
"execution_boundaries": {
|
||||
"action_buttons_allowed": false,
|
||||
"api_sender_refactor_authorized": false,
|
||||
"bot_api_call_authorized": false,
|
||||
"bot_token_change_authorized": false,
|
||||
"chat_route_change_authorized": false,
|
||||
"not_authorization": true,
|
||||
"partial_token_collection_allowed": false,
|
||||
"production_write_authorized": false,
|
||||
"raw_payload_storage_allowed": false,
|
||||
"runtime_execution_authorized": false,
|
||||
"script_modification_authorized": false,
|
||||
"secret_hash_collection_allowed": false,
|
||||
"secret_value_collection_allowed": false,
|
||||
"telegram_send_authorized": false,
|
||||
"workflow_modification_authorized": false
|
||||
},
|
||||
"generated_at": "2026-06-19T09:40:00+08:00",
|
||||
"git_commit": "4d0150e1",
|
||||
"guarded_bot_methods": [
|
||||
"sendMessage",
|
||||
"sendDocument",
|
||||
"sendPhoto",
|
||||
"sendMediaGroup",
|
||||
"editMessageText",
|
||||
"sendAnimation",
|
||||
"sendVideo",
|
||||
"sendAudio",
|
||||
"sendVoice"
|
||||
],
|
||||
"guarded_roots": [
|
||||
".gitea/workflows",
|
||||
"scripts/ops",
|
||||
"scripts/ci",
|
||||
"apps/api/src"
|
||||
],
|
||||
"mode": "repo_source_scan_no_secret_value_no_telegram_send",
|
||||
"new_bypass_findings": [],
|
||||
"operator_interpretation": [
|
||||
"new_bypass_count 維持 0 才代表沒有新增未登記 Telegram Bot API 直送旁路。",
|
||||
"既有 18 個 sendMessage 旁路仍是待 owner response 的基線,不代表已批准或已收斂。",
|
||||
"sendDocument / sendPhoto / sendMediaGroup 等附件型出口若出現在 repo source,會被視為新增旁路並阻擋。",
|
||||
"本 guard 只讀 repo source 與 committed snapshot,不送 Telegram、不讀 Bot token、不修改 workflow / script / API sender。"
|
||||
],
|
||||
"removed_baseline_signatures": [],
|
||||
"schema_version": "telegram_notification_egress_no_new_bypass_guard_v1",
|
||||
"source_snapshot": "docs/security/telegram-notification-egress-inventory.snapshot.json",
|
||||
"status": "pass_no_new_bypass",
|
||||
"summary": {
|
||||
"action_button_count": 0,
|
||||
"baseline_signature_count": 18,
|
||||
"current_direct_bot_api_call_count": 18,
|
||||
"current_direct_bot_api_file_count": 11,
|
||||
"editMessageText_call_count": 0,
|
||||
"guarded_method_count": 9,
|
||||
"new_bypass_count": 0,
|
||||
"new_bypass_file_count": 0,
|
||||
"other_guarded_method_call_count": 0,
|
||||
"removed_baseline_call_count": 0,
|
||||
"runtime_gate_count": 0,
|
||||
"sendDocument_call_count": 0,
|
||||
"sendMediaGroup_call_count": 0,
|
||||
"sendMessage_call_count": 18,
|
||||
"sendPhoto_call_count": 0,
|
||||
"source_direct_bot_api_call_count": 18,
|
||||
"source_direct_bot_api_file_count": 11
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -82,7 +82,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%;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 個遷移波次;後續需 owner response 後分批收斂,不得把 API formatter 完成誤判成全域完成 | 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` |
|
||||
| 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-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 |
|
||||
|
||||
@@ -74,6 +74,8 @@ REQUIRED_CONTROL_DOCS = [
|
||||
"docs/security/SECURITY-ASSET-CONTROL-LEDGER.md",
|
||||
"docs/security/AI-PROVIDER-OWNER-RESPONSE-ACCEPTANCE.md",
|
||||
"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/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md",
|
||||
]
|
||||
|
||||
|
||||
@@ -231,12 +231,18 @@ def validate(root: Path) -> None:
|
||||
telegram_notification_egress_inventory = load_json(
|
||||
security_dir / "telegram-notification-egress-inventory.snapshot.json"
|
||||
)
|
||||
telegram_notification_egress_no_new_bypass_guard = load_json(
|
||||
security_dir / "telegram-notification-egress-no-new-bypass-guard.snapshot.json"
|
||||
)
|
||||
telegram_notification_egress_owner_request_draft = load_json(
|
||||
security_dir / "telegram-notification-egress-owner-request-draft.snapshot.json"
|
||||
)
|
||||
telegram_notification_egress_migration_plan_draft = load_json(
|
||||
security_dir / "telegram-notification-egress-migration-plan-draft.snapshot.json"
|
||||
)
|
||||
telegram_notification_egress_owner_response_acceptance = load_json(
|
||||
security_dir / "telegram-notification-egress-owner-response-acceptance.snapshot.json"
|
||||
)
|
||||
public_runtime_config_change_evidence_acceptance = load_json(
|
||||
security_dir / "public-runtime-config-change-evidence-acceptance.snapshot.json"
|
||||
)
|
||||
@@ -21894,6 +21900,238 @@ def validate(root: Path) -> None:
|
||||
f"telegram_notification_egress_migration_plan_draft.{item['migration_candidate_id']}.{false_key}",
|
||||
item[false_key],
|
||||
)
|
||||
assert_equal(
|
||||
"telegram_notification_egress_no_new_bypass_guard.schema",
|
||||
telegram_notification_egress_no_new_bypass_guard["schema_version"],
|
||||
"telegram_notification_egress_no_new_bypass_guard_v1",
|
||||
)
|
||||
assert_equal(
|
||||
"telegram_notification_egress_no_new_bypass_guard.status",
|
||||
telegram_notification_egress_no_new_bypass_guard["status"],
|
||||
"pass_no_new_bypass",
|
||||
)
|
||||
assert_equal(
|
||||
"telegram_notification_egress_no_new_bypass_guard.mode",
|
||||
telegram_notification_egress_no_new_bypass_guard["mode"],
|
||||
"repo_source_scan_no_secret_value_no_telegram_send",
|
||||
)
|
||||
expected_telegram_egress_no_new_bypass_summary = {
|
||||
"source_direct_bot_api_call_count": 18,
|
||||
"source_direct_bot_api_file_count": 11,
|
||||
"baseline_signature_count": 18,
|
||||
"current_direct_bot_api_call_count": 18,
|
||||
"current_direct_bot_api_file_count": 11,
|
||||
"guarded_method_count": 9,
|
||||
"sendMessage_call_count": 18,
|
||||
"sendDocument_call_count": 0,
|
||||
"sendPhoto_call_count": 0,
|
||||
"sendMediaGroup_call_count": 0,
|
||||
"editMessageText_call_count": 0,
|
||||
"other_guarded_method_call_count": 0,
|
||||
"new_bypass_count": 0,
|
||||
"new_bypass_file_count": 0,
|
||||
"removed_baseline_call_count": 0,
|
||||
"runtime_gate_count": 0,
|
||||
"action_button_count": 0,
|
||||
}
|
||||
for key, expected in expected_telegram_egress_no_new_bypass_summary.items():
|
||||
assert_equal(
|
||||
f"telegram_notification_egress_no_new_bypass_guard.summary.{key}",
|
||||
telegram_notification_egress_no_new_bypass_guard["summary"][key],
|
||||
expected,
|
||||
)
|
||||
assert_equal(
|
||||
"telegram_notification_egress_no_new_bypass_guard.current_paths",
|
||||
[item["path"] for item in telegram_notification_egress_no_new_bypass_guard["current_direct_bot_api_calls"]],
|
||||
[
|
||||
".gitea/workflows/cd-dev.yaml",
|
||||
".gitea/workflows/cd-dev.yaml",
|
||||
".gitea/workflows/cd-dev.yaml",
|
||||
".gitea/workflows/cd.yaml",
|
||||
".gitea/workflows/cd.yaml",
|
||||
".gitea/workflows/cd.yaml",
|
||||
".gitea/workflows/cd.yaml",
|
||||
".gitea/workflows/cd.yaml",
|
||||
".gitea/workflows/code-review.yaml",
|
||||
".gitea/workflows/code-review.yaml",
|
||||
".gitea/workflows/deploy-alerts.yaml",
|
||||
".gitea/workflows/e2e-health.yaml",
|
||||
".gitea/workflows/run-migration.yml",
|
||||
"apps/api/src/services/channel_hub.py",
|
||||
"scripts/ops/backup-from-110.sh",
|
||||
"scripts/ops/docker-health-monitor.sh",
|
||||
"scripts/ops/dr-drill.sh",
|
||||
"scripts/ops/pg-backup.sh",
|
||||
],
|
||||
)
|
||||
assert_equal(
|
||||
"telegram_notification_egress_no_new_bypass_guard.new_bypass_findings",
|
||||
telegram_notification_egress_no_new_bypass_guard["new_bypass_findings"],
|
||||
[],
|
||||
)
|
||||
assert_equal(
|
||||
"telegram_notification_egress_no_new_bypass_guard.removed_baseline_signatures",
|
||||
telegram_notification_egress_no_new_bypass_guard["removed_baseline_signatures"],
|
||||
[],
|
||||
)
|
||||
for key, value in telegram_notification_egress_no_new_bypass_guard["execution_boundaries"].items():
|
||||
if key == "not_authorization":
|
||||
assert_true(f"telegram_notification_egress_no_new_bypass_guard.execution_boundaries.{key}", value)
|
||||
else:
|
||||
assert_false(f"telegram_notification_egress_no_new_bypass_guard.execution_boundaries.{key}", value)
|
||||
assert_equal(
|
||||
"telegram_notification_egress_owner_response_acceptance.schema",
|
||||
telegram_notification_egress_owner_response_acceptance["schema_version"],
|
||||
"telegram_notification_egress_owner_response_acceptance_v1",
|
||||
)
|
||||
assert_equal(
|
||||
"telegram_notification_egress_owner_response_acceptance.status",
|
||||
telegram_notification_egress_owner_response_acceptance["status"],
|
||||
"owner_response_acceptance_ledger_ready_no_runtime_action",
|
||||
)
|
||||
assert_equal(
|
||||
"telegram_notification_egress_owner_response_acceptance.mode",
|
||||
telegram_notification_egress_owner_response_acceptance["mode"],
|
||||
"metadata_only_no_secret_value_no_telegram_send_no_workflow_script_api_change",
|
||||
)
|
||||
expected_telegram_egress_owner_response_acceptance_summary = {
|
||||
"source_request_draft_count": 11,
|
||||
"source_migration_candidate_count": 11,
|
||||
"source_direct_bot_api_call_count": 18,
|
||||
"acceptance_candidate_count": 11,
|
||||
"workflow_acceptance_candidate_count": 6,
|
||||
"ops_script_acceptance_candidate_count": 4,
|
||||
"api_direct_acceptance_candidate_count": 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,
|
||||
"request_sent_count": 0,
|
||||
"recipient_confirmed_count": 0,
|
||||
"audit_event_emitted_count": 0,
|
||||
"owner_response_received_count": 0,
|
||||
"owner_response_accepted_count": 0,
|
||||
"owner_response_rejected_count": 0,
|
||||
"owner_response_quarantined_count": 0,
|
||||
"supplement_requested_count": 0,
|
||||
"formatter_convergence_accepted_count": 0,
|
||||
"redaction_contract_accepted_count": 0,
|
||||
"delivery_receipt_accepted_count": 0,
|
||||
"break_glass_fallback_accepted_count": 0,
|
||||
"maintenance_window_accepted_count": 0,
|
||||
"rollback_owner_accepted_count": 0,
|
||||
"postcheck_evidence_accepted_count": 0,
|
||||
"dedup_or_fingerprint_accepted_count": 0,
|
||||
"no_false_green_accepted_count": 0,
|
||||
"direct_bot_api_migration_authorized_count": 0,
|
||||
"workflow_modification_authorized_count": 0,
|
||||
"script_modification_authorized_count": 0,
|
||||
"api_sender_refactor_authorized_count": 0,
|
||||
"telegram_send_authorized_count": 0,
|
||||
"bot_api_call_authorized_count": 0,
|
||||
"workflow_dispatch_authorized_count": 0,
|
||||
"production_deploy_authorized_count": 0,
|
||||
"secret_value_collection_allowed_count": 0,
|
||||
"raw_payload_storage_allowed_count": 0,
|
||||
"production_write_authorized_count": 0,
|
||||
"runtime_gate_count": 0,
|
||||
"action_button_count": 0,
|
||||
}
|
||||
for key, expected in expected_telegram_egress_owner_response_acceptance_summary.items():
|
||||
assert_equal(
|
||||
f"telegram_notification_egress_owner_response_acceptance.summary.{key}",
|
||||
telegram_notification_egress_owner_response_acceptance["summary"][key],
|
||||
expected,
|
||||
)
|
||||
assert_equal(
|
||||
"telegram_notification_egress_owner_response_acceptance.source_paths",
|
||||
[item["source_path"] for item in telegram_notification_egress_owner_response_acceptance["acceptance_candidates"]],
|
||||
expected_telegram_egress_request_paths,
|
||||
)
|
||||
for key, value in telegram_notification_egress_owner_response_acceptance["execution_boundaries"].items():
|
||||
if key == "not_authorization":
|
||||
assert_true(
|
||||
f"telegram_notification_egress_owner_response_acceptance.execution_boundaries.{key}",
|
||||
value,
|
||||
)
|
||||
else:
|
||||
assert_false(
|
||||
f"telegram_notification_egress_owner_response_acceptance.execution_boundaries.{key}",
|
||||
value,
|
||||
)
|
||||
for item in telegram_notification_egress_owner_response_acceptance["acceptance_candidates"]:
|
||||
assert_equal(
|
||||
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.acceptance_fields",
|
||||
len(item["acceptance_fields"]),
|
||||
32,
|
||||
)
|
||||
assert_equal(
|
||||
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.required_owner_fields",
|
||||
len(item["required_owner_fields"]),
|
||||
19,
|
||||
)
|
||||
assert_equal(
|
||||
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.reviewer_checks",
|
||||
len(item["reviewer_checks"]),
|
||||
22,
|
||||
)
|
||||
assert_equal(
|
||||
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.outcome_lanes",
|
||||
len(item["outcome_lanes"]),
|
||||
10,
|
||||
)
|
||||
assert_equal(
|
||||
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.forbidden_payloads",
|
||||
len(item["forbidden_payloads"]),
|
||||
14,
|
||||
)
|
||||
assert_equal(
|
||||
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.blocked_actions",
|
||||
len(item["blocked_actions"]),
|
||||
35,
|
||||
)
|
||||
assert_true(
|
||||
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.not_authorization",
|
||||
item["not_authorization"],
|
||||
)
|
||||
for false_key in [
|
||||
"request_sent",
|
||||
"recipient_confirmed",
|
||||
"audit_event_emitted",
|
||||
"owner_response_received",
|
||||
"owner_response_accepted",
|
||||
"owner_response_rejected",
|
||||
"owner_response_quarantined",
|
||||
"supplement_requested",
|
||||
"formatter_convergence_accepted",
|
||||
"redaction_contract_accepted",
|
||||
"delivery_receipt_accepted",
|
||||
"break_glass_fallback_accepted",
|
||||
"maintenance_window_accepted",
|
||||
"rollback_owner_accepted",
|
||||
"postcheck_evidence_accepted",
|
||||
"dedup_or_fingerprint_accepted",
|
||||
"no_false_green_accepted",
|
||||
"direct_bot_api_migration_authorized",
|
||||
"workflow_modification_authorized",
|
||||
"script_modification_authorized",
|
||||
"api_sender_refactor_authorized",
|
||||
"telegram_send_authorized",
|
||||
"bot_api_call_authorized",
|
||||
"workflow_dispatch_authorized",
|
||||
"production_deploy_authorized",
|
||||
"secret_value_collection_allowed",
|
||||
"raw_payload_storage_allowed",
|
||||
"production_write_authorized",
|
||||
"runtime_gate",
|
||||
"action_buttons_allowed",
|
||||
]:
|
||||
assert_false(
|
||||
f"telegram_notification_egress_owner_response_acceptance.{item['acceptance_candidate_id']}.{false_key}",
|
||||
item[false_key],
|
||||
)
|
||||
assert_equal(
|
||||
"public_runtime_config_change_evidence_acceptance.schema",
|
||||
public_runtime_config_change_evidence_acceptance["schema_version"],
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
#!/usr/bin/env python3
|
||||
"""檢查 Telegram 通知出口不可新增未登記 direct Bot API 旁路。
|
||||
|
||||
本 guard 只掃描 repo 原始碼與 committed snapshot,不讀 secret、不呼叫
|
||||
Telegram、不修改 workflow / script / API sender。既有 direct send 仍是待
|
||||
owner response 的基線;任何新增或變形的 direct Bot API endpoint 都必須先
|
||||
進 inventory / owner request / migration plan,而不是直接合併。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import Counter
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
TAIPEI = timezone(timedelta(hours=8))
|
||||
|
||||
SOURCE_SNAPSHOT = Path("docs/security/telegram-notification-egress-inventory.snapshot.json")
|
||||
SCAN_ROOTS = (
|
||||
Path(".gitea/workflows"),
|
||||
Path("scripts/ops"),
|
||||
Path("scripts/ci"),
|
||||
Path("apps/api/src"),
|
||||
)
|
||||
SCAN_SUFFIXES = {".py", ".sh", ".js", ".yml", ".yaml"}
|
||||
GUARDED_BOT_METHODS = (
|
||||
"sendMessage",
|
||||
"sendDocument",
|
||||
"sendPhoto",
|
||||
"sendMediaGroup",
|
||||
"editMessageText",
|
||||
"sendAnimation",
|
||||
"sendVideo",
|
||||
"sendAudio",
|
||||
"sendVoice",
|
||||
)
|
||||
|
||||
BOT_ENDPOINT_RE = re.compile(
|
||||
r"api\.telegram\.org/bot.*?/(?P<method>"
|
||||
+ "|".join(re.escape(method) for method in GUARDED_BOT_METHODS)
|
||||
+ r")\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
SECRET_INTERPOLATION_RE = re.compile(r"\$\{\{\s*secrets\.[^}]+\}\}")
|
||||
BOT_TOKEN_URL_RE = re.compile(
|
||||
r"api\.telegram\.org/bot.*?/(?P<method>"
|
||||
+ "|".join(re.escape(method) for method in GUARDED_BOT_METHODS)
|
||||
+ r")\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def git_short_sha(root: Path) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--short", "HEAD"],
|
||||
cwd=root,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def iter_scannable_files(root: Path) -> list[Path]:
|
||||
files: list[Path] = []
|
||||
for scan_root in SCAN_ROOTS:
|
||||
absolute_root = root / scan_root
|
||||
if not absolute_root.exists():
|
||||
continue
|
||||
for path in absolute_root.rglob("*"):
|
||||
if path.is_file() and path.suffix in SCAN_SUFFIXES:
|
||||
files.append(path)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def sanitize_excerpt(line: str) -> str:
|
||||
excerpt = line.strip()
|
||||
excerpt = SECRET_INTERPOLATION_RE.sub("${{ secrets.<redacted> }}", excerpt)
|
||||
excerpt = BOT_TOKEN_URL_RE.sub(
|
||||
lambda match: f"api.telegram.org/bot<redacted>/{match.group('method')}",
|
||||
excerpt,
|
||||
)
|
||||
return excerpt[:180]
|
||||
|
||||
|
||||
def signature(path: str, method: str, sanitized_excerpt: str) -> str:
|
||||
return f"{path}::{method.lower()}::{sanitized_excerpt}"
|
||||
|
||||
|
||||
def load_source_snapshot(root: Path) -> dict[str, Any]:
|
||||
snapshot_path = root / SOURCE_SNAPSHOT
|
||||
return json.loads(snapshot_path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def build_baseline(source_snapshot: dict[str, Any]) -> Counter[str]:
|
||||
baseline: Counter[str] = Counter()
|
||||
for item in source_snapshot.get("direct_bot_api_calls", []):
|
||||
excerpt = item.get("sanitized_excerpt", "")
|
||||
match = BOT_ENDPOINT_RE.search(excerpt)
|
||||
method = match.group("method") if match else "sendMessage"
|
||||
baseline[signature(item["path"], method, excerpt)] += 1
|
||||
return baseline
|
||||
|
||||
|
||||
def scan_current_direct_endpoints(root: Path) -> list[dict[str, Any]]:
|
||||
findings: list[dict[str, Any]] = []
|
||||
for path in iter_scannable_files(root):
|
||||
relative_path = path.relative_to(root).as_posix()
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
for line_number, line in enumerate(text.splitlines(), start=1):
|
||||
for match in BOT_ENDPOINT_RE.finditer(line):
|
||||
method = match.group("method")
|
||||
sanitized = sanitize_excerpt(line)
|
||||
findings.append(
|
||||
{
|
||||
"path": relative_path,
|
||||
"line": line_number,
|
||||
"method": method,
|
||||
"sanitized_excerpt": sanitized,
|
||||
"signature": signature(relative_path, method, sanitized),
|
||||
}
|
||||
)
|
||||
return findings
|
||||
|
||||
|
||||
def method_counts(findings: list[dict[str, Any]]) -> dict[str, int]:
|
||||
counts = {method: 0 for method in GUARDED_BOT_METHODS}
|
||||
for item in findings:
|
||||
for method in GUARDED_BOT_METHODS:
|
||||
if item["method"].lower() == method.lower():
|
||||
counts[method] += 1
|
||||
break
|
||||
return counts
|
||||
|
||||
|
||||
def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]:
|
||||
generated = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
|
||||
source_snapshot = load_source_snapshot(root)
|
||||
baseline = build_baseline(source_snapshot)
|
||||
current_findings = scan_current_direct_endpoints(root)
|
||||
remaining_baseline = baseline.copy()
|
||||
new_bypass_findings: list[dict[str, Any]] = []
|
||||
|
||||
for item in current_findings:
|
||||
item_signature = item["signature"]
|
||||
if remaining_baseline[item_signature] > 0:
|
||||
remaining_baseline[item_signature] -= 1
|
||||
continue
|
||||
new_bypass_findings.append(item)
|
||||
|
||||
removed_baseline_signatures = [
|
||||
{"signature": item_signature, "removed_count": count}
|
||||
for item_signature, count in sorted(remaining_baseline.items())
|
||||
if count > 0
|
||||
]
|
||||
current_files = sorted({item["path"] for item in current_findings})
|
||||
new_bypass_files = sorted({item["path"] for item in new_bypass_findings})
|
||||
counts_by_method = method_counts(current_findings)
|
||||
source_summary = source_snapshot["summary"]
|
||||
|
||||
return {
|
||||
"schema_version": "telegram_notification_egress_no_new_bypass_guard_v1",
|
||||
"generated_at": generated,
|
||||
"git_commit": git_short_sha(root),
|
||||
"status": "pass_no_new_bypass" if not new_bypass_findings else "blocked_new_bypass_detected",
|
||||
"mode": "repo_source_scan_no_secret_value_no_telegram_send",
|
||||
"source_snapshot": SOURCE_SNAPSHOT.as_posix(),
|
||||
"guarded_roots": [path.as_posix() for path in SCAN_ROOTS],
|
||||
"guarded_bot_methods": list(GUARDED_BOT_METHODS),
|
||||
"summary": {
|
||||
"source_direct_bot_api_call_count": source_summary["direct_bot_api_call_count"],
|
||||
"source_direct_bot_api_file_count": source_summary["direct_bot_api_file_count"],
|
||||
"baseline_signature_count": sum(baseline.values()),
|
||||
"current_direct_bot_api_call_count": len(current_findings),
|
||||
"current_direct_bot_api_file_count": len(current_files),
|
||||
"guarded_method_count": len(GUARDED_BOT_METHODS),
|
||||
"sendMessage_call_count": counts_by_method["sendMessage"],
|
||||
"sendDocument_call_count": counts_by_method["sendDocument"],
|
||||
"sendPhoto_call_count": counts_by_method["sendPhoto"],
|
||||
"sendMediaGroup_call_count": counts_by_method["sendMediaGroup"],
|
||||
"editMessageText_call_count": counts_by_method["editMessageText"],
|
||||
"other_guarded_method_call_count": sum(
|
||||
count
|
||||
for method, count in counts_by_method.items()
|
||||
if method
|
||||
not in {
|
||||
"sendMessage",
|
||||
"sendDocument",
|
||||
"sendPhoto",
|
||||
"sendMediaGroup",
|
||||
"editMessageText",
|
||||
}
|
||||
),
|
||||
"new_bypass_count": len(new_bypass_findings),
|
||||
"new_bypass_file_count": len(new_bypass_files),
|
||||
"removed_baseline_call_count": sum(item["removed_count"] for item in removed_baseline_signatures),
|
||||
"runtime_gate_count": 0,
|
||||
"action_button_count": 0,
|
||||
},
|
||||
"execution_boundaries": {
|
||||
"runtime_execution_authorized": False,
|
||||
"telegram_send_authorized": False,
|
||||
"bot_api_call_authorized": False,
|
||||
"workflow_modification_authorized": False,
|
||||
"script_modification_authorized": False,
|
||||
"api_sender_refactor_authorized": False,
|
||||
"secret_value_collection_allowed": False,
|
||||
"secret_hash_collection_allowed": False,
|
||||
"partial_token_collection_allowed": False,
|
||||
"chat_route_change_authorized": False,
|
||||
"bot_token_change_authorized": False,
|
||||
"raw_payload_storage_allowed": False,
|
||||
"production_write_authorized": False,
|
||||
"action_buttons_allowed": False,
|
||||
"not_authorization": True,
|
||||
},
|
||||
"current_direct_bot_api_calls": current_findings,
|
||||
"new_bypass_findings": new_bypass_findings,
|
||||
"removed_baseline_signatures": removed_baseline_signatures,
|
||||
"operator_interpretation": [
|
||||
"new_bypass_count 維持 0 才代表沒有新增未登記 Telegram Bot API 直送旁路。",
|
||||
"既有 18 個 sendMessage 旁路仍是待 owner response 的基線,不代表已批准或已收斂。",
|
||||
"sendDocument / sendPhoto / sendMediaGroup 等附件型出口若出現在 repo source,會被視為新增旁路並阻擋。",
|
||||
"本 guard 只讀 repo source 與 committed snapshot,不送 Telegram、不讀 Bot token、不修改 workflow / script / API sender。",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def validate(root: Path) -> None:
|
||||
report = build_report(root)
|
||||
errors: list[str] = []
|
||||
if report["summary"]["new_bypass_count"]:
|
||||
for item in report["new_bypass_findings"]:
|
||||
errors.append(
|
||||
f"{item['path']}:{item['line']}: 新增未登記 Telegram Bot API 旁路 {item['method']}"
|
||||
)
|
||||
|
||||
if errors:
|
||||
raise SystemExit(
|
||||
"BLOCKED telegram notification egress no-new-bypass guard:\n"
|
||||
+ "\n".join(f"- {error}" for error in errors)
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="檢查 Telegram 通知出口不可新增未登記 direct Bot API 旁路")
|
||||
parser.add_argument("--root", default=".", help="repository root")
|
||||
parser.add_argument("--output", help="寫出 JSON 報告")
|
||||
parser.add_argument("--generated-at", help="固定 generated_at 時間,供 committed snapshot 使用")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.root).resolve()
|
||||
report = build_report(root, args.generated_at)
|
||||
if args.output:
|
||||
output = Path(args.output)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
|
||||
validate(root)
|
||||
summary = report["summary"]
|
||||
print(
|
||||
"TELEGRAM_NOTIFICATION_EGRESS_NO_NEW_BYPASS_GUARD_OK "
|
||||
f"current={summary['current_direct_bot_api_call_count']} "
|
||||
f"baseline={summary['baseline_signature_count']} "
|
||||
f"new={summary['new_bypass_count']} "
|
||||
f"sendDocument={summary['sendDocument_call_count']} "
|
||||
f"runtime_gate={summary['runtime_gate_count']}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,388 @@
|
||||
#!/usr/bin/env python3
|
||||
"""建立 Telegram 通知出口 owner response 驗收帳本。
|
||||
|
||||
此帳本把 Telegram 通知出口 owner request 草稿與 migration plan 草稿轉成
|
||||
reviewer 可驗收的候選項。它不送 Telegram、不呼叫 Bot API、不讀 secret,
|
||||
也不修改 workflow、script、API sender、runtime config 或 production。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
TAIPEI = timezone(timedelta(hours=8))
|
||||
|
||||
OWNER_REQUEST_SNAPSHOT = Path("docs/security/telegram-notification-egress-owner-request-draft.snapshot.json")
|
||||
MIGRATION_PLAN_SNAPSHOT = Path("docs/security/telegram-notification-egress-migration-plan-draft.snapshot.json")
|
||||
|
||||
ACCEPTANCE_FIELDS = [
|
||||
"acceptance_candidate_id",
|
||||
"source_request_draft_id",
|
||||
"source_migration_candidate_id",
|
||||
"source_path",
|
||||
"surface_kind",
|
||||
"direct_call_count",
|
||||
"proposed_wave",
|
||||
"proposed_target",
|
||||
"owner_response_ref",
|
||||
"owner_role_or_team",
|
||||
"decision",
|
||||
"decision_reason",
|
||||
"affected_scope",
|
||||
"redacted_evidence_refs",
|
||||
"message_shape_contract_ref",
|
||||
"redaction_contract_ref",
|
||||
"formatter_convergence_decision",
|
||||
"gateway_or_alertmanager_target",
|
||||
"break_glass_fallback_decision",
|
||||
"delivery_receipt_ref",
|
||||
"dedup_or_fingerprint_plan",
|
||||
"fallback_or_degraded_mode",
|
||||
"migration_or_exception_reason",
|
||||
"maintenance_window",
|
||||
"rollback_owner",
|
||||
"postcheck_evidence_ref",
|
||||
"no_secret_value_attestation",
|
||||
"no_raw_payload_attestation",
|
||||
"no_false_green_attestation",
|
||||
"reviewer_outcome",
|
||||
"followup_owner",
|
||||
"not_authorization",
|
||||
]
|
||||
|
||||
REVIEWER_CHECKS = [
|
||||
"source_owner_request_current",
|
||||
"source_migration_plan_current",
|
||||
"owner_identity_present",
|
||||
"decision_reason_present",
|
||||
"affected_scope_matches_source",
|
||||
"redacted_refs_only",
|
||||
"no_secret_or_token_value",
|
||||
"no_raw_message_payload",
|
||||
"message_shape_contract_present",
|
||||
"redaction_contract_present",
|
||||
"formatter_convergence_explicit",
|
||||
"gateway_or_alertmanager_target_valid",
|
||||
"break_glass_fallback_explicit",
|
||||
"delivery_receipt_metadata_only",
|
||||
"dedup_or_fingerprint_present",
|
||||
"maintenance_window_present",
|
||||
"rollback_owner_present",
|
||||
"postcheck_evidence_present",
|
||||
"no_false_green_attested",
|
||||
"migration_authorization_separate",
|
||||
"counts_transition_safe",
|
||||
"runtime_gate_stays_zero",
|
||||
]
|
||||
|
||||
OUTCOME_LANES = [
|
||||
"waiting_owner_response",
|
||||
"quarantine_secret_or_raw_payload",
|
||||
"reject_execution_request",
|
||||
"request_owner_route_supplement",
|
||||
"request_formatter_convergence_supplement",
|
||||
"request_redaction_or_receipt_supplement",
|
||||
"request_maintenance_or_rollback_supplement",
|
||||
"ready_for_migration_review",
|
||||
"owner_review_only_update",
|
||||
"waiting_runtime_gate",
|
||||
]
|
||||
|
||||
FORBIDDEN_PAYLOADS = [
|
||||
"bot_token_value",
|
||||
"chat_secret_value",
|
||||
"secret_hash",
|
||||
"partial_token",
|
||||
"masked_token",
|
||||
"authorization_header",
|
||||
"raw_message_payload",
|
||||
"raw_workflow_log",
|
||||
"raw_action_log",
|
||||
"raw_screenshot_with_secret",
|
||||
"internal_work_window_transcript",
|
||||
"private_namespace",
|
||||
"unredacted_internal_path",
|
||||
"unredacted_private_ip",
|
||||
]
|
||||
|
||||
BLOCKED_ACTIONS = [
|
||||
"mark_owner_response_received_without_record",
|
||||
"mark_owner_response_accepted_without_reviewer_record",
|
||||
"send_telegram",
|
||||
"call_bot_api",
|
||||
"modify_workflow",
|
||||
"modify_ops_script",
|
||||
"refactor_api_sender",
|
||||
"dispatch_workflow",
|
||||
"trigger_cd",
|
||||
"deploy_production",
|
||||
"change_chat_route",
|
||||
"change_bot_token",
|
||||
"rotate_secret",
|
||||
"read_secret_store",
|
||||
"collect_secret_value",
|
||||
"collect_secret_hash",
|
||||
"collect_partial_token",
|
||||
"collect_chat_id_secret",
|
||||
"store_raw_message_payload",
|
||||
"store_unredacted_log",
|
||||
"store_internal_work_window_transcript",
|
||||
"accept_cd_success_as_delivery_receipt",
|
||||
"accept_route_200_as_notification_delivery",
|
||||
"accept_ui_visible_as_notification_acceptance",
|
||||
"accept_telegram_sent_without_delivery_receipt",
|
||||
"skip_formatter_convergence",
|
||||
"skip_redaction_contract",
|
||||
"skip_dedup_or_fingerprint_review",
|
||||
"skip_break_glass_fallback_review",
|
||||
"authorize_migration",
|
||||
"authorize_workflow_modification",
|
||||
"authorize_script_modification",
|
||||
"authorize_api_sender_refactor",
|
||||
"open_runtime_gate",
|
||||
"add_action_button",
|
||||
]
|
||||
|
||||
|
||||
def git_short_sha(root: Path) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--short", "HEAD"],
|
||||
cwd=root,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def build_candidate(request: dict[str, Any], migration: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"acceptance_candidate_id": f"telegram_notification_egress_owner_response_acceptance:{request['source_path']}",
|
||||
"status": "waiting_owner_response",
|
||||
"source_request_draft_id": request["request_draft_id"],
|
||||
"source_migration_candidate_id": migration["migration_candidate_id"],
|
||||
"source_path": request["source_path"],
|
||||
"surface_kind": request["surface_kind"],
|
||||
"direct_call_count": request["direct_call_count"],
|
||||
"line_refs": request["line_refs"],
|
||||
"line_hash_refs": request["line_hash_refs"],
|
||||
"proposed_wave": migration["proposed_wave"],
|
||||
"proposed_target": migration["proposed_target"],
|
||||
"proposed_change_summary": migration["proposed_change_summary"],
|
||||
"owner_response_ref": None,
|
||||
"owner_role_or_team": "pending_owner_response",
|
||||
"decision": "pending_owner_response",
|
||||
"decision_reason": "pending_owner_response",
|
||||
"affected_scope": "pending_owner_response",
|
||||
"redacted_evidence_refs": [],
|
||||
"message_shape_contract_ref": None,
|
||||
"redaction_contract_ref": None,
|
||||
"formatter_convergence_decision": "pending_owner_response",
|
||||
"gateway_or_alertmanager_target": "pending_owner_response",
|
||||
"break_glass_fallback_decision": "pending_owner_response",
|
||||
"delivery_receipt_ref": None,
|
||||
"dedup_or_fingerprint_plan": "pending_owner_response",
|
||||
"fallback_or_degraded_mode": "pending_owner_response",
|
||||
"migration_or_exception_reason": "pending_owner_response",
|
||||
"maintenance_window": "pending_owner_response",
|
||||
"rollback_owner": "pending_owner_response",
|
||||
"postcheck_evidence_ref": None,
|
||||
"no_secret_value_attestation": "pending_owner_response",
|
||||
"no_raw_payload_attestation": "pending_owner_response",
|
||||
"no_false_green_attestation": "pending_owner_response",
|
||||
"reviewer_outcome": "waiting_owner_response",
|
||||
"followup_owner": "pending_owner_response",
|
||||
"acceptance_fields": ACCEPTANCE_FIELDS,
|
||||
"required_owner_fields": request["required_owner_fields"],
|
||||
"reviewer_checks": REVIEWER_CHECKS,
|
||||
"outcome_lanes": OUTCOME_LANES,
|
||||
"forbidden_payloads": FORBIDDEN_PAYLOADS,
|
||||
"blocked_actions": BLOCKED_ACTIONS,
|
||||
"not_authorization": True,
|
||||
"request_sent": False,
|
||||
"recipient_confirmed": False,
|
||||
"audit_event_emitted": False,
|
||||
"owner_response_received": False,
|
||||
"owner_response_accepted": False,
|
||||
"owner_response_rejected": False,
|
||||
"owner_response_quarantined": False,
|
||||
"supplement_requested": False,
|
||||
"formatter_convergence_accepted": False,
|
||||
"redaction_contract_accepted": False,
|
||||
"delivery_receipt_accepted": False,
|
||||
"break_glass_fallback_accepted": False,
|
||||
"maintenance_window_accepted": False,
|
||||
"rollback_owner_accepted": False,
|
||||
"postcheck_evidence_accepted": False,
|
||||
"dedup_or_fingerprint_accepted": False,
|
||||
"no_false_green_accepted": False,
|
||||
"direct_bot_api_migration_authorized": False,
|
||||
"workflow_modification_authorized": False,
|
||||
"script_modification_authorized": False,
|
||||
"api_sender_refactor_authorized": False,
|
||||
"telegram_send_authorized": False,
|
||||
"bot_api_call_authorized": False,
|
||||
"workflow_dispatch_authorized": False,
|
||||
"production_deploy_authorized": False,
|
||||
"secret_value_collection_allowed": False,
|
||||
"raw_payload_storage_allowed": False,
|
||||
"production_write_authorized": False,
|
||||
"runtime_gate": False,
|
||||
"action_buttons_allowed": False,
|
||||
}
|
||||
|
||||
|
||||
def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]:
|
||||
generated = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
|
||||
owner_request = load_json(root / OWNER_REQUEST_SNAPSHOT)
|
||||
migration_plan = load_json(root / MIGRATION_PLAN_SNAPSHOT)
|
||||
migration_by_request_id = {
|
||||
item["source_request_draft_id"]: item for item in migration_plan["migration_candidates"]
|
||||
}
|
||||
|
||||
candidates = [
|
||||
build_candidate(request, migration_by_request_id[request["request_draft_id"]])
|
||||
for request in owner_request["request_drafts"]
|
||||
]
|
||||
workflow = [item for item in candidates if item["surface_kind"] == "gitea_workflow_direct_bot_api"]
|
||||
ops = [item for item in candidates if item["surface_kind"] == "ops_script_direct_bot_api"]
|
||||
api = [item for item in candidates if item["surface_kind"] == "api_direct_bot_api"]
|
||||
|
||||
return {
|
||||
"schema_version": "telegram_notification_egress_owner_response_acceptance_v1",
|
||||
"generated_at": generated,
|
||||
"git_commit": git_short_sha(root),
|
||||
"status": "owner_response_acceptance_ledger_ready_no_runtime_action",
|
||||
"mode": "metadata_only_no_secret_value_no_telegram_send_no_workflow_script_api_change",
|
||||
"source_owner_request_snapshot": OWNER_REQUEST_SNAPSHOT.as_posix(),
|
||||
"source_owner_request_schema_version": owner_request["schema_version"],
|
||||
"source_owner_request_status": owner_request["status"],
|
||||
"source_migration_plan_snapshot": MIGRATION_PLAN_SNAPSHOT.as_posix(),
|
||||
"source_migration_plan_schema_version": migration_plan["schema_version"],
|
||||
"source_migration_plan_status": migration_plan["status"],
|
||||
"summary": {
|
||||
"source_request_draft_count": owner_request["summary"]["request_draft_count"],
|
||||
"source_migration_candidate_count": migration_plan["summary"]["migration_candidate_count"],
|
||||
"source_direct_bot_api_call_count": owner_request["summary"]["source_direct_bot_api_call_count"],
|
||||
"acceptance_candidate_count": len(candidates),
|
||||
"workflow_acceptance_candidate_count": len(workflow),
|
||||
"ops_script_acceptance_candidate_count": len(ops),
|
||||
"api_direct_acceptance_candidate_count": len(api),
|
||||
"acceptance_field_count": len(ACCEPTANCE_FIELDS),
|
||||
"required_owner_field_count": len(owner_request["request_drafts"][0]["required_owner_fields"]),
|
||||
"reviewer_check_count": len(REVIEWER_CHECKS),
|
||||
"outcome_lane_count": len(OUTCOME_LANES),
|
||||
"forbidden_payload_count": len(FORBIDDEN_PAYLOADS),
|
||||
"blocked_action_count": len(BLOCKED_ACTIONS),
|
||||
"request_sent_count": 0,
|
||||
"recipient_confirmed_count": 0,
|
||||
"audit_event_emitted_count": 0,
|
||||
"owner_response_received_count": 0,
|
||||
"owner_response_accepted_count": 0,
|
||||
"owner_response_rejected_count": 0,
|
||||
"owner_response_quarantined_count": 0,
|
||||
"supplement_requested_count": 0,
|
||||
"formatter_convergence_accepted_count": 0,
|
||||
"redaction_contract_accepted_count": 0,
|
||||
"delivery_receipt_accepted_count": 0,
|
||||
"break_glass_fallback_accepted_count": 0,
|
||||
"maintenance_window_accepted_count": 0,
|
||||
"rollback_owner_accepted_count": 0,
|
||||
"postcheck_evidence_accepted_count": 0,
|
||||
"dedup_or_fingerprint_accepted_count": 0,
|
||||
"no_false_green_accepted_count": 0,
|
||||
"direct_bot_api_migration_authorized_count": 0,
|
||||
"workflow_modification_authorized_count": 0,
|
||||
"script_modification_authorized_count": 0,
|
||||
"api_sender_refactor_authorized_count": 0,
|
||||
"telegram_send_authorized_count": 0,
|
||||
"bot_api_call_authorized_count": 0,
|
||||
"workflow_dispatch_authorized_count": 0,
|
||||
"production_deploy_authorized_count": 0,
|
||||
"secret_value_collection_allowed_count": 0,
|
||||
"raw_payload_storage_allowed_count": 0,
|
||||
"production_write_authorized_count": 0,
|
||||
"runtime_gate_count": 0,
|
||||
"action_button_count": 0,
|
||||
},
|
||||
"execution_boundaries": {
|
||||
"runtime_execution_authorized": False,
|
||||
"owner_response_mark_received_authorized": False,
|
||||
"owner_response_mark_accepted_authorized": False,
|
||||
"direct_bot_api_migration_authorized": False,
|
||||
"workflow_modification_authorized": False,
|
||||
"script_modification_authorized": False,
|
||||
"api_sender_refactor_authorized": False,
|
||||
"telegram_send_authorized": False,
|
||||
"bot_api_call_authorized": False,
|
||||
"workflow_dispatch_authorized": False,
|
||||
"production_deploy_authorized": False,
|
||||
"secret_value_collection_allowed": False,
|
||||
"raw_payload_storage_allowed": False,
|
||||
"production_write_authorized": False,
|
||||
"action_buttons_allowed": False,
|
||||
"not_authorization": True,
|
||||
},
|
||||
"acceptance_candidates": candidates,
|
||||
"operator_interpretation": [
|
||||
"此帳本只是 reviewer 驗收模板;owner response received / accepted 仍維持 0。",
|
||||
"CD success、route 200、UI 可見或 Telegram sent 狀態本身都不是 delivery receipt。",
|
||||
"workflow、script 與 API sender 收斂仍需獨立 runtime approval 與 change evidence。",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def validate(root: Path) -> None:
|
||||
report = build_report(root)
|
||||
summary = report["summary"]
|
||||
if summary["acceptance_candidate_count"] != summary["source_request_draft_count"]:
|
||||
raise SystemExit("BLOCKED telegram egress owner response acceptance: candidate/request count mismatch")
|
||||
if summary["acceptance_candidate_count"] != summary["source_migration_candidate_count"]:
|
||||
raise SystemExit("BLOCKED telegram egress owner response acceptance: candidate/migration count mismatch")
|
||||
if summary["runtime_gate_count"] != 0:
|
||||
raise SystemExit("BLOCKED telegram egress owner response acceptance: runtime gate must stay 0")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="建立 Telegram 通知出口 owner response 驗收帳本")
|
||||
parser.add_argument("--root", default=".", help="repository root")
|
||||
parser.add_argument("--output", help="write JSON snapshot")
|
||||
parser.add_argument("--generated-at", help="fixed generated_at timestamp")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = Path(args.root).resolve()
|
||||
report = build_report(root, args.generated_at)
|
||||
payload = json.dumps(report, ensure_ascii=False, indent=2) + "\n"
|
||||
if args.output:
|
||||
Path(args.output).write_text(payload, encoding="utf-8")
|
||||
else:
|
||||
sys.stdout.write(payload)
|
||||
|
||||
print(
|
||||
"TELEGRAM_NOTIFICATION_EGRESS_OWNER_RESPONSE_ACCEPTANCE_OK "
|
||||
f"candidates={report['summary']['acceptance_candidate_count']} "
|
||||
f"workflow={report['summary']['workflow_acceptance_candidate_count']} "
|
||||
f"ops={report['summary']['ops_script_acceptance_candidate_count']} "
|
||||
f"api={report['summary']['api_direct_acceptance_candidate_count']} "
|
||||
f"accepted={report['summary']['owner_response_accepted_count']} "
|
||||
f"runtime_gate={report['summary']['runtime_gate_count']}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user