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

216 lines
10 KiB
Python
Raw 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 deploy /
production readback 尚未完成」的界線。
"""
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": "blocked_waiting_formal_main_release_and_production_deploy",
"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": "predeploy_404_observed",
},
"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": 0,
"production_deploy_complete_count": 0,
"production_readback_passed_count": 0,
"predeploy_404_observed_count": 1,
"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": "blocked_waiting_formal_release_lane",
"required_evidence": "由正式 release lane 合併 feature branch 或套用等效 patch 到 main不得 force push",
"runtime_authorized": False,
},
{
"gate_id": "production_deploy",
"status": "blocked_waiting_release_lane",
"required_evidence": "Gitea CD / deploy marker 指向已合併 Wazuh fix 的 commit",
"runtime_authorized": False,
},
{
"gate_id": "production_readback",
"status": "blocked_waiting_deploy",
"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 通過不代表 production 已部署,只代表 source-side Wazuh read-only API、guard 與 feature branch push 可交接。",
"正式 release 前不得用 predeploy 404 當成功,也不得為了修 404 直接改 Nginx、Docker、K8s、firewall 或 Wazuh secret。",
"乾淨套用 proof 與 feature branch push 通過只代表 release patch 可交接,不代表已合併 main、已部署或已啟用 Wazuh live metadata。",
"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") != "blocked_waiting_formal_main_release_and_production_deploy":
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())