Files
awoooi/scripts/security/wazuh-readonly-release-owner-response-acceptance.py

234 lines
9.4 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 owner response acceptance 帳本。
本工具定義未來 owner response 如何被收件、補件、隔離、拒收或進入
reviewer validation它不讀 credential、不推送、不部署、不查 Wazuh、
不寫 runtime也不把一般批准繼續當 release 授權。
"""
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-response-acceptance.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",
]
REVIEWER_CHECKS = [
"owner_identity_present",
"release_method_allowed",
"target_scope_matches_wazuh_branch_or_patch_set",
"all_ack_flags_true",
"all_evidence_fields_present",
"redacted_refs_only",
"secret_value_absent",
"no_plaintext_token_workaround",
"no_force_push",
"no_runtime_workaround",
"post_deploy_readback_required",
"rollback_owner_present",
"live_metadata_gate_separate",
"active_response_stays_false",
"counts_transition_safe",
]
OUTCOME_LANES = [
"waiting_owner_response",
"quarantine_secret_or_raw_payload",
"reject_execution_request",
"request_supplement",
"ready_for_release_reviewer_validation",
"formal_gitea_merge_candidate",
"formal_patch_apply_candidate",
"safe_credential_push_candidate",
"waiting_production_readback",
"waiting_runtime_gate",
]
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",
"mark_general_approval_as_release_response",
"mark_predeploy_404_as_passed_readback",
]
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_response_acceptance_v1",
"generated_at": generated_at or now_iso(),
"status": "waiting_owner_response",
"mode": "metadata_only_acceptance_no_secret_no_runtime_no_push",
"summary": {
"acceptance_candidate_count": 1,
"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,
"reviewer_check_count": len(REVIEWER_CHECKS),
"outcome_lane_count": len(OUTCOME_LANES),
"blocked_action_count": len(BLOCKED_ACTIONS),
"owner_response_received_count": 0,
"owner_response_accepted_count": 0,
"owner_response_rejected_count": 0,
"owner_response_quarantined_count": 0,
"supplement_requested_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,
},
"acceptance_candidate": {
"acceptance_candidate_id": "iwooos_wazuh_readonly_release_owner_response",
"request_id": "iwooos_wazuh_readonly_release_owner_request",
"status": "waiting_owner_response",
"owner_role_or_team": "pending_owner_response",
"decision": "pending_owner_response",
"decision_reason": "pending_owner_response",
"release_method": "pending_owner_response",
"target_branch_or_patch_set": "pending_owner_response",
"post_deploy_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json",
"rollback_owner": "pending_owner_response",
"redacted_evidence_refs": [],
"ack_flags": {flag: False for flag in REQUIRED_ACK_FLAGS},
"required_ack_flags": REQUIRED_ACK_FLAGS,
"required_evidence_fields": REQUIRED_EVIDENCE_FIELDS,
"reviewer_checks": REVIEWER_CHECKS,
"outcome_lanes": OUTCOME_LANES,
"blocked_actions": BLOCKED_ACTIONS,
"not_approval": True,
"owner_response_received": False,
"owner_response_accepted": False,
"owner_response_rejected": False,
"owner_response_quarantined": False,
"supplement_requested": False,
"formal_release_lane_ready": False,
"gitea_push_authorized": False,
"patch_apply_authorized": False,
"production_deploy_authorized": False,
"production_readback_passed": False,
"runtime_gate": False,
},
"execution_boundaries": {
"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,
},
"reviewer_instructions": [
"只有具備完整欄位、脫敏 evidence refs、無 secret、無 runtime 要求的 owner response 才能進 reviewer validation。",
"一般批准繼續、截圖、口頭同意或未列 release method 的訊息都不能增加 accepted count。",
"即使 owner response accepted也只代表可進正式 release laneWazuh live metadata 與 active response 仍是不同 gate。",
"production readback 必須在部署後不加 --allow-predeploy-404 執行,且不得回 404。",
],
}
def validate(root: Path) -> None:
snapshot_path = root / SNAPSHOT_PATH
if not snapshot_path.exists():
raise SystemExit(
f"BLOCKED Wazuh release owner response acceptance 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 response acceptance {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 response acceptance 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 response acceptance not_authorization must be true"
)
elif value is not False:
raise SystemExit(
f"BLOCKED Wazuh release owner response acceptance execution_boundaries.{key}: expected false"
)
def main() -> int:
parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 API release owner response acceptance")
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_RESPONSE_ACCEPTANCE_OK "
f"received={summary['owner_response_received_count']} "
f"accepted={summary['owner_response_accepted_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())