#!/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", "ready_for_server_side_env_enable_review", "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": "ready_for_server_side_env_enable_review_no_secret_collection", "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": 1, "live_metadata_owner_response_received_count": 1, "live_metadata_owner_response_accepted_count": 1, "secret_source_metadata_accepted_count": 1, "wazuh_manager_health_ref_accepted_count": 1, "readonly_account_scope_accepted_count": 1, "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": "ready_for_server_side_env_enable_review", "production_route_readback_ref": "production_readback_passed_http_200_disabled_owner_gate", "server_side_env_keys": SERVER_SIDE_ENV_KEYS, "secret_source_metadata_ref": "secret-source-metadata-ref-redacted-v1", "wazuh_manager_health_ref": "wazuh-manager-health-ref-redacted-v1", "readonly_account_scope_ref": "readonly-account-scope-ref-redacted-v1", "post_enable_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", "owner_response_received": True, "owner_response_accepted": True, "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 已啟用,只代表啟用前欄位、metadata refs 與禁止動作已固定。", "Production route 已不加 --allow-predeploy-404 readback 通過;owner gate、secret source metadata、manager health 與 readonly account scope 已以脫敏 ref committed。", "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())