diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 6cd68ca1..8d11d690 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -232,6 +232,12 @@ - 完成度:release owner request / acceptance artifact 與 guard `100%`;正式 owner response / release ready / push / deploy / production readback `0%`。 - 邊界:本段沒有發送 request、沒有收件、沒有讀 credential、沒有推送、沒有部署、沒有 Wazuh live query、沒有 runtime action;一般「批准繼續」仍不可當 release lane owner response。 +**Live metadata env gate 補充,22:42 Asia/Taipei**: +- 新增 `scripts/security/wazuh-readonly-live-metadata-env-gate.py` 與 `docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json`,並接入 `security-mirror-progress-guard.py`。 +- Gate 固定 server-side env keys `4`、required owner fields `15`、reviewer checks `15`、outcome lanes `10`、blocked actions `23`;目前 production route readback passed `0`、live metadata owner accepted `0`、secret source metadata accepted `0`、Wazuh manager health accepted `0`、live query authorized `0`、runtime gate `0`。 +- 完成度:live metadata env gate artifact / guard `100%`;server-side env owner response、secret source metadata、post-enable readback、live query authorization 仍 `0%`。 +- 邊界:本段沒有讀 secret、沒有查 Wazuh API、沒有修改 K8s / ArgoCD / Docker / Nginx / firewall、沒有部署、沒有 active response、沒有 host write;部署後 route 200 也不能直接代表可查 Wazuh live metadata。 + ## 2026-06-24|21:04 recovery readback 與 MOMO V10.651 雙機基準收斂 **背景**:前一輪 MOMO workspace readback 指到 `V10.646`,但 21:04 live health 已回 `V10.651`。因此本輪重新比對 Gitea `wooo/ewoooc` `main`、正式站 `/health`、Mac Mini / MacBook Pro Codex workspace 與 full-stack cold-start,避免「網站可用」和「版本 / 資料最新」互相混淆。 diff --git a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md index 90a0d9aa..48c1c005 100644 --- a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md +++ b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md @@ -33,11 +33,13 @@ - `scripts/security/wazuh-readonly-release-lane-preflight.py` - `scripts/security/wazuh-readonly-release-owner-request.py` - `scripts/security/wazuh-readonly-release-owner-response-acceptance.py` +- `scripts/security/wazuh-readonly-live-metadata-env-gate.py` - `scripts/security/security-mirror-progress-guard.py` - `docs/security/wazuh-readonly-release-gate.snapshot.json` - `docs/security/wazuh-readonly-release-lane-preflight.snapshot.json` - `docs/security/wazuh-readonly-release-owner-request.snapshot.json` - `docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json` +- `docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json` - `docs/LOGBOOK.md` 完成內容: @@ -54,6 +56,7 @@ - 新增 release gate snapshot 與 guard,固定 source-side 已完成、Gitea push / production deploy / production readback 尚未完成,避免後續把 predeploy 404 誤判成通過。 - 新增 release lane preflight snapshot 與 guard,固定正式 release 前必須選擇 `formal_gitea_merge`、`formal_patch_apply` 或 `maintainer_local_push_with_safe_credential` 其中一條合規 lane,且 owner ack / evidence 未到齊前不得 push、deploy、force push、使用明文 token workaround 或改 runtime。 - 新增 release owner request 草稿與 owner response acceptance 帳本,將 required ack flags、required evidence fields、allowed release methods、blocked actions、forbidden payloads 與 reviewer checks 機器可讀化;目前 request sent、response received / accepted、release ready、runtime gate 全部維持 `0`。 +- 新增 live metadata env gate,固定部署後要先通過 production route readback、server-side env owner response、secret source metadata、Wazuh manager health ref、readonly account scope、post-enable readback、rollback 與 no-secret / no-raw-payload attestation;目前 live query authorized 仍為 `0`。 ## 已完成驗證 @@ -66,9 +69,10 @@ python3 scripts/security/wazuh-readonly-release-gate.py --root . python3 scripts/security/wazuh-readonly-release-lane-preflight.py --root . python3 scripts/security/wazuh-readonly-release-owner-request.py --root . python3 scripts/security/wazuh-readonly-release-owner-response-acceptance.py --root . +python3 scripts/security/wazuh-readonly-live-metadata-env-gate.py --root . python3 scripts/security/security-mirror-progress-guard.py --root . -python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.py -python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.py scripts/security/security-mirror-progress-guard.py +python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.py scripts/security/wazuh-readonly-live-metadata-env-gate.py +python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.py scripts/security/wazuh-readonly-live-metadata-env-gate.py scripts/security/security-mirror-progress-guard.py git diff --check ``` @@ -80,8 +84,9 @@ git diff --check - `wazuh-readonly-release-lane-preflight`:`ready=0 acks=0/6 evidence=0/6 runtime_gate=0`。 - `wazuh-readonly-release-owner-request`:`drafts=1 sent=0 accepted=0 runtime_gate=0`。 - `wazuh-readonly-release-owner-response-acceptance`:`received=0 accepted=0 acks=0/6 evidence=0/6 runtime_gate=0`。 +- `wazuh-readonly-live-metadata-env-gate`:`route_readback=0 owner=0 secret_meta=0 live_query=0 runtime_gate=0`。 - `security-mirror-progress-guard`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 -- `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=972`。 +- `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=973`。 - `py_compile`:通過。 - `git diff --check`:通過。 @@ -103,7 +108,7 @@ git am /private/tmp/awoooi-iwooos-wazuh-boundary-release-patch-/*.pat - `python3 scripts/security/wazuh-readonly-release-gate.py --root .`:`WAZUH_READONLY_RELEASE_GATE_OK source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 - `python3 scripts/security/wazuh-readonly-release-lane-preflight.py --root .`:`WAZUH_READONLY_RELEASE_LANE_PREFLIGHT_OK ready=0 acks=0/6 evidence=0/6 runtime_gate=0`。 - `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 -- `python3 scripts/ops/doc-secrets-sanity-check.py ...`:`DOC_SECRET_SANITY_OK scanned_files=972`。 +- `python3 scripts/ops/doc-secrets-sanity-check.py ...`:`DOC_SECRET_SANITY_OK scanned_files=973`。 - `python3 -m py_compile ...`:通過。 - `git diff --check`:通過。 @@ -179,6 +184,7 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json | Wazuh release gate snapshot / guard | `100%` | 已完成;固定 push/deploy/readback 仍 blocked | | Wazuh release lane preflight | `100%` | 已完成;owner acks `0/6`、evidence `0/6`、正式 release ready `0` | | Wazuh release owner request / acceptance | `100%` | 已完成只讀草稿與收件帳本;request sent `0`、response accepted `0` | +| Wazuh live metadata env gate | `100%` | 已完成只讀 gate;route readback / owner / secret metadata / live query 仍 `0` | | 乾淨套用 proof | `100%` | patch set 可落在最新 `gitea/main` 並通過同組 guard;最終 hash 以 release 前 readback 為準 | | Gitea push | `0%` | 受控 workspace HTTPS credential 缺失 | | Production deploy / readback | `0%` | 等待 release lane | @@ -191,6 +197,6 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json 1. 先依 `wazuh-readonly-release-owner-request.snapshot.json` 補 release lane owner response:選擇 formal merge、formal patch apply 或安全 credential push,並補 6 個 ack 與 6 個 evidence 欄位。 2. 解決受控 workspace Gitea HTTPS push 認證,或由正式 release lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD。 3. 部署後先驗證 `/api/iwooos/wazuh` 不再 404,且預設 disabled 邊界正確。 -4. 另開 owner gate 決定是否啟用 server-side Wazuh read-only metadata query。 +4. 另依 `wazuh-readonly-live-metadata-env-gate.snapshot.json` 補 server-side env owner response、secret source metadata、Wazuh manager health ref、readonly account scope 與 post-enable readback,才可考慮啟用 Wazuh read-only metadata query。 5. 收件 Wazuh manager health ref、agent status ref、event refs、host forensic refs 與 containment / recovery proof。 6. 仍禁止 active response、host write、firewall / Nginx / Docker / K8s runtime action、Kali active scan、secret 明文收集。 diff --git a/docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json b/docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json new file mode 100644 index 00000000..63838b52 --- /dev/null +++ b/docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json @@ -0,0 +1,145 @@ +{ + "blocked_actions": [ + "collect_wazuh_password", + "collect_wazuh_token", + "collect_wazuh_raw_payload", + "hardcode_wazuh_base_url", + "hardcode_wazuh_username", + "hardcode_wazuh_password", + "disable_tls_verification", + "enable_env_before_production_route_readback", + "enable_env_without_secret_owner", + "enable_wazuh_live_metadata_without_owner_gate", + "enable_wazuh_active_response", + "wazuh_manager_restart", + "wazuh_rule_change", + "wazuh_decoder_change", + "k8s_secret_manual_patch", + "argocd_manual_sync", + "docker_restart", + "nginx_or_gateway_workaround_for_404", + "firewall_change", + "host_write", + "kali_active_scan", + "production_deploy_without_release_lane", + "mark_predeploy_404_as_passed_readback" + ], + "execution_boundaries": { + "argocd_sync_authorized": false, + "docker_restart_authorized": false, + "firewall_change_authorized": false, + "host_write_authorized": false, + "k8s_secret_patch_authorized": false, + "kali_active_scan_authorized": false, + "nginx_gateway_workaround_authorized": false, + "not_authorization": true, + "production_deploy_authorized": false, + "raw_wazuh_payload_storage_allowed": false, + "repo_write_authorized": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "wazuh_active_response_authorized": false, + "wazuh_api_live_query_authorized": false + }, + "generated_at": "2026-06-24T22:42:00+08:00", + "live_metadata_candidate": { + "candidate_id": "iwooos_wazuh_readonly_live_metadata_env", + "not_authorization": true, + "owner_response_accepted": false, + "owner_response_received": false, + "post_enable_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "production_route_readback_ref": null, + "readonly_account_scope_ref": null, + "runtime_gate": false, + "secret_source_metadata_ref": null, + "server_side_env_keys": [ + "IWOOOS_WAZUH_READONLY_ENABLED", + "WAZUH_API_BASE_URL", + "WAZUH_API_USERNAME", + "WAZUH_API_PASSWORD" + ], + "status": "waiting_release_readback_and_live_metadata_owner_response", + "wazuh_active_response_authorized": false, + "wazuh_api_live_query_authorized": false, + "wazuh_manager_health_ref": null + }, + "mode": "repo_gate_no_secret_no_runtime_no_wazuh_query", + "operator_interpretation": [ + "此 gate 不代表 Wazuh live metadata 已啟用,只代表啟用前欄位與禁止動作已固定。", + "Production route 必須先不加 --allow-predeploy-404 readback 通過,才能考慮 server-side env enable。", + "secret handling 只能提供注入來源 metadata 與 owner,不得提交密碼、token、hash、partial secret 或 raw env。", + "Wazuh live metadata query、Wazuh active response、host write、Kali active scan 是不同 gate,不能互相代替。" + ], + "outcome_lanes": [ + "waiting_release_readback", + "waiting_live_metadata_owner_response", + "request_secret_source_metadata_supplement", + "request_wazuh_manager_health_supplement", + "request_readonly_account_scope_supplement", + "quarantine_secret_or_raw_payload", + "reject_runtime_workaround", + "ready_for_live_metadata_reviewer_validation", + "waiting_post_enable_readback", + "waiting_runtime_gate" + ], + "required_owner_fields": [ + "wazuh_live_metadata_owner", + "release_readback_ref", + "secret_injection_owner", + "secret_source_metadata_ref", + "wazuh_manager_health_ref", + "wazuh_api_tls_validation_ref", + "readonly_account_scope_ref", + "agent_alias_mapping_policy", + "post_enable_readback_command", + "rollback_owner", + "maintenance_window", + "validation_plan", + "no_secret_value_attestation", + "no_raw_payload_attestation", + "active_response_separate_gate_ack" + ], + "reviewer_checks": [ + "production_route_readback_passed_before_env_enable", + "server_side_env_keys_present_as_metadata_only", + "secret_value_absent", + "secret_source_metadata_ref_present", + "wazuh_api_base_url_https_only", + "readonly_account_scope_present", + "wazuh_manager_health_ref_present", + "agent_alias_mapping_policy_present", + "post_enable_readback_command_present", + "rollback_owner_present", + "maintenance_window_present", + "validation_plan_present", + "no_raw_wazuh_payload", + "active_response_gate_separate", + "runtime_gate_stays_zero_until_reviewer_acceptance" + ], + "schema_version": "iwooos_wazuh_readonly_live_metadata_env_gate_v1", + "server_side_env_keys": [ + "IWOOOS_WAZUH_READONLY_ENABLED", + "WAZUH_API_BASE_URL", + "WAZUH_API_USERNAME", + "WAZUH_API_PASSWORD" + ], + "status": "blocked_waiting_release_readback_and_live_metadata_owner_response", + "summary": { + "blocked_action_count": 23, + "host_write_authorized_count": 0, + "live_metadata_owner_response_accepted_count": 0, + "live_metadata_owner_response_received_count": 0, + "outcome_lane_count": 10, + "post_enable_readback_passed_count": 0, + "production_route_readback_passed_count": 0, + "readonly_account_scope_accepted_count": 0, + "required_owner_field_count": 15, + "reviewer_check_count": 15, + "runtime_gate_count": 0, + "secret_source_metadata_accepted_count": 0, + "server_side_env_key_count": 4, + "wazuh_active_response_authorized_count": 0, + "wazuh_api_live_query_authorized_count": 0, + "wazuh_manager_health_ref_accepted_count": 0 + } +} diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 9e8184d5..23a7d8d9 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -107,6 +107,10 @@ def validate(root: Path) -> None: str(root / "scripts" / "security" / "wazuh-readonly-release-owner-response-acceptance.py") ) wazuh_readonly_release_owner_response_acceptance["validate"](root) + wazuh_readonly_live_metadata_env_gate = runpy.run_path( + str(root / "scripts" / "security" / "wazuh-readonly-live-metadata-env-gate.py") + ) + wazuh_readonly_live_metadata_env_gate["validate"](root) telegram_alert_readability_guard = runpy.run_path( str(root / "scripts" / "security" / "telegram-alert-readability-guard.py") ) diff --git a/scripts/security/wazuh-readonly-live-metadata-env-gate.py b/scripts/security/wazuh-readonly-live-metadata-env-gate.py new file mode 100644 index 00000000..1255c618 --- /dev/null +++ b/scripts/security/wazuh-readonly-live-metadata-env-gate.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +""" +IwoooS Wazuh 只讀 live metadata env gate。 + +本工具只固定 server-side env / secret 注入 / production readback 的 +owner gate。它不讀 secret value、不查 Wazuh API、不改 K8s / ArgoCD / +Docker / Nginx / firewall、不部署、不推送,也不啟用 active response。 +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any + + +TAIPEI = timezone(timedelta(hours=8)) +SNAPSHOT_PATH = Path("docs/security/wazuh-readonly-live-metadata-env-gate.snapshot.json") + +SERVER_SIDE_ENV_KEYS = [ + "IWOOOS_WAZUH_READONLY_ENABLED", + "WAZUH_API_BASE_URL", + "WAZUH_API_USERNAME", + "WAZUH_API_PASSWORD", +] + +REQUIRED_OWNER_FIELDS = [ + "wazuh_live_metadata_owner", + "release_readback_ref", + "secret_injection_owner", + "secret_source_metadata_ref", + "wazuh_manager_health_ref", + "wazuh_api_tls_validation_ref", + "readonly_account_scope_ref", + "agent_alias_mapping_policy", + "post_enable_readback_command", + "rollback_owner", + "maintenance_window", + "validation_plan", + "no_secret_value_attestation", + "no_raw_payload_attestation", + "active_response_separate_gate_ack", +] + +REVIEWER_CHECKS = [ + "production_route_readback_passed_before_env_enable", + "server_side_env_keys_present_as_metadata_only", + "secret_value_absent", + "secret_source_metadata_ref_present", + "wazuh_api_base_url_https_only", + "readonly_account_scope_present", + "wazuh_manager_health_ref_present", + "agent_alias_mapping_policy_present", + "post_enable_readback_command_present", + "rollback_owner_present", + "maintenance_window_present", + "validation_plan_present", + "no_raw_wazuh_payload", + "active_response_gate_separate", + "runtime_gate_stays_zero_until_reviewer_acceptance", +] + +OUTCOME_LANES = [ + "waiting_release_readback", + "waiting_live_metadata_owner_response", + "request_secret_source_metadata_supplement", + "request_wazuh_manager_health_supplement", + "request_readonly_account_scope_supplement", + "quarantine_secret_or_raw_payload", + "reject_runtime_workaround", + "ready_for_live_metadata_reviewer_validation", + "waiting_post_enable_readback", + "waiting_runtime_gate", +] + +BLOCKED_ACTIONS = [ + "collect_wazuh_password", + "collect_wazuh_token", + "collect_wazuh_raw_payload", + "hardcode_wazuh_base_url", + "hardcode_wazuh_username", + "hardcode_wazuh_password", + "disable_tls_verification", + "enable_env_before_production_route_readback", + "enable_env_without_secret_owner", + "enable_wazuh_live_metadata_without_owner_gate", + "enable_wazuh_active_response", + "wazuh_manager_restart", + "wazuh_rule_change", + "wazuh_decoder_change", + "k8s_secret_manual_patch", + "argocd_manual_sync", + "docker_restart", + "nginx_or_gateway_workaround_for_404", + "firewall_change", + "host_write", + "kali_active_scan", + "production_deploy_without_release_lane", + "mark_predeploy_404_as_passed_readback", +] + + +def now_iso() -> str: + return datetime.now(TAIPEI).replace(microsecond=0).isoformat() + + +def build_report(generated_at: str | None = None) -> dict[str, Any]: + return { + "schema_version": "iwooos_wazuh_readonly_live_metadata_env_gate_v1", + "generated_at": generated_at or now_iso(), + "status": "blocked_waiting_release_readback_and_live_metadata_owner_response", + "mode": "repo_gate_no_secret_no_runtime_no_wazuh_query", + "summary": { + "server_side_env_key_count": len(SERVER_SIDE_ENV_KEYS), + "required_owner_field_count": len(REQUIRED_OWNER_FIELDS), + "reviewer_check_count": len(REVIEWER_CHECKS), + "outcome_lane_count": len(OUTCOME_LANES), + "blocked_action_count": len(BLOCKED_ACTIONS), + "production_route_readback_passed_count": 0, + "live_metadata_owner_response_received_count": 0, + "live_metadata_owner_response_accepted_count": 0, + "secret_source_metadata_accepted_count": 0, + "wazuh_manager_health_ref_accepted_count": 0, + "readonly_account_scope_accepted_count": 0, + "post_enable_readback_passed_count": 0, + "wazuh_api_live_query_authorized_count": 0, + "wazuh_active_response_authorized_count": 0, + "host_write_authorized_count": 0, + "runtime_gate_count": 0, + }, + "server_side_env_keys": SERVER_SIDE_ENV_KEYS, + "required_owner_fields": REQUIRED_OWNER_FIELDS, + "reviewer_checks": REVIEWER_CHECKS, + "outcome_lanes": OUTCOME_LANES, + "blocked_actions": BLOCKED_ACTIONS, + "live_metadata_candidate": { + "candidate_id": "iwooos_wazuh_readonly_live_metadata_env", + "status": "waiting_release_readback_and_live_metadata_owner_response", + "production_route_readback_ref": None, + "server_side_env_keys": SERVER_SIDE_ENV_KEYS, + "secret_source_metadata_ref": None, + "wazuh_manager_health_ref": None, + "readonly_account_scope_ref": None, + "post_enable_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "owner_response_received": False, + "owner_response_accepted": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "runtime_gate": False, + "not_authorization": True, + }, + "execution_boundaries": { + "repo_write_authorized": False, + "production_deploy_authorized": False, + "runtime_execution_authorized": False, + "secret_value_collection_allowed": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "raw_wazuh_payload_storage_allowed": False, + "host_write_authorized": False, + "kali_active_scan_authorized": False, + "k8s_secret_patch_authorized": False, + "argocd_sync_authorized": False, + "docker_restart_authorized": False, + "nginx_gateway_workaround_authorized": False, + "firewall_change_authorized": False, + "not_authorization": True, + }, + "operator_interpretation": [ + "此 gate 不代表 Wazuh live metadata 已啟用,只代表啟用前欄位與禁止動作已固定。", + "Production route 必須先不加 --allow-predeploy-404 readback 通過,才能考慮 server-side env enable。", + "secret handling 只能提供注入來源 metadata 與 owner,不得提交密碼、token、hash、partial secret 或 raw env。", + "Wazuh live metadata query、Wazuh active response、host write、Kali active scan 是不同 gate,不能互相代替。", + ], + } + + +def validate(root: Path) -> None: + snapshot_path = root / SNAPSHOT_PATH + if not snapshot_path.exists(): + raise SystemExit(f"BLOCKED Wazuh live metadata env gate snapshot missing: {SNAPSHOT_PATH}") + snapshot = json.loads(snapshot_path.read_text(encoding="utf-8")) + expected = build_report(snapshot.get("generated_at")) + + for key in ("schema_version", "status", "mode"): + if snapshot.get(key) != expected[key]: + raise SystemExit(f"BLOCKED Wazuh live metadata env gate {key} mismatch") + for key, expected_value in expected["summary"].items(): + actual = snapshot.get("summary", {}).get(key) + if actual != expected_value: + raise SystemExit( + f"BLOCKED Wazuh live metadata env gate summary.{key}: " + f"expected {expected_value!r}, got {actual!r}" + ) + for key, value in snapshot.get("execution_boundaries", {}).items(): + if key == "not_authorization": + if value is not True: + raise SystemExit("BLOCKED Wazuh live metadata env gate not_authorization must be true") + elif value is not False: + raise SystemExit(f"BLOCKED Wazuh live metadata env gate execution_boundaries.{key}: expected false") + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 live metadata env gate") + parser.add_argument("--root", default=".", help="repository root") + parser.add_argument("--output", help="寫出 JSON 報告") + parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用") + args = parser.parse_args() + + root = Path(args.root).resolve() + report = build_report(args.generated_at) + if args.output: + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + validate(root) + summary = report["summary"] + print( + "WAZUH_READONLY_LIVE_METADATA_ENV_GATE_OK " + f"route_readback={summary['production_route_readback_passed_count']} " + f"owner={summary['live_metadata_owner_response_accepted_count']} " + f"secret_meta={summary['secret_source_metadata_accepted_count']} " + f"live_query={summary['wazuh_api_live_query_authorized_count']} " + f"runtime_gate={summary['runtime_gate_count']}" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main())