From a4998f915c769f28da69c43cc06e1393f1ae5607 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 15 Jun 2026 00:12:53 +0800 Subject: [PATCH] =?UTF-8?q?fix(iwooos):=20=E6=96=B0=E5=A2=9E=20public=20ga?= =?UTF-8?q?teway=20diff=20evidence=20acceptance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/[locale]/iwooos/page.tsx | 9 +- .../HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md | 6 + .../IWOOOS-CONFIG-CONTROL-INVENTORY.md | 2 + ...PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md | 154 ++++ .../SECURITY-SUPPLY-CHAIN-PROGRESS.md | 1 + ...alue-config-control-coverage.snapshot.json | 14 +- .../iwooos-posture-projection.snapshot.json | 2 + ...way-rendered-diff-acceptance.snapshot.json | 676 ++++++++++++++++++ ...-04-15-MASTER-ai-autonomous-flywheel-v2.md | 13 + .../high-value-config-control-coverage.py | 11 +- ...public-gateway-rendered-diff-acceptance.py | 412 +++++++++++ .../security-mirror-progress-guard.py | 257 ++++++- 12 files changed, 1542 insertions(+), 15 deletions(-) create mode 100644 docs/security/PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md create mode 100644 docs/security/public-gateway-rendered-diff-acceptance.snapshot.json create mode 100644 scripts/security/public-gateway-rendered-diff-acceptance.py diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 4a14022e1..a51ed782d 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -2111,7 +2111,10 @@ const highValueConfigControlCoverageBoundaries = [ 'high_value_config_control_coverage_owner_response_accepted_count=0', 'high_value_config_control_coverage_runtime_gate_count=0', 'high_value_config_control_coverage_action_button_count=0', - 'nginx_public_gateway_coverage_percent=84', + 'nginx_public_gateway_coverage_percent=88', + 'public_gateway_rendered_diff_acceptance_candidate_count=3', + 'public_gateway_rendered_diff_acceptance_reviewer_check_count=15', + 'public_gateway_rendered_diff_acceptance_runtime_gate_count=0', 'public_gateway_preflight_inventory_source_config_count=3', 'public_gateway_preflight_inventory_route_impact_count=14', 'public_gateway_preflight_inventory_preflight_gate_count=12', @@ -2214,7 +2217,7 @@ const criticalConfigPrioritySummary = [ ] as const const criticalConfigPriorityItems: CriticalConfigPriorityItem[] = [ - { key: 'nginxGateway', rank: 'P0-1', state: '84% / live 0', icon: Server, tone: 'warn' }, + { key: 'nginxGateway', rank: 'P0-1', state: '88% / live 0', icon: Server, tone: 'warn' }, { key: 'dnsTls', rank: 'P0-2', state: '74% / probe 0', icon: Route, tone: 'warn' }, { key: 'k8sArgocd', rank: 'P0-3', state: '58% / sync 0', icon: Cloud, tone: 'warn' }, { key: 'workflowSecret', rank: 'P0-4', state: 'metadata only', icon: GitBranch, tone: 'warn' }, @@ -2233,6 +2236,8 @@ const criticalConfigPriorityBoundaries = [ 'critical_config_priority_action_button_count=0', 'nginx_public_gateway_owner_response_received=false', 'nginx_public_gateway_live_conf_evidence_received=false', + 'nginx_public_gateway_rendered_diff_accepted=false', + 'nginx_public_gateway_rendered_diff_acceptance_candidate_count=3', 'nginx_public_gateway_nginx_test_evidence_count=0', 'dns_tls_live_probe_executed=false', 'certbot_renew_authorized=false', diff --git a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md index fd9c7ba22..c2504e870 100644 --- a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md +++ b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md @@ -48,6 +48,10 @@ 固定數字為 `acceptance_candidate_count=3`、`c0_acceptance_candidate_count=2`、`c1_acceptance_candidate_count=1`、`acceptance_field_count=23`、`required_owner_response_field_count=12`、`reviewer_check_count=12`、`outcome_lane_count=7`、`blocked_action_count=18`、`owner_response_received_count=0`、`owner_response_accepted_count=0`、`runtime_gate_count=0`。此更新只讓 `nginx_public_gateway` 從 `84%` 推進到 `86%`,代表收件驗收帳本更完整;live conf、rendered diff、`nginx -t`、reload、route smoke、DNS / TLS probe、certbot renew、maintenance window、rollback owner、runtime gate 與 action button 仍全部為 `0 / false`。 +2026-06-14 再新增 `docs/security/PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md` 與 `docs/security/public-gateway-rendered-diff-acceptance.snapshot.json`,把 owner response acceptance 後的 rendered diff evidence、owner-provided `nginx -t` readback、route smoke evidence、TLS / ACME impact、maintenance window、rollback owner 與 post-check evidence 轉成只讀驗收帳本。 + +固定數字為 `diff_acceptance_candidate_count=3`、`c0_diff_acceptance_candidate_count=2`、`c1_diff_acceptance_candidate_count=1`、`diff_acceptance_field_count=25`、`required_evidence_field_count=14`、`reviewer_check_count=15`、`outcome_lane_count=8`、`blocked_action_count=22`、`rendered_diff_accepted_count=0`、`nginx_test_evidence_accepted_count=0`、`route_smoke_result_accepted_count=0`、`runtime_gate_count=0`。此更新只讓 `nginx_public_gateway` 從 `86%` 推進到 `88%`;owner response accepted、redacted live conf accepted、rendered diff accepted、`nginx -t`、reload、route smoke、DNS / TLS probe、certbot renew、production write、runtime gate 與 action button 仍全部為 `0 / false`。 + ## 1.5 2026-06-14 DNS / TLS / certbot owner response acceptance 只讀帳本 已新增 `docs/security/DOMAIN-TLS-CERTBOT-OWNER-RESPONSE-ACCEPTANCE.md` 與 `docs/security/domain-tls-certbot-owner-response-acceptance.snapshot.json`,將 4 份 DNS / TLS / certbot owner confirmation request 轉成 metadata-only owner response acceptance 只讀帳本。 @@ -215,6 +219,8 @@ python3 scripts/security/high-value-config-control-coverage.py \ 2026-06-14 再新增 `public_gateway_owner_response_acceptance_v1`,把 3 份 public gateway config 轉成 `acceptance_candidate_count=3`、`c0_acceptance_candidate_count=2`、`required_owner_response_field_count=12`、`reviewer_check_count=12`、`outcome_lane_count=7`、`blocked_action_count=18` 的 owner response acceptance 只讀帳本。此更新只讓 `nginx_public_gateway` 從 `84%` 推進到 `86%`;owner response received / accepted、redacted export received / accepted、rendered diff ready、`nginx -t`、reload、route smoke、DNS / TLS probe、certbot renew、runtime gate 與 action button 仍全部為 `0`。 +2026-06-14 再新增 `public_gateway_rendered_diff_acceptance_v1`,把 3 份 public gateway config 轉成 `diff_acceptance_candidate_count=3`、`c0_diff_acceptance_candidate_count=2`、`required_evidence_field_count=14`、`reviewer_check_count=15`、`outcome_lane_count=8`、`blocked_action_count=22` 的 rendered diff evidence acceptance 只讀帳本。此更新只讓 `nginx_public_gateway` 從 `86%` 推進到 `88%`;owner response accepted、rendered diff accepted、owner-provided `nginx -t` evidence accepted、route smoke evidence accepted、reload、DNS / TLS probe、certbot renew、runtime gate 與 action button 仍全部為 `0`。 + ## 13. P0 DNS / TLS / certbot owner response acceptance 更新 `domain_tls_certbot_owner_response_acceptance_v1` 已把 4 份 DNS / TLS / certbot owner confirmation request 轉成 owner response acceptance 只讀帳本。四份候選全部為 `C0`,固定 `acceptance_candidate_count=4`、`required_owner_response_field_count=13`、`reviewer_check_count=13`、`outcome_lane_count=7`、`blocked_action_count=20`,讓 `dns_tls_certbot` 從 `74%` 推進到 `78%`。 diff --git a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md index d67c6545b..6dccd1e57 100644 --- a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md +++ b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md @@ -83,6 +83,8 @@ 2026-06-14 已新增 `public_gateway_owner_response_acceptance_v1`,把 3 份 Public Gateway config 轉成 owner response acceptance 只讀帳本。固定 `candidates=3`、`c0=2`、`owner_fields=12`、`reviewer_checks=12`、`outcome_lanes=7`、`blocked_actions=18`,讓 Nginx public gateway 類別成熟度從 `84%` 推進到 `86%`。此更新只表示未來 owner 回覆可安全收件、補件、隔離或拒收;owner response received / accepted、redacted export、rendered diff、`nginx -t`、reload、route smoke、DNS / TLS probe、certbot renew、host write、runtime gate 仍全部為 `0 / false`。 +2026-06-14 再新增 `public_gateway_rendered_diff_acceptance_v1`,把 3 份 Public Gateway config 轉成 rendered diff evidence acceptance 只讀帳本。固定 `candidates=3`、`c0=2`、`required_evidence_fields=14`、`reviewer_checks=15`、`outcome_lanes=8`、`blocked_actions=22`,讓 Nginx public gateway 類別成熟度從 `86%` 推進到 `88%`。此更新只表示未來 owner-provided rendered diff、`nginx -t` readback、route smoke evidence、TLS / ACME impact、maintenance window、rollback owner 與 post-check evidence 有收件驗收規則;owner response accepted、rendered diff accepted、nginx test evidence accepted、route smoke evidence accepted、reload、DNS / TLS probe、certbot renew、host write、runtime gate 仍全部為 `0 / false`。 + ### 0.7 2026-06-14 DNS / TLS / certbot owner response acceptance 只讀帳本 `domain_tls_certbot_owner_response_acceptance_v1` 已把 4 份 DNS / TLS / certbot owner confirmation request 轉成 owner response acceptance 只讀帳本。固定 `candidates=4`、`c0=4`、`owner_fields=13`、`reviewer_checks=13`、`outcome_lanes=7`、`blocked_actions=20`,讓 DNS / TLS / certbot 類別成熟度從 `74%` 推進到 `78%`。 diff --git a/docs/security/PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md b/docs/security/PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md new file mode 100644 index 000000000..c40307684 --- /dev/null +++ b/docs/security/PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md @@ -0,0 +1,154 @@ +# IwoooS Public Gateway Rendered Diff Acceptance 只讀帳本 + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-06-14 | +| 狀態 | `rendered_diff_acceptance_ledger_ready_no_runtime_action` | +| 工具 | `scripts/security/public-gateway-rendered-diff-acceptance.py` | +| 輸入 | `docs/security/public-gateway-rendered-diff-gate-draft.snapshot.json`、`docs/security/public-gateway-owner-response-acceptance.snapshot.json` | +| Snapshot | `docs/security/public-gateway-rendered-diff-acceptance.snapshot.json` | +| runtime gate | `0` | + +## 1. 目的 + +Nginx public gateway 是公開網站、API、Webhook、WebSocket、TLS 與 ACME 的共同入口。前面已建立 live conf 匯出請求、redacted export 收件預檢、rendered diff gate draft 與 owner response acceptance;本文件補上下一層:未來 owner 已接受後,如何驗收 `rendered diff`、owner-provided `nginx -t` readback evidence 與 route smoke evidence。 + +這個帳本只處理證據格式、脫敏邊界、可追溯性與 reviewer 分流。它不是 owner response accepted、不是 rendered diff accepted、不是 `nginx -t` 授權、不是 Nginx reload、不是 route smoke、不是 DNS / TLS probe、不是 certbot renew、不是 host write,也不是 production write 或 runtime gate。 + +## 2. 摘要 + +| 指標 | 值 | 說明 | +|------|----|------| +| diff acceptance candidate count | `3` | 對應三份 public gateway config | +| C0 diff acceptance candidate count | `2` | 188 all sites、188 internal tools HTTPS | +| C1 diff acceptance candidate count | `1` | 110 Ollama proxy | +| diff acceptance field count | `25` | 每份 evidence acceptance 欄位 | +| required evidence field count | `14` | 未來 owner evidence 必填欄位 | +| reviewer check count | `15` | 收件、隔離、拒收、補件與 reviewer acceptance 檢查 | +| outcome lane count | `8` | 從等待 owner accepted 到等待獨立 runtime approval | +| blocked action count | `22` | 不可直接執行或不可誤讀的動作 | +| owner response accepted | `0` | 尚未收到 / 接受 | +| rendered diff received / accepted | `0 / 0` | 尚未收到 / 驗收 | +| nginx test evidence received / accepted | `0 / 0` | 尚未收到 / 驗收 | +| route smoke evidence received / accepted | `0 / 0` | 尚未收到 / 驗收 | +| runtime gate / action button | `0 / 0` | 未開啟 | + +## 3. Acceptance 欄位 + +| 欄位 | 內容規則 | +|------|----------| +| `diff_acceptance_id` | 固定對應 public gateway rendered diff acceptance | +| `owner_response_acceptance_id` | 對應上一層 owner response acceptance candidate | +| `diff_gate_id` | 對應 rendered diff gate draft | +| `config_id` | 對應 public gateway config | +| `control_tier` | 保留 C0 / C1 風險分級 | +| `host` | 只保留既有 host scope,不代表 SSH 授權 | +| `live_path` | 只保留預期 live path,不代表讀取 live conf | +| `redacted_live_conf_ref` | 只可填脫敏 ref / hash / artifact pointer | +| `rendered_diff_ref` | 只可填 ref,不得貼完整 diff payload | +| `rendered_diff_hash_ref` | 指向 hash / checksum evidence | +| `diff_scope_summary` | 摘要 affected routes / upstream / TLS / ACME 範圍 | +| `affected_routes` | 必須能對回 preflight inventory | +| `nginx_test_evidence_ref` | 只可填 owner-provided readback ref | +| `nginx_test_operator` | 操作者角色,不得是不可追溯個人暱稱 | +| `nginx_test_result` | 結果摘要,不得含 secret | +| `route_smoke_matrix_ref` | affected route smoke matrix ref | +| `route_smoke_result_ref` | route smoke readback ref | +| `tls_acme_impact_ref` | TLS / ACME 影響 ref | +| `maintenance_window` | 維護窗口或明確禁止窗口 | +| `rollback_owner` | rollback owner / team | +| `rollback_ref` | rollback plan / revision / artifact ref | +| `postcheck_evidence_ref` | post-check readback ref | +| `reviewer_outcome` | reviewer acceptance lane | +| `followup_owner` | 補件或下一階段 owner | +| `not_approval` | 必須為 `true` | + +## 4. Reviewer Checks + +| Check | 規則 | +|-------|------| +| `owner_response_accepted_first` | 必須先有 owner response accepted record | +| `redacted_live_conf_ref_only` | 不接受 raw live conf | +| `rendered_diff_ref_not_payload` | 不接受完整 diff payload | +| `diff_scope_matches_config_id` | diff scope 必須對回 config_id | +| `nginx_test_evidence_is_readback_only` | 本工具不得執行 `nginx -t` | +| `nginx_test_result_has_timestamp` | test result 需有時間、角色與結果摘要 | +| `route_smoke_matrix_complete` | smoke matrix 必須列 affected routes 與預期結果 | +| `tls_acme_impact_separated` | TLS / ACME 影響不可被 route smoke 取代 | +| `secret_value_absent` | 不得包含 secret value 或 derivative | +| `maintenance_window_present` | 未來 runtime action 前必須有窗口 | +| `rollback_owner_and_ref_present` | rollback owner 與 ref 必須存在 | +| `postcheck_plan_present` | post-check evidence ref 必須存在 | +| `no_execution_request_embedded` | evidence 不可夾帶執行要求 | +| `counts_transition_safe` | accepted / rejected 只能由 reviewer record 更新 | +| `action_button_absent` | 前台不得新增執行按鈕 | + +## 5. Outcome Lanes + +| Lane | 意義 | +|------|------| +| `waiting_owner_response_acceptance` | owner response 尚未 accepted | +| `waiting_rendered_diff_evidence` | 等待 rendered diff / nginx test / route smoke evidence ref | +| `quarantine_raw_conf_or_payload` | raw conf 或完整 payload 只能隔離 | +| `reject_secret_or_execution_request` | secret 或執行要求直接拒收 | +| `request_evidence_supplement` | 欄位不足或 route matrix 不完整需補件 | +| `ready_for_reviewer_acceptance` | metadata 合格後進 reviewer acceptance | +| `accepted_for_runtime_gate_planning` | 只可進下一層 runtime gate planning | +| `waiting_separate_runtime_approval` | `nginx -t` / reload / smoke 仍需獨立人工批准 | + +## 6. Blocked Actions + +| Action | 邊界 | +|--------|------| +| `read_live_conf_over_ssh` | 未授權不得執行 | +| `store_raw_live_conf` | 不得寫入 repo、LOGBOOK 或前端 | +| `store_full_rendered_diff_payload` | 不得保存完整 diff payload | +| `accept_unredacted_live_conf` | 不得接受 | +| `collect_secret_value` | 不得收 secret value | +| `accept_execution_request_inside_evidence` | evidence 內不得夾帶執行要求 | +| `mark_rendered_diff_accepted_without_owner_response` | 不得跳過 owner response accepted | +| `mark_rendered_diff_accepted_without_reviewer_record` | 不得跳過 reviewer record | +| `run_nginx_test_from_diff_acceptance` | 不得由本帳本執行 | +| `run_route_smoke_from_diff_acceptance` | 不得由本帳本執行 | +| `nginx_reload_from_diff_acceptance` | 不得由本帳本執行 | +| `dns_probe_from_diff_acceptance` | 不得由本帳本執行 | +| `tls_probe_from_diff_acceptance` | 不得由本帳本執行 | +| `certbot_renew_from_diff_acceptance` | 不得由本帳本執行 | +| `modify_nginx_conf` | 不得改 live conf | +| `modify_dns_tls_config` | 不得改 DNS / TLS / certbot | +| `change_public_route` | 不得改公開路由 | +| `change_admin_route` | 不得改 admin route | +| `change_websocket_route` | 不得改 WebSocket route | +| `write_production_host` | 不得主機寫入 | +| `open_runtime_gate` | 不得開 runtime gate | +| `add_action_button` | 不得新增操作按鈕 | + +## 7. 指令 + +產生 committed snapshot: + +```bash +python3 scripts/security/public-gateway-rendered-diff-acceptance.py \ + --root . \ + --rendered-diff-gate-report docs/security/public-gateway-rendered-diff-gate-draft.snapshot.json \ + --owner-response-acceptance-report docs/security/public-gateway-owner-response-acceptance.snapshot.json \ + --output docs/security/public-gateway-rendered-diff-acceptance.snapshot.json \ + --generated-at 2026-06-14T23:58:00+08:00 +``` + +驗證 guard: + +```bash +python3 scripts/security/security-mirror-progress-guard.py --root . +``` + +## 8. 完成度 + +| 工作 | 完成度 | 說明 | +|------|--------|------| +| rendered diff acceptance artifact | `100%` | 產生器、snapshot 與文件已固定 | +| owner response accepted | `0%` | 尚未收到 / 接受 | +| rendered diff evidence accepted | `0%` | 尚未收到 / 驗收 | +| nginx test evidence accepted | `0%` | 尚未收到 / 驗收 | +| route smoke evidence accepted | `0%` | 尚未收到 / 驗收 | +| runtime reload / host write | `0%` | 未授權且未執行 | diff --git a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md index 161c3e3cc..adb4fd859 100644 --- a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md +++ b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md @@ -407,6 +407,7 @@ headline 要再往上,需要 S4.9 / S4.10 / S4.11 / S4.12 任一 owner respons | Public Gateway live conf 匯出請求包 | 本地完成 | `public-gateway-live-conf-export-request.snapshot.json` 已固定 `export_request_count=3`、`c0_export_request_count=2`、`redaction_rule_count=8`、`redacted_export_received_count=0`、`raw_live_conf_stored_count=0`,並由 `security-mirror-progress-guard.py` 鎖住 export ids、redaction rules 與 false flags | 這是 owner-provided redacted export request draft,不是 request sent、live conf received、rendered diff ready、`nginx -t`、Nginx reload、route smoke、DNS / TLS probe、certbot renew、host write、production write 或 runtime gate;不需要 production browser smoke | | Public Gateway redacted export 收件預檢 | 本地完成 | `public-gateway-redacted-export-intake-preflight.snapshot.json` 已固定 `intake_candidate_count=3`、`c0_intake_candidate_count=2`、`validation_check_count=10`、`rejection_guard_count=12`、`reviewer_intake_lane_count=5`、`redacted_export_received_count=0`、`accepted_redacted_export_count=0`,並由 `security-mirror-progress-guard.py` 鎖住 intake ids、validation checks、rejection guards、reviewer lanes 與 false flags | 這是 redacted export 收件前預檢,不是 request sent、recipient confirmed、redacted export received / accepted、raw live conf stored、rendered diff ready、`nginx -t`、Nginx reload、route smoke、DNS / TLS probe、certbot renew、host write、production write 或 runtime gate;不需要 production browser smoke | | Public Gateway rendered diff / nginx gate 草稿 | 本地完成 | `public-gateway-rendered-diff-gate-draft.snapshot.json` 已固定 `diff_gate_candidate_count=3`、`c0_diff_gate_candidate_count=2`、`preflight_stage_count=7`、`blocked_action_count=14`、`redacted_export_accepted_count=0`、`rendered_diff_ready_count=0`、`nginx_test_executed_count=0`、`runtime_gate_count=0`,並由 `security-mirror-progress-guard.py` 鎖住 diff gate ids、preflight stages、blocked actions 與 false flags | 這是 rendered diff / nginx / route smoke 分階段 gate 草稿,不是 redacted export accepted、rendered diff ready、`nginx -t`、Nginx reload、route smoke、DNS / TLS probe、certbot renew、host write、production write 或 runtime gate;不需要 production browser smoke | +| Public Gateway rendered diff evidence acceptance | 本地完成 | `public-gateway-rendered-diff-acceptance.snapshot.json` 已固定 `diff_acceptance_candidate_count=3`、`c0_diff_acceptance_candidate_count=2`、`required_evidence_field_count=14`、`reviewer_check_count=15`、`outcome_lane_count=8`、`blocked_action_count=22`、`rendered_diff_accepted_count=0`、`nginx_test_evidence_accepted_count=0`、`route_smoke_result_accepted_count=0`、`runtime_gate_count=0`,並由 `security-mirror-progress-guard.py` 鎖住 acceptance ids、reviewer checks、outcome lanes、blocked actions 與 false flags | 這是 owner-provided rendered diff / nginx test / route smoke evidence 的只讀驗收帳本,不是 owner response accepted、rendered diff accepted、`nginx -t`、Nginx reload、route smoke、DNS / TLS probe、certbot renew、host write、production write 或 runtime gate;前端數字會隨本輪 commit / deploy 另行 smoke | | 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 | diff --git a/docs/security/high-value-config-control-coverage.snapshot.json b/docs/security/high-value-config-control-coverage.snapshot.json index 6a76bea71..aad4f4b61 100644 --- a/docs/security/high-value-config-control-coverage.snapshot.json +++ b/docs/security/high-value-config-control-coverage.snapshot.json @@ -4,9 +4,9 @@ "action_buttons_allowed": false, "category_id": "nginx_public_gateway", "control_tier": "C0", - "coverage_percent": 86, - "coverage_status": "owner_response_acceptance_ledger_ready_needs_owner_live_diff", - "current_gap": "已固定 owner response acceptance 只讀帳本;owner response、live conf、rendered diff、nginx -t、route smoke、maintenance window 與 rollback owner 仍全部為 0。", + "coverage_percent": 88, + "coverage_status": "rendered_diff_acceptance_ledger_ready_needs_owner_live_diff", + "current_gap": "已固定 owner response acceptance 與 rendered diff evidence acceptance 只讀帳本;owner response、live conf、rendered diff evidence、nginx -t evidence、route smoke evidence、maintenance window 與 rollback owner 仍全部為 0。", "evidence_refs": [ "docs/security/NGINX-CONFIG-DRIFT-DETECTOR.md", "docs/security/nginx-config-drift-repo.snapshot.json", @@ -15,10 +15,12 @@ "docs/security/public-gateway-preflight-inventory.snapshot.json", "docs/security/PUBLIC-GATEWAY-OWNER-RESPONSE-ACCEPTANCE.md", "docs/security/public-gateway-owner-response-acceptance.snapshot.json", + "docs/security/PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md", + "docs/security/public-gateway-rendered-diff-acceptance.snapshot.json", "docs/schemas/public_gateway_preflight_inventory_v1.schema.json" ], "label": "Nginx / reverse proxy / public route", - "next_owner_action": "補 public gateway owner 回覆、owner-provided live conf、source-to-live rendered diff、nginx -t evidence、route smoke、maintenance window 與 rollback owner。", + "next_owner_action": "補 public gateway owner 回覆、owner-provided live conf、source-to-live rendered diff ref、nginx -t evidence ref、route smoke evidence ref、maintenance window 與 rollback owner。", "owner_response_accepted": false, "owner_response_received": false, "owner_response_required": true, @@ -588,8 +590,8 @@ "websocket_route_change_authorized": false, "workflow_modification_authorized": false }, - "generated_at": "2026-06-14T23:45:00+08:00", - "git_commit": "92e451cb", + "generated_at": "2026-06-14T23:59:00+08:00", + "git_commit": "22876ee1", "lowest_coverage_categories": [ { "category_id": "docker_compose_systemd_host_config", diff --git a/docs/security/iwooos-posture-projection.snapshot.json b/docs/security/iwooos-posture-projection.snapshot.json index 051bb9a74..10c3cbd29 100644 --- a/docs/security/iwooos-posture-projection.snapshot.json +++ b/docs/security/iwooos-posture-projection.snapshot.json @@ -26,6 +26,8 @@ "docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md", "docs/security/public-gateway-owner-response-acceptance.snapshot.json", "docs/security/PUBLIC-GATEWAY-OWNER-RESPONSE-ACCEPTANCE.md", + "docs/security/public-gateway-rendered-diff-acceptance.snapshot.json", + "docs/security/PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md", "docs/schemas/public_gateway_preflight_inventory_v1.schema.json", "docs/security/k8s-argocd-owner-response-acceptance.snapshot.json", "docs/security/K8S-ARGOCD-OWNER-RESPONSE-ACCEPTANCE.md", diff --git a/docs/security/public-gateway-rendered-diff-acceptance.snapshot.json b/docs/security/public-gateway-rendered-diff-acceptance.snapshot.json new file mode 100644 index 000000000..207732aa2 --- /dev/null +++ b/docs/security/public-gateway-rendered-diff-acceptance.snapshot.json @@ -0,0 +1,676 @@ +{ + "blocked_actions": [ + "read_live_conf_over_ssh", + "store_raw_live_conf", + "store_full_rendered_diff_payload", + "accept_unredacted_live_conf", + "collect_secret_value", + "accept_execution_request_inside_evidence", + "mark_rendered_diff_accepted_without_owner_response", + "mark_rendered_diff_accepted_without_reviewer_record", + "run_nginx_test_from_diff_acceptance", + "run_route_smoke_from_diff_acceptance", + "nginx_reload_from_diff_acceptance", + "dns_probe_from_diff_acceptance", + "tls_probe_from_diff_acceptance", + "certbot_renew_from_diff_acceptance", + "modify_nginx_conf", + "modify_dns_tls_config", + "change_public_route", + "change_admin_route", + "change_websocket_route", + "write_production_host", + "open_runtime_gate", + "add_action_button" + ], + "diff_acceptance_candidates": [ + { + "acceptance_fields": [ + "diff_acceptance_id", + "owner_response_acceptance_id", + "diff_gate_id", + "config_id", + "control_tier", + "host", + "live_path", + "redacted_live_conf_ref", + "rendered_diff_ref", + "rendered_diff_hash_ref", + "diff_scope_summary", + "affected_routes", + "nginx_test_evidence_ref", + "nginx_test_operator", + "nginx_test_result", + "route_smoke_matrix_ref", + "route_smoke_result_ref", + "tls_acme_impact_ref", + "maintenance_window", + "rollback_owner", + "rollback_ref", + "postcheck_evidence_ref", + "reviewer_outcome", + "followup_owner", + "not_approval" + ], + "action_buttons_allowed": false, + "affected_routes": [], + "blocked_actions": [ + "read_live_conf_over_ssh", + "store_raw_live_conf", + "store_full_rendered_diff_payload", + "accept_unredacted_live_conf", + "collect_secret_value", + "accept_execution_request_inside_evidence", + "mark_rendered_diff_accepted_without_owner_response", + "mark_rendered_diff_accepted_without_reviewer_record", + "run_nginx_test_from_diff_acceptance", + "run_route_smoke_from_diff_acceptance", + "nginx_reload_from_diff_acceptance", + "dns_probe_from_diff_acceptance", + "tls_probe_from_diff_acceptance", + "certbot_renew_from_diff_acceptance", + "modify_nginx_conf", + "modify_dns_tls_config", + "change_public_route", + "change_admin_route", + "change_websocket_route", + "write_production_host", + "open_runtime_gate", + "add_action_button" + ], + "certbot_renew_authorized": false, + "config_id": "host188_all_sites", + "control_tier": "C0", + "diff_acceptance_id": "public_gateway_rendered_diff_acceptance:host188_all_sites", + "diff_gate_id": "public_gateway_rendered_diff_gate:host188_all_sites", + "diff_scope_summary": "pending_owner_response_acceptance", + "dns_tls_probe_authorized": false, + "followup_owner": "pending_owner_response_acceptance", + "host": "192.168.0.188", + "live_path": "/etc/nginx/sites-enabled/all-sites.conf", + "maintenance_window": "pending_owner_response_acceptance", + "maintenance_window_accepted": false, + "nginx_reload_authorized": false, + "nginx_reload_executed": false, + "nginx_test_authorized": false, + "nginx_test_evidence_accepted": false, + "nginx_test_evidence_received": false, + "nginx_test_evidence_ref": null, + "nginx_test_executed": false, + "nginx_test_operator": "pending_runtime_owner", + "nginx_test_result": "pending_owner_provided_readback", + "not_approval": true, + "outcome_lanes": [ + "waiting_owner_response_acceptance", + "waiting_rendered_diff_evidence", + "quarantine_raw_conf_or_payload", + "reject_secret_or_execution_request", + "request_evidence_supplement", + "ready_for_reviewer_acceptance", + "accepted_for_runtime_gate_planning", + "waiting_separate_runtime_approval" + ], + "owner_response_acceptance_id": "public_gateway_owner_response_acceptance:host188_all_sites", + "owner_response_accepted": false, + "postcheck_evidence_accepted": false, + "postcheck_evidence_ref": null, + "production_write_authorized": false, + "redacted_export_accepted": false, + "redacted_live_conf_ref": null, + "rendered_diff_accepted": false, + "rendered_diff_hash_ref": null, + "rendered_diff_received": false, + "rendered_diff_ref": null, + "required_evidence_fields": [ + "redacted_live_conf_ref", + "rendered_diff_ref", + "rendered_diff_hash_ref", + "diff_scope_summary", + "affected_routes", + "nginx_test_evidence_ref", + "nginx_test_result", + "route_smoke_matrix_ref", + "route_smoke_result_ref", + "tls_acme_impact_ref", + "maintenance_window", + "rollback_owner", + "rollback_ref", + "postcheck_evidence_ref" + ], + "reviewer_checks": [ + "owner_response_accepted_first", + "redacted_live_conf_ref_only", + "rendered_diff_ref_not_payload", + "diff_scope_matches_config_id", + "nginx_test_evidence_is_readback_only", + "nginx_test_result_has_timestamp", + "route_smoke_matrix_complete", + "tls_acme_impact_separated", + "secret_value_absent", + "maintenance_window_present", + "rollback_owner_and_ref_present", + "postcheck_plan_present", + "no_execution_request_embedded", + "counts_transition_safe", + "action_button_absent" + ], + "reviewer_outcome": "waiting_owner_response_acceptance", + "rollback_owner": "pending_owner_response_acceptance", + "rollback_owner_accepted": false, + "rollback_ref": null, + "route_smoke_authorized": false, + "route_smoke_executed": false, + "route_smoke_matrix_accepted": false, + "route_smoke_matrix_received": false, + "route_smoke_matrix_ref": null, + "route_smoke_result_accepted": false, + "route_smoke_result_received": false, + "route_smoke_result_ref": null, + "runtime_gate": false, + "status": "waiting_owner_response_acceptance", + "tls_acme_impact_accepted": false, + "tls_acme_impact_ref": null + }, + { + "acceptance_fields": [ + "diff_acceptance_id", + "owner_response_acceptance_id", + "diff_gate_id", + "config_id", + "control_tier", + "host", + "live_path", + "redacted_live_conf_ref", + "rendered_diff_ref", + "rendered_diff_hash_ref", + "diff_scope_summary", + "affected_routes", + "nginx_test_evidence_ref", + "nginx_test_operator", + "nginx_test_result", + "route_smoke_matrix_ref", + "route_smoke_result_ref", + "tls_acme_impact_ref", + "maintenance_window", + "rollback_owner", + "rollback_ref", + "postcheck_evidence_ref", + "reviewer_outcome", + "followup_owner", + "not_approval" + ], + "action_buttons_allowed": false, + "affected_routes": [], + "blocked_actions": [ + "read_live_conf_over_ssh", + "store_raw_live_conf", + "store_full_rendered_diff_payload", + "accept_unredacted_live_conf", + "collect_secret_value", + "accept_execution_request_inside_evidence", + "mark_rendered_diff_accepted_without_owner_response", + "mark_rendered_diff_accepted_without_reviewer_record", + "run_nginx_test_from_diff_acceptance", + "run_route_smoke_from_diff_acceptance", + "nginx_reload_from_diff_acceptance", + "dns_probe_from_diff_acceptance", + "tls_probe_from_diff_acceptance", + "certbot_renew_from_diff_acceptance", + "modify_nginx_conf", + "modify_dns_tls_config", + "change_public_route", + "change_admin_route", + "change_websocket_route", + "write_production_host", + "open_runtime_gate", + "add_action_button" + ], + "certbot_renew_authorized": false, + "config_id": "host188_internal_tools_https", + "control_tier": "C0", + "diff_acceptance_id": "public_gateway_rendered_diff_acceptance:host188_internal_tools_https", + "diff_gate_id": "public_gateway_rendered_diff_gate:host188_internal_tools_https", + "diff_scope_summary": "pending_owner_response_acceptance", + "dns_tls_probe_authorized": false, + "followup_owner": "pending_owner_response_acceptance", + "host": "192.168.0.188", + "live_path": "owner_confirmation_required", + "maintenance_window": "pending_owner_response_acceptance", + "maintenance_window_accepted": false, + "nginx_reload_authorized": false, + "nginx_reload_executed": false, + "nginx_test_authorized": false, + "nginx_test_evidence_accepted": false, + "nginx_test_evidence_received": false, + "nginx_test_evidence_ref": null, + "nginx_test_executed": false, + "nginx_test_operator": "pending_runtime_owner", + "nginx_test_result": "pending_owner_provided_readback", + "not_approval": true, + "outcome_lanes": [ + "waiting_owner_response_acceptance", + "waiting_rendered_diff_evidence", + "quarantine_raw_conf_or_payload", + "reject_secret_or_execution_request", + "request_evidence_supplement", + "ready_for_reviewer_acceptance", + "accepted_for_runtime_gate_planning", + "waiting_separate_runtime_approval" + ], + "owner_response_acceptance_id": "public_gateway_owner_response_acceptance:host188_internal_tools_https", + "owner_response_accepted": false, + "postcheck_evidence_accepted": false, + "postcheck_evidence_ref": null, + "production_write_authorized": false, + "redacted_export_accepted": false, + "redacted_live_conf_ref": null, + "rendered_diff_accepted": false, + "rendered_diff_hash_ref": null, + "rendered_diff_received": false, + "rendered_diff_ref": null, + "required_evidence_fields": [ + "redacted_live_conf_ref", + "rendered_diff_ref", + "rendered_diff_hash_ref", + "diff_scope_summary", + "affected_routes", + "nginx_test_evidence_ref", + "nginx_test_result", + "route_smoke_matrix_ref", + "route_smoke_result_ref", + "tls_acme_impact_ref", + "maintenance_window", + "rollback_owner", + "rollback_ref", + "postcheck_evidence_ref" + ], + "reviewer_checks": [ + "owner_response_accepted_first", + "redacted_live_conf_ref_only", + "rendered_diff_ref_not_payload", + "diff_scope_matches_config_id", + "nginx_test_evidence_is_readback_only", + "nginx_test_result_has_timestamp", + "route_smoke_matrix_complete", + "tls_acme_impact_separated", + "secret_value_absent", + "maintenance_window_present", + "rollback_owner_and_ref_present", + "postcheck_plan_present", + "no_execution_request_embedded", + "counts_transition_safe", + "action_button_absent" + ], + "reviewer_outcome": "waiting_owner_response_acceptance", + "rollback_owner": "pending_owner_response_acceptance", + "rollback_owner_accepted": false, + "rollback_ref": null, + "route_smoke_authorized": false, + "route_smoke_executed": false, + "route_smoke_matrix_accepted": false, + "route_smoke_matrix_received": false, + "route_smoke_matrix_ref": null, + "route_smoke_result_accepted": false, + "route_smoke_result_received": false, + "route_smoke_result_ref": null, + "runtime_gate": false, + "status": "waiting_owner_response_acceptance", + "tls_acme_impact_accepted": false, + "tls_acme_impact_ref": null + }, + { + "acceptance_fields": [ + "diff_acceptance_id", + "owner_response_acceptance_id", + "diff_gate_id", + "config_id", + "control_tier", + "host", + "live_path", + "redacted_live_conf_ref", + "rendered_diff_ref", + "rendered_diff_hash_ref", + "diff_scope_summary", + "affected_routes", + "nginx_test_evidence_ref", + "nginx_test_operator", + "nginx_test_result", + "route_smoke_matrix_ref", + "route_smoke_result_ref", + "tls_acme_impact_ref", + "maintenance_window", + "rollback_owner", + "rollback_ref", + "postcheck_evidence_ref", + "reviewer_outcome", + "followup_owner", + "not_approval" + ], + "action_buttons_allowed": false, + "affected_routes": [], + "blocked_actions": [ + "read_live_conf_over_ssh", + "store_raw_live_conf", + "store_full_rendered_diff_payload", + "accept_unredacted_live_conf", + "collect_secret_value", + "accept_execution_request_inside_evidence", + "mark_rendered_diff_accepted_without_owner_response", + "mark_rendered_diff_accepted_without_reviewer_record", + "run_nginx_test_from_diff_acceptance", + "run_route_smoke_from_diff_acceptance", + "nginx_reload_from_diff_acceptance", + "dns_probe_from_diff_acceptance", + "tls_probe_from_diff_acceptance", + "certbot_renew_from_diff_acceptance", + "modify_nginx_conf", + "modify_dns_tls_config", + "change_public_route", + "change_admin_route", + "change_websocket_route", + "write_production_host", + "open_runtime_gate", + "add_action_button" + ], + "certbot_renew_authorized": false, + "config_id": "host110_ollama_proxy", + "control_tier": "C1", + "diff_acceptance_id": "public_gateway_rendered_diff_acceptance:host110_ollama_proxy", + "diff_gate_id": "public_gateway_rendered_diff_gate:host110_ollama_proxy", + "diff_scope_summary": "pending_owner_response_acceptance", + "dns_tls_probe_authorized": false, + "followup_owner": "pending_owner_response_acceptance", + "host": "192.168.0.110", + "live_path": "/etc/nginx/sites-enabled/110-ollama-proxy.conf", + "maintenance_window": "pending_owner_response_acceptance", + "maintenance_window_accepted": false, + "nginx_reload_authorized": false, + "nginx_reload_executed": false, + "nginx_test_authorized": false, + "nginx_test_evidence_accepted": false, + "nginx_test_evidence_received": false, + "nginx_test_evidence_ref": null, + "nginx_test_executed": false, + "nginx_test_operator": "pending_runtime_owner", + "nginx_test_result": "pending_owner_provided_readback", + "not_approval": true, + "outcome_lanes": [ + "waiting_owner_response_acceptance", + "waiting_rendered_diff_evidence", + "quarantine_raw_conf_or_payload", + "reject_secret_or_execution_request", + "request_evidence_supplement", + "ready_for_reviewer_acceptance", + "accepted_for_runtime_gate_planning", + "waiting_separate_runtime_approval" + ], + "owner_response_acceptance_id": "public_gateway_owner_response_acceptance:host110_ollama_proxy", + "owner_response_accepted": false, + "postcheck_evidence_accepted": false, + "postcheck_evidence_ref": null, + "production_write_authorized": false, + "redacted_export_accepted": false, + "redacted_live_conf_ref": null, + "rendered_diff_accepted": false, + "rendered_diff_hash_ref": null, + "rendered_diff_received": false, + "rendered_diff_ref": null, + "required_evidence_fields": [ + "redacted_live_conf_ref", + "rendered_diff_ref", + "rendered_diff_hash_ref", + "diff_scope_summary", + "affected_routes", + "nginx_test_evidence_ref", + "nginx_test_result", + "route_smoke_matrix_ref", + "route_smoke_result_ref", + "tls_acme_impact_ref", + "maintenance_window", + "rollback_owner", + "rollback_ref", + "postcheck_evidence_ref" + ], + "reviewer_checks": [ + "owner_response_accepted_first", + "redacted_live_conf_ref_only", + "rendered_diff_ref_not_payload", + "diff_scope_matches_config_id", + "nginx_test_evidence_is_readback_only", + "nginx_test_result_has_timestamp", + "route_smoke_matrix_complete", + "tls_acme_impact_separated", + "secret_value_absent", + "maintenance_window_present", + "rollback_owner_and_ref_present", + "postcheck_plan_present", + "no_execution_request_embedded", + "counts_transition_safe", + "action_button_absent" + ], + "reviewer_outcome": "waiting_owner_response_acceptance", + "rollback_owner": "pending_owner_response_acceptance", + "rollback_owner_accepted": false, + "rollback_ref": null, + "route_smoke_authorized": false, + "route_smoke_executed": false, + "route_smoke_matrix_accepted": false, + "route_smoke_matrix_received": false, + "route_smoke_matrix_ref": null, + "route_smoke_result_accepted": false, + "route_smoke_result_received": false, + "route_smoke_result_ref": null, + "runtime_gate": false, + "status": "waiting_owner_response_acceptance", + "tls_acme_impact_accepted": false, + "tls_acme_impact_ref": null + } + ], + "diff_acceptance_fields": [ + "diff_acceptance_id", + "owner_response_acceptance_id", + "diff_gate_id", + "config_id", + "control_tier", + "host", + "live_path", + "redacted_live_conf_ref", + "rendered_diff_ref", + "rendered_diff_hash_ref", + "diff_scope_summary", + "affected_routes", + "nginx_test_evidence_ref", + "nginx_test_operator", + "nginx_test_result", + "route_smoke_matrix_ref", + "route_smoke_result_ref", + "tls_acme_impact_ref", + "maintenance_window", + "rollback_owner", + "rollback_ref", + "postcheck_evidence_ref", + "reviewer_outcome", + "followup_owner", + "not_approval" + ], + "execution_boundaries": { + "action_buttons_allowed": false, + "certbot_renew_authorized": false, + "dns_tls_probe_authorized": false, + "nginx_reload_authorized": false, + "nginx_reload_executed": false, + "nginx_test_authorized": false, + "nginx_test_executed": false, + "not_authorization": true, + "production_write_authorized": false, + "read_live_conf_over_ssh": false, + "rendered_diff_accepted": false, + "route_smoke_authorized": false, + "route_smoke_executed": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "store_full_rendered_diff_payload": false, + "store_raw_live_conf": false + }, + "generated_at": "2026-06-14T23:58:00+08:00", + "git_commit": "22876ee1", + "next_steps": [ + "等待 owner response accepted;未 accepted 前不得驗收 rendered diff evidence。", + "收到 rendered diff / nginx test / route smoke evidence 後,先做 raw payload、secret、scope 與 route matrix 檢查。", + "evidence accepted 也只能進 runtime gate planning;`nginx -t`、reload、route smoke、DNS / TLS probe 與 production write 仍需獨立人工批准。" + ], + "outcome_lanes": [ + { + "lane_id": "waiting_owner_response_acceptance", + "meaning": "owner response 尚未 accepted,rendered diff evidence 不可驗收。" + }, + { + "lane_id": "waiting_rendered_diff_evidence", + "meaning": "等待 owner-provided rendered diff / nginx test / route smoke evidence ref。" + }, + { + "lane_id": "quarantine_raw_conf_or_payload", + "meaning": "收到 raw live conf、完整 diff payload 或不可保存內容時只能隔離。" + }, + { + "lane_id": "reject_secret_or_execution_request", + "meaning": "出現 secret value 或夾帶執行要求時直接拒收。" + }, + { + "lane_id": "request_evidence_supplement", + "meaning": "欄位不足、scope 不清或 route matrix 不完整時要求補件。" + }, + { + "lane_id": "ready_for_reviewer_acceptance", + "meaning": "metadata 合格後只進 reviewer acceptance,不自動執行。" + }, + { + "lane_id": "accepted_for_runtime_gate_planning", + "meaning": "即使 evidence accepted,也只可進下一層 runtime gate planning。" + }, + { + "lane_id": "waiting_separate_runtime_approval", + "meaning": "nginx -t、reload、route smoke、DNS / TLS probe 仍需獨立人工批准。" + } + ], + "required_evidence_fields": [ + "redacted_live_conf_ref", + "rendered_diff_ref", + "rendered_diff_hash_ref", + "diff_scope_summary", + "affected_routes", + "nginx_test_evidence_ref", + "nginx_test_result", + "route_smoke_matrix_ref", + "route_smoke_result_ref", + "tls_acme_impact_ref", + "maintenance_window", + "rollback_owner", + "rollback_ref", + "postcheck_evidence_ref" + ], + "reviewer_checks": [ + { + "check_id": "owner_response_accepted_first", + "instruction": "必須先有 owner response accepted record,否則不得驗收 rendered diff evidence。" + }, + { + "check_id": "redacted_live_conf_ref_only", + "instruction": "只能接受脫敏 live conf ref、hash 或 artifact pointer,不得收 raw conf。" + }, + { + "check_id": "rendered_diff_ref_not_payload", + "instruction": "rendered diff 必須是 ref / hash,不得把完整 diff payload 寫入 repo 或 LOGBOOK。" + }, + { + "check_id": "diff_scope_matches_config_id", + "instruction": "diff scope 必須對回 public gateway config_id 與 affected route 清冊。" + }, + { + "check_id": "nginx_test_evidence_is_readback_only", + "instruction": "nginx test evidence 只能是 owner 提供的 readback ref,不得由本工具執行 nginx -t。" + }, + { + "check_id": "nginx_test_result_has_timestamp", + "instruction": "nginx test result 需有時間、環境、操作者角色與結果摘要,但不得含 secret。" + }, + { + "check_id": "route_smoke_matrix_complete", + "instruction": "route smoke matrix 必須列 affected routes、預期 status、TLS / WebSocket / ACME checks。" + }, + { + "check_id": "tls_acme_impact_separated", + "instruction": "TLS / ACME 影響必須與 reload 決策分離,不能用 route smoke 取代 cert ownership。" + }, + { + "check_id": "secret_value_absent", + "instruction": "不得出現 token、cookie、private key、完整憑證內容或 secret derivative。" + }, + { + "check_id": "maintenance_window_present", + "instruction": "任何未來 runtime action 前都必須有維護窗口或明確禁止窗口。" + }, + { + "check_id": "rollback_owner_and_ref_present", + "instruction": "rollback owner 與 rollback ref 必須存在,且不可指向 raw secret。" + }, + { + "check_id": "postcheck_plan_present", + "instruction": "postcheck evidence ref 需描述 API、Web、WebSocket、ACME 或 affected route 的驗證結果。" + }, + { + "check_id": "no_execution_request_embedded", + "instruction": "payload 不得夾帶 reload、route change、DNS / TLS probe 或 certbot renew 要求。" + }, + { + "check_id": "counts_transition_safe", + "instruction": "只有 reviewer record 可更新 accepted / rejected;不得同時開 runtime gate。" + }, + { + "check_id": "action_button_absent", + "instruction": "前台與 AwoooP 只能顯示只讀狀態,不得新增執行按鈕。" + } + ], + "schema_version": "public_gateway_rendered_diff_acceptance_v1", + "source_owner_response_acceptance_schema_version": "public_gateway_owner_response_acceptance_v1", + "source_owner_response_acceptance_status": "owner_response_acceptance_ledger_ready_no_runtime_action", + "source_rendered_diff_gate_schema_version": "public_gateway_rendered_diff_gate_draft_v1", + "source_rendered_diff_gate_status": "rendered_diff_gate_draft_ready_no_runtime_action", + "status": "rendered_diff_acceptance_ledger_ready_no_runtime_action", + "summary": { + "action_button_count": 0, + "blocked_action_count": 22, + "c0_diff_acceptance_candidate_count": 2, + "c1_diff_acceptance_candidate_count": 1, + "certbot_renew_authorized_count": 0, + "diff_acceptance_candidate_count": 3, + "diff_acceptance_field_count": 25, + "dns_tls_probe_authorized_count": 0, + "maintenance_window_accepted_count": 0, + "nginx_reload_authorized_count": 0, + "nginx_reload_executed_count": 0, + "nginx_test_authorized_count": 0, + "nginx_test_evidence_accepted_count": 0, + "nginx_test_evidence_received_count": 0, + "nginx_test_executed_count": 0, + "outcome_lane_count": 8, + "owner_response_accepted_count": 0, + "postcheck_evidence_accepted_count": 0, + "redacted_export_accepted_count": 0, + "rendered_diff_accepted_count": 0, + "rendered_diff_received_count": 0, + "required_evidence_field_count": 14, + "reviewer_check_count": 15, + "rollback_owner_accepted_count": 0, + "route_smoke_authorized_count": 0, + "route_smoke_executed_count": 0, + "route_smoke_matrix_accepted_count": 0, + "route_smoke_matrix_received_count": 0, + "route_smoke_result_accepted_count": 0, + "route_smoke_result_received_count": 0, + "runtime_gate_count": 0, + "source_diff_gate_candidate_count": 3, + "source_owner_response_acceptance_candidate_count": 3, + "tls_acme_impact_accepted_count": 0 + } +} 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 0335c590b..5ed5bcd24 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 @@ -4888,6 +4888,19 @@ Trigger commit `f5cd37b7` 與 deploy marker `0ba92357` 已把 governance UI 的 **裁決:** 這是 owner response acceptance 只讀帳本,不是 owner response received / accepted、live gateway truth、rendered diff ready、`nginx -t`、Nginx reload、DNS / TLS probe、certbot renew、route smoke、host write、active scan、production write 或 runtime gate;IwoooS headline 仍維持 `64%`,active runtime gate 仍 `0`。 +### 2026-06-14 23:58 (台北) — §3.2 / §5 — 新增 Public Gateway Rendered Diff Acceptance 只讀帳本 — 把 Nginx diff / test / smoke evidence 驗收固定成 guard artifact + +**觸發**:使用者指出 Nginx 常被亂變動,單靠 owner response acceptance 仍不足以防止未來把 rendered diff、`nginx -t` readback 或 route smoke evidence 誤判成 reload / route change 授權。需要新增 evidence acceptance ledger,讓 owner-provided redacted live conf、rendered diff ref、nginx test evidence ref、route smoke matrix、TLS / ACME impact、maintenance window、rollback owner 與 post-check evidence 有固定驗收路徑。 + +**已推進:** +- 新增 `scripts/security/public-gateway-rendered-diff-acceptance.py`,讀取 Public Gateway rendered diff gate draft 與 owner response acceptance 兩份 snapshot,產生 3 份 rendered diff acceptance candidate。 +- 新增 `docs/security/public-gateway-rendered-diff-acceptance.snapshot.json`,固定 `diff_acceptance_candidate_count=3`、`c0_diff_acceptance_candidate_count=2`、`c1_diff_acceptance_candidate_count=1`、`required_evidence_field_count=14`、`reviewer_check_count=15`、`outcome_lane_count=8`、`blocked_action_count=22`。 +- 新增 `docs/security/PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md`,說明 evidence 欄位、reviewer checks、outcome lanes、blocked actions、完成度與 0 / false 邊界。 +- `security-mirror-progress-guard.py` 已鎖住新 snapshot 的 schema、summary、execution false flags、candidate ids、reviewer checks、outcome lanes、blocked actions 與每份候選 false flags。 +- 高價值配置 coverage、IwoooS 配置控管總清單、供應鏈進度、posture source paths 與前端 evidence marker 已同步;`nginx_public_gateway` 只讀治理成熟度從 `86%` 推進到 `88%`,但整體平均仍 `68%`,IwoooS headline 仍 `64%`,runtime gate 仍為 `0`。 + +**裁決:** 這是 rendered diff / nginx test / route smoke evidence 的只讀驗收帳本,不是 owner response accepted、redacted live conf accepted、rendered diff accepted、`nginx -t`、Nginx reload、route smoke、DNS / TLS probe、certbot renew、host write、active scan、production write 或 runtime gate;所有執行仍需獨立人工批准。 + ### 2026-06-14 23:55 (台北) — §3.2 / §5 — 新增 agent-bounty-protocol Owner Request Draft — 把 MCP / A2A / treasury 邊界轉成人工送件草稿 **觸發**:`agent-bounty-protocol` 已納入 IwoooS 只讀視野,但既有 handoff 仍停在 onboarding package。由於它包含外部 agent、MCP / A2A、cron / daemon、webhook、admin / treasury、claim / submit 與 payout / withdrawal 風險,需要先轉成 owner request draft,但仍不得讀 `.env`、改 repo、deploy、啟動 daemon、claim、submit、payout 或 send notification。 diff --git a/scripts/security/high-value-config-control-coverage.py b/scripts/security/high-value-config-control-coverage.py index 68da00d13..18730bd87 100644 --- a/scripts/security/high-value-config-control-coverage.py +++ b/scripts/security/high-value-config-control-coverage.py @@ -24,8 +24,8 @@ TAIPEI = timezone(timedelta(hours=8)) CONTROL_STATUS_BY_CATEGORY = { "nginx_public_gateway": { - "coverage_status": "owner_response_acceptance_ledger_ready_needs_owner_live_diff", - "coverage_percent": 86, + "coverage_status": "rendered_diff_acceptance_ledger_ready_needs_owner_live_diff", + "coverage_percent": 88, "evidence_refs": [ "docs/security/NGINX-CONFIG-DRIFT-DETECTOR.md", "docs/security/nginx-config-drift-repo.snapshot.json", @@ -34,10 +34,12 @@ CONTROL_STATUS_BY_CATEGORY = { "docs/security/public-gateway-preflight-inventory.snapshot.json", "docs/security/PUBLIC-GATEWAY-OWNER-RESPONSE-ACCEPTANCE.md", "docs/security/public-gateway-owner-response-acceptance.snapshot.json", + "docs/security/PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md", + "docs/security/public-gateway-rendered-diff-acceptance.snapshot.json", "docs/schemas/public_gateway_preflight_inventory_v1.schema.json", ], - "current_gap": "已固定 owner response acceptance 只讀帳本;owner response、live conf、rendered diff、nginx -t、route smoke、maintenance window 與 rollback owner 仍全部為 0。", - "next_owner_action": "補 public gateway owner 回覆、owner-provided live conf、source-to-live rendered diff、nginx -t evidence、route smoke、maintenance window 與 rollback owner。", + "current_gap": "已固定 owner response acceptance 與 rendered diff evidence acceptance 只讀帳本;owner response、live conf、rendered diff evidence、nginx -t evidence、route smoke evidence、maintenance window 與 rollback owner 仍全部為 0。", + "next_owner_action": "補 public gateway owner 回覆、owner-provided live conf、source-to-live rendered diff ref、nginx -t evidence ref、route smoke evidence ref、maintenance window 與 rollback owner。", }, "dns_tls_certbot": { "coverage_status": "owner_response_acceptance_ledger_ready_needs_certificate_owner_evidence", @@ -323,6 +325,7 @@ def build_report(root: Path, generated_at: str | None) -> dict[str, Any]: "repo_only_inventory_ready_needs_live_route_evidence", "repo_only_preflight_contract_ready_needs_owner_live_diff", "owner_response_acceptance_ledger_ready_needs_owner_live_diff", + "rendered_diff_acceptance_ledger_ready_needs_owner_live_diff", "owner_response_acceptance_ledger_ready_needs_runtime_evidence", "owner_response_acceptance_ledger_ready_needs_restore_drill_owner", "owner_response_acceptance_ledger_ready_needs_network_owner", diff --git a/scripts/security/public-gateway-rendered-diff-acceptance.py b/scripts/security/public-gateway-rendered-diff-acceptance.py new file mode 100644 index 000000000..d287634c8 --- /dev/null +++ b/scripts/security/public-gateway-rendered-diff-acceptance.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python3 +""" +IwoooS Public Gateway rendered diff acceptance 只讀帳本產生器。 + +本工具讀取 Public Gateway rendered diff gate draft 與 owner response +acceptance snapshot,定義未來 rendered diff、nginx test evidence 與 route +smoke evidence 如何被收件、補件、隔離或拒收。它不讀 live conf、不產生 +diff、不執行 nginx -t、不 reload、不做 route smoke、不連 DNS / TLS。 +""" + +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)) + +ACCEPTANCE_FIELDS = [ + "diff_acceptance_id", + "owner_response_acceptance_id", + "diff_gate_id", + "config_id", + "control_tier", + "host", + "live_path", + "redacted_live_conf_ref", + "rendered_diff_ref", + "rendered_diff_hash_ref", + "diff_scope_summary", + "affected_routes", + "nginx_test_evidence_ref", + "nginx_test_operator", + "nginx_test_result", + "route_smoke_matrix_ref", + "route_smoke_result_ref", + "tls_acme_impact_ref", + "maintenance_window", + "rollback_owner", + "rollback_ref", + "postcheck_evidence_ref", + "reviewer_outcome", + "followup_owner", + "not_approval", +] + +REQUIRED_EVIDENCE_FIELDS = [ + "redacted_live_conf_ref", + "rendered_diff_ref", + "rendered_diff_hash_ref", + "diff_scope_summary", + "affected_routes", + "nginx_test_evidence_ref", + "nginx_test_result", + "route_smoke_matrix_ref", + "route_smoke_result_ref", + "tls_acme_impact_ref", + "maintenance_window", + "rollback_owner", + "rollback_ref", + "postcheck_evidence_ref", +] + +REVIEWER_CHECKS = [ + { + "check_id": "owner_response_accepted_first", + "instruction": "必須先有 owner response accepted record,否則不得驗收 rendered diff evidence。", + }, + { + "check_id": "redacted_live_conf_ref_only", + "instruction": "只能接受脫敏 live conf ref、hash 或 artifact pointer,不得收 raw conf。", + }, + { + "check_id": "rendered_diff_ref_not_payload", + "instruction": "rendered diff 必須是 ref / hash,不得把完整 diff payload 寫入 repo 或 LOGBOOK。", + }, + { + "check_id": "diff_scope_matches_config_id", + "instruction": "diff scope 必須對回 public gateway config_id 與 affected route 清冊。", + }, + { + "check_id": "nginx_test_evidence_is_readback_only", + "instruction": "nginx test evidence 只能是 owner 提供的 readback ref,不得由本工具執行 nginx -t。", + }, + { + "check_id": "nginx_test_result_has_timestamp", + "instruction": "nginx test result 需有時間、環境、操作者角色與結果摘要,但不得含 secret。", + }, + { + "check_id": "route_smoke_matrix_complete", + "instruction": "route smoke matrix 必須列 affected routes、預期 status、TLS / WebSocket / ACME checks。", + }, + { + "check_id": "tls_acme_impact_separated", + "instruction": "TLS / ACME 影響必須與 reload 決策分離,不能用 route smoke 取代 cert ownership。", + }, + { + "check_id": "secret_value_absent", + "instruction": "不得出現 token、cookie、private key、完整憑證內容或 secret derivative。", + }, + { + "check_id": "maintenance_window_present", + "instruction": "任何未來 runtime action 前都必須有維護窗口或明確禁止窗口。", + }, + { + "check_id": "rollback_owner_and_ref_present", + "instruction": "rollback owner 與 rollback ref 必須存在,且不可指向 raw secret。", + }, + { + "check_id": "postcheck_plan_present", + "instruction": "postcheck evidence ref 需描述 API、Web、WebSocket、ACME 或 affected route 的驗證結果。", + }, + { + "check_id": "no_execution_request_embedded", + "instruction": "payload 不得夾帶 reload、route change、DNS / TLS probe 或 certbot renew 要求。", + }, + { + "check_id": "counts_transition_safe", + "instruction": "只有 reviewer record 可更新 accepted / rejected;不得同時開 runtime gate。", + }, + { + "check_id": "action_button_absent", + "instruction": "前台與 AwoooP 只能顯示只讀狀態,不得新增執行按鈕。", + }, +] + +OUTCOME_LANES = [ + { + "lane_id": "waiting_owner_response_acceptance", + "meaning": "owner response 尚未 accepted,rendered diff evidence 不可驗收。", + }, + { + "lane_id": "waiting_rendered_diff_evidence", + "meaning": "等待 owner-provided rendered diff / nginx test / route smoke evidence ref。", + }, + { + "lane_id": "quarantine_raw_conf_or_payload", + "meaning": "收到 raw live conf、完整 diff payload 或不可保存內容時只能隔離。", + }, + { + "lane_id": "reject_secret_or_execution_request", + "meaning": "出現 secret value 或夾帶執行要求時直接拒收。", + }, + { + "lane_id": "request_evidence_supplement", + "meaning": "欄位不足、scope 不清或 route matrix 不完整時要求補件。", + }, + { + "lane_id": "ready_for_reviewer_acceptance", + "meaning": "metadata 合格後只進 reviewer acceptance,不自動執行。", + }, + { + "lane_id": "accepted_for_runtime_gate_planning", + "meaning": "即使 evidence accepted,也只可進下一層 runtime gate planning。", + }, + { + "lane_id": "waiting_separate_runtime_approval", + "meaning": "nginx -t、reload、route smoke、DNS / TLS probe 仍需獨立人工批准。", + }, +] + +BLOCKED_ACTIONS = [ + "read_live_conf_over_ssh", + "store_raw_live_conf", + "store_full_rendered_diff_payload", + "accept_unredacted_live_conf", + "collect_secret_value", + "accept_execution_request_inside_evidence", + "mark_rendered_diff_accepted_without_owner_response", + "mark_rendered_diff_accepted_without_reviewer_record", + "run_nginx_test_from_diff_acceptance", + "run_route_smoke_from_diff_acceptance", + "nginx_reload_from_diff_acceptance", + "dns_probe_from_diff_acceptance", + "tls_probe_from_diff_acceptance", + "certbot_renew_from_diff_acceptance", + "modify_nginx_conf", + "modify_dns_tls_config", + "change_public_route", + "change_admin_route", + "change_websocket_route", + "write_production_host", + "open_runtime_gate", + "add_action_button", +] + + +def git_short_sha(root: Path) -> str: + try: + result = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + cwd=root, + check=True, + capture_output=True, + text=True, + ) + return result.stdout.strip() + except Exception: + return "unknown" + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def candidate_from_owner_acceptance(item: dict[str, Any]) -> dict[str, Any]: + config_id = item["config_id"] + return { + "diff_acceptance_id": f"public_gateway_rendered_diff_acceptance:{config_id}", + "status": "waiting_owner_response_acceptance", + "owner_response_acceptance_id": item["acceptance_candidate_id"], + "diff_gate_id": item["diff_gate_id"], + "config_id": config_id, + "control_tier": item["control_tier"], + "host": item["host"], + "live_path": item["live_path"], + "redacted_live_conf_ref": None, + "rendered_diff_ref": None, + "rendered_diff_hash_ref": None, + "diff_scope_summary": "pending_owner_response_acceptance", + "affected_routes": [], + "nginx_test_evidence_ref": None, + "nginx_test_operator": "pending_runtime_owner", + "nginx_test_result": "pending_owner_provided_readback", + "route_smoke_matrix_ref": None, + "route_smoke_result_ref": None, + "tls_acme_impact_ref": None, + "maintenance_window": "pending_owner_response_acceptance", + "rollback_owner": "pending_owner_response_acceptance", + "rollback_ref": None, + "postcheck_evidence_ref": None, + "reviewer_outcome": "waiting_owner_response_acceptance", + "followup_owner": "pending_owner_response_acceptance", + "acceptance_fields": ACCEPTANCE_FIELDS, + "required_evidence_fields": REQUIRED_EVIDENCE_FIELDS, + "reviewer_checks": [item["check_id"] for item in REVIEWER_CHECKS], + "outcome_lanes": [item["lane_id"] for item in OUTCOME_LANES], + "blocked_actions": BLOCKED_ACTIONS, + "not_approval": True, + "owner_response_accepted": False, + "redacted_export_accepted": False, + "rendered_diff_received": False, + "rendered_diff_accepted": False, + "nginx_test_evidence_received": False, + "nginx_test_evidence_accepted": False, + "route_smoke_matrix_received": False, + "route_smoke_matrix_accepted": False, + "route_smoke_result_received": False, + "route_smoke_result_accepted": False, + "tls_acme_impact_accepted": False, + "maintenance_window_accepted": False, + "rollback_owner_accepted": False, + "postcheck_evidence_accepted": False, + "nginx_test_authorized": False, + "nginx_test_executed": False, + "nginx_reload_authorized": False, + "nginx_reload_executed": False, + "route_smoke_authorized": False, + "route_smoke_executed": False, + "dns_tls_probe_authorized": False, + "certbot_renew_authorized": False, + "production_write_authorized": False, + "runtime_gate": False, + "action_buttons_allowed": False, + } + + +def build_report( + root: Path, + diff_gate_report: dict[str, Any], + owner_acceptance_report: dict[str, Any], + generated_at: str | None, +) -> dict[str, Any]: + report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds") + owner_candidates = owner_acceptance_report.get("acceptance_candidates", []) + acceptance_candidates = [candidate_from_owner_acceptance(item) for item in owner_candidates] + c0_candidates = [item for item in acceptance_candidates if item["control_tier"] == "C0"] + c1_candidates = [item for item in acceptance_candidates if item["control_tier"] == "C1"] + + return { + "schema_version": "public_gateway_rendered_diff_acceptance_v1", + "generated_at": report_time, + "git_commit": git_short_sha(root), + "source_rendered_diff_gate_schema_version": diff_gate_report.get("schema_version"), + "source_rendered_diff_gate_status": diff_gate_report.get("status"), + "source_owner_response_acceptance_schema_version": owner_acceptance_report.get("schema_version"), + "source_owner_response_acceptance_status": owner_acceptance_report.get("status"), + "status": "rendered_diff_acceptance_ledger_ready_no_runtime_action", + "summary": { + "source_diff_gate_candidate_count": diff_gate_report.get("summary", {}).get( + "diff_gate_candidate_count", 0 + ), + "source_owner_response_acceptance_candidate_count": owner_acceptance_report.get( + "summary", {} + ).get("acceptance_candidate_count", 0), + "diff_acceptance_candidate_count": len(acceptance_candidates), + "c0_diff_acceptance_candidate_count": len(c0_candidates), + "c1_diff_acceptance_candidate_count": len(c1_candidates), + "diff_acceptance_field_count": len(ACCEPTANCE_FIELDS), + "required_evidence_field_count": len(REQUIRED_EVIDENCE_FIELDS), + "reviewer_check_count": len(REVIEWER_CHECKS), + "outcome_lane_count": len(OUTCOME_LANES), + "blocked_action_count": len(BLOCKED_ACTIONS), + "owner_response_accepted_count": 0, + "redacted_export_accepted_count": 0, + "rendered_diff_received_count": 0, + "rendered_diff_accepted_count": 0, + "nginx_test_evidence_received_count": 0, + "nginx_test_evidence_accepted_count": 0, + "route_smoke_matrix_received_count": 0, + "route_smoke_matrix_accepted_count": 0, + "route_smoke_result_received_count": 0, + "route_smoke_result_accepted_count": 0, + "tls_acme_impact_accepted_count": 0, + "maintenance_window_accepted_count": 0, + "rollback_owner_accepted_count": 0, + "postcheck_evidence_accepted_count": 0, + "nginx_test_authorized_count": 0, + "nginx_test_executed_count": 0, + "nginx_reload_authorized_count": 0, + "nginx_reload_executed_count": 0, + "route_smoke_authorized_count": 0, + "route_smoke_executed_count": 0, + "dns_tls_probe_authorized_count": 0, + "certbot_renew_authorized_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + }, + "execution_boundaries": { + "read_live_conf_over_ssh": False, + "store_raw_live_conf": False, + "store_full_rendered_diff_payload": False, + "secret_value_collection_allowed": False, + "rendered_diff_accepted": False, + "nginx_test_authorized": False, + "nginx_test_executed": False, + "nginx_reload_authorized": False, + "nginx_reload_executed": False, + "route_smoke_authorized": False, + "route_smoke_executed": False, + "dns_tls_probe_authorized": False, + "certbot_renew_authorized": False, + "production_write_authorized": False, + "runtime_execution_authorized": False, + "action_buttons_allowed": False, + "not_authorization": True, + }, + "diff_acceptance_fields": ACCEPTANCE_FIELDS, + "required_evidence_fields": REQUIRED_EVIDENCE_FIELDS, + "reviewer_checks": REVIEWER_CHECKS, + "outcome_lanes": OUTCOME_LANES, + "blocked_actions": BLOCKED_ACTIONS, + "diff_acceptance_candidates": acceptance_candidates, + "next_steps": [ + "等待 owner response accepted;未 accepted 前不得驗收 rendered diff evidence。", + "收到 rendered diff / nginx test / route smoke evidence 後,先做 raw payload、secret、scope 與 route matrix 檢查。", + "evidence accepted 也只能進 runtime gate planning;`nginx -t`、reload、route smoke、DNS / TLS probe 與 production write 仍需獨立人工批准。", + ], + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS Public Gateway rendered diff acceptance 只讀帳本產生器") + parser.add_argument("--root", default=".", help="repo root") + parser.add_argument( + "--rendered-diff-gate-report", + default="docs/security/public-gateway-rendered-diff-gate-draft.snapshot.json", + help="public-gateway-rendered-diff-gate-draft.py 輸出的 JSON", + ) + parser.add_argument( + "--owner-response-acceptance-report", + default="docs/security/public-gateway-owner-response-acceptance.snapshot.json", + help="public-gateway-owner-response-acceptance.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() + diff_gate_report = load_json(root / args.rendered_diff_gate_report) + owner_acceptance_report = load_json(root / args.owner_response_acceptance_report) + report = build_report(root, diff_gate_report, owner_acceptance_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( + "PUBLIC_GATEWAY_RENDERED_DIFF_ACCEPTANCE_OK " + f"candidates={summary['diff_acceptance_candidate_count']} " + f"c0={summary['c0_diff_acceptance_candidate_count']} " + f"checks={summary['reviewer_check_count']} " + f"lanes={summary['outcome_lane_count']} " + f"accepted={summary['rendered_diff_accepted_count']} " + f"runtime_gate={summary['runtime_gate_count']}", + file=sys.stderr, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 200c0ee4f..8118e6923 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -155,6 +155,9 @@ def validate(root: Path) -> None: public_gateway_owner_response_acceptance = load_json( security_dir / "public-gateway-owner-response-acceptance.snapshot.json" ) + public_gateway_rendered_diff_acceptance = load_json( + security_dir / "public-gateway-rendered-diff-acceptance.snapshot.json" + ) s49_request_draft = load_json(security_dir / "gitea-inventory-owner-attestation-request-draft.snapshot.json") kali_status = load_json(security_dir / "kali-integration-status.snapshot.json") iwooos_projection_page = ( @@ -2763,18 +2766,20 @@ def validate(root: Path) -> None: assert_equal( "high_value_config_coverage.coverage_categories.nginx.coverage_percent", nginx_gateway_category["coverage_percent"], - 86, + 88, ) assert_equal( "high_value_config_coverage.coverage_categories.nginx.coverage_status", nginx_gateway_category["coverage_status"], - "owner_response_acceptance_ledger_ready_needs_owner_live_diff", + "rendered_diff_acceptance_ledger_ready_needs_owner_live_diff", ) for evidence_ref in [ "docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md", "docs/security/public-gateway-preflight-inventory.snapshot.json", "docs/security/PUBLIC-GATEWAY-OWNER-RESPONSE-ACCEPTANCE.md", "docs/security/public-gateway-owner-response-acceptance.snapshot.json", + "docs/security/PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md", + "docs/security/public-gateway-rendered-diff-acceptance.snapshot.json", "docs/schemas/public_gateway_preflight_inventory_v1.schema.json", ]: assert_contains( @@ -5253,11 +5258,220 @@ def validate(root: Path) -> None: f"public_gateway_owner_response_acceptance.{item['acceptance_candidate_id']}.{false_key}", item[false_key], ) + assert_equal( + "public_gateway_rendered_diff_acceptance.schema", + public_gateway_rendered_diff_acceptance["schema_version"], + "public_gateway_rendered_diff_acceptance_v1", + ) + assert_equal( + "public_gateway_rendered_diff_acceptance.source_rendered_diff_gate_schema_version", + public_gateway_rendered_diff_acceptance["source_rendered_diff_gate_schema_version"], + "public_gateway_rendered_diff_gate_draft_v1", + ) + assert_equal( + "public_gateway_rendered_diff_acceptance.source_owner_response_acceptance_schema_version", + public_gateway_rendered_diff_acceptance["source_owner_response_acceptance_schema_version"], + "public_gateway_owner_response_acceptance_v1", + ) + assert_equal( + "public_gateway_rendered_diff_acceptance.status", + public_gateway_rendered_diff_acceptance["status"], + "rendered_diff_acceptance_ledger_ready_no_runtime_action", + ) + expected_public_gateway_rendered_diff_acceptance_summary = { + "source_diff_gate_candidate_count": 3, + "source_owner_response_acceptance_candidate_count": 3, + "diff_acceptance_candidate_count": 3, + "c0_diff_acceptance_candidate_count": 2, + "c1_diff_acceptance_candidate_count": 1, + "diff_acceptance_field_count": 25, + "required_evidence_field_count": 14, + "reviewer_check_count": 15, + "outcome_lane_count": 8, + "blocked_action_count": 22, + "owner_response_accepted_count": 0, + "redacted_export_accepted_count": 0, + "rendered_diff_received_count": 0, + "rendered_diff_accepted_count": 0, + "nginx_test_evidence_received_count": 0, + "nginx_test_evidence_accepted_count": 0, + "route_smoke_matrix_received_count": 0, + "route_smoke_matrix_accepted_count": 0, + "route_smoke_result_received_count": 0, + "route_smoke_result_accepted_count": 0, + "tls_acme_impact_accepted_count": 0, + "maintenance_window_accepted_count": 0, + "rollback_owner_accepted_count": 0, + "postcheck_evidence_accepted_count": 0, + "nginx_test_authorized_count": 0, + "nginx_test_executed_count": 0, + "nginx_reload_authorized_count": 0, + "nginx_reload_executed_count": 0, + "route_smoke_authorized_count": 0, + "route_smoke_executed_count": 0, + "dns_tls_probe_authorized_count": 0, + "certbot_renew_authorized_count": 0, + "runtime_gate_count": 0, + "action_button_count": 0, + } + for key, expected in expected_public_gateway_rendered_diff_acceptance_summary.items(): + assert_equal( + f"public_gateway_rendered_diff_acceptance.summary.{key}", + public_gateway_rendered_diff_acceptance["summary"][key], + expected, + ) + for key, value in public_gateway_rendered_diff_acceptance["execution_boundaries"].items(): + if key == "not_authorization": + assert_true(f"public_gateway_rendered_diff_acceptance.execution_boundaries.{key}", value) + else: + assert_false(f"public_gateway_rendered_diff_acceptance.execution_boundaries.{key}", value) + expected_public_gateway_rendered_diff_acceptance_ids = [ + "public_gateway_rendered_diff_acceptance:host188_all_sites", + "public_gateway_rendered_diff_acceptance:host188_internal_tools_https", + "public_gateway_rendered_diff_acceptance:host110_ollama_proxy", + ] + assert_equal( + "public_gateway_rendered_diff_acceptance.diff_acceptance_ids", + [ + item["diff_acceptance_id"] + for item in public_gateway_rendered_diff_acceptance["diff_acceptance_candidates"] + ], + expected_public_gateway_rendered_diff_acceptance_ids, + ) + expected_public_gateway_rendered_diff_acceptance_reviewer_checks = [ + "owner_response_accepted_first", + "redacted_live_conf_ref_only", + "rendered_diff_ref_not_payload", + "diff_scope_matches_config_id", + "nginx_test_evidence_is_readback_only", + "nginx_test_result_has_timestamp", + "route_smoke_matrix_complete", + "tls_acme_impact_separated", + "secret_value_absent", + "maintenance_window_present", + "rollback_owner_and_ref_present", + "postcheck_plan_present", + "no_execution_request_embedded", + "counts_transition_safe", + "action_button_absent", + ] + assert_equal( + "public_gateway_rendered_diff_acceptance.reviewer_check_ids", + [item["check_id"] for item in public_gateway_rendered_diff_acceptance["reviewer_checks"]], + expected_public_gateway_rendered_diff_acceptance_reviewer_checks, + ) + expected_public_gateway_rendered_diff_acceptance_outcome_lanes = [ + "waiting_owner_response_acceptance", + "waiting_rendered_diff_evidence", + "quarantine_raw_conf_or_payload", + "reject_secret_or_execution_request", + "request_evidence_supplement", + "ready_for_reviewer_acceptance", + "accepted_for_runtime_gate_planning", + "waiting_separate_runtime_approval", + ] + assert_equal( + "public_gateway_rendered_diff_acceptance.outcome_lane_ids", + [item["lane_id"] for item in public_gateway_rendered_diff_acceptance["outcome_lanes"]], + expected_public_gateway_rendered_diff_acceptance_outcome_lanes, + ) + expected_public_gateway_rendered_diff_acceptance_blocked_actions = [ + "read_live_conf_over_ssh", + "store_raw_live_conf", + "store_full_rendered_diff_payload", + "accept_unredacted_live_conf", + "collect_secret_value", + "accept_execution_request_inside_evidence", + "mark_rendered_diff_accepted_without_owner_response", + "mark_rendered_diff_accepted_without_reviewer_record", + "run_nginx_test_from_diff_acceptance", + "run_route_smoke_from_diff_acceptance", + "nginx_reload_from_diff_acceptance", + "dns_probe_from_diff_acceptance", + "tls_probe_from_diff_acceptance", + "certbot_renew_from_diff_acceptance", + "modify_nginx_conf", + "modify_dns_tls_config", + "change_public_route", + "change_admin_route", + "change_websocket_route", + "write_production_host", + "open_runtime_gate", + "add_action_button", + ] + assert_equal( + "public_gateway_rendered_diff_acceptance.blocked_actions", + public_gateway_rendered_diff_acceptance["blocked_actions"], + expected_public_gateway_rendered_diff_acceptance_blocked_actions, + ) + for item in public_gateway_rendered_diff_acceptance["diff_acceptance_candidates"]: + assert_equal( + f"public_gateway_rendered_diff_acceptance.{item['diff_acceptance_id']}.field_count", + len(item["acceptance_fields"]), + 25, + ) + assert_equal( + f"public_gateway_rendered_diff_acceptance.{item['diff_acceptance_id']}.required_evidence_fields", + len(item["required_evidence_fields"]), + 14, + ) + assert_equal( + f"public_gateway_rendered_diff_acceptance.{item['diff_acceptance_id']}.reviewer_checks", + len(item["reviewer_checks"]), + 15, + ) + assert_equal( + f"public_gateway_rendered_diff_acceptance.{item['diff_acceptance_id']}.outcome_lanes", + len(item["outcome_lanes"]), + 8, + ) + assert_equal( + f"public_gateway_rendered_diff_acceptance.{item['diff_acceptance_id']}.blocked_actions", + len(item["blocked_actions"]), + 22, + ) + assert_true( + f"public_gateway_rendered_diff_acceptance.{item['diff_acceptance_id']}.not_approval", + item["not_approval"], + ) + for false_key in [ + "owner_response_accepted", + "redacted_export_accepted", + "rendered_diff_received", + "rendered_diff_accepted", + "nginx_test_evidence_received", + "nginx_test_evidence_accepted", + "route_smoke_matrix_received", + "route_smoke_matrix_accepted", + "route_smoke_result_received", + "route_smoke_result_accepted", + "tls_acme_impact_accepted", + "maintenance_window_accepted", + "rollback_owner_accepted", + "postcheck_evidence_accepted", + "nginx_test_authorized", + "nginx_test_executed", + "nginx_reload_authorized", + "nginx_reload_executed", + "route_smoke_authorized", + "route_smoke_executed", + "dns_tls_probe_authorized", + "certbot_renew_authorized", + "production_write_authorized", + "runtime_gate", + "action_buttons_allowed", + ]: + assert_false( + f"public_gateway_rendered_diff_acceptance.{item['diff_acceptance_id']}.{false_key}", + item[false_key], + ) for source_path in [ "docs/security/public-gateway-preflight-inventory.snapshot.json", "docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md", "docs/security/public-gateway-owner-response-acceptance.snapshot.json", "docs/security/PUBLIC-GATEWAY-OWNER-RESPONSE-ACCEPTANCE.md", + "docs/security/public-gateway-rendered-diff-acceptance.snapshot.json", + "docs/security/PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md", "docs/schemas/public_gateway_preflight_inventory_v1.schema.json", "docs/security/k8s-argocd-owner-response-acceptance.snapshot.json", "docs/security/K8S-ARGOCD-OWNER-RESPONSE-ACCEPTANCE.md", @@ -14949,7 +15163,10 @@ def validate(root: Path) -> None: "high_value_config_control_coverage_owner_response_accepted_count=0", "high_value_config_control_coverage_runtime_gate_count=0", "high_value_config_control_coverage_action_button_count=0", - "nginx_public_gateway_coverage_percent=84", + "nginx_public_gateway_coverage_percent=88", + "public_gateway_rendered_diff_acceptance_candidate_count=3", + "public_gateway_rendered_diff_acceptance_reviewer_check_count=15", + "public_gateway_rendered_diff_acceptance_runtime_gate_count=0", "public_gateway_preflight_inventory_source_config_count=3", "public_gateway_preflight_inventory_route_impact_count=14", "public_gateway_preflight_inventory_preflight_gate_count=12", @@ -15048,6 +15265,40 @@ def validate(root: Path) -> None: iwooos_projection_page, text, ) + assert_text_contains( + "iwooos_page.critical_config_priority_testid", + iwooos_projection_page, + 'data-testid="iwooos-critical-config-priority-board"', + ) + assert_text_contains( + "iwooos_page.critical_config_priority_nginx_percent", + iwooos_projection_page, + "{ key: 'nginxGateway', rank: 'P0-1', state: '88% / live 0'", + ) + for text in [ + "critical_config_priority_frontstage_summary_count=4", + "critical_config_priority_item_count=6", + "critical_config_priority_p0_category_count=6", + "critical_config_priority_owner_response_received_count=0", + "critical_config_priority_owner_response_accepted_count=0", + "critical_config_priority_live_evidence_received_count=0", + "critical_config_priority_runtime_gate_count=0", + "critical_config_priority_action_button_count=0", + "nginx_public_gateway_owner_response_received=false", + "nginx_public_gateway_live_conf_evidence_received=false", + "nginx_public_gateway_rendered_diff_accepted=false", + "nginx_public_gateway_rendered_diff_acceptance_candidate_count=3", + "nginx_public_gateway_nginx_test_evidence_count=0", + "runtime_execution_authorized=false", + "active_runtime_gate_count=0", + "action_buttons_allowed=false", + "not_authorization=true", + ]: + assert_text_contains( + "iwooos_page.critical_config_priority_boundary", + iwooos_projection_page, + text, + ) assert_contains( "web_messages.zh-TW.iwooos.highValueConfigControlCoverage", list(web_messages_zh["iwooos"].keys()),