Files
awoooi/scripts/security/wazuh-readonly-live-metadata-env-gate.py

234 lines
9.3 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 只讀 live metadata env gate。
本工具只固定 server-side env / secret 注入 / production readback 的
owner gate。它不讀 secret value、不查 Wazuh API、不改 K8s / ArgoCD /
Docker / Nginx / firewall、不部署、不推送也不啟用 active response。
"""
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-live-metadata-env-gate.snapshot.json")
SERVER_SIDE_ENV_KEYS = [
"IWOOOS_WAZUH_READONLY_ENABLED",
"WAZUH_API_BASE_URL",
"WAZUH_API_USERNAME",
"WAZUH_API_PASSWORD",
]
REQUIRED_OWNER_FIELDS = [
"wazuh_live_metadata_owner",
"release_readback_ref",
"secret_injection_owner",
"secret_source_metadata_ref",
"wazuh_manager_health_ref",
"wazuh_api_tls_validation_ref",
"readonly_account_scope_ref",
"agent_alias_mapping_policy",
"post_enable_readback_command",
"rollback_owner",
"maintenance_window",
"validation_plan",
"no_secret_value_attestation",
"no_raw_payload_attestation",
"active_response_separate_gate_ack",
]
REVIEWER_CHECKS = [
"production_route_readback_passed_before_env_enable",
"server_side_env_keys_present_as_metadata_only",
"secret_value_absent",
"secret_source_metadata_ref_present",
"wazuh_api_base_url_https_only",
"readonly_account_scope_present",
"wazuh_manager_health_ref_present",
"agent_alias_mapping_policy_present",
"post_enable_readback_command_present",
"rollback_owner_present",
"maintenance_window_present",
"validation_plan_present",
"no_raw_wazuh_payload",
"active_response_gate_separate",
"runtime_gate_stays_zero_until_reviewer_acceptance",
]
OUTCOME_LANES = [
"waiting_release_readback",
"waiting_live_metadata_owner_response",
"request_secret_source_metadata_supplement",
"request_wazuh_manager_health_supplement",
"request_readonly_account_scope_supplement",
"quarantine_secret_or_raw_payload",
"reject_runtime_workaround",
"ready_for_live_metadata_reviewer_validation",
"waiting_post_enable_readback",
"waiting_runtime_gate",
]
BLOCKED_ACTIONS = [
"collect_wazuh_password",
"collect_wazuh_token",
"collect_wazuh_raw_payload",
"hardcode_wazuh_base_url",
"hardcode_wazuh_username",
"hardcode_wazuh_password",
"disable_tls_verification",
"enable_env_before_production_route_readback",
"enable_env_without_secret_owner",
"enable_wazuh_live_metadata_without_owner_gate",
"enable_wazuh_active_response",
"wazuh_manager_restart",
"wazuh_rule_change",
"wazuh_decoder_change",
"k8s_secret_manual_patch",
"argocd_manual_sync",
"docker_restart",
"nginx_or_gateway_workaround_for_404",
"firewall_change",
"host_write",
"kali_active_scan",
"production_deploy_without_release_lane",
"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_live_metadata_env_gate_v1",
"generated_at": generated_at or now_iso(),
"status": "blocked_waiting_live_metadata_owner_response",
"mode": "repo_gate_no_secret_no_runtime_no_wazuh_query",
"summary": {
"server_side_env_key_count": len(SERVER_SIDE_ENV_KEYS),
"required_owner_field_count": len(REQUIRED_OWNER_FIELDS),
"reviewer_check_count": len(REVIEWER_CHECKS),
"outcome_lane_count": len(OUTCOME_LANES),
"blocked_action_count": len(BLOCKED_ACTIONS),
"production_route_readback_passed_count": 1,
"live_metadata_owner_response_received_count": 0,
"live_metadata_owner_response_accepted_count": 0,
"secret_source_metadata_accepted_count": 0,
"wazuh_manager_health_ref_accepted_count": 0,
"readonly_account_scope_accepted_count": 0,
"post_enable_readback_passed_count": 0,
"wazuh_api_live_query_authorized_count": 0,
"wazuh_active_response_authorized_count": 0,
"host_write_authorized_count": 0,
"runtime_gate_count": 0,
},
"server_side_env_keys": SERVER_SIDE_ENV_KEYS,
"required_owner_fields": REQUIRED_OWNER_FIELDS,
"reviewer_checks": REVIEWER_CHECKS,
"outcome_lanes": OUTCOME_LANES,
"blocked_actions": BLOCKED_ACTIONS,
"live_metadata_candidate": {
"candidate_id": "iwooos_wazuh_readonly_live_metadata_env",
"status": "waiting_live_metadata_owner_response",
"production_route_readback_ref": "production_readback_passed_http_200_disabled_owner_gate",
"server_side_env_keys": SERVER_SIDE_ENV_KEYS,
"secret_source_metadata_ref": None,
"wazuh_manager_health_ref": None,
"readonly_account_scope_ref": None,
"post_enable_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json",
"owner_response_received": False,
"owner_response_accepted": False,
"wazuh_api_live_query_authorized": False,
"wazuh_active_response_authorized": False,
"runtime_gate": False,
"not_authorization": True,
},
"execution_boundaries": {
"repo_write_authorized": False,
"production_deploy_authorized": False,
"runtime_execution_authorized": False,
"secret_value_collection_allowed": False,
"wazuh_api_live_query_authorized": False,
"wazuh_active_response_authorized": False,
"raw_wazuh_payload_storage_allowed": False,
"host_write_authorized": False,
"kali_active_scan_authorized": False,
"k8s_secret_patch_authorized": False,
"argocd_sync_authorized": False,
"docker_restart_authorized": False,
"nginx_gateway_workaround_authorized": False,
"firewall_change_authorized": False,
"not_authorization": True,
},
"operator_interpretation": [
"此 gate 不代表 Wazuh live metadata 已啟用,只代表啟用前欄位與禁止動作已固定。",
"Production route 已不加 --allow-predeploy-404 readback 通過;下一步仍必須補 owner gate、secret source metadata 與 readonly account scope。",
"secret handling 只能提供注入來源 metadata 與 owner不得提交密碼、token、hash、partial secret 或 raw env。",
"Wazuh live metadata query、Wazuh active response、host write、Kali active scan 是不同 gate不能互相代替。",
],
}
def validate(root: Path) -> None:
snapshot_path = root / SNAPSHOT_PATH
if not snapshot_path.exists():
raise SystemExit(f"BLOCKED Wazuh live metadata env gate 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 live metadata env gate {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 live metadata env gate 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 live metadata env gate not_authorization must be true")
elif value is not False:
raise SystemExit(f"BLOCKED Wazuh live metadata env gate execution_boundaries.{key}: expected false")
def main() -> int:
parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 live metadata env 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(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_LIVE_METADATA_ENV_GATE_OK "
f"route_readback={summary['production_route_readback_passed_count']} "
f"owner={summary['live_metadata_owner_response_accepted_count']} "
f"secret_meta={summary['secret_source_metadata_accepted_count']} "
f"live_query={summary['wazuh_api_live_query_authorized_count']} "
f"runtime_gate={summary['runtime_gate_count']}"
)
return 0
if __name__ == "__main__":
sys.exit(main())