diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 3b9bd4f2b..d4cffecbf 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,31 @@ +## 2026-06-14|Docker / systemd / Host Service owner request draft 本地完成 + +**背景**:Docker Compose、systemd、repair-bot、Ansible service role 與 host config backup capture 都屬於容易影響 110 / 188 服務穩定性的高價值配置。既有 repo-only 清冊已納入 9 個 surface,但尚未轉成 owner 可回覆的欄位包;因此本段先建立人工送件前 request draft,不碰 live host。 + +**完成項目**: +- 新增 `scripts/security/host-service-owner-request-draft.py`,從 `host-service-config-inventory.snapshot.json` 產生 metadata-only owner request draft。 +- 新增 `docs/security/host-service-owner-request-draft.snapshot.json`,固定 `request_draft_count=9`、`write_capable_request_draft_count=3`、`live_evidence_required_request_count=8`、`request_field_count=22`、`required_owner_field_count=12`、`blocked_action_count=14`。 +- 新增 `docs/security/HOST-SERVICE-OWNER-REQUEST-DRAFT.md`,說明 9 份 request draft、owner 必填欄位、blocked actions、完成度與 0 / false 邊界。 +- `security-mirror-progress-guard.py` 已鎖住新 snapshot 的 schema、summary、execution false flags、request ids、blocked actions 與每份草稿 false flags。 +- `HOST-SERVICE-CONFIG-INVENTORY.md`、`HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md`、`IWOOOS-CONFIG-CONTROL-INVENTORY.md`、`SECURITY-SUPPLY-CHAIN-PROGRESS.md`、P0 主控板與 MASTER 已同步。 + +**本地驗證**: +- `python3 -m json.tool docs/security/host-service-owner-request-draft.snapshot.json` 通過。 +- `python3 -m py_compile scripts/security/host-service-owner-request-draft.py scripts/security/security-mirror-progress-guard.py` 通過。 +- 產生器 smoke:`HOST_SERVICE_OWNER_REQUEST_DRAFT_OK drafts=9 write_capable=3 fields=12 sent=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/ops/doc-secrets-sanity-check.py docs .gitea` → `DOC_SECRET_SANITY_OK scanned_files=826`。 +- `git diff --check` 通過。 +- diff-only 新增行工作溝通片語掃描通過;未加入工作溝通原文、委派 XML、內嵌瀏覽器上下文或內部抱怨內容。 +- Snapshot assertion 通過:`HOST_SERVICE_OWNER_REQUEST_DRAFT_ASSERTIONS_OK drafts=9 write_capable=3 fields=12 sent=0 runtime_gate=0`。 + +**完成度與邊界**: +- Host Service owner request draft artifact:`100%`。 +- Docker / systemd 類別成熟度:維持 `50%`,因為尚未收到 owner response 或 live evidence。 +- Production browser verification:不適用;本輪只改 repo 內文件、snapshot、guard 與產生器,不變更前端 bundle 或 runtime。 +- request sent、recipient confirmed、owner response received / accepted、live host read、Docker Compose action、systemctl action、repair-bot execution、Ansible apply、secret collection、host write、active scan、runtime gate、action buttons、production write 全部維持 `0 / false`。 + ## 2026-06-14|P0-21 push readback 與同步基線回填 **背景**:P0-21 K8s / ArgoCD owner request draft 已完成並推送到 `gitea/main`,但 P0 主控板仍保留較舊的 `gitea/main=551d8144` 觀察值。為避免後續 IwoooS / AwoooP 平行 Session 用舊基線判讀,本段只回填推送後真相與同步狀態。 diff --git a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md index aa241c843..4a1db90f0 100644 --- a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md +++ b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md @@ -53,7 +53,7 @@ | 優先 | 類別 | 目前成熟度 | 下一步 | |------|------|------------|--------| -| P1-1 | Docker Compose / systemd / host service config | `50%` | repo-only 清冊已納入 9 個 surface;仍缺 110 / 188 live hash、restart window、rollback owner 與 post-check 指標 | +| P1-1 | Docker Compose / systemd / host service config | `50%` | repo-only 清冊已納入 9 個 surface,owner request draft 已轉成 9 份草稿;仍缺 owner response、110 / 188 live hash、restart window、rollback owner 與 post-check 指標 | | P1-2 | SSH / sudoers / known_hosts / firewall / WireGuard / NodePort | `54%` | repo-only 清冊已納入 16 個 SSH / network access surface;仍缺 live evidence、owner 與 rollback | | P1-3 | Backup / restore / escrow / retention | `58%` | repo-only 清冊已納入 38 個 surface;仍缺 restore drill approval package、offsite / escrow owner、retention owner、rollback owner 與 no-secret-value evidence | | P1-4 | Prometheus / Alertmanager / Grafana / SigNoz / Sentry / Langfuse | `62%` | repo-only 清冊已納入 60 個 monitoring / alerting / observability surface;仍缺 live drift evidence、reload owner、receiver owner、route smoke 與 receipt proof | @@ -155,6 +155,8 @@ python3 scripts/security/high-value-config-control-coverage.py \ `host_service_config_inventory_v1` 已把 Docker Compose、systemd / repair-bot、Ansible service role 與 host config backup capture 納入 repo-only 清冊,共 `9` 個 surface、`3` 個 write-capable surface、`2` 個 repair-bot whitelist、`1` 個 systemd restart surface。此更新只讓 `docker_compose_systemd_host_config` 從 `42%` 推進到 `50%`;owner response、live evidence、restart window、rollback owner、runtime gate 與 action button 仍全部為 `0`。 +2026-06-14 再新增 `host_service_owner_request_draft_v1`,把 9 個 surface 轉成 `request_draft_count=9`、`write_capable_request_draft_count=3`、`live_evidence_required_request_count=8`、`required_owner_field_count=12`、`blocked_action_count=14` 的人工送件前草稿。此更新仍不調高類別成熟度,因為 request sent、owner response received / accepted、live evidence、restart window、rollback owner、runtime gate 與 action button 仍全部為 `0`。 + ## 9. P1-2 SSH / network access 清冊更新 `ssh_network_access_inventory_v1` 已把 SSH target、known_hosts workflow、CI deploy SSH、monitoring SSH、backup SSH capture、sudoers wrapper、NetworkPolicy、NodePort、WireGuard runbook 與 alert SSH action catalog 納入 repo-only 清冊,共 `16` 個 surface、`6` 個 write-capable surface、`2` 個 NetworkPolicy、`2` 個 NodePort、`1` 個 sudoers surface 與 `1` 個 WireGuard surface。此更新只讓 `ssh_firewall_network_access` 從 `48%` 推進到 `54%`;owner response、live evidence、maintenance window、rollback owner、runtime gate 與 action button 仍全部為 `0`。 diff --git a/docs/security/HOST-SERVICE-CONFIG-INVENTORY.md b/docs/security/HOST-SERVICE-CONFIG-INVENTORY.md index 12fa70173..965eda211 100644 --- a/docs/security/HOST-SERVICE-CONFIG-INVENTORY.md +++ b/docs/security/HOST-SERVICE-CONFIG-INVENTORY.md @@ -15,6 +15,10 @@ 此清冊不是 host truth,也不是重啟批准;它只讓 P1-1 從「尚需 inventory」推進到「repo-only inventory ready」。 +2026-06-14 已新增 `docs/security/HOST-SERVICE-OWNER-REQUEST-DRAFT.md` 與 `docs/security/host-service-owner-request-draft.snapshot.json`,將 9 個 surface 轉成 owner request draft。固定 `request_draft_count=9`、`write_capable_request_draft_count=3`、`live_evidence_required_request_count=8`、`required_owner_field_count=12`、`blocked_action_count=14`、`request_sent_count=0`、`owner_response_received_count=0`、`runtime_gate_count=0`。 + +此 artifact 只表示 owner request 的 required shape,不代表 request sent、recipient confirmed、owner response received / accepted、live host read、Docker Compose action、systemctl action、repair-bot execution、Ansible apply、secret collection、host write、production write 或 runtime gate。 + ## 2. 覆蓋摘要 | 指標 | 目前值 | 說明 | @@ -102,3 +106,4 @@ python3 scripts/security/host-service-config-inventory.py \ | owner response 收件 | `0%` | 尚未收到或接受任何 owner response | | live evidence collection | `0%` | 未 SSH、未讀 live host、未 active scan | | restart / apply gate | `0%` | 未開啟 docker compose / systemctl / Ansible / repair-bot 操作 | +| owner request draft | `100%` | 已新增 9 份 request draft、snapshot、文件與 guard;request sent / received / accepted 仍為 0 | diff --git a/docs/security/HOST-SERVICE-OWNER-REQUEST-DRAFT.md b/docs/security/HOST-SERVICE-OWNER-REQUEST-DRAFT.md new file mode 100644 index 000000000..db7274544 --- /dev/null +++ b/docs/security/HOST-SERVICE-OWNER-REQUEST-DRAFT.md @@ -0,0 +1,107 @@ +# IwoooS Docker / systemd / 主機服務 Owner Request Draft + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-06-14 | +| 狀態 | `owner_request_draft_ready_not_dispatched` | +| 工具 | `scripts/security/host-service-owner-request-draft.py` | +| Snapshot | `docs/security/host-service-owner-request-draft.snapshot.json` | +| Source inventory | `docs/security/host-service-config-inventory.snapshot.json` | +| runtime gate | `0` | + +## 1. 目的 + +本文件承接 Docker / systemd / host service repo-only 清冊,把 9 個 surface 轉成人工送件前 request draft。它讓 110 / 188 / multi-host 的 Compose、repair-bot、Ansible role 與 host config backup capture 有一致的 owner 回覆欄位。 + +這不是主機真相、不是 live hash 收件、不是 restart window 已接受,也不是 `docker compose`、`systemctl`、repair-bot、Ansible 或 SSH 授權。 + +## 2. 摘要 + +| 指標 | 目前值 | 說明 | +|------|--------|------| +| request draft | `9` | 每個 host service surface 一份草稿 | +| write-capable request draft | `3` | Ansible role、110 repair-bot、188 repair-bot | +| live evidence required request | `8` | local dev compose 之外都需 owner 提供脫敏 live evidence | +| request field | `22` | 草稿欄位總數 | +| required owner field | `12` | owner 必填欄位 | +| blocked action | `14` | SSH、compose、systemctl、repair-bot、Ansible、sudo、active scan、runtime gate 等 | +| request sent / recipient confirmed | `0 / 0` | 尚未送件 | +| owner response received / accepted | `0 / 0` | 尚未收到或驗收 | +| live evidence received | `0` | 不 SSH、不讀 live host | +| restart window / rollback owner / post-check accepted | `0 / 0 / 0` | 不得重啟或 apply | +| runtime gate / action button | `0 / 0` | 不提供操作入口 | + +## 3. Request Draft 範圍 + +| Request | Host scope | 類型 | 風險焦點 | +|---------|------------|------|----------| +| `host_service_owner_request:local_dev_compose` | `local_dev_only` | Docker Compose | 避免 local dev compose 被誤用成 production source | +| `host_service_owner_request:monitoring_110_compose` | `192.168.0.110` | Docker Compose | 110 monitoring stack live hash、restart window、post-check | +| `host_service_owner_request:monitoring_exporters_188_compose` | `192.168.0.188` | Docker Compose | 188 exporter env source、live hash、rollback owner | +| `host_service_owner_request:sentry_110_reference_compose` | `192.168.0.110` | reference compose | Sentry 實際 source-of-truth 與 official revision | +| `host_service_owner_request:langfuse_110_compose` | `192.168.0.110` | Docker Compose | Langfuse compose live hash 與 secret placeholder disposition | +| `host_service_owner_request:ansible_docker_compose_service_role` | `multi_host` | Ansible executor role | allowed service_dir、check-mode、人工 gate | +| `host_service_owner_request:repair_bot_110_whitelist` | `192.168.0.110` | repair whitelist | authorized_keys binding、disable switch、audit log、post-check | +| `host_service_owner_request:repair_bot_188_whitelist` | `192.168.0.188` | repair whitelist | systemd restart gate、sudoers boundary、disable switch、route smoke | +| `host_service_owner_request:config_backup_host_capture` | `110_188_120_121_cluster` | backup capture | latest backup status、restore drill owner、secret handling proof | + +## 4. Owner 必填欄位 + +1. `owner_role_or_team` +2. `decision` +3. `decision_reason` +4. `affected_scope` +5. `redacted_evidence_refs` +6. `live_config_hash_ref` +7. `maintenance_window` +8. `restart_window` +9. `rollback_owner` +10. `post_check_plan` +11. `disable_switch` +12. `followup_owner` + +## 5. 禁止動作 + +1. `ssh_read` +2. `ssh_write` +3. `docker_compose_up` +4. `docker_compose_down` +5. `systemctl_restart` +6. `systemctl_reload` +7. `repair_bot_execute` +8. `ansible_apply` +9. `sudo_action` +10. `host_file_write` +11. `firewall_change` +12. `secret_value_collection` +13. `active_scan` +14. `runtime_gate_open` + +## 6. 指令 + +產生 committed snapshot: + +```bash +python3 scripts/security/host-service-owner-request-draft.py \ + --root . \ + --inventory-report docs/security/host-service-config-inventory.snapshot.json \ + --output docs/security/host-service-owner-request-draft.snapshot.json \ + --generated-at 2026-06-14T22:20:00+08:00 +``` + +驗證 guard: + +```bash +python3 scripts/security/security-mirror-progress-guard.py --root . +``` + +## 7. 完成度 + +| 工作 | 完成度 | 說明 | +|------|--------|------| +| owner request draft artifact | `100%` | 9 份 request draft、snapshot、文件與 guard 已固定 | +| request dispatch | `0%` | 尚未送件 | +| owner response received / accepted | `0%` | 尚未收到,尚未驗收 | +| live evidence collection | `0%` | 未 SSH、未讀 live host、未 active scan | +| restart / apply / repair-bot / Ansible gate | `0%` | 未授權且未執行 | +| runtime gate / production write | `0%` | 未授權且未執行 | diff --git a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md index d6c08947c..39e0acc75 100644 --- a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md +++ b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md @@ -45,6 +45,8 @@ 此更新仍不是 live host truth:110 / 188 live hash、restart window、rollback owner、post-check 指標與 owner response received / accepted 全部仍為 `0`,也不得執行 `docker compose`、`systemctl`、repair-bot、Ansible apply 或任何 SSH 讀寫。 +2026-06-14 已新增 `docs/security/HOST-SERVICE-OWNER-REQUEST-DRAFT.md` 與 `docs/security/host-service-owner-request-draft.snapshot.json`,將 9 個 Docker / systemd / host service surface 轉成人工送件前 owner request draft。固定 `drafts=9`、`write_capable=3`、`live_evidence_required=8`、`owner_fields=12`、`blocked_actions=14`,但 request sent、owner response received / accepted、live evidence、restart window、rollback owner、host write、runtime gate 仍全部為 `0 / false`。 + ### 0.3 2026-06-11 SSH / network access repo-only 清冊 `ssh_network_access_inventory_v1` 已把 SSH target、known_hosts workflow、CI deploy SSH、monitoring SSH、backup SSH capture、sudoers wrapper、NetworkPolicy、NodePort、WireGuard runbook 與 alert SSH action catalog 納入只讀 snapshot。清冊目前共有 `16` 個 surface、`11` 個 SSH source surface、`6` 個 write-capable surface、`2` 個 NetworkPolicy、`2` 個 NodePort、`1` 個 sudoers surface 與 `1` 個 WireGuard surface,讓 SSH / network 類別成熟度從 `48%` 推進到 `54%`。 @@ -82,7 +84,7 @@ | P1 | 高價值配置變更 Gate | 已有 C0-C3 清冊與 Hard Rule,但原本缺少可重跑 path 分類 | reviewer 只能靠人工記憶判斷 Nginx、workflow、secret、K8s、DNS、AI provider 是否需 owner gate | 已新增 `scripts/security/high-value-config-change-gate.py`;本階段只分類,不接 CI blocking | | P1 | DNS / TLS / certbot | 多產品共用 188 / 110 public gateway,憑證路徑與 renewal 仍分散在 runbook / template | 憑證過期、錯誤 cert path、ACME challenge 被覆蓋會造成公開服務中斷 | 納入 C0,需建立 domain / cert / renewal 清冊 | | P1 | workflow / runner / deploy key / secret name | 已有 Gitea / GitHub readiness 盤點,但尚未把配置變更和 IwoooS 高價值配置共用 gate 合併 | workflow 或 runner 改錯會直接影響部署與 secret 注入 | 納入 C0,維持只讀 owner response,不收 secret value | -| P1 | Docker Compose / systemd live config | 110 / 188 多服務由 compose、systemd 與 recovery scripts 管理 | restart policy、port、volume、env 改動會影響 Harbor、Sentry、Langfuse、Gitea、agent-bounty-protocol | 納入 C1,先做只讀 inventory | +| P1 | Docker Compose / systemd live config | 110 / 188 多服務由 compose、systemd 與 recovery scripts 管理 | restart policy、port、volume、env 改動會影響 Harbor、Sentry、Langfuse、Gitea、agent-bounty-protocol | 已有 repo-only inventory 與 owner request draft;下一步只收脫敏 owner response / live hash,不主動 SSH | | P1 | AI provider / Ollama proxy | 110 Nginx proxy、GCP-A/B、111 fallback、API provider route 多處配置 | provider route drift 會造成成本、可用性、資料外送與模型品質風險 | 納入 C1,任何切換仍需 dry-run / benchmark / owner gate | | P1 | agent-bounty-protocol runtime / treasury / A2A / MCP | 已納入只讀範圍,但尚未有 production host、compose、domain、TLS、rollback owner 完整資料 | 外部 agent、claim / submit、payout 或 webhook 若未控管,風險高於一般網站 | 納入 C2,仍不改該 repo、不讀 `.env`、不部署 | @@ -189,6 +191,7 @@ Nginx 是目前必須最先資安控管的配置,原因是它同時控制公 | Monitoring / alerting / observability 清冊 | `100%` | 已新增 `monitoring_alerting_observability_inventory_v1`,納入 60 個 repo-only surface;成熟度 `56% -> 62%` | | owner packet 前台只讀接入 | `100%` | `/zh-TW/iwooos` 已顯示高價值配置 owner packet 草案、C0/C1 packet 數、request / received / accepted 仍為 0 與禁止執行邊界 | | owner response request / received / accepted | `0%` | Packet 只是草案;尚未送件、尚未收件、尚未 reviewer accepted | +| Docker / systemd owner request draft | `100%` | 已將 9 個 host service surface 轉成 owner request draft;request sent / received / accepted 仍為 0 | | CI blocking / workflow gate | `0%` | 本階段刻意不修改 `.gitea/workflows`,避免初期資安流程摩擦過大 | | owner-provided live Nginx file compare | `70%` | 工具可吃 owner 匯出的 live conf 檔比較;本階段不主動 SSH 取得 | | live Nginx evidence collection | `0%` | 尚未 SSH / Ansible check-mode / live hash;需 owner 與維護窗口規則 | @@ -203,7 +206,7 @@ Nginx 是目前必須最先資安控管的配置,原因是它同時控制公 3. P0:補 DNS / TLS / certbot domain inventory,先只讀,不 renew、不 reload。 4. P0:把 workflow / runner / secret name owner response 與高價值配置 C0 gate 串成同一個 IwoooS 狀態。 5. P0:把 agent-bounty-protocol compose / MCP / A2A / treasury 高價值配置欄位接入同一個 owner packet queue;不啟用 runtime。 -6. P1:盤點 110 / 188 Docker Compose 與 systemd live config,標記 Harbor、Sentry、Langfuse、Gitea、agent-bounty-protocol 影響面。 +6. P1:向 owner 收 110 / 188 Docker Compose 與 systemd 脫敏 live hash、restart window、rollback owner 與 post-check 指標;不主動 SSH、不重啟、不跑 repair-bot。 7. P1:把 backup / restore / offsite / escrow owner response packet 接入 AwoooP 只讀狀態;驗收前 backup run、restore drill、offsite sync、remote delete、escrow marker write、retention change 全部維持 `0 / false`。 8. P1:把 Prometheus / Alertmanager / Grafana / SigNoz / Sentry / Langfuse owner response packet 接入 AwoooP 只讀狀態;驗收前 reload、receiver route change、silence change、Telegram send 與 alert chain smoke 全部維持 `0 / false`。 9. P1:補 Kali 112、111、168 維護窗口 owner 欄位,仍不做 upgrade / restart / scan。 diff --git a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md index f612c5773..73e932182 100644 --- a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md +++ b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md @@ -395,6 +395,7 @@ headline 要再往上,需要 S4.9 / S4.10 / S4.11 / S4.12 任一 owner respons | DNS / TLS / certbot Owner Confirmation Request | 本地完成 | `domain-tls-certbot-owner-confirmation-request.snapshot.json` 已固定 `owner_confirmation_request_count=4`、`c0_owner_confirmation_request_count=4`、`required_owner_field_count=9`、`confirmation_question_count=5`、`rejection_guard_count=12`、`owner_response_received_count=0`、`owner_response_accepted_count=0`、`runtime_gate_count=0`,並由 `security-mirror-progress-guard.py` 鎖住 request ids、confirmation questions、rejection guards 與 false flags | 這是 SAN / wildcard / 共用憑證覆蓋關係的 owner confirmation request 草稿,不是 request sent、recipient confirmed、owner response received / accepted、DNS query、TLS probe、certbot renew、Nginx reload、route smoke、host write、production write 或 runtime gate;不需要 production browser smoke | | K8s / ArgoCD manifest repo-only 清冊 | 本地完成 | `k8s-argocd-manifest-inventory.snapshot.json` 已固定 `file_count=49`、`c0_file_count=36`、`yaml_manifest_file_count=45`、`unique_kind_count=20`、`top_level_kind_marker_count=56`、`required_owner_field_count=11`、`evidence_gap_count=8`、`blocked_action_count=13`,並由 `security-mirror-progress-guard.py` 鎖住 group ids、kind counts、blocked actions 與 false flags | 這是 repo-only manifest inventory,不是 live cluster read、ArgoCD API read、ArgoCD sync、kubectl apply / patch / delete、Helm upgrade、secret collection、manual pod restart、scale workload、RBAC / NetworkPolicy change、restore backup、production write 或 runtime gate;不需要 production browser smoke | | K8s / ArgoCD Owner Request Draft | 已推送 / 已同步 | `e8de19d7` 已進 `gitea/main`;`k8s-argocd-owner-request-draft.snapshot.json` 已固定 `request_draft_count=4`、`c0_request_draft_count=3`、`request_field_count=20`、`required_owner_field_count=11`、`evidence_gap_count=8`、`blocked_action_count=13`、`request_sent_count=0`、`owner_response_received_count=0`、`runtime_gate_count=0`,並由 `security-mirror-progress-guard.py` 鎖住 request ids、blocked actions 與 false flags;AwoooP 平行 Session 已同步 | 這是人工送件前 request draft,不是 request sent、recipient confirmed、owner response received / accepted、rendered manifest diff、ArgoCD API read、ArgoCD sync、kubectl action、live cluster read、secret collection、production write 或 runtime gate;不需要 production browser smoke | +| Docker / systemd / Host Service Owner Request Draft | 本地完成 | `host-service-owner-request-draft.snapshot.json` 已固定 `request_draft_count=9`、`write_capable_request_draft_count=3`、`live_evidence_required_request_count=8`、`request_field_count=22`、`required_owner_field_count=12`、`blocked_action_count=14`、`request_sent_count=0`、`owner_response_received_count=0`、`runtime_gate_count=0`,並由 `security-mirror-progress-guard.py` 鎖住 request ids、blocked actions 與 false flags | 這是 Docker Compose、systemd / repair-bot、Ansible role 與 host config backup 的人工送件前 request draft,不是 request sent、owner response received / accepted、live host read、Docker Compose action、systemctl action、repair-bot execution、Ansible apply、secret collection、host write、production write 或 runtime gate;不需要 production browser smoke | | S3 approval gate | 進行中 | `security_approval_gate_v1` 已建立 8 個人工 gate items:7 pending、1 block candidate、0 approved | 不得繞過人工批准;批准後仍需 follow-up runtime gate | | S3.0 人工批准 Gate 契約 | 完成草案 | 定義批准範圍、決策選項、required reviewers、still forbidden 與 follow-up runtime gate | AwoooP 可記錄決策,不可執行 gate item | | S3.1 人工決策紀錄契約 | 完成草案 | `security_approval_decision_record_v1` 已建立;目前 0 筆 decision records、0 個 runtime action 授權 | AwoooP 可稽核決策,不可把決策當執行 | diff --git a/docs/security/host-service-owner-request-draft.snapshot.json b/docs/security/host-service-owner-request-draft.snapshot.json new file mode 100644 index 000000000..90ce23328 --- /dev/null +++ b/docs/security/host-service-owner-request-draft.snapshot.json @@ -0,0 +1,1078 @@ +{ + "blocked_actions": [ + "ssh_read", + "ssh_write", + "docker_compose_up", + "docker_compose_down", + "systemctl_restart", + "systemctl_reload", + "repair_bot_execute", + "ansible_apply", + "sudo_action", + "host_file_write", + "firewall_change", + "secret_value_collection", + "active_scan", + "runtime_gate_open" + ], + "execution_boundaries": { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "ansible_apply_authorized": false, + "docker_compose_action_authorized": false, + "host_write_authorized": false, + "live_evidence_received": false, + "live_host_read_authorized": false, + "not_authorization": true, + "owner_response_accepted": false, + "owner_response_received": false, + "recipient_confirmed": false, + "repair_bot_execution_authorized": false, + "request_sent": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "systemctl_action_authorized": false + }, + "generated_at": "2026-06-14T22:20:00+08:00", + "git_commit": "2dc8c19f", + "next_steps": [ + "人工送件前確認每個 host scope 的 owner role / team 與回覆窗口。", + "owner 只能提供脫敏 live hash、config source ref、maintenance window、rollback owner 與 post-check plan。", + "收到回覆後先做欄位完整性、敏感 payload 隔離與 restart / rollback gate 檢查,不得直接重啟或 apply。" + ], + "request_drafts": [ + { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "affected_scope": "pending_affected_scope", + "ansible_apply_authorized": false, + "blocked_actions": [ + "ssh_read", + "ssh_write", + "docker_compose_up", + "docker_compose_down", + "systemctl_restart", + "systemctl_reload", + "repair_bot_execute", + "ansible_apply", + "sudo_action", + "host_file_write", + "firewall_change", + "secret_value_collection", + "active_scan", + "runtime_gate_open" + ], + "config_kind": "docker_compose_source", + "control_tier": "C1", + "decision": "pending_owner_decision", + "decision_reason": "pending_decision_reason", + "disable_switch": "pending_disable_switch", + "docker_compose_action_authorized": false, + "expected_host_scope": "local_dev_only", + "followup_owner": "pending_followup_owner", + "host_write_authorized": false, + "label": "AWOOOI local development compose", + "live_config_hash_ref": null, + "live_evidence_received": false, + "maintenance_window": "pending_maintenance_window", + "not_approval": true, + "owner_response_accepted": false, + "owner_response_received": false, + "owner_role_or_team": "pending_owner_role_or_team", + "post_check_plan": "pending_post_check_plan", + "post_check_plan_accepted": false, + "recipient_confirmed": false, + "redacted_evidence_refs": [], + "repair_bot_execution_authorized": false, + "repo_sha256": "4a27bcde139b5aef6a9f3080187af5bec73d1efd9c09ed2752b0baaa5f507024", + "repo_source_path": "docker-compose.yml", + "request_fields": [ + "request_id", + "surface_id", + "label", + "expected_host_scope", + "config_kind", + "service_scope", + "control_tier", + "repo_source_path", + "repo_sha256", + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner", + "not_approval" + ], + "request_id": "host_service_owner_request:local_dev_compose", + "request_sent": false, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner" + ], + "requires_live_evidence": false, + "restart_window": "pending_restart_window", + "restart_window_accepted": false, + "rollback_owner": "pending_rollback_owner", + "rollback_owner_accepted": false, + "runtime_gate": false, + "secret_value_collection_allowed": false, + "service_scope": [ + "web", + "api", + "postgres", + "redis" + ], + "source_inventory_ref": "docs/security/host-service-config-inventory.snapshot.json", + "source_line_count": 137, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "status": "draft_not_dispatched", + "surface_id": "local_dev_compose", + "systemctl_action_authorized": false, + "write_capable_surface": false + }, + { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "affected_scope": "pending_affected_scope", + "ansible_apply_authorized": false, + "blocked_actions": [ + "ssh_read", + "ssh_write", + "docker_compose_up", + "docker_compose_down", + "systemctl_restart", + "systemctl_reload", + "repair_bot_execute", + "ansible_apply", + "sudo_action", + "host_file_write", + "firewall_change", + "secret_value_collection", + "active_scan", + "runtime_gate_open" + ], + "config_kind": "docker_compose_source", + "control_tier": "C1", + "decision": "pending_owner_decision", + "decision_reason": "pending_decision_reason", + "disable_switch": "pending_disable_switch", + "docker_compose_action_authorized": false, + "expected_host_scope": "192.168.0.110", + "followup_owner": "pending_followup_owner", + "host_write_authorized": false, + "label": "110 monitoring docker compose", + "live_config_hash_ref": null, + "live_evidence_received": false, + "maintenance_window": "pending_maintenance_window", + "not_approval": true, + "owner_response_accepted": false, + "owner_response_received": false, + "owner_role_or_team": "pending_owner_role_or_team", + "post_check_plan": "pending_post_check_plan", + "post_check_plan_accepted": false, + "recipient_confirmed": false, + "redacted_evidence_refs": [], + "repair_bot_execution_authorized": false, + "repo_sha256": "00126e9a5cb7a3cf2bf02cfddefea11f05849b46835a4e602eac4777fcb25281", + "repo_source_path": "k8s/monitoring/docker-compose-110.yml", + "request_fields": [ + "request_id", + "surface_id", + "label", + "expected_host_scope", + "config_kind", + "service_scope", + "control_tier", + "repo_source_path", + "repo_sha256", + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner", + "not_approval" + ], + "request_id": "host_service_owner_request:monitoring_110_compose", + "request_sent": false, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner" + ], + "requires_live_evidence": true, + "restart_window": "pending_restart_window", + "restart_window_accepted": false, + "rollback_owner": "pending_rollback_owner", + "rollback_owner_accepted": false, + "runtime_gate": false, + "secret_value_collection_allowed": false, + "service_scope": [ + "cadvisor", + "prometheus", + "grafana", + "blackbox-exporter", + "alertmanager", + "github-exporter" + ], + "source_inventory_ref": "docs/security/host-service-config-inventory.snapshot.json", + "source_line_count": 148, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "status": "draft_not_dispatched", + "surface_id": "monitoring_110_compose", + "systemctl_action_authorized": false, + "write_capable_surface": false + }, + { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "affected_scope": "pending_affected_scope", + "ansible_apply_authorized": false, + "blocked_actions": [ + "ssh_read", + "ssh_write", + "docker_compose_up", + "docker_compose_down", + "systemctl_restart", + "systemctl_reload", + "repair_bot_execute", + "ansible_apply", + "sudo_action", + "host_file_write", + "firewall_change", + "secret_value_collection", + "active_scan", + "runtime_gate_open" + ], + "config_kind": "docker_compose_source", + "control_tier": "C1", + "decision": "pending_owner_decision", + "decision_reason": "pending_decision_reason", + "disable_switch": "pending_disable_switch", + "docker_compose_action_authorized": false, + "expected_host_scope": "192.168.0.188", + "followup_owner": "pending_followup_owner", + "host_write_authorized": false, + "label": "188 database exporters compose", + "live_config_hash_ref": null, + "live_evidence_received": false, + "maintenance_window": "pending_maintenance_window", + "not_approval": true, + "owner_response_accepted": false, + "owner_response_received": false, + "owner_role_or_team": "pending_owner_role_or_team", + "post_check_plan": "pending_post_check_plan", + "post_check_plan_accepted": false, + "recipient_confirmed": false, + "redacted_evidence_refs": [], + "repair_bot_execution_authorized": false, + "repo_sha256": "3ffb3bd2e98091d18e60b74721904777c27f279c37ab6e873b82e6ef73eb87d4", + "repo_source_path": "ops/monitoring/docker-compose.exporters.yaml", + "request_fields": [ + "request_id", + "surface_id", + "label", + "expected_host_scope", + "config_kind", + "service_scope", + "control_tier", + "repo_source_path", + "repo_sha256", + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner", + "not_approval" + ], + "request_id": "host_service_owner_request:monitoring_exporters_188_compose", + "request_sent": false, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner" + ], + "requires_live_evidence": true, + "restart_window": "pending_restart_window", + "restart_window_accepted": false, + "rollback_owner": "pending_rollback_owner", + "rollback_owner_accepted": false, + "runtime_gate": false, + "secret_value_collection_allowed": false, + "service_scope": [ + "postgres-exporter", + "redis-exporter" + ], + "source_inventory_ref": "docs/security/host-service-config-inventory.snapshot.json", + "source_line_count": 69, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "status": "draft_not_dispatched", + "surface_id": "monitoring_exporters_188_compose", + "systemctl_action_authorized": false, + "write_capable_surface": false + }, + { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "affected_scope": "pending_affected_scope", + "ansible_apply_authorized": false, + "blocked_actions": [ + "ssh_read", + "ssh_write", + "docker_compose_up", + "docker_compose_down", + "systemctl_restart", + "systemctl_reload", + "repair_bot_execute", + "ansible_apply", + "sudo_action", + "host_file_write", + "firewall_change", + "secret_value_collection", + "active_scan", + "runtime_gate_open" + ], + "config_kind": "docker_compose_reference", + "control_tier": "C1", + "decision": "pending_owner_decision", + "decision_reason": "pending_decision_reason", + "disable_switch": "pending_disable_switch", + "docker_compose_action_authorized": false, + "expected_host_scope": "192.168.0.110", + "followup_owner": "pending_followup_owner", + "host_write_authorized": false, + "label": "110 Sentry self-hosted reference compose", + "live_config_hash_ref": null, + "live_evidence_received": false, + "maintenance_window": "pending_maintenance_window", + "not_approval": true, + "owner_response_accepted": false, + "owner_response_received": false, + "owner_role_or_team": "pending_owner_role_or_team", + "post_check_plan": "pending_post_check_plan", + "post_check_plan_accepted": false, + "recipient_confirmed": false, + "redacted_evidence_refs": [], + "repair_bot_execution_authorized": false, + "repo_sha256": "bba852dc0d73934998fa375130168615f9ac7611ce3f3efaa901e3b7e222eae3", + "repo_source_path": "ops/sentry-self-hosted/docker-compose.yml", + "request_fields": [ + "request_id", + "surface_id", + "label", + "expected_host_scope", + "config_kind", + "service_scope", + "control_tier", + "repo_source_path", + "repo_sha256", + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner", + "not_approval" + ], + "request_id": "host_service_owner_request:sentry_110_reference_compose", + "request_sent": false, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner" + ], + "requires_live_evidence": true, + "restart_window": "pending_restart_window", + "restart_window_accepted": false, + "rollback_owner": "pending_rollback_owner", + "rollback_owner_accepted": false, + "runtime_gate": false, + "secret_value_collection_allowed": false, + "service_scope": [ + "sentry-placeholder-reference" + ], + "source_inventory_ref": "docs/security/host-service-config-inventory.snapshot.json", + "source_line_count": 49, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "status": "draft_not_dispatched", + "surface_id": "sentry_110_reference_compose", + "systemctl_action_authorized": false, + "write_capable_surface": false + }, + { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "affected_scope": "pending_affected_scope", + "ansible_apply_authorized": false, + "blocked_actions": [ + "ssh_read", + "ssh_write", + "docker_compose_up", + "docker_compose_down", + "systemctl_restart", + "systemctl_reload", + "repair_bot_execute", + "ansible_apply", + "sudo_action", + "host_file_write", + "firewall_change", + "secret_value_collection", + "active_scan", + "runtime_gate_open" + ], + "config_kind": "docker_compose_source", + "control_tier": "C1", + "decision": "pending_owner_decision", + "decision_reason": "pending_decision_reason", + "disable_switch": "pending_disable_switch", + "docker_compose_action_authorized": false, + "expected_host_scope": "192.168.0.110", + "followup_owner": "pending_followup_owner", + "host_write_authorized": false, + "label": "110 Langfuse compose", + "live_config_hash_ref": null, + "live_evidence_received": false, + "maintenance_window": "pending_maintenance_window", + "not_approval": true, + "owner_response_accepted": false, + "owner_response_received": false, + "owner_role_or_team": "pending_owner_role_or_team", + "post_check_plan": "pending_post_check_plan", + "post_check_plan_accepted": false, + "recipient_confirmed": false, + "redacted_evidence_refs": [], + "repair_bot_execution_authorized": false, + "repo_sha256": "6c703a27525e62ef4d4d3c4cba8a89d64f646b01020782e35d22a3bf73f2dc83", + "repo_source_path": "infra/langfuse/docker-compose.yml", + "request_fields": [ + "request_id", + "surface_id", + "label", + "expected_host_scope", + "config_kind", + "service_scope", + "control_tier", + "repo_source_path", + "repo_sha256", + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner", + "not_approval" + ], + "request_id": "host_service_owner_request:langfuse_110_compose", + "request_sent": false, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner" + ], + "requires_live_evidence": true, + "restart_window": "pending_restart_window", + "restart_window_accepted": false, + "rollback_owner": "pending_rollback_owner", + "rollback_owner_accepted": false, + "runtime_gate": false, + "secret_value_collection_allowed": false, + "service_scope": [ + "langfuse", + "langfuse-db" + ], + "source_inventory_ref": "docs/security/host-service-config-inventory.snapshot.json", + "source_line_count": 71, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "status": "draft_not_dispatched", + "surface_id": "langfuse_110_compose", + "systemctl_action_authorized": false, + "write_capable_surface": false + }, + { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "affected_scope": "pending_affected_scope", + "ansible_apply_authorized": false, + "blocked_actions": [ + "ssh_read", + "ssh_write", + "docker_compose_up", + "docker_compose_down", + "systemctl_restart", + "systemctl_reload", + "repair_bot_execute", + "ansible_apply", + "sudo_action", + "host_file_write", + "firewall_change", + "secret_value_collection", + "active_scan", + "runtime_gate_open" + ], + "config_kind": "ansible_service_executor", + "control_tier": "C1", + "decision": "pending_owner_decision", + "decision_reason": "pending_decision_reason", + "disable_switch": "pending_disable_switch", + "docker_compose_action_authorized": false, + "expected_host_scope": "multi_host", + "followup_owner": "pending_followup_owner", + "host_write_authorized": false, + "label": "Ansible docker-compose-service role", + "live_config_hash_ref": null, + "live_evidence_received": false, + "maintenance_window": "pending_maintenance_window", + "not_approval": true, + "owner_response_accepted": false, + "owner_response_received": false, + "owner_role_or_team": "pending_owner_role_or_team", + "post_check_plan": "pending_post_check_plan", + "post_check_plan_accepted": false, + "recipient_confirmed": false, + "redacted_evidence_refs": [], + "repair_bot_execution_authorized": false, + "repo_sha256": "cee214a8651f46c2d8be05054dddadc243a26bff51a64bd9cf42dd2ec0b7b1b3", + "repo_source_path": "infra/ansible/roles/docker-compose-service/tasks/main.yml", + "request_fields": [ + "request_id", + "surface_id", + "label", + "expected_host_scope", + "config_kind", + "service_scope", + "control_tier", + "repo_source_path", + "repo_sha256", + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner", + "not_approval" + ], + "request_id": "host_service_owner_request:ansible_docker_compose_service_role", + "request_sent": false, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner" + ], + "requires_live_evidence": true, + "restart_window": "pending_restart_window", + "restart_window_accepted": false, + "rollback_owner": "pending_rollback_owner", + "rollback_owner_accepted": false, + "runtime_gate": false, + "secret_value_collection_allowed": false, + "service_scope": [ + "docker compose up -d" + ], + "source_inventory_ref": "docs/security/host-service-config-inventory.snapshot.json", + "source_line_count": 18, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "status": "draft_not_dispatched", + "surface_id": "ansible_docker_compose_service_role", + "systemctl_action_authorized": false, + "write_capable_surface": true + }, + { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "affected_scope": "pending_affected_scope", + "ansible_apply_authorized": false, + "blocked_actions": [ + "ssh_read", + "ssh_write", + "docker_compose_up", + "docker_compose_down", + "systemctl_restart", + "systemctl_reload", + "repair_bot_execute", + "ansible_apply", + "sudo_action", + "host_file_write", + "firewall_change", + "secret_value_collection", + "active_scan", + "runtime_gate_open" + ], + "config_kind": "host_repair_whitelist", + "control_tier": "C1", + "decision": "pending_owner_decision", + "decision_reason": "pending_decision_reason", + "disable_switch": "pending_disable_switch", + "docker_compose_action_authorized": false, + "expected_host_scope": "192.168.0.110", + "followup_owner": "pending_followup_owner", + "host_write_authorized": false, + "label": "110 repair-bot compose whitelist", + "live_config_hash_ref": null, + "live_evidence_received": false, + "maintenance_window": "pending_maintenance_window", + "not_approval": true, + "owner_response_accepted": false, + "owner_response_received": false, + "owner_role_or_team": "pending_owner_role_or_team", + "post_check_plan": "pending_post_check_plan", + "post_check_plan_accepted": false, + "recipient_confirmed": false, + "redacted_evidence_refs": [], + "repair_bot_execution_authorized": false, + "repo_sha256": "093d4f85c398806dee62c2831fa4fe7e1f8fddca6e3cfcc9dbe4d5e0d66cdf3b", + "repo_source_path": "scripts/repair-bot/repair-bot-110.sh", + "request_fields": [ + "request_id", + "surface_id", + "label", + "expected_host_scope", + "config_kind", + "service_scope", + "control_tier", + "repo_source_path", + "repo_sha256", + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner", + "not_approval" + ], + "request_id": "host_service_owner_request:repair_bot_110_whitelist", + "request_sent": false, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner" + ], + "requires_live_evidence": true, + "restart_window": "pending_restart_window", + "restart_window_accepted": false, + "rollback_owner": "pending_rollback_owner", + "rollback_owner_accepted": false, + "runtime_gate": false, + "secret_value_collection_allowed": false, + "service_scope": [ + "sentry", + "harbor", + "gitea", + "gitea-runner", + "langfuse", + "alertmanager", + "signoz" + ], + "source_inventory_ref": "docs/security/host-service-config-inventory.snapshot.json", + "source_line_count": 67, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "status": "draft_not_dispatched", + "surface_id": "repair_bot_110_whitelist", + "systemctl_action_authorized": false, + "write_capable_surface": true + }, + { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "affected_scope": "pending_affected_scope", + "ansible_apply_authorized": false, + "blocked_actions": [ + "ssh_read", + "ssh_write", + "docker_compose_up", + "docker_compose_down", + "systemctl_restart", + "systemctl_reload", + "repair_bot_execute", + "ansible_apply", + "sudo_action", + "host_file_write", + "firewall_change", + "secret_value_collection", + "active_scan", + "runtime_gate_open" + ], + "config_kind": "host_repair_whitelist", + "control_tier": "C1", + "decision": "pending_owner_decision", + "decision_reason": "pending_decision_reason", + "disable_switch": "pending_disable_switch", + "docker_compose_action_authorized": false, + "expected_host_scope": "192.168.0.188", + "followup_owner": "pending_followup_owner", + "host_write_authorized": false, + "label": "188 repair-bot compose/systemd whitelist", + "live_config_hash_ref": null, + "live_evidence_received": false, + "maintenance_window": "pending_maintenance_window", + "not_approval": true, + "owner_response_accepted": false, + "owner_response_received": false, + "owner_role_or_team": "pending_owner_role_or_team", + "post_check_plan": "pending_post_check_plan", + "post_check_plan_accepted": false, + "recipient_confirmed": false, + "redacted_evidence_refs": [], + "repair_bot_execution_authorized": false, + "repo_sha256": "fb2eb786d04edbf5d5be581a53bbe188ac66f0895aa016328b031c72f6182918", + "repo_source_path": "scripts/repair-bot/repair-bot-188.sh", + "request_fields": [ + "request_id", + "surface_id", + "label", + "expected_host_scope", + "config_kind", + "service_scope", + "control_tier", + "repo_source_path", + "repo_sha256", + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner", + "not_approval" + ], + "request_id": "host_service_owner_request:repair_bot_188_whitelist", + "request_sent": false, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner" + ], + "requires_live_evidence": true, + "restart_window": "pending_restart_window", + "restart_window_accepted": false, + "rollback_owner": "pending_rollback_owner", + "rollback_owner_accepted": false, + "runtime_gate": false, + "secret_value_collection_allowed": false, + "service_scope": [ + "openclaw", + "minio", + "signoz", + "redis", + "nginx", + "ollama" + ], + "source_inventory_ref": "docs/security/host-service-config-inventory.snapshot.json", + "source_line_count": 85, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "status": "draft_not_dispatched", + "surface_id": "repair_bot_188_whitelist", + "systemctl_action_authorized": false, + "write_capable_surface": true + }, + { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "affected_scope": "pending_affected_scope", + "ansible_apply_authorized": false, + "blocked_actions": [ + "ssh_read", + "ssh_write", + "docker_compose_up", + "docker_compose_down", + "systemctl_restart", + "systemctl_reload", + "repair_bot_execute", + "ansible_apply", + "sudo_action", + "host_file_write", + "firewall_change", + "secret_value_collection", + "active_scan", + "runtime_gate_open" + ], + "config_kind": "backup_capture_contract", + "control_tier": "C1", + "decision": "pending_owner_decision", + "decision_reason": "pending_decision_reason", + "disable_switch": "pending_disable_switch", + "docker_compose_action_authorized": false, + "expected_host_scope": "110_188_120_121_cluster", + "followup_owner": "pending_followup_owner", + "host_write_authorized": false, + "label": "host config backup capture contract", + "live_config_hash_ref": null, + "live_evidence_received": false, + "maintenance_window": "pending_maintenance_window", + "not_approval": true, + "owner_response_accepted": false, + "owner_response_received": false, + "owner_role_or_team": "pending_owner_role_or_team", + "post_check_plan": "pending_post_check_plan", + "post_check_plan_accepted": false, + "recipient_confirmed": false, + "redacted_evidence_refs": [], + "repair_bot_execution_authorized": false, + "repo_sha256": "d24301cff44e464bd19ce0792362be16916ccde8c92f92351a19ef4ee988f15e", + "repo_source_path": "scripts/backup/backup-configs.sh", + "request_fields": [ + "request_id", + "surface_id", + "label", + "expected_host_scope", + "config_kind", + "service_scope", + "control_tier", + "repo_source_path", + "repo_sha256", + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner", + "not_approval" + ], + "request_id": "host_service_owner_request:config_backup_host_capture", + "request_sent": false, + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner" + ], + "requires_live_evidence": true, + "restart_window": "pending_restart_window", + "restart_window_accepted": false, + "rollback_owner": "pending_rollback_owner", + "rollback_owner_accepted": false, + "runtime_gate": false, + "secret_value_collection_allowed": false, + "service_scope": [ + "systemd", + "docker", + "nginx", + "cron", + "k8s", + "host-configs" + ], + "source_inventory_ref": "docs/security/host-service-config-inventory.snapshot.json", + "source_line_count": 359, + "ssh_read_authorized": false, + "ssh_write_authorized": false, + "status": "draft_not_dispatched", + "surface_id": "config_backup_host_capture", + "systemctl_action_authorized": false, + "write_capable_surface": false + } + ], + "request_fields": [ + "request_id", + "surface_id", + "label", + "expected_host_scope", + "config_kind", + "service_scope", + "control_tier", + "repo_source_path", + "repo_sha256", + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner", + "not_approval" + ], + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner" + ], + "schema_version": "host_service_owner_request_draft_v1", + "source_inventory_schema_version": "host_service_config_inventory_v1", + "source_inventory_status": "repo_only_inventory_ready", + "status": "owner_request_draft_ready_not_dispatched", + "summary": { + "action_button_count": 0, + "active_scan_authorized_count": 0, + "ansible_apply_authorized_count": 0, + "blocked_action_count": 14, + "docker_compose_action_authorized_count": 0, + "host_write_authorized_count": 0, + "live_evidence_received_count": 0, + "live_evidence_required_request_count": 8, + "owner_response_accepted_count": 0, + "owner_response_received_count": 0, + "post_check_plan_accepted_count": 0, + "recipient_confirmed_count": 0, + "repair_bot_execution_authorized_count": 0, + "request_draft_count": 9, + "request_field_count": 22, + "request_sent_count": 0, + "required_owner_field_count": 12, + "restart_window_accepted_count": 0, + "rollback_owner_accepted_count": 0, + "runtime_gate_count": 0, + "secret_value_collection_allowed_count": 0, + "ssh_read_authorized_count": 0, + "ssh_write_authorized_count": 0, + "systemctl_action_authorized_count": 0, + "write_capable_request_draft_count": 3 + } +} diff --git a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md index d051927c4..db061a12c 100644 --- a/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md +++ b/docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md @@ -695,6 +695,7 @@ Alert / Sentry / SigNoz / Gitea / Market Watch / Operator | `docs/security/domain-tls-certbot-owner-confirmation-request.snapshot.json` + `scripts/security/domain-tls-certbot-owner-confirmation-request.py` | DNS / TLS / certbot Owner Confirmation Request 已本地完成;承接 domain / TLS / certbot repo-only 清冊,固定 4 份 owner confirmation request、4 份 C0、9 欄 required owner fields、16 欄 request fields、5 題 confirmation questions 與 12 類 rejection guard;request sent、recipient confirmed、owner response received / accepted、DNS query、TLS probe、certbot renew、Nginx reload、runtime gate、action buttons 仍全部為 `0` | 這是 SAN / wildcard / 共用憑證覆蓋關係的 owner confirmation request 草稿,不是 request sent、owner response received / accepted、DNS query、live TLS probe、certbot renew、Nginx reload、route smoke、host write、production write 或 runtime gate | | `docs/security/k8s-argocd-manifest-inventory.snapshot.json` + `scripts/security/k8s-argocd-manifest-inventory.py` | K8s / ArgoCD manifest repo-only 清冊已本地完成;掃描 `k8s/awoooi-prod`、`k8s/argocd`、`k8s/velero`、`k8s/monitoring`,固定 4 個 scan groups、49 個 files、36 個 C0 files、45 個 YAML manifests、20 個 unique top-level kind markers、11 欄 required owner fields、8 個 evidence gaps 與 13 類 blocked action;owner response、rendered manifest diff、ArgoCD health readback、ArgoCD sync、kubectl action、live cluster read、runtime gate、action buttons 仍全部為 `0` | 這是 repo-only manifest inventory,不是 live cluster read、ArgoCD API read、ArgoCD sync、kubectl apply / patch / delete、Helm upgrade、secret collection、manual pod restart、scale workload、RBAC / NetworkPolicy change、restore backup、production write 或 runtime gate | | `docs/security/k8s-argocd-owner-request-draft.snapshot.json` + `scripts/security/k8s-argocd-owner-request-draft.py` | K8s / ArgoCD Owner Request Draft 已本地完成;承接 manifest repo-only 清冊,固定 4 份 request draft、3 份 C0、1 份 C1、20 欄 request fields、11 欄 required owner fields、8 個 evidence gaps 與 13 類 blocked action;request sent、recipient confirmed、owner response received / accepted、rendered manifest diff、ArgoCD health readback、ArgoCD sync、kubectl action、live cluster read、runtime gate、action buttons 仍全部為 `0` | 這是人工送件前 request draft,不是 request sent、owner response received / accepted、ArgoCD API read、ArgoCD sync、kubectl action、live cluster read、secret collection、production write 或 runtime gate | +| `docs/security/host-service-owner-request-draft.snapshot.json` + `scripts/security/host-service-owner-request-draft.py` | Docker / systemd / Host Service Owner Request Draft 已本地完成;承接 host-service repo-only 清冊,固定 9 份 request draft、3 份 write-capable request draft、8 份需 live evidence request、22 欄 request fields、12 欄 required owner fields 與 14 類 blocked action;request sent、recipient confirmed、owner response received / accepted、live evidence、restart window、rollback owner、host write、runtime gate、action buttons 仍全部為 `0` | 這是人工送件前 request draft,不是 request sent、owner response received / accepted、live host read、Docker Compose action、systemctl action、repair-bot execution、Ansible apply、secret collection、host write、production write 或 runtime gate | | `docs/evaluations/ai_agent_live_read_model_gate_2026-06-11.json` + `GET /api/v1/agents/agent-live-read-model-gate` | P2-403B AgentSession / Redis Streams live read model gate;定義 safe fields、Redis envelope、worker gate、rollback plan 與 no-write smoke,不連 DB、不讀寫 Redis、不啟動 worker | #### 3.2.1c 2026-06-11 AI Agent 主動營運委派與版本生命週期契約 @@ -841,6 +842,7 @@ Repo / registry / release notes / K8s / host / observability / backup evidence 76. 建立 DNS / TLS / certbot Owner Confirmation Request。✅ 本地完成;新增 `domain-tls-certbot-owner-confirmation-request.py`、snapshot 與說明文件,將 4 個 certificate path 需確認的 domain 轉成 `owner_confirmation_request_count=4`、`c0_owner_confirmation_request_count=4`、`required_owner_field_count=9`、`confirmation_question_count=5`、`rejection_guard_count=12` 的 owner confirmation request 草稿;request sent、recipient confirmed、owner response received / accepted、DNS query、TLS probe、certbot renew、Nginx reload、host write、runtime gate、action buttons 仍全部為 `0`。這不是 request sent、owner response received / accepted、DNS query、live TLS probe、certbot renew、Nginx reload、route smoke、production write 或 runtime gate。 77. 建立 K8s / ArgoCD manifest repo-only 清冊。✅ 本地完成;新增 `k8s-argocd-manifest-inventory.py`、snapshot 與說明文件,將 `k8s/awoooi-prod`、`k8s/argocd`、`k8s/velero`、`k8s/monitoring` 轉成 `file_count=49`、`c0_file_count=36`、`yaml_manifest_file_count=45`、`unique_kind_count=20`、`top_level_kind_marker_count=56`、`blocked_action_count=13` 的 repo-only manifest inventory;owner response、rendered manifest diff、ArgoCD health readback、ArgoCD sync、kubectl action、live cluster read、secret collection、runtime gate、action buttons 仍全部為 `0`。這不是 live cluster read、ArgoCD API read、ArgoCD sync、kubectl action、Helm upgrade、secret collection、manual pod restart、scale workload、RBAC / NetworkPolicy change、restore backup、production write 或 runtime gate。 78. 建立 K8s / ArgoCD Owner Request Draft。✅ 本地完成;新增 `k8s-argocd-owner-request-draft.py`、snapshot 與說明文件,將四個 scan group 轉成 `request_draft_count=4`、`c0_request_draft_count=3`、`c1_request_draft_count=1`、`request_field_count=20`、`required_owner_field_count=11`、`evidence_gap_count=8`、`blocked_action_count=13` 的人工送件前草稿;request sent、recipient confirmed、owner response received / accepted、rendered manifest diff、ArgoCD health readback、ArgoCD sync、kubectl action、live cluster read、secret collection、runtime gate、action buttons 仍全部為 `0`。這不是 request sent、owner response received / accepted、ArgoCD API read、ArgoCD sync、kubectl action、live cluster read、secret collection、production write 或 runtime gate。 +79. 建立 Docker / systemd / Host Service Owner Request Draft。✅ 本地完成;新增 `host-service-owner-request-draft.py`、snapshot 與說明文件,將 9 個 host service surface 轉成 `request_draft_count=9`、`write_capable_request_draft_count=3`、`live_evidence_required_request_count=8`、`request_field_count=22`、`required_owner_field_count=12`、`blocked_action_count=14` 的人工送件前草稿;request sent、recipient confirmed、owner response received / accepted、live evidence、restart window、rollback owner、post-check、host write、Docker Compose action、systemctl action、repair-bot execution、Ansible apply、secret collection、runtime gate、action buttons 仍全部為 `0`。這不是 request sent、owner response received / accepted、live host read、Docker Compose action、systemctl action、repair-bot execution、Ansible apply、secret collection、host write、production write 或 runtime gate。 #### 3.2.1d 2026-06-11 Agent 互動、學習與成長證據面 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 9ed90a7db..e85544b65 100644 --- a/docs/workplans/2026-06-04-iwooos-security-governance-p0.md +++ b/docs/workplans/2026-06-04-iwooos-security-governance-p0.md @@ -22,6 +22,7 @@ | 最新 P2-D2 AwoooP Runs fallback 文案基準 | code `7f6028c3`、deploy marker `bf016e91`、CD `2590`、code-review `2591`;Runs / Callback / Source Flow fallback 文案已正式驗證 | | 最新 AwoooP Tenants 全域產品資產台帳 D1 基準 | code `fef94df8`、deploy marker `180a6543`、code-review `2967`、CD `2966`;正式 API 顯示產品 / 專案 `16`、網站 / 服務入口 `31`、source-control candidate repo `10`,已納入 `2026fifa.wooo.work`、WOOO Open Design、n8n、Grist、Vault、Ollama、Monitor 與 AWOOOI API / AIOps 服務入口;owner response / runtime gate / action button 仍 `0` | | 最新 P0 Public Gateway / DNS TLS / K8s 配置控管基準 | P0-15 `5068654d` live conf 匯出請求包、P0-16 `f856df1c` redacted export 收件預檢、P0-17 `762f73a6` rendered diff / nginx gate 草稿、P0-19 `551d8144` DNS / TLS / certbot owner confirmation request 草稿、P0-20 `e8876c45` K8s / ArgoCD manifest repo-only 清冊、P0-21 `e8de19d7` K8s / ArgoCD owner request draft;Public Gateway 三段固定 requests / candidates `3`、C0 `2`,DNS / TLS 固定 owner confirmation requests `4`、C0 `4`,K8s manifest 固定 files `49`、C0 `36`、YAML `45`、kinds `20`,K8s owner request 固定 drafts `4`、C0 `3`;request sent、owner response received / accepted、redacted export received / accepted、raw conf stored、rendered diff ready、`nginx -t`、reload、DNS query、TLS probe、certbot renew、ArgoCD sync、kubectl action、route smoke、runtime gate、action button 全部 `0` | +| 最新 P1 Docker / systemd / Host Service 配置控管基準 | repo-only inventory 已固定 `surface=9`、`write_capable=3`、`live_evidence_required=8`、成熟度 `42% -> 50%`;本輪新增 owner request draft,固定 `drafts=9`、`owner_fields=12`、`blocked_actions=14`、request sent / received / accepted / live evidence / restart window / rollback owner / host write / runtime gate 全部 `0` | | 最新 AI Agent automation P1-305 / P1-306 基準 | code `4f0787f8`、deploy marker `af3a9d48`、CD `2592`、code-review `2593`;任務批准邊界與進度彙總已正式驗證;backlog `70%`、done `16/23`、下一步 `P1-001` | | 最新 AI Agent automation P1-001 基準 | code `de3007b7`、stability fix `fd33591c`、deploy marker `8caba233`、LOGBOOK / marker 對齊 `37c0e171`;runtime surface `22`、Secret surface `4`、live gaps `6`、backlog `74%`、done `17/23`、下一步 `P1-002` | | 最新 AI Agent automation P1-002 基準 | code `943faaee`、stability fix `ff266926`、deploy marker `01b8712d`、LOGBOOK / marker 對齊 `70c01003`;Gitea workflows `9`、runner contracts `4`、需 runner attestation workflows `8`、backlog `78%`、done `18/23`、下一步 `P1-003` | @@ -224,6 +225,7 @@ S4.9 是目前 IwoooS 64% 能往前的第一優先 gate。驗收前所有 count | 優先 | 工作 | 細項 | 驗證 | |------|------|------|------| | P1 | GitHub primary readiness 只讀重盤 | repo visibility、refs、tags、workflow、secret name、runner、rollback ADR | 只讀 inventory;不建立 repo、不同步 refs | +| P1 | Docker / systemd / Host Service owner request draft | 9 個 repo-only surface 已轉成人工送件前草稿,涵蓋 110 / 188 Docker Compose、repair-bot、Ansible role 與 host config backup capture | request sent、owner response、live hash、restart、repair-bot、Ansible apply、host write、runtime gate 全部維持 0 / false | | P1 | Kali 112 維護窗口草案 | 1994 pending updates、`networking.service` failed、服務硬化 0/4、rollback、post-check | P1-7 草案已完成;不 `apt upgrade`、不 restart | | P1 | 111 / 168 主機 scope 補強 | P1-8 已補 `DEV-HOSTS-111-168-SCOPE-HANDOFF.md`、snapshot 與 schema;scope、maintenance window、credential handling、rollback owner、validation 指標已可交接 | observe-only;不 credentialed scan、不改 route / CORS / firewall / service | | P1 | VibeWork 納入 IwoooS | P1-9 已補 `VIBEWORK-IWOOOS-ONBOARDING-HANDOFF.md`、snapshot 與 schema;repo、product、surface、owner、evidence refs、資料分級、部署邊界與獨立產品邊界已可交接 | 繁中 docs/specs;不合併產品責任、不部署、不改 refs | @@ -294,6 +296,7 @@ P1 只讀重盤階段整體完成度:`70%`。它代表 freshness / inventory / | P1-7 | Kali 112 maintenance window 草案 | 已補 `KALI-112-MAINTENANCE-WINDOW-DRAFT.md`、snapshot 與 schema;packages、`networking.service` failed、hardening 0/4、rollback、post-check 已進 owner handoff | 文件草案,不執行 `apt upgrade` / restart / hardening / scan | | P1-8 | 111 / 168 開發主機 scope | 已補 `DEV-HOSTS-111-168-SCOPE-HANDOFF.md`、`dev-hosts-111-168-scope-handoff.snapshot.json`、`dev_host_scope_handoff_v1.schema.json`;111 fallback truth / model inventory / service posture 與 168 dev origin / repo hygiene / CORS / local exposure 已拆成 handoff | scope handoff `100%`;主機執行 `0%`;不 credentialed scan、不讀未授權資料、不改 fallback route、不改 CORS / firewall / service | | P1-9 | VibeWork 納入 IwoooS | 已補 `VIBEWORK-IWOOOS-ONBOARDING-HANDOFF.md`、`vibework-iwooos-onboarding-handoff.snapshot.json`、`vibework_iwooos_onboarding_handoff_v1.schema.json`;repo / product / surface / owner / evidence refs / 資料分級 / 部署邊界 / 獨立產品邊界已拆成 handoff | onboarding handoff `100%`;runtime / deploy / repo mutation `0 / false`;docs/specs 繁中,產品責任不合併 | +| P1-10 | Docker / systemd / Host Service owner request draft | 已新增 `HOST-SERVICE-OWNER-REQUEST-DRAFT.md`、`host-service-owner-request-draft.snapshot.json` 與產生器;9 個 surface、3 個 write-capable、8 個需 live evidence、12 個 owner 必填欄位、14 類 blocked action | 草稿 artifact `100%`;request sent / received / accepted、live evidence、restart window、rollback owner、host write、runtime gate 仍全部 `0 / false` | ## 7. 2026-06-04 本輪驗證紀錄 diff --git a/scripts/security/host-service-owner-request-draft.py b/scripts/security/host-service-owner-request-draft.py new file mode 100644 index 000000000..0858cc43f --- /dev/null +++ b/scripts/security/host-service-owner-request-draft.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +""" +IwoooS Docker / systemd / host service owner request draft 產生器。 + +本工具讀取 host-service repo-only 清冊,將 9 個 surface 轉成人工送件前 +request draft。它不 SSH、不讀 live host、不執行 docker compose、 +不執行 systemctl、不呼叫 repair-bot、不跑 Ansible、不收 secret value。 +""" + +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)) + +REQUEST_FIELDS = [ + "request_id", + "surface_id", + "label", + "expected_host_scope", + "config_kind", + "service_scope", + "control_tier", + "repo_source_path", + "repo_sha256", + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner", + "not_approval", +] + +REQUIRED_OWNER_FIELDS = [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "live_config_hash_ref", + "maintenance_window", + "restart_window", + "rollback_owner", + "post_check_plan", + "disable_switch", + "followup_owner", +] + +BLOCKED_ACTIONS = [ + "ssh_read", + "ssh_write", + "docker_compose_up", + "docker_compose_down", + "systemctl_restart", + "systemctl_reload", + "repair_bot_execute", + "ansible_apply", + "sudo_action", + "host_file_write", + "firewall_change", + "secret_value_collection", + "active_scan", + "runtime_gate_open", +] + + +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 request_for(surface: dict[str, Any]) -> dict[str, Any]: + surface_id = surface["surface_id"] + write_capable = surface["config_kind"] in {"host_repair_whitelist", "ansible_service_executor"} + return { + "request_id": f"host_service_owner_request:{surface_id}", + "status": "draft_not_dispatched", + "surface_id": surface_id, + "label": surface["label"], + "expected_host_scope": surface["expected_host_scope"], + "config_kind": surface["config_kind"], + "service_scope": surface["service_scope"], + "control_tier": surface["control_tier"], + "repo_source_path": surface["source_path"], + "repo_sha256": surface["sha256"], + "source_line_count": surface["line_count"], + "requires_live_evidence": surface["requires_live_evidence"], + "write_capable_surface": write_capable, + "source_inventory_ref": "docs/security/host-service-config-inventory.snapshot.json", + "request_fields": REQUEST_FIELDS, + "required_owner_fields": REQUIRED_OWNER_FIELDS, + "blocked_actions": BLOCKED_ACTIONS, + "owner_role_or_team": "pending_owner_role_or_team", + "decision": "pending_owner_decision", + "decision_reason": "pending_decision_reason", + "affected_scope": "pending_affected_scope", + "redacted_evidence_refs": [], + "live_config_hash_ref": None, + "maintenance_window": "pending_maintenance_window", + "restart_window": "pending_restart_window", + "rollback_owner": "pending_rollback_owner", + "post_check_plan": "pending_post_check_plan", + "disable_switch": "pending_disable_switch", + "followup_owner": "pending_followup_owner", + "not_approval": True, + "request_sent": False, + "recipient_confirmed": False, + "owner_response_received": False, + "owner_response_accepted": False, + "live_evidence_received": False, + "restart_window_accepted": False, + "rollback_owner_accepted": False, + "post_check_plan_accepted": False, + "host_write_authorized": False, + "ssh_read_authorized": False, + "ssh_write_authorized": False, + "docker_compose_action_authorized": False, + "systemctl_action_authorized": False, + "repair_bot_execution_authorized": False, + "ansible_apply_authorized": False, + "secret_value_collection_allowed": False, + "active_scan_authorized": False, + "runtime_gate": False, + "action_buttons_allowed": False, + } + + +def build_report(root: Path, inventory: dict[str, Any], generated_at: str | None) -> dict[str, Any]: + report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds") + requests = [request_for(surface) for surface in inventory["config_surfaces"]] + write_capable_requests = [item for item in requests if item["write_capable_surface"]] + live_evidence_requests = [item for item in requests if item["requires_live_evidence"]] + + return { + "schema_version": "host_service_owner_request_draft_v1", + "generated_at": report_time, + "git_commit": git_short_sha(root), + "source_inventory_schema_version": inventory.get("schema_version"), + "source_inventory_status": inventory.get("status"), + "status": "owner_request_draft_ready_not_dispatched", + "summary": { + "request_draft_count": len(requests), + "write_capable_request_draft_count": len(write_capable_requests), + "live_evidence_required_request_count": len(live_evidence_requests), + "request_field_count": len(REQUEST_FIELDS), + "required_owner_field_count": len(REQUIRED_OWNER_FIELDS), + "blocked_action_count": len(BLOCKED_ACTIONS), + "request_sent_count": 0, + "recipient_confirmed_count": 0, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "live_evidence_received_count": 0, + "restart_window_accepted_count": 0, + "rollback_owner_accepted_count": 0, + "post_check_plan_accepted_count": 0, + "host_write_authorized_count": 0, + "ssh_read_authorized_count": 0, + "ssh_write_authorized_count": 0, + "docker_compose_action_authorized_count": 0, + "systemctl_action_authorized_count": 0, + "repair_bot_execution_authorized_count": 0, + "ansible_apply_authorized_count": 0, + "secret_value_collection_allowed_count": 0, + "active_scan_authorized_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + }, + "execution_boundaries": { + "request_sent": False, + "recipient_confirmed": False, + "owner_response_received": False, + "owner_response_accepted": False, + "live_host_read_authorized": False, + "live_evidence_received": False, + "host_write_authorized": False, + "ssh_read_authorized": False, + "ssh_write_authorized": False, + "docker_compose_action_authorized": False, + "systemctl_action_authorized": False, + "repair_bot_execution_authorized": False, + "ansible_apply_authorized": False, + "secret_value_collection_allowed": False, + "active_scan_authorized": False, + "runtime_execution_authorized": False, + "action_buttons_allowed": False, + "not_authorization": True, + }, + "request_fields": REQUEST_FIELDS, + "required_owner_fields": REQUIRED_OWNER_FIELDS, + "blocked_actions": BLOCKED_ACTIONS, + "request_drafts": requests, + "next_steps": [ + "人工送件前確認每個 host scope 的 owner role / team 與回覆窗口。", + "owner 只能提供脫敏 live hash、config source ref、maintenance window、rollback owner 與 post-check plan。", + "收到回覆後先做欄位完整性、敏感 payload 隔離與 restart / rollback gate 檢查,不得直接重啟或 apply。", + ], + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS host service owner request draft 產生器") + parser.add_argument("--root", default=".", help="repo root") + parser.add_argument( + "--inventory-report", + default="docs/security/host-service-config-inventory.snapshot.json", + help="host-service-config-inventory.py 輸出的 JSON", + ) + parser.add_argument("--output", help="寫出 JSON 報告") + parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用") + args = parser.parse_args() + + root = Path(args.root).resolve() + inventory = load_json(root / args.inventory_report) + report = build_report(root, inventory, args.generated_at) + payload = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + + if args.output: + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(payload + "\n", encoding="utf-8") + else: + print(payload) + + summary = report["summary"] + print( + "HOST_SERVICE_OWNER_REQUEST_DRAFT_OK " + f"drafts={summary['request_draft_count']} " + f"write_capable={summary['write_capable_request_draft_count']} " + f"fields={summary['required_owner_field_count']} " + f"sent={summary['request_sent_count']} " + f"runtime_gate={summary['runtime_gate_count']}", + file=sys.stderr, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 2cbaea095..0c758a3f9 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -95,6 +95,9 @@ def validate(root: Path) -> None: security_dir / "high-value-config-owner-request-draft.snapshot.json" ) host_service_config_inventory = load_json(security_dir / "host-service-config-inventory.snapshot.json") + host_service_owner_request_draft = load_json( + security_dir / "host-service-owner-request-draft.snapshot.json" + ) ssh_network_access_inventory = load_json(security_dir / "ssh-network-access-inventory.snapshot.json") backup_restore_escrow_inventory = load_json(security_dir / "backup-restore-escrow-inventory.snapshot.json") monitoring_alerting_observability_inventory = load_json( @@ -2710,6 +2713,114 @@ def validate(root: Path) -> None: [item["surface_id"] for item in host_service_config_inventory["write_capable_surfaces"]], surface_id, ) + assert_equal( + "host_service_owner_request_draft.schema", + host_service_owner_request_draft["schema_version"], + "host_service_owner_request_draft_v1", + ) + assert_equal( + "host_service_owner_request_draft.status", + host_service_owner_request_draft["status"], + "owner_request_draft_ready_not_dispatched", + ) + assert_equal( + "host_service_owner_request_draft.source_inventory_schema_version", + host_service_owner_request_draft["source_inventory_schema_version"], + "host_service_config_inventory_v1", + ) + expected_host_service_owner_request_summary = { + "request_draft_count": 9, + "write_capable_request_draft_count": 3, + "live_evidence_required_request_count": 8, + "request_field_count": 22, + "required_owner_field_count": 12, + "blocked_action_count": 14, + "request_sent_count": 0, + "recipient_confirmed_count": 0, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "live_evidence_received_count": 0, + "restart_window_accepted_count": 0, + "rollback_owner_accepted_count": 0, + "post_check_plan_accepted_count": 0, + "host_write_authorized_count": 0, + "ssh_read_authorized_count": 0, + "ssh_write_authorized_count": 0, + "docker_compose_action_authorized_count": 0, + "systemctl_action_authorized_count": 0, + "repair_bot_execution_authorized_count": 0, + "ansible_apply_authorized_count": 0, + "secret_value_collection_allowed_count": 0, + "active_scan_authorized_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + } + for key, expected in expected_host_service_owner_request_summary.items(): + assert_equal( + f"host_service_owner_request_draft.summary.{key}", + host_service_owner_request_draft["summary"][key], + expected, + ) + for key, value in host_service_owner_request_draft["execution_boundaries"].items(): + if key == "not_authorization": + assert_true(f"host_service_owner_request_draft.execution_boundaries.{key}", value) + else: + assert_false(f"host_service_owner_request_draft.execution_boundaries.{key}", value) + assert_equal( + "host_service_owner_request_draft.request_drafts.count", + len(host_service_owner_request_draft["request_drafts"]), + 9, + ) + host_service_owner_request_ids = [ + item["request_id"] for item in host_service_owner_request_draft["request_drafts"] + ] + for request_id in [ + "host_service_owner_request:repair_bot_110_whitelist", + "host_service_owner_request:repair_bot_188_whitelist", + "host_service_owner_request:ansible_docker_compose_service_role", + "host_service_owner_request:config_backup_host_capture", + ]: + assert_contains( + "host_service_owner_request_draft.request_drafts", + host_service_owner_request_ids, + request_id, + ) + for draft in host_service_owner_request_draft["request_drafts"]: + assert_equal( + f"host_service_owner_request_draft.{draft['request_id']}.required_owner_fields", + len(draft["required_owner_fields"]), + 12, + ) + assert_equal( + f"host_service_owner_request_draft.{draft['request_id']}.blocked_actions", + len(draft["blocked_actions"]), + 14, + ) + for false_key in [ + "request_sent", + "recipient_confirmed", + "owner_response_received", + "owner_response_accepted", + "live_evidence_received", + "restart_window_accepted", + "rollback_owner_accepted", + "post_check_plan_accepted", + "host_write_authorized", + "ssh_read_authorized", + "ssh_write_authorized", + "docker_compose_action_authorized", + "systemctl_action_authorized", + "repair_bot_execution_authorized", + "ansible_apply_authorized", + "secret_value_collection_allowed", + "active_scan_authorized", + "runtime_gate", + "action_buttons_allowed", + ]: + assert_false( + f"host_service_owner_request_draft.{draft['request_id']}.{false_key}", + draft[false_key], + ) assert_equal( "ssh_network_access_inventory.schema", ssh_network_access_inventory["schema_version"],