231 lines
8.8 KiB
Python
231 lines
8.8 KiB
Python
#!/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())
|