diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 0be6fcad..6c06fc73 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,43 @@ +## 2026-06-11|IwoooS 高價值配置 Owner Packet 草案 + +**背景**:P0.2 已建立高價值配置變更 Gate,但若只有分類結果,owner 仍需要人工整理欄位。本階段把分類結果轉成 owner response packet 草案,並對齊 S4.9 canonical owner response envelope,避免高價值配置流程和 S4.9 欄位分裂。 + +**同步狀態:** +- 開工前 `gitea/main` 先前進到 `edbb1194 chore(cd): deploy 6bae94f [skip ci]`,只改 `k8s/awoooi-prod/kustomization.yaml`。 +- 隨後遠端前進到 `ccf87213 chore(cd): trigger Alertmanager context repair deploy`;本地已 fast-forward 對齊後才開始 P0.3。 +- 驗證前遠端再前進到 `8c11af7c feat(governance): 定義 Agent 主動溝通學習契約`;本地以 stash → fast-forward → stash pop 合併,保留雙方 `docs/LOGBOOK.md` 內容。 + +**完成內容:** +- 新增 `scripts/security/high-value-config-owner-packet.py`,讀取 `high-value-config-change-gate` JSON,為 impacted categories 產生 owner response packet 草案。 +- 新增 `docs/security/HIGH-VALUE-CONFIG-OWNER-PACKET.md`,固定 canonical 欄位、allowed decisions、packet 狀態、禁止事項與完成度。 +- 新增 `docs/security/high-value-config-owner-packet.snapshot.json`,本階段 `packet_count=1`、`c0_packet_count=0`、`c1_packet_count=0`、`request_sent_count=0`、`received_response_count=0`、`accepted_response_count=0`、`runtime_gate_count=0`。 +- 更新 `scripts/security/high-value-config-change-gate.py`,將 `owner_role_team` 對齊為 S4.9 canonical `owner_role_or_team`,並保留 `owner_role_team`、`owner_role`、`owner_team`、`responsible_team` 等 alias。 +- 更新 `docs/security/HIGH-VALUE-CONFIG-CHANGE-GATE.md` 與 `docs/security/high-value-config-change-gate.snapshot.json`,同步 canonical 欄位與 P0.3 變更檔分類;目前仍只命中 C3 security evidence / tooling。 +- 更新 `docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md`,將 Gate → owner response packet 草案完成度列為 `100%`,canonical owner 欄位對齊列為 `100%`,owner response request / received / accepted 仍列為 `0%`。 + +**本地驗證:** +- `python3 -m py_compile scripts/security/high-value-config-change-gate.py scripts/security/high-value-config-owner-packet.py` 通過。 +- `python3 -m json.tool docs/security/high-value-config-change-gate.snapshot.json` 通過。 +- `python3 -m json.tool docs/security/high-value-config-owner-packet.snapshot.json` 通過。 +- `python3 scripts/security/high-value-config-change-gate.py --root . ... --output docs/security/high-value-config-change-gate.snapshot.json` 通過,`changed_file_count=8`、`impacted_c0_category_count=0`、`impacted_c1_category_count=0`。 +- `python3 scripts/security/high-value-config-owner-packet.py --root . --gate-report docs/security/high-value-config-change-gate.snapshot.json --output docs/security/high-value-config-owner-packet.snapshot.json` 通過,`packets=1`、`c0=0`、`c1=0`、`runtime_gate=0`。 +- C0 Nginx 範例 owner packet smoke 通過:`infra/ansible/roles/nginx/templates/188-all-sites.conf.j2` 產出 `nginx_public_gateway` packet,`c0_packet_count=1`、`runtime_gate_count=0`。 +- `python3 scripts/security/security-mirror-progress-guard.py --root .` 通過。 +- `python3 scripts/security/source-control-owner-response-guard.py --root .` 通過。 +- `node scripts/ci/check-gitea-step-env-secrets.js` 通過。 +- `python3 scripts/ops/doc-secrets-sanity-check.py docs .gitea` 通過,`scanned_files=646`。 +- P0 高風險字串掃描通過:關閉 SSH host key 驗證的逐字參數、舊 Gitea token、Grafana 密碼常值、舊 MinIO credential、舊 MinIO token、Prometheus inline bearer token 均未命中。 +- `git diff --check` 通過。 + +**完成度與邊界:** +- Gate → owner response packet 草案:`100%`。 +- Canonical 欄位對齊:`100%`。 +- owner response request sent / received / accepted:`0%`。 +- runtime gate:`0%`。 +- 本階段沒有前端變更,production desktop / mobile 頁面驗證不適用。 +- 未修改 workflow、未 SSH、未讀 live Nginx、未執行 `nginx -t` / reload / restart、未 DNS / TLS / ArgoCD / kubectl / host write / secret rotation / active scan / agent-bounty runtime。 +- IwoooS 整體仍維持 `64%`;active runtime gate 仍為 `0`;owner response received / accepted 仍為 `0 / false`。 + ## 2026-06-11|IwoooS 高價值配置變更 Gate 第一波 **背景**:接續「所有重要配置都要被資安控管」要求,前一波已建立高價值配置清冊與 Nginx 只讀漂移偵測器。本階段補上可重跑的分類型 Gate,讓 reviewer 不必只靠人工記憶判斷變更是否碰到 Nginx、DNS / TLS、K8s、secret、workflow、runner、backup、monitoring、host service、network、AI provider 或 agent-bounty-protocol runtime 邊界。 diff --git a/docs/security/HIGH-VALUE-CONFIG-CHANGE-GATE.md b/docs/security/HIGH-VALUE-CONFIG-CHANGE-GATE.md index 48f0df42..15b09aa7 100644 --- a/docs/security/HIGH-VALUE-CONFIG-CHANGE-GATE.md +++ b/docs/security/HIGH-VALUE-CONFIG-CHANGE-GATE.md @@ -78,7 +78,7 @@ python3 scripts/security/high-value-config-change-gate.py \ 所有 C0 / C1 高價值配置變更至少要補: -1. `owner_role_team` +1. `owner_role_or_team` 2. `decision` 3. `decision_reason` 4. `affected_scope` diff --git a/docs/security/HIGH-VALUE-CONFIG-OWNER-PACKET.md b/docs/security/HIGH-VALUE-CONFIG-OWNER-PACKET.md new file mode 100644 index 00000000..39cc0521 --- /dev/null +++ b/docs/security/HIGH-VALUE-CONFIG-OWNER-PACKET.md @@ -0,0 +1,85 @@ +# IwoooS 高價值配置 Owner Response Packet + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-06-11 | +| 狀態 | `owner_packet_draft_ready` | +| 工具 | `scripts/security/high-value-config-owner-packet.py` | +| 輸入 | `docs/security/high-value-config-change-gate.snapshot.json` | +| Snapshot | `docs/security/high-value-config-owner-packet.snapshot.json` | +| runtime gate | `0` | + +## 1. 目的 + +P0.2 已能將變更分類成 C0 / C1 / C2 / C3。本文件與工具負責下一步:把分類結果轉成 owner response packet 草案,讓 Nginx、DNS / TLS、K8s、secret、workflow、runner、backup、monitoring、host service、network、AI provider、agent-bounty-protocol 等高價值配置變更有一致補件欄位。 + +本階段仍是只讀草案,不送 request、不收 owner response、不標記 received / accepted、不建立 action button、不接 blocking CI、不執行 runtime。 + +## 2. Canonical 欄位 + +高價值配置 packet 對齊 S4.9 canonical owner response envelope,使用以下欄位: + +1. `owner_role_or_team` +2. `decision` +3. `decision_reason` +4. `affected_scope` +5. `redacted_evidence_refs` +6. `followup_owner` +7. `rollback_owner` +8. `maintenance_window` +9. `validation_plan` + +`decision` 只能是 `confirm`、`defer`、`reject`、`request_more_evidence`。`confirm` 只代表 owner 對該分類補件方向確認,不代表 reload、deploy、sync、scan、payout 或 host write。 + +## 3. 指令 + +使用 committed gate snapshot 產生 owner packet: + +```bash +python3 scripts/security/high-value-config-owner-packet.py \ + --root . \ + --gate-report docs/security/high-value-config-change-gate.snapshot.json \ + --output docs/security/high-value-config-owner-packet.snapshot.json +``` + +查看即時分類結果的 packet: + +```bash +python3 scripts/security/high-value-config-change-gate.py \ + --root . \ + --changed-file infra/ansible/roles/nginx/templates/188-all-sites.conf.j2 \ + --output /tmp/high-value-config-gate-nginx.json + +python3 scripts/security/high-value-config-owner-packet.py \ + --root . \ + --gate-report /tmp/high-value-config-gate-nginx.json +``` + +## 4. Packet 狀態 + +| 狀態 | 意義 | Gate 影響 | +|------|------|-----------| +| `draft_waiting_owner_response` | 只是草案,尚未送件或收件 | request / received / accepted 全部維持 0 | +| `request_more_evidence` | owner 欄位缺漏或 scope 不清 | 不增加 accepted | +| `quarantine_sensitive_payload` | 疑似含 secret、token、cookie、private key、未脫敏 evidence | 不保存 raw payload | +| `reject_execution_request` | 夾帶 reload、deploy、sync、host write、scan、payout 等執行要求 | 不建立 action button | +| `ready_for_reviewer_validation` | 欄位完整且無敏感 payload / 執行要求 | 只進 reviewer checklist,仍非 accepted | + +## 5. 絕對禁止 + +1. 不把 packet 草案當成 request sent。 +2. 不把 owner 口頭同意當成 response received。 +3. 不把 `confirm` 當成 runtime reload / deploy / scan / payout 授權。 +4. 不收 secret value、partial token、private key、cookie、session、authorization header、runner token 或 webhook secret。 +5. 不把內部工作視窗對話、抱怨或 Session 指令放入前端產品文案。 +6. 不用 packet 產生器修改 `.gitea/workflows`、K8s、Nginx、DNS、TLS、主機或 agent-bounty runtime。 + +## 6. 完成度 + +| 工作 | 完成度 | 說明 | +|------|--------|------| +| Gate → owner packet 草案 | `100%` | 可從 gate JSON 產生 impacted category packet | +| Canonical 欄位對齊 | `100%` | 已對齊 S4.9 `owner_role_or_team` 等欄位,P0.2 gate 也接受 alias | +| owner response 收件 | `0%` | 尚未送 request、尚未收到 owner response | +| reviewer accepted | `0%` | 尚未進 reviewer checklist | +| runtime gate | `0%` | 未授權且未開啟 | diff --git a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md index 5ccab14c..7ef1b8ec 100644 --- a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md +++ b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md @@ -130,6 +130,9 @@ Nginx 是目前必須最先資安控管的配置,原因是它同時控制公 | repo-only Nginx drift detector | `100%` | 已新增 `scripts/security/nginx-config-drift-detector.py` 與 repo source-of-truth snapshot | | 高價值配置變更分類 Gate | `100%` | 已新增 `scripts/security/high-value-config-change-gate.py`,可用 git diff 或手動檔案分類 C0/C1/C2/C3 並列出 owner / rollback / evidence / 驗證欄位 | | owner response evidence JSON 欄位檢查 | `70%` | Gate 可檢查必要欄位與 false flags;尚未接正式收件 API 或 AwoooP queue | +| Gate → owner response packet 草案 | `100%` | 已新增 `scripts/security/high-value-config-owner-packet.py`,可將 impacted category 轉成 canonical owner response packet 草案 | +| canonical owner 欄位對齊 | `100%` | 高價值配置 Gate 已對齊 S4.9 `owner_role_or_team`,並保留 `owner_role_team` 等 alias 支援 | +| owner response request / received / accepted | `0%` | Packet 只是草案;尚未送件、尚未收件、尚未 reviewer accepted | | 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 與維護窗口規則 | @@ -139,7 +142,7 @@ Nginx 是目前必須最先資安控管的配置,原因是它同時控制公 ## 7. 下一階段優先順序 -1. P0:把高價值配置 Gate 的分類結果接成 owner response packet 草案;先只讀,不接 blocking CI。 +1. P0:將 owner response packet 草案接入 IwoooS / AwoooP 只讀狀態,顯示 request / received / accepted 仍為 0。 2. P0:由 owner 提供脫敏 live Nginx conf 匯出檔,重跑 compare mode;不自動覆寫、不 reload。 3. P0:補 DNS / TLS / certbot domain inventory,先只讀,不 renew、不 reload。 4. P0:把 workflow / runner / secret name owner response 與高價值配置 C0 gate 串成同一個 IwoooS 狀態。 diff --git a/docs/security/high-value-config-change-gate.snapshot.json b/docs/security/high-value-config-change-gate.snapshot.json index e69127f0..520f8ac4 100644 --- a/docs/security/high-value-config-change-gate.snapshot.json +++ b/docs/security/high-value-config-change-gate.snapshot.json @@ -42,6 +42,27 @@ "strongest_priority": "P3", "strongest_tier": "C3" }, + { + "categories": [ + { + "category_id": "security_evidence_tooling", + "control_tier": "C3", + "label": "Security evidence / snapshot / guard tooling", + "priority": "P3", + "required_gate": "security_evidence_owner_review_required", + "required_validation": [ + "snapshot_parse", + "guard_smoke", + "doc_secret_sanity", + "no_runtime_gate_increase" + ] + } + ], + "matched": true, + "path": "docs/security/HIGH-VALUE-CONFIG-OWNER-PACKET.md", + "strongest_priority": "P3", + "strongest_tier": "C3" + }, { "categories": [ { @@ -84,6 +105,27 @@ "strongest_priority": "P3", "strongest_tier": "C3" }, + { + "categories": [ + { + "category_id": "security_evidence_tooling", + "control_tier": "C3", + "label": "Security evidence / snapshot / guard tooling", + "priority": "P3", + "required_gate": "security_evidence_owner_review_required", + "required_validation": [ + "snapshot_parse", + "guard_smoke", + "doc_secret_sanity", + "no_runtime_gate_increase" + ] + } + ], + "matched": true, + "path": "docs/security/high-value-config-owner-packet.snapshot.json", + "strongest_priority": "P3", + "strongest_tier": "C3" + }, { "categories": [ { @@ -104,6 +146,27 @@ "path": "scripts/security/high-value-config-change-gate.py", "strongest_priority": "P3", "strongest_tier": "C3" + }, + { + "categories": [ + { + "category_id": "security_evidence_tooling", + "control_tier": "C3", + "label": "Security evidence / snapshot / guard tooling", + "priority": "P3", + "required_gate": "security_evidence_owner_review_required", + "required_validation": [ + "snapshot_parse", + "guard_smoke", + "doc_secret_sanity", + "no_runtime_gate_increase" + ] + } + ], + "matched": true, + "path": "scripts/security/high-value-config-owner-packet.py", + "strongest_priority": "P3", + "strongest_tier": "C3" } ], "control_category_inventory": [ @@ -120,7 +183,7 @@ "priority": "P0", "required_gate": "public_gateway_owner_response_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -156,7 +219,7 @@ "priority": "P0", "required_gate": "domain_tls_owner_response_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -188,7 +251,7 @@ "priority": "P0", "required_gate": "gitops_owner_response_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -223,7 +286,7 @@ "priority": "P0", "required_gate": "secret_metadata_owner_response_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -258,7 +321,7 @@ "priority": "P0", "required_gate": "workflow_source_control_owner_response_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -292,7 +355,7 @@ "priority": "P0", "required_gate": "public_runtime_config_owner_response_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -324,7 +387,7 @@ "priority": "P0", "required_gate": "backup_restore_owner_response_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -355,7 +418,7 @@ "priority": "P0", "required_gate": "agent_bounty_owner_response_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -389,7 +452,7 @@ "priority": "P1", "required_gate": "monitoring_observability_owner_response_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -423,7 +486,7 @@ "priority": "P1", "required_gate": "host_service_owner_response_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -459,7 +522,7 @@ "priority": "P1", "required_gate": "network_access_owner_response_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -492,7 +555,7 @@ "priority": "P1", "required_gate": "ai_provider_owner_response_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -523,7 +586,7 @@ "priority": "P2", "required_gate": "product_surface_owner_response_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -553,7 +616,7 @@ "priority": "P3", "required_gate": "security_evidence_owner_review_required", "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -573,7 +636,7 @@ ], "diff": { "base": null, - "changed_file_count": 5, + "changed_file_count": 8, "head": "HEAD" }, "execution_boundaries": { @@ -587,8 +650,8 @@ "ssh_executed": false, "workflow_modified": false }, - "generated_at": "2026-06-11T12:30:00+08:00", - "git_commit": "e1cacdf3", + "generated_at": "2026-06-11T13:00:00+08:00", + "git_commit": "ccf87213", "impacted_categories": [ { "category_id": "security_evidence_tooling", @@ -614,7 +677,7 @@ "complete": false, "invalid_false_flags": [], "missing_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -639,7 +702,7 @@ "action_buttons_allowed" ], "required_owner_fields": [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -651,11 +714,11 @@ ], "schema_version": "high_value_config_change_gate_v1", "summary": { - "changed_file_count": 5, + "changed_file_count": 8, "impacted_c0_category_count": 0, "impacted_c1_category_count": 0, "impacted_category_count": 1, - "matched_high_value_file_count": 5, + "matched_high_value_file_count": 8, "owner_evidence_complete": false, "owner_evidence_provided": false, "runtime_execution_authorized": false, diff --git a/docs/security/high-value-config-owner-packet.snapshot.json b/docs/security/high-value-config-owner-packet.snapshot.json new file mode 100644 index 00000000..ae805b17 --- /dev/null +++ b/docs/security/high-value-config-owner-packet.snapshot.json @@ -0,0 +1,729 @@ +{ + "allowed_decisions": [ + "confirm", + "defer", + "reject", + "request_more_evidence" + ], + "canonical_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "control_category_inventory": [ + { + "category_id": "nginx_public_gateway", + "control_tier": "C0", + "label": "Nginx / reverse proxy / public route", + "path_patterns": [ + "infra/ansible/roles/nginx/templates/*.j2", + "infra/ansible/playbooks/nginx-sync.yml", + "ops/nginx/**", + "docs/runbooks/disaster-recovery/DR-Nginx.md" + ], + "priority": "P0", + "required_gate": "public_gateway_owner_response_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "rendered_diff", + "nginx_t", + "affected_route_smoke", + "admin_route_smoke_if_affected", + "acme_path_smoke_if_affected", + "rollback_ref" + ] + }, + { + "category_id": "dns_tls_certbot", + "control_tier": "C0", + "label": "DNS / TLS / certbot / certificate path", + "path_patterns": [ + "docs/runbooks/REGISTRY-CERTBOT-188.md", + "docs/runbooks/**/*CERTBOT*.md", + "docs/runbooks/**/*TLS*.md", + "ops/**/*cert*", + "ops/**/*tls*", + "infra/**/*cert*", + "infra/**/*tls*", + "k8s/**/*tls*" + ], + "priority": "P0", + "required_gate": "domain_tls_owner_response_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "domain_inventory", + "certificate_path_check", + "renewal_window", + "acme_path_smoke", + "public_https_smoke", + "rollback_ref" + ] + }, + { + "category_id": "k8s_production_gitops", + "control_tier": "C0", + "label": "K8s / ArgoCD / production manifests", + "path_patterns": [ + "k8s/awoooi-prod/**", + "k8s/argocd/**", + "k8s/velero/**", + "k8s/monitoring/**" + ], + "priority": "P0", + "required_gate": "gitops_owner_response_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "gitops_diff", + "argocd_health_readback", + "sync_authorization_check", + "rollback_revision", + "post_deploy_health_if_executed" + ] + }, + { + "category_id": "secret_metadata", + "control_tier": "C0", + "label": "Secret metadata / injection / redaction", + "path_patterns": [ + "k8s/**/*secret*", + "k8s/**/*Secret*", + ".gitea/workflows/*.yml", + ".gitea/workflows/*.yaml", + ".github/workflows/*.yml", + ".github/workflows/*.yaml", + "docs/runbooks/SECRETS-MANAGEMENT.md", + "docs/security/SECRETS_REFERENCE.md" + ], + "priority": "P0", + "required_gate": "secret_metadata_owner_response_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "secret_name_parity", + "metadata_only_check", + "no_secret_value_check", + "rotation_owner", + "injection_readback_if_deployed" + ] + }, + { + "category_id": "gitea_workflow_runner_source_control", + "control_tier": "C0", + "label": "Gitea workflow / runner / deploy key / webhook / branch protection", + "path_patterns": [ + ".gitea/workflows/**", + ".github/workflows/**", + "ops/runner/**", + "scripts/setup-runner*.sh", + "scripts/**/*runner*", + "docs/security/SOURCE-CONTROL-*", + "docs/security/GITEA-*", + "docs/security/GITHUB-*" + ], + "priority": "P0", + "required_gate": "workflow_source_control_owner_response_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "workflow_diff", + "runner_label_owner", + "deploy_key_metadata_only", + "webhook_metadata_only", + "branch_protection_metadata", + "no_token_value_check" + ] + }, + { + "category_id": "public_admin_api_runtime_config", + "control_tier": "C0", + "label": "Public / admin / API / frontend runtime config", + "path_patterns": [ + "apps/web/next.config.*", + "apps/web/src/lib/config.*", + "apps/api/src/core/config.py", + "apps/api/src/api/v1/monitoring.py", + "apps/api/src/middleware/**", + "apps/web/src/middleware.*" + ], + "priority": "P0", + "required_gate": "public_runtime_config_owner_response_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "public_url_check", + "frontend_internal_ip_ban", + "cors_boundary_check", + "admin_auth_boundary_check", + "desktop_mobile_smoke_if_frontend" + ] + }, + { + "category_id": "backup_restore_credential", + "control_tier": "C0", + "label": "Backup / restore / escrow / retention", + "path_patterns": [ + "scripts/backup/**", + "k8s/velero/**", + "docs/runbooks/disaster-recovery/**", + "docs/runbooks/**/*RESTORE*.md", + "docs/runbooks/**/*BACKUP*.md" + ], + "priority": "P0", + "required_gate": "backup_restore_owner_response_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "credential_absence_check", + "restore_drill_gate", + "retention_policy", + "escrow_owner", + "rollback_ref" + ] + }, + { + "category_id": "agent_bounty_protocol_runtime", + "control_tier": "C0", + "label": "agent-bounty-protocol runtime / MCP / A2A / treasury boundary", + "path_patterns": [ + "docs/security/AGENT-BOUNTY-IWOOOS-ONBOARDING-HANDOFF.md", + "docs/security/agent-bounty-iwooos-onboarding-handoff.snapshot.json", + "docs/schemas/agent_bounty_iwooos_onboarding_handoff_v1.schema.json", + "agent-bounty-protocol/**" + ], + "priority": "P0", + "required_gate": "agent_bounty_owner_response_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "repo_owner_scope", + "runtime_gate_false", + "no_payout_or_treasury_execution", + "no_mcp_a2a_runtime_execution", + "redacted_evidence_refs_only" + ] + }, + { + "category_id": "monitoring_alerting_observability", + "control_tier": "C1", + "label": "Prometheus / Alertmanager / Grafana / SigNoz / Sentry / Langfuse", + "path_patterns": [ + "ops/monitoring/**", + "ops/alertmanager/**", + "ops/grafana/**", + "ops/signoz/**", + "ops/sentry-self-hosted/**", + "infra/langfuse/**", + "k8s/monitoring/**" + ], + "priority": "P1", + "required_gate": "monitoring_observability_owner_response_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "rule_diff", + "receiver_diff", + "reload_gate", + "failure_notification_policy", + "public_route_smoke_if_affected" + ] + }, + { + "category_id": "docker_compose_systemd_host_config", + "control_tier": "C1", + "label": "Docker Compose / systemd / host service config", + "path_patterns": [ + "docker-compose*.yml", + "docker-compose*.yaml", + "ops/**/docker-compose*.yml", + "ops/**/docker-compose*.yaml", + "scripts/reboot-recovery/**", + "scripts/**/*.service", + "ops/**/*.service" + ], + "priority": "P1", + "required_gate": "host_service_owner_response_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "port_conflict_check", + "volume_diff", + "env_name_diff", + "restart_window", + "rollback_owner" + ] + }, + { + "category_id": "ssh_firewall_network_access", + "control_tier": "C1", + "label": "SSH / sudoers / known_hosts / firewall / WireGuard / NodePort", + "path_patterns": [ + "infra/ansible/inventory/**", + "infra/ansible/**/*known_hosts*", + "infra/ansible/**/*ssh*", + "scripts/**/*ssh*", + "scripts/**/*known_hosts*", + "ops/**/*wireguard*", + "ops/**/*firewall*", + "k8s/**/*network*", + "k8s/**/*Network*" + ], + "priority": "P1", + "required_gate": "network_access_owner_response_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "target_whitelist", + "host_key_policy", + "ingress_egress_matrix", + "rollback_owner", + "maintenance_window" + ] + }, + { + "category_id": "ai_provider_model_routing", + "control_tier": "C1", + "label": "AI provider / model routing / Ollama proxy / cost and privacy", + "path_patterns": [ + "apps/api/src/services/ai_providers/**", + "apps/api/src/services/**/*model*", + "apps/api/src/services/**/*provider*", + "infra/ansible/roles/nginx/templates/110-ollama-proxy.conf.j2", + "docs/ai/**", + "docs/**/*Ollama*" + ], + "priority": "P1", + "required_gate": "ai_provider_owner_response_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "dry_run", + "benchmark", + "cost_review", + "privacy_review", + "fallback_order_check" + ] + }, + { + "category_id": "product_surface_runtime_routes", + "control_tier": "C2", + "label": "AWOOOI / AwoooP / IwoooS / VibeWork / other product runtime routes", + "path_patterns": [ + "apps/web/src/app/**", + "apps/web/messages/*.json", + "docs/security/VIBEWORK-IWOOOS-ONBOARDING-HANDOFF.md", + "docs/security/vibework-iwooos-onboarding-handoff.snapshot.json" + ], + "priority": "P2", + "required_gate": "product_surface_owner_response_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "product_boundary_check", + "i18n_traditional_chinese_check", + "no_internal_transcript_check", + "desktop_mobile_smoke_if_frontend" + ] + }, + { + "category_id": "security_evidence_tooling", + "control_tier": "C3", + "label": "Security evidence / snapshot / guard tooling", + "path_patterns": [ + "docs/security/**", + "docs/schemas/**", + "scripts/security/**", + "docs/LOGBOOK.md" + ], + "priority": "P3", + "required_gate": "security_evidence_owner_review_required", + "required_owner_fields": [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan" + ], + "required_validation": [ + "snapshot_parse", + "guard_smoke", + "doc_secret_sanity", + "no_runtime_gate_increase" + ] + } + ], + "execution_boundaries": { + "action_buttons_allowed": false, + "host_write_authorized": false, + "request_sent": false, + "response_accepted": false, + "response_received": false, + "runtime_execution_authorized": false, + "secret_value_collected": false + }, + "generated_at": "2026-06-11T13:00:00+08:00", + "git_commit": "ccf87213", + "next_steps": [ + "若 packet_count > 0,將 packet 交給 owner 補 canonical 欄位;不得把草案視為已送件。", + "若 owner 回覆含 secret 或執行要求,先 quarantine 或 reject_execution_request。", + "只有 reviewer checklist 完成後才可進 accepted;accepted 仍不開 runtime gate。" + ], + "packets": [ + { + "affected_files": [ + "docs/LOGBOOK.md", + "docs/security/HIGH-VALUE-CONFIG-CHANGE-GATE.md", + "docs/security/HIGH-VALUE-CONFIG-OWNER-PACKET.md", + "docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md", + "docs/security/high-value-config-change-gate.snapshot.json", + "docs/security/high-value-config-owner-packet.snapshot.json", + "scripts/security/high-value-config-change-gate.py", + "scripts/security/high-value-config-owner-packet.py" + ], + "allowed_decisions": [ + "confirm", + "defer", + "reject", + "request_more_evidence" + ], + "blocked_requests": [ + "repo_create", + "visibility_change", + "refs_sync", + "refs_delete", + "force_push", + "workflow_modify", + "runner_enable", + "secret_value_submit", + "ssh_host_modify", + "nginx_reload", + "dns_tls_modify", + "argocd_sync", + "kubectl_apply", + "active_scan", + "agent_bounty_runtime_execute", + "payout_or_withdrawal" + ], + "category_id": "security_evidence_tooling", + "control_tier": "C3", + "false_flags": { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "dns_tls_change_authorized": false, + "force_push_authorized": false, + "host_write_authorized": false, + "nginx_reload_authorized": false, + "refs_sync_authorized": false, + "request_sent": false, + "response_accepted": false, + "response_received": false, + "runner_change_authorized": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "workflow_modification_authorized": false + }, + "field_templates": [ + { + "field": "owner_role_or_team", + "instruction": "填角色或團隊,不填私人帳號、密碼、token 或私人聯絡資訊。", + "required": true + }, + { + "field": "decision", + "instruction": "只能填 confirm、defer、reject、request_more_evidence;不得附帶 runtime 執行批准。", + "required": true + }, + { + "field": "decision_reason", + "instruction": "填脫敏短理由;不可貼 raw log、raw API body、未脫敏截圖或內部對話。", + "required": true + }, + { + "field": "affected_scope", + "instruction": "填受影響 repo、host、domain、namespace、route、service、secret name 或產品邊界。", + "required": true + }, + { + "field": "redacted_evidence_refs", + "instruction": "只填文件路徑、snapshot id、ticket id、commit、hash 或脫敏 metadata pointer。", + "required": true + }, + { + "field": "followup_owner", + "instruction": "填後續補證、審查或決策負責角色 / 團隊。", + "required": true + }, + { + "field": "rollback_owner", + "instruction": "填回滾負責角色 / 團隊;不是直接執行授權。", + "required": true + }, + { + "field": "maintenance_window", + "instruction": "填維護窗口或明確寫 deferred / not scheduled;不得用口頭同意代替。", + "required": true + }, + { + "field": "validation_plan", + "instruction": "填 preflight、post-check、rollback check;若只讀文件變更,寫 guard / json / doc secret sanity。", + "required": true + } + ], + "label": "Security evidence / snapshot / guard tooling", + "outcome_lanes": [ + "keep_waiting_owner_response", + "request_more_evidence", + "quarantine_sensitive_payload", + "reject_execution_request", + "ready_for_reviewer_validation" + ], + "packet_id": "high_value_config_owner_packet:security_evidence_tooling", + "priority": "P3", + "redaction_rules": [ + "只收 redacted evidence refs,不收 secret value。", + "疑似 token、cookie、authorization header、private key、runner token 或 webhook secret 一律 quarantine。", + "內部工作視窗對話、抱怨、口頭同意不得進產品文案或 LOGBOOK raw text。" + ], + "required_gate": "security_evidence_owner_review_required", + "required_validation": [ + "snapshot_parse", + "guard_smoke", + "doc_secret_sanity", + "no_runtime_gate_increase" + ], + "reviewer_checklist": [ + "canonical owner fields 全部存在。", + "decision 只使用允許值。", + "affected scope 可映射到 repo / host / domain / route / service / secret name。", + "redacted evidence refs 不含 raw payload。", + "沒有夾帶執行要求。", + "C0 / C1 若要進 runtime,需獨立人工批准與維護窗口。" + ], + "status": "draft_waiting_owner_response" + } + ], + "schema_version": "high_value_config_owner_packet_v1", + "source_gate_schema_version": "high_value_config_change_gate_v1", + "source_gate_summary": { + "changed_file_count": 8, + "impacted_c0_category_count": 0, + "impacted_c1_category_count": 0, + "impacted_category_count": 1, + "matched_high_value_file_count": 8, + "owner_evidence_complete": false, + "owner_evidence_provided": false, + "runtime_execution_authorized": false, + "strongest_priority": "P3", + "strongest_tier": "C3" + }, + "status": "draft_waiting_owner_response", + "summary": { + "accepted_response_count": 0, + "c0_packet_count": 0, + "c1_packet_count": 0, + "packet_count": 1, + "received_response_count": 0, + "request_sent_count": 0, + "runtime_gate_count": 0 + }, + "universal_owner_response_template": { + "allowed_decisions": [ + "confirm", + "defer", + "reject", + "request_more_evidence" + ], + "false_flags": { + "action_buttons_allowed": false, + "active_scan_authorized": false, + "dns_tls_change_authorized": false, + "force_push_authorized": false, + "host_write_authorized": false, + "nginx_reload_authorized": false, + "refs_sync_authorized": false, + "request_sent": false, + "response_accepted": false, + "response_received": false, + "runner_change_authorized": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "workflow_modification_authorized": false + }, + "field_templates": [ + { + "field": "owner_role_or_team", + "instruction": "填角色或團隊,不填私人帳號、密碼、token 或私人聯絡資訊。", + "required": true + }, + { + "field": "decision", + "instruction": "只能填 confirm、defer、reject、request_more_evidence;不得附帶 runtime 執行批准。", + "required": true + }, + { + "field": "decision_reason", + "instruction": "填脫敏短理由;不可貼 raw log、raw API body、未脫敏截圖或內部對話。", + "required": true + }, + { + "field": "affected_scope", + "instruction": "填受影響 repo、host、domain、namespace、route、service、secret name 或產品邊界。", + "required": true + }, + { + "field": "redacted_evidence_refs", + "instruction": "只填文件路徑、snapshot id、ticket id、commit、hash 或脫敏 metadata pointer。", + "required": true + }, + { + "field": "followup_owner", + "instruction": "填後續補證、審查或決策負責角色 / 團隊。", + "required": true + }, + { + "field": "rollback_owner", + "instruction": "填回滾負責角色 / 團隊;不是直接執行授權。", + "required": true + }, + { + "field": "maintenance_window", + "instruction": "填維護窗口或明確寫 deferred / not scheduled;不得用口頭同意代替。", + "required": true + }, + { + "field": "validation_plan", + "instruction": "填 preflight、post-check、rollback check;若只讀文件變更,寫 guard / json / doc secret sanity。", + "required": true + } + ] + } +} diff --git a/scripts/security/high-value-config-change-gate.py b/scripts/security/high-value-config-change-gate.py index 3cd513ee..aa4c5dd5 100644 --- a/scripts/security/high-value-config-change-gate.py +++ b/scripts/security/high-value-config-change-gate.py @@ -26,7 +26,7 @@ TIER_ORDER = {"C0": 0, "C1": 1, "C2": 2, "C3": 3} PRIORITY_ORDER = {"P0": 0, "P1": 1, "P2": 2, "P3": 3} REQUIRED_OWNER_FIELDS = [ - "owner_role_team", + "owner_role_or_team", "decision", "decision_reason", "affected_scope", @@ -37,6 +37,18 @@ REQUIRED_OWNER_FIELDS = [ "validation_plan", ] +OWNER_FIELD_ALIASES = { + "owner_role_or_team": ["owner_role_or_team", "owner_role_team", "owner_role", "owner_team", "responsible_team"], + "decision": ["decision", "owner_decision", "scope_decision", "disposition"], + "decision_reason": ["decision_reason", "reason", "decision_summary", "owner_note", "rationale"], + "affected_scope": ["affected_scope", "affected_sources", "affected_repos", "host_scope", "target_scope"], + "redacted_evidence_refs": ["redacted_evidence_refs", "evidence_refs", "redacted_refs", "source_refs"], + "followup_owner": ["followup_owner", "next_owner", "followup_team", "resolution_owner"], + "rollback_owner": ["rollback_owner", "rollback_team"], + "maintenance_window": ["maintenance_window", "change_window"], + "validation_plan": ["validation_plan", "validation_checks", "verification_plan"], +} + REQUIRED_FALSE_FLAGS = [ "runtime_execution_authorized", "host_write_authorized", @@ -433,6 +445,11 @@ def load_evidence(path: Path | None) -> dict[str, Any] | None: return json.loads(path.read_text(encoding="utf-8")) +def has_owner_field(evidence: dict[str, Any], field: str) -> bool: + aliases = OWNER_FIELD_ALIASES.get(field, [field]) + return any(bool(evidence.get(alias)) for alias in aliases) + + def validate_evidence(evidence: dict[str, Any] | None) -> dict[str, Any]: if evidence is None: return { @@ -443,7 +460,7 @@ def validate_evidence(evidence: dict[str, Any] | None) -> dict[str, Any]: "note": "未提供 owner response evidence;本階段只能分類,不得執行 runtime 變更。", } - missing = [field for field in REQUIRED_OWNER_FIELDS if not evidence.get(field)] + missing = [field for field in REQUIRED_OWNER_FIELDS if not has_owner_field(evidence, field)] false_flags = evidence.get("false_flags", {}) invalid_false_flags = [ flag for flag in REQUIRED_FALSE_FLAGS if false_flags.get(flag) is not False diff --git a/scripts/security/high-value-config-owner-packet.py b/scripts/security/high-value-config-owner-packet.py new file mode 100644 index 00000000..3ac4c575 --- /dev/null +++ b/scripts/security/high-value-config-owner-packet.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +""" +IwoooS 高價值配置 owner response packet 產生器。 + +本工具讀取 high-value-config-change-gate 的 JSON 報告,為 impacted +categories 產出 owner response packet 草案。它只產生欄位、補件規則與 +禁止事項,不送件、不收回覆、不建立 action button、不執行 runtime。 +""" + +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)) + +CANONICAL_OWNER_FIELDS = [ + "owner_role_or_team", + "decision", + "decision_reason", + "affected_scope", + "redacted_evidence_refs", + "followup_owner", + "rollback_owner", + "maintenance_window", + "validation_plan", +] + +ALLOWED_DECISIONS = ["confirm", "defer", "reject", "request_more_evidence"] + +FIELD_INSTRUCTIONS = { + "owner_role_or_team": "填角色或團隊,不填私人帳號、密碼、token 或私人聯絡資訊。", + "decision": "只能填 confirm、defer、reject、request_more_evidence;不得附帶 runtime 執行批准。", + "decision_reason": "填脫敏短理由;不可貼 raw log、raw API body、未脫敏截圖或內部對話。", + "affected_scope": "填受影響 repo、host、domain、namespace、route、service、secret name 或產品邊界。", + "redacted_evidence_refs": "只填文件路徑、snapshot id、ticket id、commit、hash 或脫敏 metadata pointer。", + "followup_owner": "填後續補證、審查或決策負責角色 / 團隊。", + "rollback_owner": "填回滾負責角色 / 團隊;不是直接執行授權。", + "maintenance_window": "填維護窗口或明確寫 deferred / not scheduled;不得用口頭同意代替。", + "validation_plan": "填 preflight、post-check、rollback check;若只讀文件變更,寫 guard / json / doc secret sanity。", +} + +FALSE_FLAGS = [ + "request_sent", + "response_received", + "response_accepted", + "runtime_execution_authorized", + "host_write_authorized", + "nginx_reload_authorized", + "dns_tls_change_authorized", + "workflow_modification_authorized", + "runner_change_authorized", + "refs_sync_authorized", + "force_push_authorized", + "secret_value_collection_allowed", + "active_scan_authorized", + "action_buttons_allowed", +] + +BLOCKED_REQUESTS = [ + "repo_create", + "visibility_change", + "refs_sync", + "refs_delete", + "force_push", + "workflow_modify", + "runner_enable", + "secret_value_submit", + "ssh_host_modify", + "nginx_reload", + "dns_tls_modify", + "argocd_sync", + "kubectl_apply", + "active_scan", + "agent_bounty_runtime_execute", + "payout_or_withdrawal", +] + +OUTCOME_LANES = [ + "keep_waiting_owner_response", + "request_more_evidence", + "quarantine_sensitive_payload", + "reject_execution_request", + "ready_for_reviewer_validation", +] + + +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 field_templates() -> list[dict[str, Any]]: + return [ + { + "field": field, + "required": True, + "instruction": FIELD_INSTRUCTIONS[field], + } + for field in CANONICAL_OWNER_FIELDS + ] + + +def false_flag_map() -> dict[str, bool]: + return {flag: False for flag in FALSE_FLAGS} + + +def files_for_category(gate_report: dict[str, Any], category_id: str) -> list[str]: + paths: list[str] = [] + for item in gate_report.get("changed_files", []): + if any(category.get("category_id") == category_id for category in item.get("categories", [])): + paths.append(item["path"]) + return sorted(paths) + + +def category_inventory_by_id(gate_report: dict[str, Any]) -> dict[str, dict[str, Any]]: + return { + item["category_id"]: item + for item in gate_report.get("control_category_inventory", []) + } + + +def packet_for_category(gate_report: dict[str, Any], category: dict[str, Any]) -> dict[str, Any]: + inventory = category_inventory_by_id(gate_report).get(category["category_id"], {}) + affected_files = files_for_category(gate_report, category["category_id"]) + return { + "packet_id": f"high_value_config_owner_packet:{category['category_id']}", + "status": "draft_waiting_owner_response", + "category_id": category["category_id"], + "label": category["label"], + "priority": category["priority"], + "control_tier": category["control_tier"], + "required_gate": category["required_gate"], + "affected_files": affected_files, + "allowed_decisions": ALLOWED_DECISIONS, + "field_templates": field_templates(), + "required_validation": category.get("required_validation") or inventory.get("required_validation", []), + "redaction_rules": [ + "只收 redacted evidence refs,不收 secret value。", + "疑似 token、cookie、authorization header、private key、runner token 或 webhook secret 一律 quarantine。", + "內部工作視窗對話、抱怨、口頭同意不得進產品文案或 LOGBOOK raw text。", + ], + "blocked_requests": BLOCKED_REQUESTS, + "outcome_lanes": OUTCOME_LANES, + "false_flags": false_flag_map(), + "reviewer_checklist": [ + "canonical owner fields 全部存在。", + "decision 只使用允許值。", + "affected scope 可映射到 repo / host / domain / route / service / secret name。", + "redacted evidence refs 不含 raw payload。", + "沒有夾帶執行要求。", + "C0 / C1 若要進 runtime,需獨立人工批准與維護窗口。", + ], + } + + +def build_report(root: Path, gate_report: dict[str, Any], generated_at: str | None) -> dict[str, Any]: + report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds") + impacted_categories = gate_report.get("impacted_categories", []) + packets = [packet_for_category(gate_report, category) for category in impacted_categories] + c0_packets = [packet for packet in packets if packet["control_tier"] == "C0"] + c1_packets = [packet for packet in packets if packet["control_tier"] == "C1"] + + return { + "schema_version": "high_value_config_owner_packet_v1", + "generated_at": report_time, + "git_commit": git_short_sha(root), + "source_gate_schema_version": gate_report.get("schema_version"), + "source_gate_summary": gate_report.get("summary", {}), + "status": "draft_waiting_owner_response", + "canonical_owner_fields": CANONICAL_OWNER_FIELDS, + "allowed_decisions": ALLOWED_DECISIONS, + "execution_boundaries": { + "request_sent": False, + "response_received": False, + "response_accepted": False, + "runtime_execution_authorized": False, + "host_write_authorized": False, + "secret_value_collected": False, + "action_buttons_allowed": False, + }, + "summary": { + "packet_count": len(packets), + "c0_packet_count": len(c0_packets), + "c1_packet_count": len(c1_packets), + "request_sent_count": 0, + "received_response_count": 0, + "accepted_response_count": 0, + "runtime_gate_count": 0, + }, + "universal_owner_response_template": { + "allowed_decisions": ALLOWED_DECISIONS, + "field_templates": field_templates(), + "false_flags": false_flag_map(), + }, + "packets": packets, + "control_category_inventory": gate_report.get("control_category_inventory", []), + "next_steps": [ + "若 packet_count > 0,將 packet 交給 owner 補 canonical 欄位;不得把草案視為已送件。", + "若 owner 回覆含 secret 或執行要求,先 quarantine 或 reject_execution_request。", + "只有 reviewer checklist 完成後才可進 accepted;accepted 仍不開 runtime gate。", + ], + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS 高價值配置 owner response packet 產生器") + parser.add_argument("--root", default=".", help="repo root") + parser.add_argument( + "--gate-report", + default="docs/security/high-value-config-change-gate.snapshot.json", + help="high-value-config-change-gate.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() + gate_report = load_json(root / args.gate_report) + report = build_report(root, gate_report, 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( + "HIGH_VALUE_CONFIG_OWNER_PACKET_OK " + f"packets={summary['packet_count']} " + f"c0={summary['c0_packet_count']} " + f"c1={summary['c1_packet_count']} " + f"runtime_gate={summary['runtime_gate_count']}", + file=sys.stderr, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main())