#!/usr/bin/env python3 """ IwoooS Wazuh 只讀 API release owner response acceptance 帳本。 本工具定義未來 owner response 如何被收件、補件、隔離、拒收或進入 reviewer validation;它不讀 credential、不推送、不部署、不查 Wazuh、 不寫 runtime,也不把一般批准繼續當 release 授權。 """ 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-release-owner-response-acceptance.snapshot.json") REQUIRED_ACK_FLAGS = [ "approve_formal_release_lane", "confirm_no_plaintext_token_workaround", "confirm_no_force_push", "confirm_no_runtime_workaround", "confirm_production_readback_after_deploy", "confirm_wazuh_live_metadata_requires_separate_owner_gate", ] REQUIRED_EVIDENCE_FIELDS = [ "release_lane_owner", "release_method", "target_branch_or_patch_set", "post_deploy_readback_command", "rollback_owner", "blocked_runtime_actions_ack", ] REVIEWER_CHECKS = [ "owner_identity_present", "release_method_allowed", "target_scope_matches_wazuh_branch_or_patch_set", "all_ack_flags_true", "all_evidence_fields_present", "redacted_refs_only", "secret_value_absent", "no_plaintext_token_workaround", "no_force_push", "no_runtime_workaround", "post_deploy_readback_required", "rollback_owner_present", "live_metadata_gate_separate", "active_response_stays_false", "counts_transition_safe", ] OUTCOME_LANES = [ "waiting_owner_response", "quarantine_secret_or_raw_payload", "reject_execution_request", "request_supplement", "ready_for_release_reviewer_validation", "formal_gitea_merge_candidate", "formal_patch_apply_candidate", "safe_credential_push_candidate", "waiting_production_readback", "waiting_runtime_gate", ] BLOCKED_ACTIONS = [ "plain_text_gitea_token_in_remote_url", "copy_token_from_dirty_workspace", "force_push", "nginx_or_gateway_workaround_for_404", "docker_restart_for_wazuh_route", "k8s_or_argocd_manual_apply_for_wazuh_route", "firewall_change_for_wazuh_route", "wazuh_secret_or_manager_change_for_api_404", "enable_wazuh_live_metadata_without_owner_gate", "enable_wazuh_active_response", "host_write_or_kali_active_scan", "mark_general_approval_as_release_response", "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_release_owner_response_acceptance_v1", "generated_at": generated_at or now_iso(), "status": "waiting_owner_response", "mode": "metadata_only_acceptance_no_secret_no_runtime_no_push", "summary": { "acceptance_candidate_count": 1, "required_ack_flag_count": len(REQUIRED_ACK_FLAGS), "accepted_ack_flag_count": 0, "required_evidence_field_count": len(REQUIRED_EVIDENCE_FIELDS), "accepted_evidence_field_count": 0, "reviewer_check_count": len(REVIEWER_CHECKS), "outcome_lane_count": len(OUTCOME_LANES), "blocked_action_count": len(BLOCKED_ACTIONS), "owner_response_received_count": 0, "owner_response_accepted_count": 0, "owner_response_rejected_count": 0, "owner_response_quarantined_count": 0, "supplement_requested_count": 0, "formal_release_lane_ready_count": 0, "gitea_push_authorized_count": 0, "patch_apply_authorized_count": 0, "production_deploy_authorized_count": 0, "production_readback_passed_count": 0, "runtime_gate_count": 0, }, "acceptance_candidate": { "acceptance_candidate_id": "iwooos_wazuh_readonly_release_owner_response", "request_id": "iwooos_wazuh_readonly_release_owner_request", "status": "waiting_owner_response", "owner_role_or_team": "pending_owner_response", "decision": "pending_owner_response", "decision_reason": "pending_owner_response", "release_method": "pending_owner_response", "target_branch_or_patch_set": "pending_owner_response", "post_deploy_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", "rollback_owner": "pending_owner_response", "redacted_evidence_refs": [], "ack_flags": {flag: False for flag in REQUIRED_ACK_FLAGS}, "required_ack_flags": REQUIRED_ACK_FLAGS, "required_evidence_fields": REQUIRED_EVIDENCE_FIELDS, "reviewer_checks": REVIEWER_CHECKS, "outcome_lanes": OUTCOME_LANES, "blocked_actions": BLOCKED_ACTIONS, "not_approval": True, "owner_response_received": False, "owner_response_accepted": False, "owner_response_rejected": False, "owner_response_quarantined": False, "supplement_requested": False, "formal_release_lane_ready": False, "gitea_push_authorized": False, "patch_apply_authorized": False, "production_deploy_authorized": False, "production_readback_passed": False, "runtime_gate": False, }, "execution_boundaries": { "repo_write_authorized": False, "gitea_push_authorized": False, "patch_apply_authorized": False, "production_deploy_authorized": False, "runtime_execution_authorized": False, "secret_value_collection_allowed": False, "plain_text_token_workaround_allowed": False, "force_push_allowed": False, "wazuh_api_live_query_authorized": False, "wazuh_active_response_authorized": False, "host_write_authorized": False, "kali_active_scan_authorized": False, "not_authorization": True, }, "reviewer_instructions": [ "只有具備完整欄位、脫敏 evidence refs、無 secret、無 runtime 要求的 owner response 才能進 reviewer validation。", "一般批准繼續、截圖、口頭同意或未列 release method 的訊息都不能增加 accepted count。", "即使 owner response accepted,也只代表可進正式 release lane;Wazuh live metadata 與 active response 仍是不同 gate。", "production readback 必須在部署後不加 --allow-predeploy-404 執行,且不得回 404。", ], } def validate(root: Path) -> None: snapshot_path = root / SNAPSHOT_PATH if not snapshot_path.exists(): raise SystemExit( f"BLOCKED Wazuh release owner response acceptance 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 release owner response acceptance {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 release owner response acceptance 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 release owner response acceptance not_authorization must be true" ) elif value is not False: raise SystemExit( f"BLOCKED Wazuh release owner response acceptance execution_boundaries.{key}: expected false" ) def main() -> int: parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 API release owner response acceptance") 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_RELEASE_OWNER_RESPONSE_ACCEPTANCE_OK " f"received={summary['owner_response_received_count']} " f"accepted={summary['owner_response_accepted_count']} " f"acks={summary['accepted_ack_flag_count']}/{summary['required_ack_flag_count']} " f"evidence={summary['accepted_evidence_field_count']}/{summary['required_evidence_field_count']} " f"runtime_gate={summary['runtime_gate_count']}" ) return 0 if __name__ == "__main__": sys.exit(main())