216 lines
10 KiB
Python
216 lines
10 KiB
Python
#!/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 env;active 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())
|