Files
awoooi/scripts/security/wazuh-readonly-release-lane-preflight.py

196 lines
8.2 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 lane preflight。
本工具只檢查 repo 內 committed snapshot固定 release 前必須由正式 lane
提供非敏感證據;不讀 git credential、不推送、不部署、不查 Wazuh、不改主機。
"""
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-lane-preflight.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",
]
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_lane_preflight_v1",
"generated_at": generated_at or now_iso(),
"status": "blocked_waiting_formal_release_lane_owner_response",
"mode": "repo_preflight_no_secret_no_runtime_no_push",
"required_ack_flags": REQUIRED_ACK_FLAGS,
"required_evidence_fields": REQUIRED_EVIDENCE_FIELDS,
"allowed_release_methods": ALLOWED_RELEASE_METHODS,
"summary": {
"required_ack_flag_count": len(REQUIRED_ACK_FLAGS),
"accepted_ack_flag_count": 0,
"required_evidence_field_count": len(REQUIRED_EVIDENCE_FIELDS),
"accepted_evidence_field_count": 0,
"allowed_release_method_count": len(ALLOWED_RELEASE_METHODS),
"formal_release_lane_ready_count": 0,
"safe_credential_available_count": 0,
"patch_apply_authorized_count": 0,
"gitea_push_authorized_count": 0,
"production_deploy_authorized_count": 0,
"production_readback_required_count": 1,
"production_readback_passed_count": 0,
"plain_text_token_workaround_allowed_count": 0,
"force_push_allowed_count": 0,
"runtime_workaround_allowed_count": 0,
"wazuh_live_metadata_owner_gate_ready_count": 0,
"runtime_gate_count": 0,
},
"release_lanes": [
{
"lane_id": "formal_gitea_merge",
"status": "blocked_owner_response_required",
"meaning": "由具備正式 Gitea 權限者合併 Wazuh 分支;不得 force push。",
"runtime_authorized": False,
},
{
"lane_id": "formal_patch_apply",
"status": "blocked_owner_response_required",
"meaning": "由正式 release lane 套用已驗證 patch set不得跳過 production readback。",
"runtime_authorized": False,
},
{
"lane_id": "maintainer_local_push_with_safe_credential",
"status": "blocked_safe_credential_required",
"meaning": "只接受安全的 credential helper / SSH key / 正式 release token不得使用明文 token workaround。",
"runtime_authorized": False,
},
],
"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",
],
"post_deploy_readback": {
"required": True,
"command": "python3 scripts/security/wazuh-readonly-production-readback.py --json",
"must_not_return_http_404": True,
"runtime_gate_expected": 0,
},
"execution_boundaries": {
"not_authorization": True,
"secret_value_collection_allowed": False,
"plain_text_token_workaround_allowed": False,
"force_push_allowed": False,
"repo_write_authorized": False,
"production_deploy_authorized": False,
"runtime_execution_authorized": False,
"wazuh_api_live_query_authorized": False,
"wazuh_active_response_authorized": False,
"host_write_authorized": False,
"kali_active_scan_authorized": False,
},
"operator_interpretation": [
"此 preflight 通過前,不得把 Gitea credential blocker 視為可繞過。",
"正式 release 可以選 formal merge、formal patch apply 或安全 credential push但都需要 owner response 與 deploy 後 readback。",
"不得用 Nginx、Docker、K8s、firewall、Wazuh secret 或主機重啟來修 public API 404。",
"Wazuh live metadata 查詢與 active response 是不同 gate本 preflight 不授權任何 runtime action。",
],
}
def load_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def validate(root: Path) -> None:
snapshot_path = root / SNAPSHOT_PATH
if not snapshot_path.exists():
raise SystemExit(
f"BLOCKED Wazuh release lane preflight snapshot missing: {SNAPSHOT_PATH.as_posix()}"
)
snapshot = load_json(snapshot_path)
expected = build_report(snapshot.get("generated_at"))
if snapshot.get("schema_version") != expected["schema_version"]:
raise SystemExit("BLOCKED Wazuh release lane preflight schema_version mismatch")
if snapshot.get("status") != expected["status"]:
raise SystemExit("BLOCKED Wazuh release lane preflight status 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 lane preflight 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 lane preflight not_authorization must be true")
elif value is not False:
raise SystemExit(f"BLOCKED Wazuh release lane preflight execution_boundaries.{key}: expected false")
def main() -> int:
parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 API release lane preflight")
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_LANE_PREFLIGHT_OK "
f"ready={summary['formal_release_lane_ready_count']} "
f"acks={summary['accepted_ack_flag_count']}/{summary['required_ack_flag_count']} "
f"evidence={summary['accepted_evidence_field_count']}/{summary['required_evidence_field_count']} "
f"runtime_gate={summary['runtime_gate_count']}"
)
return 0
if __name__ == "__main__":
sys.exit(main())