Files
awoooi/scripts/security/wazuh-readonly-release-owner-request.py

231 lines
8.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())