#!/usr/bin/env python3 """ IwoooS Wazuh 只讀 API release owner request 草稿。 本工具只產生 / 驗證 repo 內 committed snapshot;不送 request、不讀 credential、不推送、不部署、不查 Wazuh、不改 runtime。 """ 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-request.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", ] ALLOWED_RELEASE_METHODS = [ "formal_gitea_merge", "formal_patch_apply", "maintainer_local_push_with_safe_credential", ] FORBIDDEN_PAYLOADS = [ "token", "secret", "private_key", "cookie", "session", "authorization_header", "runner_token", "webhook_secret", "wazuh_password", "wazuh_raw_payload", "git_credential", "repo_archive", ] 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", ] HANDOFF_ENVELOPE_FIELDS = [ "request_id", "stage_id", "recipient_role_or_team", "sender_role_or_team", "requested_response_window", "allowed_release_methods", "required_ack_flags", "required_evidence_fields", "target_branch_or_patch_set", "post_deploy_readback_command", "forbidden_payloads", "blocked_runtime_actions", "followup_owner", "not_approval", ] 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_request_v1", "generated_at": generated_at or now_iso(), "status": "draft_not_dispatched_waiting_release_lane_owner", "mode": "repo_request_draft_no_secret_no_runtime_no_push", "summary": { "request_draft_count": 1, "required_ack_flag_count": len(REQUIRED_ACK_FLAGS), "required_evidence_field_count": len(REQUIRED_EVIDENCE_FIELDS), "allowed_release_method_count": len(ALLOWED_RELEASE_METHODS), "forbidden_payload_count": len(FORBIDDEN_PAYLOADS), "blocked_action_count": len(BLOCKED_ACTIONS), "handoff_envelope_field_count": len(HANDOFF_ENVELOPE_FIELDS), "request_sent_count": 0, "recipient_confirmed_count": 0, "owner_response_received_count": 0, "owner_response_accepted_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, }, "request_draft": { "request_id": "iwooos_wazuh_readonly_release_owner_request", "stage_id": "P0-IWOOOS-WAZUH-RELEASE", "recipient_role_or_team": "pending_release_lane_owner", "sender_role_or_team": "iwooos_security_reviewer", "requested_response_window": "not_scheduled", "allowed_release_methods": ALLOWED_RELEASE_METHODS, "required_ack_flags": REQUIRED_ACK_FLAGS, "required_evidence_fields": REQUIRED_EVIDENCE_FIELDS, "target_branch": "codex/iwooos-wazuh-boundary-guard-20260624", "target_branch_readback": "git log --oneline gitea/main..HEAD", "target_patch_set_readback": "git format-patch gitea/main..HEAD after final docs commit; record sha256 outside committed docs", "post_deploy_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", "redacted_evidence_refs": [ "docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md", "docs/security/wazuh-readonly-release-gate.snapshot.json", "docs/security/wazuh-readonly-release-lane-preflight.snapshot.json", ], "forbidden_payloads": FORBIDDEN_PAYLOADS, "blocked_runtime_actions": BLOCKED_ACTIONS, "followup_owner": "pending_followup_owner", "not_approval": True, "request_sent": False, "recipient_confirmed": False, "owner_response_received": False, "owner_response_accepted": False, "runtime_gate": False, "action_buttons_allowed": False, }, "execution_boundaries": { "dispatch_authorized": False, "request_sent": False, "recipient_confirmed": False, "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, }, "handoff_envelope_fields": HANDOFF_ENVELOPE_FIELDS, "send_after_conditions": [ "先確認 gitea/main、Wazuh 分支與另一個 AwoooP Session 基線。", "只送脫敏欄位與 refs;不得附 secret、raw Wazuh payload、git credential 或 runtime 操作要求。", "一般批准繼續不是 release owner response。", "收到 response 後仍需先通過 owner response acceptance ledger,不能直接 push 或 deploy。", ], } def validate(root: Path) -> None: snapshot_path = root / SNAPSHOT_PATH if not snapshot_path.exists(): raise SystemExit(f"BLOCKED Wazuh release owner request 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 request {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 request 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 request not_authorization must be true") elif value is not False: raise SystemExit(f"BLOCKED Wazuh release owner request execution_boundaries.{key}: expected false") def main() -> int: parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 API release owner request 草稿") 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_REQUEST_OK " f"drafts={summary['request_draft_count']} " f"sent={summary['request_sent_count']} " f"accepted={summary['owner_response_accepted_count']} " f"runtime_gate={summary['runtime_gate_count']}" ) return 0 if __name__ == "__main__": sys.exit(main())