diff --git a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md index 235020f33..f47243c71 100644 --- a/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md +++ b/docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md @@ -86,6 +86,14 @@ 固定數字為 `change_evidence_candidate_count=14`、`write_capable_change_evidence_candidate_count=6`、`policy_or_exposure_candidate_count=5`、`change_evidence_field_count=32`、`required_evidence_field_count=16`、`reviewer_check_count=16`、`outcome_lane_count=8`、`blocked_action_count=24`、`change_evidence_received_count=0`、`change_evidence_accepted_count=0`、`firewall_change_authorized_count=0`、`port_close_authorized_count=0`、`port_open_authorized_count=0`、`runtime_gate_count=0`。此更新讓 `ssh_firewall_network_access` 從 `58%` 推進到 `60%`;它只補齊端口 / 防火牆變更的 actor、before / after state、service dependency、customer impact、cross-project sync、rollback 與 post-check evidence 驗收規則,不代表 SSH、live firewall read、firewall change、port close / open、route smoke、host restart、production write 或 runtime gate 已授權。 +## 1.8 2026-06-15 高價值配置集中 Guard + +已新增 `docs/security/IWOOOS-CONFIG-CONTROL-GUARD.md` 與 `scripts/security/iwooos-config-control-guard.py`,把 14 類高價值配置、C0 類別、evidence refs、owner response / change evidence 帳本、supply-chain contract manifest 與 `0 / false` 邊界串成集中驗證。 + +此 guard 已由 `security-mirror-progress-guard.py` 呼叫。後續只要跑主進度 guard,就會同步檢查 Nginx、DNS / TLS、K8s / ArgoCD、Secrets / runner、Public runtime、SSH / firewall、Backup / DR、Monitoring、agent-bounty-protocol 與 supply-chain contract 的 repo snapshot 基線。 + +此更新只讓「配置控管集中驗證」從 `0%` 推進到 `100%`;owner response 收件、live evidence、Nginx reload、firewall change、workflow / runner / secret 變更、backup / restore、active scan、agent-bounty runtime 與 production deploy 仍全部維持 `0 / false`。 + ## 2. 覆蓋摘要 | 指標 | 目前值 | 說明 | @@ -223,12 +231,19 @@ python3 scripts/security/high-value-config-control-coverage.py \ --output docs/security/high-value-config-control-coverage.snapshot.json ``` +集中驗證 guard: + +```bash +python3 scripts/security/iwooos-config-control-guard.py --root . +``` + ## 7. 完成度 | 工作 | 完成度 | 說明 | |------|--------|------| | 全高價值配置類別註冊 | `100%` | 14 類全部來自既有 Gate 定義 | | 覆蓋 snapshot / schema | `100%` | 已新增可重跑 snapshot 與 JSON schema | +| 高價值配置集中 guard | `100%` | 已新增 `scripts/security/iwooos-config-control-guard.py`,並串接 `security-mirror-progress-guard.py`;14 類配置、主要 owner / change evidence 帳本、supply-chain manifest 與 `0 / false` 邊界可集中驗證 | | DNS / TLS / certbot owner response acceptance | `100%` | 已新增 `domain_tls_certbot_owner_response_acceptance_v1`,4 個 C0 candidate、13 個 owner 必填欄位、13 個 reviewer checks、7 條 outcome lanes、20 類 blocked action;成熟度 `74% -> 78%` | | Docker / systemd / host service owner response acceptance | `100%` | 已新增 `host_service_owner_response_acceptance_v1`,9 個 candidate、3 個 write-capable、14 個 reviewer checks、7 條 outcome lanes、20 類 blocked action;成熟度 `50% -> 54%` | | CD / Runner / Secret injection change evidence acceptance | `100%` | 已新增 `cd_runner_secret_injection_change_evidence_acceptance_v1`,5 個 candidate、4 個 C0、19 個 reviewer checks、8 條 outcome lanes、32 類 blocked action;secret metadata 成熟度 `66% -> 68%`,workflow / runner 成熟度 `70% -> 72%` | diff --git a/docs/security/IWOOOS-CONFIG-CONTROL-GUARD.md b/docs/security/IWOOOS-CONFIG-CONTROL-GUARD.md new file mode 100644 index 000000000..57a36e8bf --- /dev/null +++ b/docs/security/IWOOOS-CONFIG-CONTROL-GUARD.md @@ -0,0 +1,80 @@ +# IwoooS 高價值配置控管 Guard + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-06-15 | +| 狀態 | `repo_snapshot_guard_ready` | +| 腳本 | `scripts/security/iwooos-config-control-guard.py` | +| 模式 | repo snapshot only,不連線主機、不讀 secret、不做 runtime 動作 | +| runtime gate | `0` | + +## 1. 目的 + +此 guard 將「所有重要配置都要被資安控管」從文件盤點推進成可重複執行的驗證基線。它讀取既有 Markdown 與 JSON snapshot,確認下列配置面都有只讀控管帳本、owner gate、拒收條件與 `0 / false` 邊界: + +| 類別 | 代表配置面 | +|------|------------| +| Public gateway | Nginx、reverse proxy、公開 route、rendered diff、`nginx -t` 證據收件規則 | +| DNS / TLS | certbot、certificate path、ACME route、renewal owner | +| K8s / ArgoCD | production manifest、GitOps change evidence、rollback revision | +| Secrets / Runner | workflow、runner attestation、secret name parity、injection route | +| Runtime config | public / admin / API route、CORS、frontend env、Sentry tunnel、webhook / callback | +| Network | SSH、sudoers、known_hosts、防火牆、NodePort、WireGuard | +| Backup / DR | backup、restore、offsite、escrow、retention、Velero | +| Monitoring | Prometheus、Alertmanager、Grafana、SigNoz、Sentry、Langfuse、Telegram route | +| Cross-product | VibeWork、agent-bounty-protocol、StockPlatform、Bitan、Tsenyang 等產品邊界 | + +## 2. 驗證內容 + +`iwooos-config-control-guard.py` 目前固定檢查: + +1. `high-value-config-control-coverage.snapshot.json` 必須有 14 類配置,C0 類別 8 個,owner response required 14 個,owner response received / accepted 為 `0 / 0`,runtime gate 與 action button 為 `0`。 +2. 每個高價值配置類別的 evidence refs 必須能在 repo 中找到對應文件、snapshot、schema、腳本或 source path。 +3. Public gateway、DNS / TLS、Docker / systemd、SSH / firewall、Backup / restore、K8s / ArgoCD、CD / runner / secret、Public runtime、Monitoring、agent-bounty-protocol 等帳本必須符合既定 schema、status、candidate count、reviewer checks、outcome lanes 與 blocked actions。 +4. 各帳本 summary 中的 `*_authorized_count`、`*_executed_count`、`*_received_count`、`*_accepted_count`、`*_allowed_count`、`runtime_gate_count`、`action_button_count`、`request_sent_count` 必須維持 `0`。 +5. `execution_boundaries` 中所有 runtime / host / workflow / secret / scan / deploy 授權旗標必須維持 `false`;只有 `not_authorization=true` 是安全宣告。 +6. `security-supply-chain-contract-manifest.snapshot.json` 必須維持 `36` 個 contract,default enforcement 為 `mirror_only`,且每個 contract 都有 forbidden actions 與存在的 schema / snapshot / human docs ref。 + +## 3. 指令 + +```bash +python3 scripts/security/iwooos-config-control-guard.py --root . +``` + +預期輸出: + +```text +IWOOOS_CONFIG_CONTROL_GUARD_OK +``` + +主進度 guard 已串接此 guard: + +```bash +python3 scripts/security/security-mirror-progress-guard.py --root . +``` + +預期仍為: + +```text +SECURITY_MIRROR_PROGRESS_GUARD_OK +``` + +## 4. 邊界 + +此 guard 通過只代表 repo snapshot 層的配置控管基線完整,不代表: + +- owner response 已收到或接受。 +- Nginx reload、`nginx -t`、DNS query、TLS probe、certbot renew 已授權。 +- ArgoCD sync、kubectl、workflow 修改、runner 啟用、secret 讀取 / 輪替已授權。 +- SSH、firewall、port open / close、WireGuard / NodePort / NetworkPolicy 變更已授權。 +- backup run、restore drill、offsite sync、retention change、escrow marker write 已授權。 +- active scan、Kali `/execute`、agent-bounty runtime、payout、withdrawal 或 production deploy 已授權。 + +## 5. 完成度 + +| 工作 | 完成度 | 說明 | +|------|--------|------| +| 高價值配置集中 guard | `100%` | 已新增腳本並可獨立執行 | +| 主進度 guard 串接 | `100%` | `security-mirror-progress-guard.py` 已呼叫此 guard | +| dry-run 證據同步 | `100%` | `security-mirror-dry-run.snapshot.json` 已新增 `CHECK_CONFIG_CONTROL_GUARD` | +| runtime / host / secret / scan / deploy 授權 | `0%` | 全部維持 `0 / false` | diff --git a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md index b33c4c289..2525a2235 100644 --- a/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md +++ b/docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md @@ -210,6 +210,7 @@ Nginx 是目前必須最先資安控管的配置,原因是它同時控制公 8. Backup / restore / offsite / escrow / retention 清冊可見只代表需被控管;不得把 runbook 命令、snapshot、AwoooP approval 或 IwoooS UI 當作 backup run、restore drill、rclone sync、remote delete、restic prune、escrow marker write 或 Velero restore 授權。 9. DNS / TLS / certbot owner response 只能收脫敏 metadata ref、coverage basis、expiry metadata、renewal owner、ACME route owner、maintenance window、rollback owner 與 validation plan;不得因 owner 回覆而自動做 DNS query、live TLS probe、certbot renew、Nginx reload、route smoke、DNS record 變更、certificate path 變更或 ACME route 變更。 10. Public / admin / API / frontend runtime config 變更必須先通過 affected route、auth boundary、API readback、CORS diff、frontend env diff、i18n redaction、desktop / mobile smoke、sensitive string scan、rollback owner 與 post-check evidence;前台不得顯示 raw owner namespace、repo slug、內部狀態碼、內部協作內容或未脫敏截圖。 +11. 高價值配置控管必須能由 `scripts/security/iwooos-config-control-guard.py` 集中驗證;guard 通過只代表 repo snapshot 基線完整,不代表 owner response、live evidence、reload、restart、workflow / secret / runner 變更、backup / restore、scan、runtime 或 deploy 授權。 ## 5. 需要調整的既有規範 @@ -238,6 +239,7 @@ Nginx 是目前必須最先資安控管的配置,原因是它同時控制公 | 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 支援 | | 全域配置覆蓋矩陣 | `100%` | 已新增 `scripts/security/high-value-config-control-coverage.py`、snapshot 與 schema,14 類高價值配置可重跑檢查 | +| 高價值配置集中 guard | `100%` | 已新增 `scripts/security/iwooos-config-control-guard.py`,並串接 `security-mirror-progress-guard.py`;14 類配置、主要 owner / change evidence 帳本、supply-chain manifest 與 `0 / false` 邊界可集中驗證 | | Backup / restore / escrow 清冊 | `100%` | 已新增 `backup_restore_escrow_inventory_v1`,納入 38 個 repo-only surface;成熟度 `52% -> 58%` | | Backup / restore / escrow owner response acceptance | `100%` | 已新增 `backup_restore_owner_response_acceptance_v1`,38 個 candidate、27 個 write-capable、13 個 reviewer checks、7 條 outcome lanes、22 類 blocked action;成熟度 `58% -> 62%` | | Monitoring / alerting / observability 清冊 | `100%` | 已新增 `monitoring_alerting_observability_inventory_v1`,納入 60 個 repo-only surface;成熟度 `56% -> 62%` | diff --git a/docs/security/security-mirror-dry-run.snapshot.json b/docs/security/security-mirror-dry-run.snapshot.json index f8948900e..b5bef1d5c 100644 --- a/docs/security/security-mirror-dry-run.snapshot.json +++ b/docs/security/security-mirror-dry-run.snapshot.json @@ -127,6 +127,26 @@ "switch_github_primary" ] }, + { + "step_id": "CHECK_CONFIG_CONTROL_GUARD", + "expected_observation": "AwoooP dry-run 必須確認 14 類高價值配置控管 snapshot 齊備,Nginx、DNS / TLS、K8s、Secrets、runner、public runtime、防火牆、backup、monitoring 與 agent-bounty-protocol 的 owner response / change evidence 帳本仍維持只讀,不代表 reload、restart、sync、deploy、secret、scan 或 runtime gate 授權。", + "evidence_refs": [ + "docs/security/high-value-config-control-coverage.snapshot.json", + "docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md", + "scripts/security/iwooos-config-control-guard.py" + ], + "pass_condition": "`python3 scripts/security/iwooos-config-control-guard.py` 回傳 IWOOOS_CONFIG_CONTROL_GUARD_OK,且 owner_response_received_count=0、owner_response_accepted_count=0、runtime_gate_count=0、action_button_count=0。", + "execution_allowed": false, + "blocked_actions": [ + "treat_config_coverage_as_runtime_approval", + "reload_nginx", + "modify_firewall", + "modify_workflow_or_secret", + "enable_runner", + "run_active_scan", + "production_deploy" + ] + }, { "step_id": "CHECK_LOW_NOISE_CHANNEL", "expected_observation": "Channel Event 初期只發低噪音摘要或人工批准必要事件。", @@ -164,15 +184,16 @@ ], "latest_local_validation": { "status": "repo_snapshot_guard_pass", - "date": "2026-05-18", + "date": "2026-06-15", "scope": "repo_snapshot_only", - "command": "python3 scripts/security/security-mirror-progress-guard.py && python3 scripts/security/source-control-owner-response-guard.py", - "result": "SECURITY_MIRROR_PROGRESS_GUARD_OK; SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK", + "command": "python3 scripts/security/security-mirror-progress-guard.py && python3 scripts/security/source-control-owner-response-guard.py && python3 scripts/security/iwooos-config-control-guard.py", + "result": "SECURITY_MIRROR_PROGRESS_GUARD_OK; SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK; IWOOOS_CONFIG_CONTROL_GUARD_OK", "validated_steps": [ "LOAD_CONTRACT_INDEXES", "CHECK_ACCEPTANCE_AND_QUARANTINE", "CHECK_PROGRESS_GUARD", "CHECK_OWNER_RESPONSE_GUARD", + "CHECK_CONFIG_CONTROL_GUARD", "CONFIRM_NO_RUNTIME_ACTION" ], "runtime_actions_executed": false, diff --git a/scripts/security/iwooos-config-control-guard.py b/scripts/security/iwooos-config-control-guard.py new file mode 100644 index 000000000..b5594e6c0 --- /dev/null +++ b/scripts/security/iwooos-config-control-guard.py @@ -0,0 +1,515 @@ +#!/usr/bin/env python3 +"""驗證 IwoooS 高價值配置控管 snapshot 維持只讀邊界。 + +本 guard 只讀取 repo 內已提交的 Markdown / JSON snapshot,不連線主機、不 +SSH、不讀 secret、不執行 Nginx / K8s / workflow / runner / backup / scan +動作。它的用途是把 Nginx、TLS、K8s、Secrets、runner、防火牆、backup、 +monitoring、public runtime 與 agent-bounty-protocol 等高價值配置,固定成 +可重複驗證的資安控管基線。 +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + + +EXPECTED_CATEGORIES = { + "nginx_public_gateway": 88, + "dns_tls_certbot": 78, + "k8s_production_gitops": 64, + "secret_metadata": 68, + "gitea_workflow_runner_source_control": 72, + "public_admin_api_runtime_config": 64, + "backup_restore_credential": 62, + "agent_bounty_protocol_runtime": 68, + "monitoring_alerting_observability": 62, + "docker_compose_systemd_host_config": 54, + "ssh_firewall_network_access": 60, + "ai_provider_model_routing": 60, + "product_surface_runtime_routes": 72, + "security_evidence_tooling": 86, +} + +REQUIRED_C0_CATEGORIES = { + "nginx_public_gateway", + "dns_tls_certbot", + "k8s_production_gitops", + "secret_metadata", + "gitea_workflow_runner_source_control", + "public_admin_api_runtime_config", + "backup_restore_credential", + "agent_bounty_protocol_runtime", +} + +REQUIRED_CONTROL_DOCS = [ + "docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md", + "docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md", + "docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md", + "docs/security/PUBLIC-GATEWAY-OWNER-RESPONSE-ACCEPTANCE.md", + "docs/security/PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md", + "docs/security/DOMAIN-TLS-CERTBOT-OWNER-RESPONSE-ACCEPTANCE.md", + "docs/security/HOST-SERVICE-OWNER-RESPONSE-ACCEPTANCE.md", + "docs/security/SSH-NETWORK-OWNER-RESPONSE-ACCEPTANCE.md", + "docs/security/PORT-FIREWALL-CHANGE-EVIDENCE-ACCEPTANCE.md", + "docs/security/BACKUP-RESTORE-OWNER-RESPONSE-ACCEPTANCE.md", + "docs/security/K8S-ARGOCD-OWNER-RESPONSE-ACCEPTANCE.md", + "docs/security/K8S-ARGOCD-CHANGE-EVIDENCE-ACCEPTANCE.md", + "docs/security/CD-RUNNER-SECRET-INJECTION-CHANGE-EVIDENCE-ACCEPTANCE.md", + "docs/security/PUBLIC-RUNTIME-CONFIG-CHANGE-EVIDENCE-ACCEPTANCE.md", + "docs/security/MONITORING-OWNER-REQUEST-DRAFT.md", + "docs/security/AGENT-BOUNTY-OWNER-REQUEST-DRAFT.md", + "docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md", +] + +SUMMARY_ZERO_MARKERS = ( + "_authorized_count", + "_executed_count", + "_received_count", + "_accepted_count", + "_allowed_count", + "_confirmed_count", + "_quarantined_count", + "_rejected_count", + "_ready_count", +) + +SUMMARY_ZERO_KEYS = { + "action_button_count", + "request_sent_count", + "runtime_gate_count", + "runtime_approval_package_ready_count", + "supplement_requested_count", + "impact_supplement_requested_count", +} + +TRUE_BOUNDARY_KEYS = {"not_authorization"} + +FALSE_BOUNDARY_KEYS = [ + "runtime_execution_authorized", + "host_write_authorized", + "nginx_reload_authorized", + "public_gateway_reload_authorized", + "certbot_renew_authorized", + "argocd_sync_authorized", + "kubectl_action_authorized", + "workflow_modification_authorized", + "runner_change_authorized", + "secret_value_collection_allowed", + "backup_run_authorized", + "restore_run_authorized", + "active_scan_authorized", + "action_buttons_allowed", +] + +ARTIFACT_SPECS = [ + { + "label": "public gateway owner response acceptance", + "path": "docs/security/public-gateway-owner-response-acceptance.snapshot.json", + "schema": "public_gateway_owner_response_acceptance_v1", + "status": "owner_response_acceptance_ledger_ready_no_runtime_action", + "list_counts": { + "acceptance_candidates": 3, + "blocked_actions": 18, + "reviewer_checks": 12, + "outcome_lanes": 7, + "required_owner_response_fields": 12, + }, + "summary_counts": { + "acceptance_candidate_count": 3, + "c0_acceptance_candidate_count": 2, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "runtime_gate_count": 0, + }, + }, + { + "label": "public gateway rendered diff acceptance", + "path": "docs/security/public-gateway-rendered-diff-acceptance.snapshot.json", + "schema": "public_gateway_rendered_diff_acceptance_v1", + "status": "rendered_diff_acceptance_ledger_ready_no_runtime_action", + "list_counts": { + "blocked_actions": 22, + "reviewer_checks": 15, + "outcome_lanes": 8, + "required_evidence_fields": 14, + }, + "summary_counts": { + "diff_acceptance_candidate_count": 3, + "c0_diff_acceptance_candidate_count": 2, + "rendered_diff_received_count": 0, + "rendered_diff_accepted_count": 0, + "runtime_gate_count": 0, + }, + }, + { + "label": "domain tls certbot owner response acceptance", + "path": "docs/security/domain-tls-certbot-owner-response-acceptance.snapshot.json", + "schema": "domain_tls_certbot_owner_response_acceptance_v1", + "status": "owner_response_acceptance_ledger_ready_no_runtime_action", + "list_counts": { + "acceptance_candidates": 4, + "blocked_actions": 20, + "reviewer_checks": 13, + "outcome_lanes": 7, + "required_owner_response_fields": 13, + }, + "summary_counts": { + "acceptance_candidate_count": 4, + "c0_acceptance_candidate_count": 4, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "runtime_gate_count": 0, + }, + }, + { + "label": "host service owner response acceptance", + "path": "docs/security/host-service-owner-response-acceptance.snapshot.json", + "schema": "host_service_owner_response_acceptance_v1", + "status": "owner_response_acceptance_ledger_ready_no_runtime_action", + "list_counts": { + "acceptance_candidates": 9, + "blocked_actions": 20, + "reviewer_checks": 14, + "outcome_lanes": 7, + }, + "summary_counts": { + "acceptance_candidate_count": 9, + "write_capable_acceptance_candidate_count": 3, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "runtime_gate_count": 0, + }, + }, + { + "label": "ssh network owner response acceptance", + "path": "docs/security/ssh-network-owner-response-acceptance.snapshot.json", + "schema": "ssh_network_owner_response_acceptance_v1", + "status": "owner_response_acceptance_ledger_ready_no_runtime_action", + "list_counts": { + "acceptance_candidates": 16, + "blocked_actions": 22, + "reviewer_checks": 15, + "outcome_lanes": 7, + "required_owner_fields": 13, + }, + "summary_counts": { + "acceptance_candidate_count": 16, + "write_capable_acceptance_candidate_count": 6, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "runtime_gate_count": 0, + }, + }, + { + "label": "port firewall change evidence acceptance", + "path": "docs/security/port-firewall-change-evidence-acceptance.snapshot.json", + "schema": "port_firewall_change_evidence_acceptance_v1", + "status": "change_evidence_acceptance_ready_no_runtime_action", + "list_counts": { + "change_evidence_candidates": 14, + "blocked_actions": 24, + "reviewer_checks": 16, + "outcome_lanes": 8, + "required_evidence_fields": 16, + }, + "summary_counts": { + "change_evidence_candidate_count": 14, + "write_capable_change_evidence_candidate_count": 6, + "change_evidence_received_count": 0, + "change_evidence_accepted_count": 0, + "runtime_gate_count": 0, + }, + }, + { + "label": "backup restore owner response acceptance", + "path": "docs/security/backup-restore-owner-response-acceptance.snapshot.json", + "schema": "backup_restore_owner_response_acceptance_v1", + "status": "owner_response_acceptance_ledger_ready_no_runtime_action", + "list_counts": { + "acceptance_candidates": 38, + "blocked_actions": 22, + "reviewer_checks": 13, + "outcome_lanes": 7, + "required_owner_fields": 14, + }, + "summary_counts": { + "acceptance_candidate_count": 38, + "write_capable_acceptance_candidate_count": 27, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "runtime_gate_count": 0, + }, + }, + { + "label": "k8s argocd owner response acceptance", + "path": "docs/security/k8s-argocd-owner-response-acceptance.snapshot.json", + "schema": "k8s_argocd_owner_response_acceptance_v1", + "status": "owner_response_acceptance_ledger_ready_no_runtime_action", + "list_counts": { + "acceptance_candidates": 4, + "blocked_actions": 18, + "reviewer_checks": 12, + "outcome_lanes": 7, + "required_owner_fields": 11, + }, + "summary_counts": { + "acceptance_candidate_count": 4, + "c0_acceptance_candidate_count": 3, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "runtime_gate_count": 0, + }, + }, + { + "label": "k8s argocd change evidence acceptance", + "path": "docs/security/k8s-argocd-change-evidence-acceptance.snapshot.json", + "schema": "k8s_argocd_change_evidence_acceptance_v1", + "status": "change_evidence_acceptance_ledger_ready_no_runtime_action", + "list_counts": { + "change_evidence_candidates": 4, + "blocked_actions": 28, + "reviewer_checks": 18, + "outcome_lanes": 8, + "required_evidence_fields": 18, + }, + "summary_counts": { + "change_evidence_candidate_count": 4, + "c0_change_evidence_candidate_count": 3, + "change_evidence_received_count": 0, + "change_evidence_accepted_count": 0, + "runtime_gate_count": 0, + }, + }, + { + "label": "cd runner secret injection change evidence acceptance", + "path": "docs/security/cd-runner-secret-injection-change-evidence-acceptance.snapshot.json", + "schema": "cd_runner_secret_injection_change_evidence_acceptance_v1", + "status": "change_evidence_acceptance_ledger_ready_no_runtime_action", + "list_counts": { + "change_evidence_candidates": 5, + "blocked_actions": 32, + "reviewer_checks": 19, + "outcome_lanes": 8, + "required_evidence_fields": 19, + }, + "summary_counts": { + "change_evidence_candidate_count": 5, + "c0_change_evidence_candidate_count": 4, + "change_evidence_received_count": 0, + "change_evidence_accepted_count": 0, + "runtime_gate_count": 0, + }, + }, + { + "label": "public runtime config change evidence acceptance", + "path": "docs/security/public-runtime-config-change-evidence-acceptance.snapshot.json", + "schema": "public_runtime_config_change_evidence_acceptance_v1", + "status": "change_evidence_acceptance_ledger_ready_no_runtime_action", + "list_counts": { + "change_evidence_candidates": 6, + "blocked_actions": 32, + "reviewer_checks": 21, + "outcome_lanes": 8, + "required_evidence_fields": 21, + }, + "summary_counts": { + "change_evidence_candidate_count": 6, + "c0_change_evidence_candidate_count": 5, + "change_evidence_received_count": 0, + "change_evidence_accepted_count": 0, + "runtime_gate_count": 0, + }, + }, + { + "label": "monitoring owner request draft", + "path": "docs/security/monitoring-owner-request-draft.snapshot.json", + "schema": "monitoring_owner_request_draft_v1", + "status": "owner_request_draft_ready_not_dispatched", + "list_counts": { + "request_drafts": 60, + "blocked_actions": 24, + "required_owner_fields": 14, + }, + "summary_counts": { + "request_draft_count": 60, + "write_capable_request_draft_count": 11, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "runtime_gate_count": 0, + }, + }, + { + "label": "agent bounty owner request draft", + "path": "docs/security/agent-bounty-owner-request-draft.snapshot.json", + "schema": "agent_bounty_owner_request_draft_v1", + "status": "owner_request_draft_ready_not_dispatched", + "list_counts": { + "request_drafts": 11, + "blocked_actions": 28, + "required_owner_fields": 22, + }, + "summary_counts": { + "request_draft_count": 11, + "write_capable_request_draft_count": 8, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "runtime_gate_count": 0, + }, + }, +] + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def fail(message: str) -> None: + raise SystemExit(f"BLOCKED {message}") + + +def assert_equal(label: str, actual: Any, expected: Any) -> None: + if actual != expected: + fail(f"{label}: expected {expected!r}, got {actual!r}") + + +def assert_at_least(label: str, actual: int, expected_minimum: int) -> None: + if actual < expected_minimum: + fail(f"{label}: expected >= {expected_minimum!r}, got {actual!r}") + + +def assert_path_exists(root: Path, relative_path: str) -> None: + path = root / relative_path + if not path.exists(): + fail(f"path missing: {relative_path}") + + +def assert_summary_zero_boundaries(label: str, summary: dict[str, Any]) -> None: + for key, value in summary.items(): + if not isinstance(value, int): + continue + should_be_zero = key in SUMMARY_ZERO_KEYS or any(key.endswith(marker) for marker in SUMMARY_ZERO_MARKERS) + if should_be_zero and value != 0: + fail(f"{label}.summary.{key}: expected 0, got {value!r}") + + +def assert_execution_boundaries_false(label: str, data: dict[str, Any]) -> None: + boundaries = data.get("execution_boundaries", {}) + if not isinstance(boundaries, dict): + return + for key, value in boundaries.items(): + if key in TRUE_BOUNDARY_KEYS: + if value is not True: + fail(f"{label}.execution_boundaries.{key}: expected true, got {value!r}") + continue + if value is not False: + fail(f"{label}.execution_boundaries.{key}: expected false, got {value!r}") + + +def validate_coverage_snapshot(root: Path) -> None: + coverage_path = root / "docs/security/high-value-config-control-coverage.snapshot.json" + coverage = load_json(coverage_path) + summary = coverage["summary"] + + assert_equal("coverage.schema_version", coverage["schema_version"], "high_value_config_control_coverage_v1") + assert_equal("coverage.status", coverage["status"], "coverage_matrix_ready") + assert_equal("coverage.source_category_definition", coverage["source_category_definition"], "scripts/security/high-value-config-change-gate.py") + assert_equal("coverage.summary.category_count", summary["category_count"], len(EXPECTED_CATEGORIES)) + assert_equal("coverage.summary.registered_control_count", summary["registered_control_count"], len(EXPECTED_CATEGORIES)) + assert_equal("coverage.summary.c0_category_count", summary["c0_category_count"], len(REQUIRED_C0_CATEGORIES)) + assert_equal("coverage.summary.c1_category_count", summary["c1_category_count"], 4) + assert_equal("coverage.summary.c2_category_count", summary["c2_category_count"], 1) + assert_equal("coverage.summary.c3_category_count", summary["c3_category_count"], 1) + assert_equal("coverage.summary.owner_response_required_count", summary["owner_response_required_count"], len(EXPECTED_CATEGORIES)) + assert_equal("coverage.summary.owner_response_received_count", summary["owner_response_received_count"], 0) + assert_equal("coverage.summary.owner_response_accepted_count", summary["owner_response_accepted_count"], 0) + assert_equal("coverage.summary.runtime_gate_count", summary["runtime_gate_count"], 0) + assert_equal("coverage.summary.action_button_count", summary["action_button_count"], 0) + assert_at_least("coverage.summary.average_coverage_percent", summary["average_coverage_percent"], 68) + assert_at_least("coverage.summary.needs_live_evidence_count", summary["needs_live_evidence_count"], 9) + + categories = {item["category_id"]: item for item in coverage["coverage_categories"]} + assert_equal("coverage.category ids", set(categories), set(EXPECTED_CATEGORIES)) + for category_id, expected_minimum in EXPECTED_CATEGORIES.items(): + category = categories[category_id] + assert_at_least(f"coverage.{category_id}.coverage_percent", category["coverage_percent"], expected_minimum) + assert_equal(f"coverage.{category_id}.owner_response_required", category["owner_response_required"], True) + if category_id in REQUIRED_C0_CATEGORIES: + assert_equal(f"coverage.{category_id}.control_tier", category["control_tier"], "C0") + for ref in category.get("evidence_refs", []): + if ref.startswith(("docs/", "scripts/", "k8s/", "infra/", "ops/")): + assert_path_exists(root, ref) + + assert_execution_boundaries_false("coverage", coverage) + boundaries = coverage["execution_boundaries"] + for key in FALSE_BOUNDARY_KEYS: + assert_equal(f"coverage.execution_boundaries.{key}", boundaries.get(key), False) + + +def validate_artifact_spec(root: Path, spec: dict[str, Any]) -> None: + path = root / spec["path"] + assert_path_exists(root, spec["path"]) + data = load_json(path) + summary = data.get("summary", {}) + label = spec["label"] + + assert_equal(f"{label}.schema_version", data.get("schema_version"), spec["schema"]) + assert_equal(f"{label}.status", data.get("status"), spec["status"]) + for key, expected_count in spec.get("list_counts", {}).items(): + value = data.get(key) + if not isinstance(value, list): + fail(f"{label}.{key}: expected list, got {type(value).__name__}") + assert_equal(f"{label}.{key}.count", len(value), expected_count) + for key, expected_value in spec.get("summary_counts", {}).items(): + assert_equal(f"{label}.summary.{key}", summary.get(key), expected_value) + assert_summary_zero_boundaries(label, summary) + assert_execution_boundaries_false(label, data) + + +def validate_supply_chain_manifest(root: Path) -> None: + manifest = load_json(root / "docs/security/security-supply-chain-contract-manifest.snapshot.json") + assert_equal("supply_chain.schema_version", manifest["schema_version"], "security_supply_chain_contract_manifest_v1") + assert_equal("supply_chain.default_enforcement_level", manifest["default_enforcement_level"], "mirror_only") + assert_equal("supply_chain.contract_count", manifest["contract_count"], 36) + assert_equal("supply_chain.contract list count", len(manifest["contracts"]), manifest["contract_count"]) + + for item in manifest["contracts"]: + contract = item["contract"] + if not item.get("forbidden_actions"): + fail(f"supply_chain.{contract}.forbidden_actions: expected non-empty list") + for ref_key in ["snapshot_paths", "human_docs"]: + for ref in item.get(ref_key, []): + assert_path_exists(root, ref) + schema_path = item.get("schema_path") + if schema_path: + assert_path_exists(root, schema_path) + if item.get("consumption_mode") not in {"mirror_only", "read_only_policy", "approval_only", "suggest_only"}: + fail(f"supply_chain.{contract}.consumption_mode: unsafe value {item.get('consumption_mode')!r}") + + +def validate(root: Path) -> None: + for relative_path in REQUIRED_CONTROL_DOCS: + assert_path_exists(root, relative_path) + validate_coverage_snapshot(root) + validate_supply_chain_manifest(root) + for spec in ARTIFACT_SPECS: + validate_artifact_spec(root, spec) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--root", + default=Path(__file__).resolve().parents[2], + type=Path, + help="Repository root. Defaults to the current script's repository.", + ) + args = parser.parse_args() + validate(args.root.resolve()) + print("IWOOOS_CONFIG_CONTROL_GUARD_OK") + + +if __name__ == "__main__": + main() diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 6829fe999..9185d2ee9 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -9,6 +9,7 @@ from __future__ import annotations import argparse import json +import runpy from pathlib import Path from typing import Any @@ -74,6 +75,8 @@ def collect_string_values(value: Any) -> list[str]: def validate(root: Path) -> None: security_dir = root / "docs" / "security" + config_control_guard = runpy.run_path(str(root / "scripts" / "security" / "iwooos-config-control-guard.py")) + config_control_guard["validate"](root) manifest = load_json(security_dir / "security-supply-chain-contract-manifest.snapshot.json") readiness = load_json(security_dir / "security-mirror-readiness.snapshot.json") @@ -24961,7 +24964,7 @@ def validate(root: Path) -> None: assert_equal( "dry_run.latest_local_validation.result", local_validation["result"], - "SECURITY_MIRROR_PROGRESS_GUARD_OK; SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK", + "SECURITY_MIRROR_PROGRESS_GUARD_OK; SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK; IWOOOS_CONFIG_CONTROL_GUARD_OK", ) assert_contains("dry_run.latest_local_validation.validated_steps", local_validation["validated_steps"], "CHECK_PROGRESS_GUARD") assert_contains( @@ -24969,6 +24972,11 @@ def validate(root: Path) -> None: local_validation["validated_steps"], "CHECK_OWNER_RESPONSE_GUARD", ) + assert_contains( + "dry_run.latest_local_validation.validated_steps", + local_validation["validated_steps"], + "CHECK_CONFIG_CONTROL_GUARD", + ) assert_false("dry_run.latest_local_validation.runtime_actions_executed", local_validation["runtime_actions_executed"]) assert_false("dry_run.latest_local_validation.payloads_ingested", local_validation["payloads_ingested"]) assert_false("dry_run.latest_local_validation.production_ingestion_enabled", local_validation["production_ingestion_enabled"])