Some checks failed
CD Pipeline / tests (push) Waiting to run
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
235 lines
9.5 KiB
Python
235 lines
9.5 KiB
Python
#!/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",
|
||
"ready_for_server_side_env_enable_review",
|
||
"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": "ready_for_server_side_env_enable_review_no_secret_collection",
|
||
"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": 1,
|
||
"live_metadata_owner_response_accepted_count": 1,
|
||
"secret_source_metadata_accepted_count": 1,
|
||
"wazuh_manager_health_ref_accepted_count": 1,
|
||
"readonly_account_scope_accepted_count": 1,
|
||
"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": "ready_for_server_side_env_enable_review",
|
||
"production_route_readback_ref": "production_readback_passed_http_200_disabled_owner_gate",
|
||
"server_side_env_keys": SERVER_SIDE_ENV_KEYS,
|
||
"secret_source_metadata_ref": "secret-source-metadata-ref-redacted-v1",
|
||
"wazuh_manager_health_ref": "wazuh-manager-health-ref-redacted-v1",
|
||
"readonly_account_scope_ref": "readonly-account-scope-ref-redacted-v1",
|
||
"post_enable_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json",
|
||
"owner_response_received": True,
|
||
"owner_response_accepted": True,
|
||
"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 已啟用,只代表啟用前欄位、metadata refs 與禁止動作已固定。",
|
||
"Production route 已不加 --allow-predeploy-404 readback 通過;owner gate、secret source metadata、manager health 與 readonly account scope 已以脫敏 ref committed。",
|
||
"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())
|