Files
awoooi/scripts/security/wazuh-readonly-release-gate.py

216 lines
10 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 gate。
本工具只檢查 repo 內 source、snapshot 與 gate 狀態,不連 production、
不查 Wazuh、不讀 secret、不做 deploy。目的在於固定「source-side 與
feature branch push、formal main release 與 production route readback 已完成」
以及「Wazuh live metadata / active response / host write 仍未授權」的界線。
"""
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-gate.snapshot.json")
REQUIRED_SOURCE_PATHS = [
"apps/api/src/api/v1/iwooos.py",
"apps/api/tests/test_iwooos_wazuh_api.py",
"apps/web/src/app/api/iwooos/wazuh/route.ts",
"docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md",
"scripts/security/wazuh-readonly-production-readback.py",
"scripts/security/wazuh-readonly-route-boundary-guard.py",
]
def now_iso() -> str:
return datetime.now(TAIPEI).replace(microsecond=0).isoformat()
def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]:
missing_paths = [path for path in REQUIRED_SOURCE_PATHS if not (root / path).exists()]
source_ready = not missing_paths
return {
"schema_version": "iwooos_wazuh_readonly_release_gate_v1",
"generated_at": generated_at or now_iso(),
"status": "released_waiting_wazuh_live_metadata_owner_gate",
"mode": "repo_release_gate_no_runtime_no_secret_collection",
"release_lane_evidence": {
"source_branch": "codex/iwooos-wazuh-boundary-guard-20260624",
"source_fix_commit_readback": "run git log --oneline gitea/main..HEAD before release; do not hardcode a rebase-sensitive commit hash",
"source_head_readback": "run git rev-parse HEAD after the final docs commit; do not hardcode a self-referential commit hash",
"base_ref": "gitea/main",
"base_commit_readback": "run git rev-parse gitea/main before release; do not hardcode a moving main commit",
"release_patch_set_readback": "generate with git format-patch gitea/main..HEAD after the final docs commit, then record sha256 outside the committed file",
"apply_check_status": "passed_external_readback_required_after_final_commit",
"feature_branch_push_status": "completed_readback_required_before_release",
"production_readback_status": "production_readback_passed",
},
"required_source_paths": REQUIRED_SOURCE_PATHS,
"summary": {
"source_side_fix_complete_count": 1 if source_ready else 0,
"route_boundary_guard_complete_count": 1 if (root / "scripts/security/wazuh-readonly-route-boundary-guard.py").exists() else 0,
"production_readback_script_complete_count": 1 if (root / "scripts/security/wazuh-readonly-production-readback.py").exists() else 0,
"release_handoff_complete_count": 1 if (root / "docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md").exists() else 0,
"release_patch_apply_proof_complete_count": 1,
"missing_required_source_path_count": len(missing_paths),
"gitea_push_complete_count": 1,
"gitea_push_blocker_observed_count": 0,
"formal_main_release_complete_count": 1,
"production_deploy_complete_count": 1,
"production_readback_passed_count": 1,
"predeploy_404_observed_count": 0,
"wazuh_server_side_env_enabled_count": 0,
"wazuh_event_ref_accepted_count": 0,
"host_forensics_ref_accepted_count": 0,
"active_response_authorized_count": 0,
"host_write_authorized_count": 0,
"runtime_gate_count": 0,
},
"release_gates": [
{
"gate_id": "source_side_fastapi_route",
"status": "passed",
"required_evidence": "FastAPI /api/iwooos/wazuh 與 /api/v1/iwooos/wazuh source path present",
"runtime_authorized": False,
},
{
"gate_id": "source_boundary_guard",
"status": "passed",
"required_evidence": "wazuh-readonly-route-boundary-guard.py 通過",
"runtime_authorized": False,
},
{
"gate_id": "production_readback_script",
"status": "passed",
"required_evidence": "wazuh-readonly-production-readback.py 可在 release 後不接受 404",
"runtime_authorized": False,
},
{
"gate_id": "release_patch_apply_proof",
"status": "passed",
"required_evidence": "同等 patch 已可乾淨套用到最新 gitea/main 並通過同組 guard",
"runtime_authorized": False,
},
{
"gate_id": "gitea_branch_push",
"status": "passed_feature_branch_readback",
"required_evidence": "codex/iwooos-wazuh-boundary-guard-20260624 feature branch 已可由 git ls-remote 讀回",
"runtime_authorized": False,
},
{
"gate_id": "formal_main_release",
"status": "passed_main_fast_forward_readback",
"required_evidence": "main 已快轉到包含 Wazuh fix 的 commit不得 force push",
"runtime_authorized": False,
},
{
"gate_id": "production_deploy",
"status": "passed_deploy_marker_readback",
"required_evidence": "Gitea CD deploy marker 指向已合併 Wazuh fix 的 commit",
"runtime_authorized": False,
},
{
"gate_id": "production_readback",
"status": "passed_disabled_owner_gate_readback",
"required_evidence": "python3 scripts/security/wazuh-readonly-production-readback.py --json 通過且不回 404",
"runtime_authorized": False,
},
{
"gate_id": "wazuh_live_metadata_env",
"status": "blocked_owner_gate_required",
"required_evidence": "server-side env 與 owner gate不得硬編 secret",
"runtime_authorized": False,
},
],
"execution_boundaries": {
"runtime_execution_authorized": False,
"production_deploy_authorized": False,
"wazuh_api_live_query_authorized": False,
"wazuh_active_response_authorized": False,
"host_read_authorized": False,
"host_write_authorized": False,
"kali_active_scan_authorized": False,
"secret_value_collection_allowed": False,
"raw_wazuh_payload_storage_allowed": False,
"internal_ip_public_display_allowed": False,
"agent_identity_public_display_allowed": False,
"force_push_allowed": False,
"not_authorization": True,
},
"missing_required_source_paths": missing_paths,
"operator_interpretation": [
"此 gate 通過代表 source-side、feature branch、main release、deploy marker 與 production route readback 已完成。",
"production route 回 200 只代表 IwoooS Wazuh read-only route 已部署;目前狀態仍為 disabled_waiting_iwooos_wazuh_owner_gate。",
"不得把 route 200、UI 可見、agent transport 或 service active 當成 Wazuh manager registry 已驗收。",
"live Wazuh metadata query 必須另走 owner gate 與 server-side envactive response、host write、Kali active scan 仍為 0 / false。",
],
}
def load_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def validate(root: Path) -> None:
report = build_report(root)
snapshot_path = root / SNAPSHOT_PATH
if not snapshot_path.exists():
raise SystemExit(f"BLOCKED Wazuh release gate snapshot missing: {SNAPSHOT_PATH.as_posix()}")
snapshot = load_json(snapshot_path)
expected_summary = report["summary"]
for key, expected in expected_summary.items():
actual = snapshot.get("summary", {}).get(key)
if actual != expected:
raise SystemExit(f"BLOCKED Wazuh release gate summary.{key}: expected {expected!r}, got {actual!r}")
if snapshot.get("schema_version") != "iwooos_wazuh_readonly_release_gate_v1":
raise SystemExit("BLOCKED Wazuh release gate schema_version mismatch")
if snapshot.get("status") != "released_waiting_wazuh_live_metadata_owner_gate":
raise SystemExit("BLOCKED Wazuh release gate status mismatch")
for key, value in snapshot.get("execution_boundaries", {}).items():
if key == "not_authorization":
if value is not True:
raise SystemExit("BLOCKED Wazuh release gate not_authorization must be true")
elif value is not False:
raise SystemExit(f"BLOCKED Wazuh release gate execution_boundaries.{key}: expected false")
def main() -> int:
parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 API release 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(root, 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_GATE_OK "
f"source={summary['source_side_fix_complete_count']} "
f"push={summary['gitea_push_complete_count']} "
f"main={summary['formal_main_release_complete_count']} "
f"deploy={summary['production_deploy_complete_count']} "
f"readback={summary['production_readback_passed_count']} "
f"runtime_gate={summary['runtime_gate_count']}"
)
return 0
if __name__ == "__main__":
sys.exit(main())