From 3c5ca49cbfca9a34ed90f604f730755e9163fc76 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 28 Jun 2026 19:10:37 +0800 Subject: [PATCH] feat(iwooos): add wazuh allowlisted dry-run gate --- apps/api/src/api/v1/iwooos.py | 71 ++ .../iwooos_runtime_security_readback.py | 72 +- ...os_wazuh_allowlisted_check_mode_dry_run.py | 692 ++++++++++++++++++ .../test_iwooos_runtime_security_readback.py | 223 +++++- apps/web/messages/en.json | 13 +- apps/web/messages/zh-TW.json | 13 +- apps/web/src/app/[locale]/iwooos/page.tsx | 8 + apps/web/src/lib/api-client.ts | 78 ++ docs/LOGBOOK.md | 36 + ...lowlisted-check-mode-dry-run.snapshot.json | 237 ++++++ 10 files changed, 1436 insertions(+), 7 deletions(-) create mode 100644 apps/api/src/services/iwooos_wazuh_allowlisted_check_mode_dry_run.py create mode 100644 docs/security/wazuh-allowlisted-check-mode-dry-run.snapshot.json diff --git a/apps/api/src/api/v1/iwooos.py b/apps/api/src/api/v1/iwooos.py index 77e0788e3..bcb37af8f 100644 --- a/apps/api/src/api/v1/iwooos.py +++ b/apps/api/src/api/v1/iwooos.py @@ -26,6 +26,12 @@ from src.services.iwooos_runtime_security_readback import ( from src.services.iwooos_security_control_coverage import ( load_latest_iwooos_security_control_coverage, ) +from src.services.iwooos_wazuh_allowlisted_check_mode_dry_run import ( + load_latest_iwooos_wazuh_allowlisted_check_mode_dry_run, +) +from src.services.iwooos_wazuh_allowlisted_check_mode_dry_run import ( + validate_iwooos_wazuh_allowlisted_check_mode_dry_run_packet as validate_wazuh_allowlisted_check_mode_dry_run_packet_payload, +) from src.services.iwooos_wazuh_live_metadata_gate import ( load_latest_iwooos_wazuh_live_metadata_gate, ) @@ -373,6 +379,71 @@ async def validate_iwooos_wazuh_runtime_controlled_apply_packet( ) from exc +@router.get( + "/api/v1/iwooos/wazuh-allowlisted-check-mode-dry-run", + response_model=dict[str, Any], + summary="取得 Wazuh allowlisted check-mode dry-run 只讀讀回", + description=( + "讀取已提交的 Wazuh allowlisted check-mode dry-run contract,回傳公開別名 selector、" + "check-mode plan、dry-run evidence refs、redaction attestation、post dry-run verifier、" + "rollback revalidation、KM / PlayBook writeback 與 0 / false 邊界。此端點不查 Wazuh API、" + "不讀主機、不保存 raw output、不讀或回傳機密明文、不啟用主動回應、不改 Nginx / " + "Docker / K8s / firewall。" + ), +) +async def get_iwooos_wazuh_allowlisted_check_mode_dry_run() -> dict[str, Any]: + """回傳 Wazuh allowlisted check-mode dry-run 公開安全只讀狀態。""" + try: + payload = await asyncio.to_thread( + load_latest_iwooos_wazuh_allowlisted_check_mode_dry_run + ) + return redact_public_lan_topology(payload) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"IwoooS Wazuh allowlisted check-mode dry-run 無效:{exc}", + ) from exc + + +@router.post( + "/api/v1/iwooos/wazuh-allowlisted-check-mode-dry-run/validate-dry-run-packet", + response_model=dict[str, Any], + summary="驗證 Wazuh allowlisted check-mode dry-run 脫敏 packet", + description=( + "針對單次 owner / reviewer 提供的 redacted Wazuh allowlisted check-mode dry-run packet " + "進行 no-persist readiness validation,回傳 accepted-for-readback / needs supplement / " + "quarantined / rejected runtime action 分流。此端點不保存 payload、不查 Wazuh API、不讀主機、" + "不保存 raw output、不讀或回傳機密明文、不啟用主動回應、不改 Nginx / Docker / " + "K8s / firewall,也不更新 runtime gate 總帳。" + ), +) +async def validate_iwooos_wazuh_allowlisted_check_mode_dry_run_packet( + dry_run_packet: dict[str, Any], +) -> dict[str, Any]: + """回傳單次 Wazuh allowlisted check-mode dry-run packet 的公開安全驗證結果。""" + try: + payload = await asyncio.to_thread( + validate_wazuh_allowlisted_check_mode_dry_run_packet_payload, + dry_run_packet, + ) + return redact_public_lan_topology(payload) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"IwoooS Wazuh allowlisted check-mode dry-run packet 驗證器無效:{exc}", + ) from exc + + @router.get( "/api/v1/iwooos/wazuh-runtime-gate-owner-review-readback", response_model=dict[str, Any], diff --git a/apps/api/src/services/iwooos_runtime_security_readback.py b/apps/api/src/services/iwooos_runtime_security_readback.py index 8ed3c5406..376092470 100644 --- a/apps/api/src/services/iwooos_runtime_security_readback.py +++ b/apps/api/src/services/iwooos_runtime_security_readback.py @@ -24,6 +24,7 @@ _SNAPSHOT_FILES = { "wazuh_owner_evidence_preflight": "wazuh-agent-visibility-owner-evidence-preflight.snapshot.json", "wazuh_runtime_apply_preflight": "wazuh-runtime-controlled-apply-preflight.snapshot.json", "wazuh_runtime_owner_review": "wazuh-runtime-gate-owner-review-readback.snapshot.json", + "wazuh_allowlisted_check_mode_dry_run": "wazuh-allowlisted-check-mode-dry-run.snapshot.json", "kali_status": "kali-integration-status.snapshot.json", "soc_control": "soc-siem-kali-wazuh-integration-control.snapshot.json", "alert_readability": "telegram-alert-readability-guard.snapshot.json", @@ -39,6 +40,7 @@ _EXPECTED_SCHEMAS = { "wazuh_owner_evidence_preflight": "wazuh_agent_visibility_owner_evidence_preflight_v1", "wazuh_runtime_apply_preflight": "wazuh_runtime_controlled_apply_preflight_v1", "wazuh_runtime_owner_review": "wazuh_runtime_gate_owner_review_readback_v1", + "wazuh_allowlisted_check_mode_dry_run": "wazuh_allowlisted_check_mode_dry_run_v1", "kali_status": "kali_integration_status_v1", "soc_control": "soc_siem_kali_wazuh_integration_control_v1", "alert_readability": "telegram_alert_readability_guard_v1", @@ -89,6 +91,9 @@ def load_latest_iwooos_runtime_security_readback( snapshots["wazuh_runtime_apply_preflight"] ) runtime_owner_review_summary = _summary(snapshots["wazuh_runtime_owner_review"]) + allowlisted_dry_run_summary = _summary( + snapshots["wazuh_allowlisted_check_mode_dry_run"] + ) soc_summary = _summary(snapshots["soc_control"]) alert_summary = _summary(snapshots["alert_readability"]) dispatch_summary = _summary(snapshots["owner_dispatch"]) @@ -120,7 +125,7 @@ def load_latest_iwooos_runtime_security_readback( "source_refs": source_refs, "summary": { "source_snapshot_count": len(source_refs), - "p0_lane_count": 11, + "p0_lane_count": 12, "control_plane_visibility_percent": _average_percent( soc_summary.get("coverage_percent_after_soc_integration_control"), intrusion_summary.get("coverage_percent_after_prevention_control"), @@ -281,6 +286,30 @@ def load_latest_iwooos_runtime_security_readback( "wazuh_runtime_owner_review_runtime_gate_count": _int( runtime_owner_review_summary.get("runtime_gate_count") ), + "wazuh_allowlisted_check_mode_dry_run_target_selector_count": _int( + allowlisted_dry_run_summary.get("allowlisted_target_selector_count") + ), + "wazuh_allowlisted_check_mode_dry_run_check_mode_plan_count": _int( + allowlisted_dry_run_summary.get("check_mode_plan_count") + ), + "wazuh_allowlisted_check_mode_dry_run_evidence_ref_count": _int( + allowlisted_dry_run_summary.get("dry_run_evidence_ref_count") + ), + "wazuh_allowlisted_check_mode_dry_run_result_ref_count": _int( + allowlisted_dry_run_summary.get("dry_run_result_ref_count") + ), + "wazuh_allowlisted_check_mode_dry_run_packet_received_count": _int( + allowlisted_dry_run_summary.get("dry_run_packet_received_count") + ), + "wazuh_allowlisted_check_mode_dry_run_packet_accepted_count": _int( + allowlisted_dry_run_summary.get("dry_run_packet_accepted_count") + ), + "wazuh_allowlisted_check_mode_dry_run_post_verifier_count": _int( + allowlisted_dry_run_summary.get("post_dry_run_verifier_count") + ), + "wazuh_allowlisted_check_mode_dry_run_runtime_gate_count": _int( + allowlisted_dry_run_summary.get("runtime_gate_count") + ), "kali_active_scan_authorized_count": _int( soc_summary.get("kali_active_scan_authorized_count") ), @@ -535,6 +564,46 @@ def load_latest_iwooos_runtime_security_readback( "docs/security/wazuh-runtime-gate-owner-review-readback.snapshot.json" ], ), + _lane( + "wazuh_allowlisted_check_mode_dry_run", + snapshots["wazuh_allowlisted_check_mode_dry_run"].get( + "status", + "allowlisted_check_mode_dry_run_staged_no_runtime_action", + ), + 65 + if _int(allowlisted_dry_run_summary.get("dry_run_packet_accepted_count")) + else 0, + "steady" + if _int(allowlisted_dry_run_summary.get("dry_run_packet_accepted_count")) + else "locked", + "進入 post-dry-run verifier readback;runtime gate 仍關閉", + { + "target_selectors": allowlisted_dry_run_summary.get( + "allowlisted_target_selector_count", 0 + ), + "check_mode": allowlisted_dry_run_summary.get( + "check_mode_plan_count", 0 + ), + "dry_run_evidence": allowlisted_dry_run_summary.get( + "dry_run_evidence_ref_count", 0 + ), + "dry_run_result": allowlisted_dry_run_summary.get( + "dry_run_result_ref_count", 0 + ), + "packet_accepted": allowlisted_dry_run_summary.get( + "dry_run_packet_accepted_count", 0 + ), + "post_dry_run_verifier": allowlisted_dry_run_summary.get( + "post_dry_run_verifier_count", 0 + ), + "runtime_gate": allowlisted_dry_run_summary.get( + "runtime_gate_count", 0 + ), + }, + [ + "docs/security/wazuh-allowlisted-check-mode-dry-run.snapshot.json" + ], + ), _lane( "wazuh_dashboard_api", "degraded_api_connection_not_green", @@ -665,6 +734,7 @@ def load_latest_iwooos_runtime_security_readback( "Wazuh 負責人證據預檢 ready 不代表已收件、已接受或可啟用 active response", "Wazuh controlled apply preflight ready 不代表 runtime gate 已開或已執行修復", "Wazuh runtime gate owner-review accepted 只代表 review readiness,不代表已查 live Wazuh 或可寫主機", + "Wazuh allowlisted check-mode dry-run staged 只代表脫敏 dry-run refs 已讀回,不代表 runtime gate、live query 或 host write 已授權", ], } diff --git a/apps/api/src/services/iwooos_wazuh_allowlisted_check_mode_dry_run.py b/apps/api/src/services/iwooos_wazuh_allowlisted_check_mode_dry_run.py new file mode 100644 index 000000000..c7b37e56e --- /dev/null +++ b/apps/api/src/services/iwooos_wazuh_allowlisted_check_mode_dry_run.py @@ -0,0 +1,692 @@ +""" +IwoooS Wazuh allowlisted check-mode dry-run readback. + +This service exposes a committed no-persist dry-run readiness contract. It only +reviews redacted references and never queries live Wazuh, reads host data, reads +secrets, persists raw payloads, or authorizes runtime actions. +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + +from src.services.snapshot_paths import default_security_dir + +_DEFAULT_SECURITY_DIR = default_security_dir(Path(__file__)) +_SNAPSHOT_FILE = "wazuh-allowlisted-check-mode-dry-run.snapshot.json" +_EXPECTED_SCHEMA = "wazuh_allowlisted_check_mode_dry_run_v1" + +_REQUIRED_FALSE_BOUNDARIES = { + "active_scan_authorized", + "alertmanager_reload_authorized", + "auto_block_authorized", + "credentialed_scan_authorized", + "firewall_change_authorized", + "host_write_authorized", + "kali_execute_authorized", + "kali_scan_authorized", + "nginx_reload_authorized", + "production_write_authorized", + "runtime_execution_authorized", + "runtime_gate_open", + "secret_value_collection_allowed", + "telegram_send_authorized", + "wazuh_active_response_authorized", + "wazuh_api_live_query_authorized", +} + +_SENSITIVE_TEXT_PATTERNS = { + "internal_ip": re.compile( + r"\b(?:10|127|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b" + ), + "authorization_header": re.compile(r"Authorization\s*:", re.IGNORECASE), + "bearer_token": re.compile(r"Bearer\s+[A-Za-z0-9._-]{10,}", re.IGNORECASE), + "basic_auth": re.compile(r"Basic\s+[A-Za-z0-9+/=]{10,}", re.IGNORECASE), + "password_assignment": re.compile( + r"password\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE + ), + "token_assignment": re.compile(r"token\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE), + "cookie_assignment": re.compile( + r"cookie\s*[:=]\s*['\"][^'\"]+['\"]", re.IGNORECASE + ), + "client_keys": re.compile(r"client\.keys", re.IGNORECASE), + "private_key": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), + "raw_session_text": re.compile( + r"(工作視窗|批准!繼續|source_thread_id|raw session)", re.IGNORECASE + ), +} + +_FORBIDDEN_KEY_FRAGMENTS = { + "authorization_header", + "basic_auth", + "bearer_token", + "client_keys", + "cookie", + "env_file", + "full_cli_output", + "full_journal", + "hostname", + "internal_ip", + "password", + "private_key", + "raw_agent_identity", + "raw_dashboard_request", + "raw_env", + "raw_hostname", + "raw_log", + "raw_runtime_volume", + "raw_wazuh_payload", + "session", + "stderr", + "stdout", + "stored_api_password", + "token", + "unredacted_screenshot", + "wazuh_api_password", +} + +_RUNTIME_ACTION_KEYS = { + "active_response_enable", + "agent_reenroll", + "agent_restart", + "ansible_apply", + "ansible_playbook_run", + "apply_now", + "argocd_sync", + "credentialed_scan", + "database_migration", + "docker_restart", + "execute_now", + "exploit_attempt", + "firewall_change", + "force_push", + "host_write", + "k8s_apply", + "kali_active_scan", + "nginx_reload", + "repo_ref_delete", + "runtime_execution_authorized", + "secret_rotation", + "systemd_restart", + "wazuh_active_response", + "wazuh_agent_reenroll", + "wazuh_agent_restart", + "wazuh_api_live_query", + "wazuh_manager_restart", + "workflow_trigger", +} + +_RUNTIME_ACK_KEYS = { + "dry_run_redaction_attestation", + "host_write_boundary_ack", + "live_wazuh_query_boundary_ack", + "runtime_boundary_ack", + "secret_boundary_ack", +} + + +def load_latest_iwooos_wazuh_allowlisted_check_mode_dry_run( + security_dir: Path | None = None, +) -> dict[str, Any]: + """Load the public-safe Wazuh allowlisted check-mode dry-run contract.""" + directory = security_dir or _DEFAULT_SECURITY_DIR + snapshot = _load_snapshot(directory) + _require_boundaries(snapshot) + + summary = _summary(snapshot) + merged_summary = { + "expected_scope_alias_count": _int(summary.get("expected_scope_alias_count")), + "allowlisted_target_selector_count": _int( + summary.get("allowlisted_target_selector_count") + ), + "check_mode_plan_count": _int(summary.get("check_mode_plan_count")), + "dry_run_evidence_ref_count": _int(summary.get("dry_run_evidence_ref_count")), + "dry_run_result_ref_count": _int(summary.get("dry_run_result_ref_count")), + "dry_run_redaction_attestation_count": _int( + summary.get("dry_run_redaction_attestation_count") + ), + "post_dry_run_verifier_count": _int(summary.get("post_dry_run_verifier_count")), + "rollback_revalidation_count": _int(summary.get("rollback_revalidation_count")), + "km_playbook_writeback_ready_count": _int( + summary.get("km_playbook_writeback_ready_count") + ), + "dry_run_packet_received_count": _int(summary.get("dry_run_packet_received_count")), + "dry_run_packet_review_ready_count": _int( + summary.get("dry_run_packet_review_ready_count") + ), + "dry_run_packet_accepted_count": _int(summary.get("dry_run_packet_accepted_count")), + "dry_run_packet_supplement_required_count": _int( + summary.get("dry_run_packet_supplement_required_count") + ), + "dry_run_packet_quarantined_count": _int( + summary.get("dry_run_packet_quarantined_count") + ), + "dry_run_runtime_action_rejected_count": _int( + summary.get("dry_run_runtime_action_rejected_count") + ), + "forbidden_payload_count": _int(summary.get("forbidden_payload_count")), + "forbidden_action_count": _int(summary.get("forbidden_action_count")), + "runtime_gate_count": _int(summary.get("runtime_gate_count")), + "wazuh_api_live_query_authorized_count": _int( + summary.get("wazuh_api_live_query_authorized_count") + ), + "wazuh_active_response_authorized_count": _int( + summary.get("wazuh_active_response_authorized_count") + ), + "host_write_authorized_count": _int(summary.get("host_write_authorized_count")), + "secret_value_collection_allowed_count": _int( + summary.get("secret_value_collection_allowed_count") + ), + } + + return { + "schema_version": "iwooos_wazuh_allowlisted_check_mode_dry_run_readback_v1", + "source_schema_version": snapshot["schema_version"], + "status": snapshot.get( + "status", "allowlisted_check_mode_dry_run_staged_no_runtime_action" + ), + "mode": snapshot.get( + "mode", "committed_dry_run_readback_no_live_wazuh_no_secret_collection" + ), + "source_refs": [ + f"docs/security/{_SNAPSHOT_FILE}", + "docs/security/wazuh-runtime-gate-owner-review-readback.snapshot.json", + ], + "dry_run_packet_validation_endpoint": ( + "/api/v1/iwooos/wazuh-allowlisted-check-mode-dry-run/validate-dry-run-packet" + ), + "dry_run_packet_validation_mode": "no_persist_dry_run_readback_no_runtime_action", + "summary": merged_summary, + "target_selectors": _target_selectors(snapshot.get("target_selectors")), + "required_dry_run_fields": _strings(snapshot.get("required_dry_run_fields")), + "dry_run_items": _dry_run_items(snapshot.get("dry_run_items")), + "outcome_lanes": _strings(snapshot.get("outcome_lanes")), + "forbidden_payloads": _strings(snapshot.get("forbidden_payloads")), + "forbidden_actions": _strings(snapshot.get("forbidden_actions")), + "boundary_markers": _boundary_markers(merged_summary), + "boundaries": { + "payload_persisted": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "wazuh_agent_reenroll_authorized": False, + "wazuh_agent_restart_authorized": False, + "wazuh_manager_restart_authorized": False, + "host_write_authorized": False, + "active_scan_authorized": False, + "kali_execute_authorized": False, + "nginx_reload_authorized": False, + "secret_value_collection_allowed": False, + "runtime_execution_authorized": False, + "runtime_gate_open": False, + "not_authorization": True, + }, + "no_false_green_rules": _strings(snapshot.get("no_false_green_rules")), + } + + +def validate_iwooos_wazuh_allowlisted_check_mode_dry_run_packet( + dry_run_packet: dict[str, Any], + security_dir: Path | None = None, +) -> dict[str, Any]: + """Validate one redacted dry-run packet without executing or persisting it.""" + contract = load_latest_iwooos_wazuh_allowlisted_check_mode_dry_run(security_dir) + snapshot = _load_snapshot(security_dir or _DEFAULT_SECURITY_DIR) + required_fields = _strings(snapshot.get("required_dry_run_fields")) + expected_aliases = { + item["node_alias"] + for item in contract["target_selectors"] + if item.get("node_alias") + } + + findings: list[dict[str, Any]] = [] + if not isinstance(dry_run_packet, dict): + findings.append( + _finding( + "ACM-01", + "blocker", + "request_allowlisted_check_mode_dry_run_supplement", + "allowlisted check-mode dry-run packet must be a JSON object.", + [], + ) + ) + return _validation_result( + contract, "request_allowlisted_check_mode_dry_run_supplement", findings + ) + + sensitive_hits = _collect_sensitive_hits(dry_run_packet) + if sensitive_hits: + findings.append( + _finding( + "ACM-04", + "critical", + "quarantine_sensitive_payload", + "dry-run packet contains forbidden or likely unredacted content; response omits raw values.", + [hit["path"] for hit in sensitive_hits[:12]], + {"categories": sorted({hit["category"] for hit in sensitive_hits})}, + ) + ) + return _validation_result(contract, "quarantine_sensitive_payload", findings) + + runtime_hits = _collect_runtime_action_hits(dry_run_packet) + if runtime_hits: + findings.append( + _finding( + "ACM-05", + "critical", + "reject_runtime_action_request", + "dry-run packet requested runtime execution; this validator only records redacted dry-run readiness.", + runtime_hits[:12], + ) + ) + return _validation_result(contract, "reject_runtime_action_request", findings) + + missing_fields = [ + field for field in required_fields if not _present(dry_run_packet.get(field)) + ] + if missing_fields: + findings.append( + _finding( + "ACM-01", + "blocker", + "request_allowlisted_check_mode_dry_run_supplement", + "allowlisted check-mode dry-run packet is missing required fields.", + missing_fields, + ) + ) + + alias_issue = _validate_aliases( + dry_run_packet.get("target_selector_aliases"), expected_aliases + ) + if alias_issue: + findings.append( + _finding( + "ACM-02", + "blocker", + "request_target_selector_fix", + alias_issue, + ["target_selector_aliases"], + ) + ) + + if ( + dry_run_packet.get("dry_run_intent") + != "stage_allowlisted_check_mode_dry_run_readback_only" + ): + findings.append( + _finding( + "ACM-03", + "blocker", + "request_dry_run_intent_fix", + "dry_run_intent must be stage_allowlisted_check_mode_dry_run_readback_only.", + ["dry_run_intent"], + ) + ) + + if dry_run_packet.get("dry_run_result_state") != "staged_passed_no_runtime_action": + findings.append( + _finding( + "ACM-06", + "blocker", + "request_dry_run_result_state_fix", + "dry_run_result_state must be staged_passed_no_runtime_action.", + ["dry_run_result_state"], + ) + ) + + expected_acks = { + "dry_run_redaction_attestation": "redacted_refs_only_no_raw_output", + "runtime_boundary_ack": "runtime_gate_remains_closed", + "live_wazuh_query_boundary_ack": "no_live_wazuh_query_performed", + "host_write_boundary_ack": "no_host_write_performed", + "secret_boundary_ack": "no_secret_value_collected", + } + bad_ack_fields = [ + field + for field, expected in expected_acks.items() + if dry_run_packet.get(field) != expected + ] + if bad_ack_fields: + findings.append( + _finding( + "ACM-07", + "blocker", + "request_boundary_ack_fix", + "dry-run boundary acks must keep runtime, live query, host write and secret collection closed.", + bad_ack_fields, + ) + ) + + outcome = ( + _first_blocking_lane(findings) + or "accepted_for_allowlisted_check_mode_dry_run_readback_only" + ) + if outcome == "accepted_for_allowlisted_check_mode_dry_run_readback_only": + findings.append( + _finding( + "ACM-08", + "info", + "allowlisted_check_mode_dry_run_readback_ready", + "dry-run packet passed no-persist readiness validation; runtime gate remains closed.", + [ + "dry_run_evidence_ref", + "dry_run_result_ref", + "post_dry_run_verifier_ref", + ], + ) + ) + return _validation_result(contract, outcome, findings) + + +def _load_snapshot(directory: Path) -> dict[str, Any]: + path = directory / _SNAPSHOT_FILE + if not path.is_file(): + raise FileNotFoundError( + f"{path}: Wazuh allowlisted check-mode dry-run snapshot not found" + ) + with path.open(encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, dict): + raise ValueError(f"{path}: expected JSON object") + if payload.get("schema_version") != _EXPECTED_SCHEMA: + raise ValueError(f"{path}: expected schema_version={_EXPECTED_SCHEMA}") + return payload + + +def _summary(payload: dict[str, Any]) -> dict[str, Any]: + summary = payload.get("summary") + return summary if isinstance(summary, dict) else {} + + +def _int(value: Any) -> int: + return value if isinstance(value, int) else 0 + + +def _strings(value: Any) -> list[str]: + if not isinstance(value, list): + return [] + return [item for item in value if isinstance(item, str)] + + +def _target_selectors(value: Any) -> list[dict[str, Any]]: + if not isinstance(value, list): + return [] + selectors: list[dict[str, Any]] = [] + for item in value: + if not isinstance(item, dict): + continue + selectors.append( + { + "node_alias": str(item.get("node_alias", "")), + "scope": str(item.get("scope", "")), + "selector_kind": str(item.get("selector_kind", "")), + "check_mode_allowed": item.get("check_mode_allowed") is True, + "runtime_write_allowed": item.get("runtime_write_allowed") is True, + } + ) + return selectors + + +def _dry_run_items(value: Any) -> list[dict[str, Any]]: + if not isinstance(value, list): + return [] + items: list[dict[str, Any]] = [] + for item in value: + if not isinstance(item, dict): + continue + items.append( + { + "item_id": str(item.get("item_id", "")), + "title": str(item.get("title", "")), + "state_key": str(item.get("state_key", "")), + "accepted": item.get("accepted") is True, + "required_fields": _strings(item.get("required_fields")), + "next_gate": str(item.get("next_gate", "")), + } + ) + return items + + +def _boundary_markers(summary: dict[str, int]) -> list[str]: + return [ + "wazuh_allowlisted_check_mode_dry_run_visible=true", + "wazuh_allowlisted_check_mode_dry_run_validation_api_available=true", + f"wazuh_allowlisted_check_mode_dry_run_target_selector_count={summary['allowlisted_target_selector_count']}", + f"wazuh_allowlisted_check_mode_dry_run_check_mode_plan_count={summary['check_mode_plan_count']}", + f"wazuh_allowlisted_check_mode_dry_run_evidence_ref_count={summary['dry_run_evidence_ref_count']}", + f"wazuh_allowlisted_check_mode_dry_run_result_ref_count={summary['dry_run_result_ref_count']}", + f"wazuh_allowlisted_check_mode_dry_run_packet_accepted_count={summary['dry_run_packet_accepted_count']}", + f"wazuh_allowlisted_check_mode_dry_run_runtime_gate_count={summary['runtime_gate_count']}", + "wazuh_api_live_query_authorized=false", + "wazuh_active_response_authorized=false", + "host_write_authorized=false", + "secret_value_collection_allowed=false", + "not_authorization=true", + ] + + +def _require_boundaries(payload: dict[str, Any]) -> None: + summary = _summary(payload) + for key in ( + "dry_run_packet_supplement_required_count", + "dry_run_packet_quarantined_count", + "dry_run_runtime_action_rejected_count", + "runtime_gate_count", + "wazuh_api_live_query_authorized_count", + "wazuh_active_response_authorized_count", + "host_write_authorized_count", + "secret_value_collection_allowed_count", + ): + if _int(summary.get(key)) != 0: + raise ValueError(f"Wazuh allowlisted check-mode dry-run summary.{key} must remain 0") + + expected_alias_count = _int(summary.get("expected_scope_alias_count")) + target_selector_count = _int(summary.get("allowlisted_target_selector_count")) + target_selectors = _target_selectors(payload.get("target_selectors")) + if ( + target_selector_count != expected_alias_count + or len(target_selectors) != expected_alias_count + ): + raise ValueError( + "Wazuh allowlisted check-mode dry-run target selectors must match expected alias count" + ) + if any(item.get("runtime_write_allowed") is True for item in target_selectors): + raise ValueError( + "Wazuh allowlisted check-mode dry-run target selectors must not allow runtime writes" + ) + if any(item.get("check_mode_allowed") is not True for item in target_selectors): + raise ValueError( + "Wazuh allowlisted check-mode dry-run target selectors must allow check-mode review" + ) + + readiness_keys = ( + "check_mode_plan_count", + "dry_run_evidence_ref_count", + "dry_run_result_ref_count", + "dry_run_redaction_attestation_count", + "post_dry_run_verifier_count", + "rollback_revalidation_count", + "km_playbook_writeback_ready_count", + "dry_run_packet_received_count", + "dry_run_packet_review_ready_count", + "dry_run_packet_accepted_count", + ) + if any(_int(summary.get(key)) <= 0 for key in readiness_keys): + raise ValueError( + "Wazuh allowlisted check-mode dry-run readiness counters must be positive" + ) + + boundaries = payload.get("execution_boundaries") + if not isinstance(boundaries, dict): + raise ValueError("Wazuh allowlisted check-mode dry-run execution_boundaries missing") + for key in _REQUIRED_FALSE_BOUNDARIES: + if boundaries.get(key) is not False: + raise ValueError( + f"Wazuh allowlisted check-mode dry-run execution_boundaries.{key} must remain false" + ) + if boundaries.get("not_authorization") is not True: + raise ValueError("Wazuh allowlisted check-mode dry-run not_authorization must remain true") + + +def _validation_result( + contract: dict[str, Any], + outcome_lane: str, + findings: list[dict[str, Any]], +) -> dict[str, Any]: + accepted = outcome_lane == "accepted_for_allowlisted_check_mode_dry_run_readback_only" + quarantined = outcome_lane == "quarantine_sensitive_payload" + rejected_runtime = outcome_lane == "reject_runtime_action_request" + supplement_required = not accepted and not quarantined and not rejected_runtime + return { + "schema_version": "iwooos_wazuh_allowlisted_check_mode_dry_run_validation_result_v1", + "contract_schema_version": contract["schema_version"], + "status": outcome_lane, + "mode": "no_persist_allowlisted_check_mode_dry_run_no_runtime_no_secret_collection", + "outcome_lane": outcome_lane, + "accepted_for_allowlisted_check_mode_dry_run_readback_only": accepted, + "quarantined": quarantined, + "runtime_action_rejected": rejected_runtime, + "summary": { + "dry_run_packet_received_count": 1, + "dry_run_packet_review_ready_count": 1 if accepted else 0, + "dry_run_packet_accepted_count": 1 if accepted else 0, + "dry_run_packet_supplement_required_count": 1 if supplement_required else 0, + "dry_run_packet_quarantined_count": 1 if quarantined else 0, + "dry_run_runtime_action_rejected_count": 1 if rejected_runtime else 0, + "runtime_gate_count": 0, + "wazuh_api_live_query_authorized_count": 0, + "wazuh_active_response_authorized_count": 0, + "host_write_authorized_count": 0, + "secret_value_collection_allowed_count": 0, + "finding_count": len(findings), + }, + "validation_findings": findings, + "boundary_markers": [ + "wazuh_allowlisted_check_mode_dry_run_validation_received_count=1", + f"wazuh_allowlisted_check_mode_dry_run_validation_accepted_count={1 if accepted else 0}", + f"wazuh_allowlisted_check_mode_dry_run_validation_quarantined_count={1 if quarantined else 0}", + f"wazuh_allowlisted_check_mode_dry_run_validation_runtime_action_rejected_count={1 if rejected_runtime else 0}", + "wazuh_allowlisted_check_mode_dry_run_validation_runtime_gate_count=0", + "wazuh_allowlisted_check_mode_dry_run_validation_no_persist=true", + "wazuh_api_live_query_authorized=false", + "wazuh_active_response_authorized=false", + "host_write_authorized=false", + "secret_value_collection_allowed=false", + "not_authorization=true", + ], + "boundaries": { + "payload_persisted": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "wazuh_agent_reenroll_authorized": False, + "wazuh_agent_restart_authorized": False, + "wazuh_manager_restart_authorized": False, + "host_write_authorized": False, + "active_scan_authorized": False, + "kali_execute_authorized": False, + "nginx_reload_authorized": False, + "secret_value_collection_allowed": False, + "runtime_execution_authorized": False, + "runtime_gate_open": False, + "not_authorization": True, + }, + "next_gate": "post_dry_run_verifier_readback_before_runtime_gate" + if accepted + else "allowlisted_check_mode_dry_run_packet_fix_and_resubmit", + } + + +def _finding( + check_id: str, + severity: str, + lane: str, + message: str, + field_paths: list[str], + extra: dict[str, Any] | None = None, +) -> dict[str, Any]: + payload: dict[str, Any] = { + "check_id": check_id, + "severity": severity, + "lane": lane, + "message": message, + "field_paths": field_paths, + } + if extra: + payload.update(extra) + return payload + + +def _present(value: Any) -> bool: + if value is None: + return False + if isinstance(value, str): + return bool(value.strip()) + if isinstance(value, list | dict | tuple | set): + return bool(value) + return True + + +def _validate_aliases(value: Any, expected_aliases: set[str]) -> str | None: + aliases = value if isinstance(value, list) else [] + if not aliases or not all(isinstance(item, str) for item in aliases): + return "target_selector_aliases must be an array of public alias strings." + alias_set = set(aliases) + if len(aliases) != len(alias_set): + return "target_selector_aliases must not contain duplicates." + if alias_set != expected_aliases: + missing = sorted(expected_aliases - alias_set) + extra = sorted(alias_set - expected_aliases) + return f"target_selector_aliases must match expected public aliases; missing={missing} extra={extra}" + return None + + +def _collect_sensitive_hits(value: Any, path: str = "$") -> list[dict[str, str]]: + hits: list[dict[str, str]] = [] + if isinstance(value, dict): + for key, item in value.items(): + key_text = str(key) + key_lower = key_text.lower() + for fragment in _FORBIDDEN_KEY_FRAGMENTS: + if fragment in key_lower: + hits.append({"path": f"{path}.{key_text}", "category": fragment}) + hits.extend(_collect_sensitive_hits(item, f"{path}.{key_text}")) + elif isinstance(value, list): + for index, item in enumerate(value): + hits.extend(_collect_sensitive_hits(item, f"{path}[{index}]")) + elif isinstance(value, str): + for category, pattern in _SENSITIVE_TEXT_PATTERNS.items(): + if pattern.search(value): + hits.append({"path": path, "category": category}) + return hits + + +def _collect_runtime_action_hits(value: Any, path: str = "$") -> list[str]: + hits: list[str] = [] + if isinstance(value, dict): + for key, item in value.items(): + key_text = str(key) + key_lower = key_text.lower() + if key_lower in _RUNTIME_ACTION_KEYS: + hits.append(f"{path}.{key_text}") + if key_lower in _RUNTIME_ACK_KEYS: + continue + hits.extend(_collect_runtime_action_hits(item, f"{path}.{key_text}")) + elif isinstance(value, list): + for index, item in enumerate(value): + hits.extend(_collect_runtime_action_hits(item, f"{path}[{index}]")) + elif isinstance(value, str): + lowered = value.lower() + for key in _RUNTIME_ACTION_KEYS: + if key in lowered: + hits.append(path) + break + return hits + + +def _first_blocking_lane(findings: list[dict[str, Any]]) -> str | None: + for finding in findings: + if finding.get("severity") in {"critical", "blocker"}: + return str(finding.get("lane")) + return None diff --git a/apps/api/tests/test_iwooos_runtime_security_readback.py b/apps/api/tests/test_iwooos_runtime_security_readback.py index 751dd7e63..61db06757 100644 --- a/apps/api/tests/test_iwooos_runtime_security_readback.py +++ b/apps/api/tests/test_iwooos_runtime_security_readback.py @@ -7,6 +7,9 @@ from src.api.v1.iwooos import router from src.services.iwooos_runtime_security_readback import ( load_latest_iwooos_runtime_security_readback, ) +from src.services.iwooos_wazuh_allowlisted_check_mode_dry_run import ( + load_latest_iwooos_wazuh_allowlisted_check_mode_dry_run, +) from src.services.iwooos_wazuh_runtime_controlled_apply_preflight import ( load_latest_iwooos_wazuh_runtime_controlled_apply_preflight, ) @@ -98,13 +101,41 @@ def _valid_runtime_gate_owner_review_packet() -> dict[str, object]: } +def _valid_allowlisted_check_mode_dry_run_packet() -> dict[str, object]: + return { + "dry_run_intent": "stage_allowlisted_check_mode_dry_run_readback_only", + "target_selector_aliases": [ + "managed_core_node_a", + "managed_core_node_b", + "managed_core_node_c", + "managed_edge_node_a", + "managed_edge_node_b", + "managed_lab_node_a", + ], + "check_mode_plan_ref": "playbooks/wazuh-allowlisted-check-mode#redacted-plan", + "dry_run_evidence_ref": "evidence/iwooos/wazuh-allowlisted-dry-run-redacted-v1", + "dry_run_result_ref": "evidence/iwooos/wazuh-allowlisted-dry-run-result-redacted-v1", + "dry_run_result_state": "staged_passed_no_runtime_action", + "dry_run_redaction_attestation": "redacted_refs_only_no_raw_output", + "post_dry_run_verifier_ref": "verifiers/iwooos-wazuh-post-dry-run-readback#public-safe", + "rollback_revalidation_ref": "playbooks/wazuh-controlled-apply-rollback#revalidated-redacted", + "km_playbook_writeback_ref": "km/playbook-trust/wazuh-allowlisted-check-mode-dry-run-v1", + "followup_owner": "iwooos-security-reviewer", + "audit_receipt_ref": "audit/iwooos-wazuh-allowlisted-check-mode-dry-run-redacted-v1", + "runtime_boundary_ack": "runtime_gate_remains_closed", + "live_wazuh_query_boundary_ack": "no_live_wazuh_query_performed", + "host_write_boundary_ack": "no_host_write_performed", + "secret_boundary_ack": "no_secret_value_collected", + } + + def test_iwooos_runtime_security_readback_preserves_zero_runtime_gates() -> None: payload = load_latest_iwooos_runtime_security_readback() assert payload["schema_version"] == "iwooos_runtime_security_readback_v1" assert payload["status"] == "blocked_waiting_owner_evidence_and_runtime_gates" - assert payload["summary"]["source_snapshot_count"] == 12 - assert payload["summary"]["p0_lane_count"] == 11 + assert payload["summary"]["source_snapshot_count"] == 13 + assert payload["summary"]["p0_lane_count"] == 12 assert payload["summary"]["runtime_gate_count"] == 0 assert payload["summary"]["owner_response_received_count"] == 0 assert payload["summary"]["owner_response_accepted_count"] == 0 @@ -170,6 +201,40 @@ def test_iwooos_runtime_security_readback_preserves_zero_runtime_gates() -> None ) assert payload["summary"]["wazuh_runtime_owner_review_packet_accepted_count"] == 1 assert payload["summary"]["wazuh_runtime_owner_review_runtime_gate_count"] == 0 + assert ( + payload["summary"][ + "wazuh_allowlisted_check_mode_dry_run_target_selector_count" + ] + == 6 + ) + assert ( + payload["summary"]["wazuh_allowlisted_check_mode_dry_run_check_mode_plan_count"] + == 1 + ) + assert ( + payload["summary"]["wazuh_allowlisted_check_mode_dry_run_evidence_ref_count"] + == 1 + ) + assert ( + payload["summary"]["wazuh_allowlisted_check_mode_dry_run_result_ref_count"] + == 1 + ) + assert ( + payload["summary"]["wazuh_allowlisted_check_mode_dry_run_packet_received_count"] + == 1 + ) + assert ( + payload["summary"]["wazuh_allowlisted_check_mode_dry_run_packet_accepted_count"] + == 1 + ) + assert ( + payload["summary"]["wazuh_allowlisted_check_mode_dry_run_post_verifier_count"] + == 1 + ) + assert ( + payload["summary"]["wazuh_allowlisted_check_mode_dry_run_runtime_gate_count"] + == 0 + ) assert payload["summary"]["kali_active_scan_authorized_count"] == 0 assert payload["summary"]["kali_execute_authorized_count"] == 0 assert payload["summary"]["alert_receipt_runtime_send_count"] == 0 @@ -190,6 +255,7 @@ def test_iwooos_runtime_security_readback_lanes_are_candidate_only() -> None: "wazuh_owner_evidence_preflight", "wazuh_runtime_controlled_apply_preflight", "wazuh_runtime_gate_owner_review", + "wazuh_allowlisted_check_mode_dry_run", "wazuh_dashboard_api", "kali_intake", "alert_readability", @@ -218,6 +284,15 @@ def test_iwooos_runtime_security_readback_lanes_are_candidate_only() -> None: ) for lane in payload["lanes"] ) + assert all( + lane["lane_id"] != "wazuh_allowlisted_check_mode_dry_run" + or ( + lane["completion_percent"] == 65 + and lane["metrics"]["packet_accepted"] == 1 + and lane["metrics"]["runtime_gate"] == 0 + ) + for lane in payload["lanes"] + ) assert all( lane["lane_id"] != "wazuh_owner_evidence_preflight" or lane["metrics"]["owner_accepted"] == 0 @@ -802,3 +877,147 @@ def test_iwooos_wazuh_runtime_gate_owner_review_validator_rejects_runtime_action assert data["runtime_action_rejected"] is True assert data["summary"]["owner_review_runtime_action_rejected_count"] == 1 assert data["summary"]["runtime_gate_count"] == 0 + + +def test_iwooos_wazuh_allowlisted_check_mode_dry_run_api_is_public_safe( + monkeypatch, +) -> None: + monkeypatch.delenv("IWOOOS_WAZUH_READONLY_ENABLED", raising=False) + monkeypatch.delenv("WAZUH_API_BASE_URL", raising=False) + monkeypatch.delenv("WAZUH_API_USERNAME", raising=False) + monkeypatch.delenv("WAZUH_API_PASSWORD", raising=False) + + payload = load_latest_iwooos_wazuh_allowlisted_check_mode_dry_run() + assert ( + payload["schema_version"] + == "iwooos_wazuh_allowlisted_check_mode_dry_run_readback_v1" + ) + assert payload["status"] == "allowlisted_check_mode_dry_run_staged_no_runtime_action" + assert payload["summary"]["dry_run_packet_accepted_count"] == 1 + assert payload["summary"]["allowlisted_target_selector_count"] == 6 + assert payload["summary"]["runtime_gate_count"] == 0 + assert payload["boundaries"]["runtime_execution_authorized"] is False + + response = _client().get("/api/v1/iwooos/wazuh-allowlisted-check-mode-dry-run") + + assert response.status_code == 200 + data = response.json() + assert ( + data["schema_version"] + == "iwooos_wazuh_allowlisted_check_mode_dry_run_readback_v1" + ) + assert data["summary"]["dry_run_packet_received_count"] == 1 + assert data["summary"]["dry_run_packet_review_ready_count"] == 1 + assert data["summary"]["dry_run_packet_accepted_count"] == 1 + assert data["summary"]["runtime_gate_count"] == 0 + assert data["summary"]["wazuh_api_live_query_authorized_count"] == 0 + assert data["summary"]["wazuh_active_response_authorized_count"] == 0 + assert data["summary"]["host_write_authorized_count"] == 0 + assert data["summary"]["secret_value_collection_allowed_count"] == 0 + assert data["boundaries"]["payload_persisted"] is False + assert data["boundaries"]["wazuh_api_live_query_authorized"] is False + assert data["boundaries"]["wazuh_active_response_authorized"] is False + assert data["boundaries"]["host_write_authorized"] is False + assert data["boundaries"]["runtime_gate_open"] is False + assert data["boundaries"]["not_authorization"] is True + assert len(data["target_selectors"]) == 6 + assert len(data["dry_run_items"]) == 6 + assert any( + marker == "wazuh_allowlisted_check_mode_dry_run_validation_api_available=true" + for marker in data["boundary_markers"] + ) + assert "192.168.0." not in response.text + assert "工作視窗" not in response.text + assert "批准!繼續" not in response.text + assert "WAZUH_API_PASSWORD" not in response.text + + +def test_iwooos_wazuh_allowlisted_check_mode_dry_run_validator_accepts_redacted_packet( + monkeypatch, +) -> None: + monkeypatch.delenv("IWOOOS_WAZUH_READONLY_ENABLED", raising=False) + monkeypatch.delenv("WAZUH_API_BASE_URL", raising=False) + monkeypatch.delenv("WAZUH_API_USERNAME", raising=False) + monkeypatch.delenv("WAZUH_API_PASSWORD", raising=False) + + client = _client() + before = client.get("/api/v1/iwooos/wazuh-allowlisted-check-mode-dry-run").json() + response = client.post( + "/api/v1/iwooos/wazuh-allowlisted-check-mode-dry-run/validate-dry-run-packet", + json=_valid_allowlisted_check_mode_dry_run_packet(), + ) + after = client.get("/api/v1/iwooos/wazuh-allowlisted-check-mode-dry-run").json() + + assert response.status_code == 200 + data = response.json() + assert ( + data["schema_version"] + == "iwooos_wazuh_allowlisted_check_mode_dry_run_validation_result_v1" + ) + assert data["status"] == "accepted_for_allowlisted_check_mode_dry_run_readback_only" + assert data["accepted_for_allowlisted_check_mode_dry_run_readback_only"] is True + assert data["summary"]["dry_run_packet_received_count"] == 1 + assert data["summary"]["dry_run_packet_review_ready_count"] == 1 + assert data["summary"]["dry_run_packet_accepted_count"] == 1 + assert data["summary"]["runtime_gate_count"] == 0 + assert data["summary"]["wazuh_api_live_query_authorized_count"] == 0 + assert data["summary"]["wazuh_active_response_authorized_count"] == 0 + assert data["summary"]["host_write_authorized_count"] == 0 + assert data["summary"]["secret_value_collection_allowed_count"] == 0 + assert data["boundaries"]["payload_persisted"] is False + assert data["boundaries"]["runtime_execution_authorized"] is False + assert data["boundaries"]["runtime_gate_open"] is False + assert before["summary"] == after["summary"] + assert "192.168.0." not in response.text + assert "工作視窗" not in response.text + assert "批准!繼續" not in response.text + + +def test_iwooos_wazuh_allowlisted_check_mode_dry_run_validator_quarantines_sensitive_payload( + monkeypatch, +) -> None: + monkeypatch.delenv("IWOOOS_WAZUH_READONLY_ENABLED", raising=False) + monkeypatch.delenv("WAZUH_API_BASE_URL", raising=False) + monkeypatch.delenv("WAZUH_API_USERNAME", raising=False) + monkeypatch.delenv("WAZUH_API_PASSWORD", raising=False) + + packet = _valid_allowlisted_check_mode_dry_run_packet() + packet[ + "redacted_evidence_ref" + ] = "dry-run raw output includes 10.1.2.3 and Authorization: Bearer abcdefghijklmnop" + response = _client().post( + "/api/v1/iwooos/wazuh-allowlisted-check-mode-dry-run/validate-dry-run-packet", + json=packet, + ) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "quarantine_sensitive_payload" + assert data["quarantined"] is True + assert data["summary"]["dry_run_packet_quarantined_count"] == 1 + assert data["summary"]["runtime_gate_count"] == 0 + assert "10.1.2.3" not in response.text + assert "Bearer abcdefghijklmnop" not in response.text + + +def test_iwooos_wazuh_allowlisted_check_mode_dry_run_validator_rejects_runtime_action( + monkeypatch, +) -> None: + monkeypatch.delenv("IWOOOS_WAZUH_READONLY_ENABLED", raising=False) + monkeypatch.delenv("WAZUH_API_BASE_URL", raising=False) + monkeypatch.delenv("WAZUH_API_USERNAME", raising=False) + monkeypatch.delenv("WAZUH_API_PASSWORD", raising=False) + + packet = _valid_allowlisted_check_mode_dry_run_packet() + packet["wazuh_active_response"] = True + response = _client().post( + "/api/v1/iwooos/wazuh-allowlisted-check-mode-dry-run/validate-dry-run-packet", + json=packet, + ) + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "reject_runtime_action_request" + assert data["runtime_action_rejected"] is True + assert data["summary"]["dry_run_runtime_action_rejected_count"] == 1 + assert data["summary"]["runtime_gate_count"] == 0 diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index fe6b71b5f..058d0fe51 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -20429,8 +20429,8 @@ }, "runtimeSecurityReadback": { "eyebrow": "IwoooS Runtime 資安讀回", - "title": "十一條 P0 資安線先接到同一張讀回板", - "subtitle": "這張板讀取後端彙整的只讀快照,並附上 Wazuh 正式只讀路由、controlled apply preflight 與 owner-review readback 的公開安全彙總;它不保存原始 Wazuh 載荷、不啟動掃描、不送訊息、不改主機。", + "title": "十二條 P0 資安線先接到同一張讀回板", + "subtitle": "這張板讀取後端彙整的只讀快照,並附上 Wazuh 正式只讀路由、controlled apply preflight、owner-review readback 與 allowlisted check-mode dry-run 的公開安全彙總;它不保存原始 Wazuh 載荷、不啟動掃描、不送訊息、不改主機。", "statusLabel": "讀回狀態", "statusDetail": "讀回成功只代表 IwoooS 能看見目前的證據邊界;runtime 寫入、主動回應、掃描、重啟、Nginx reload、workflow 修改與機密操作仍全部關閉。", "laneStatusLabel": "目前狀態", @@ -20450,6 +20450,7 @@ "wazuh_owner_evidence_preflight": "負責人證據預檢已就緒,尚未授權執行期動作", "wazuh_runtime_controlled_apply_preflight": "受控執行預檢已就緒,執行期閘門仍關閉", "wazuh_runtime_gate_owner_review": "owner-review packet 已接受為 review readiness,執行期閘門仍關閉", + "wazuh_allowlisted_check_mode_dry_run": "allowlisted check-mode dry-run 已 staged,執行期閘門仍關閉", "wazuh_dashboard_api": "API 連線退化,禁止顯示綠燈", "kali_intake": "部分執行期健康已整合,仍待完整驗收", "alert_readability": "格式合約已就緒,尚未有實發收件證據", @@ -20486,6 +20487,10 @@ "label": "Owner review", "detail": "review packet 已接受為 readback readiness;不代表 live query、active response 或 host write。" }, + "allowlistedDryRun": { + "label": "Dry-run staged", + "detail": "allowlisted check-mode dry-run refs 已讀回;不代表 live Wazuh query、active response 或 host write。" + }, "ownerAccepted": { "label": "負責人驗收", "detail": "收到 / 接受都必須由正式 owner response 證明。" @@ -20524,6 +20529,10 @@ "title": "Wazuh Runtime Gate Owner Review", "body": "owner-review decision、target selector、diff、check-mode / dry-run evidence、rollback、verifier 與 writeback 已讀回;它只代表 review readiness,不查 live Wazuh、不開 active response、不寫主機。" }, + "wazuh_allowlisted_check_mode_dry_run": { + "title": "Wazuh Allowlisted Check-Mode Dry-Run", + "body": "allowlisted public aliases、check-mode plan、dry-run evidence refs、redaction attestation、post dry-run verifier、rollback revalidation 與 KM / PlayBook writeback 已讀回;它仍不查 live Wazuh、不保存 raw output、不寫主機。" + }, "wazuh_dashboard_api": { "title": "Wazuh Dashboard API", "body": "API connection / API version 還要補 readback;index pattern 通過不能宣稱 Wazuh 全綠。" diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index fe6b71b5f..058d0fe51 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -20429,8 +20429,8 @@ }, "runtimeSecurityReadback": { "eyebrow": "IwoooS Runtime 資安讀回", - "title": "十一條 P0 資安線先接到同一張讀回板", - "subtitle": "這張板讀取後端彙整的只讀快照,並附上 Wazuh 正式只讀路由、controlled apply preflight 與 owner-review readback 的公開安全彙總;它不保存原始 Wazuh 載荷、不啟動掃描、不送訊息、不改主機。", + "title": "十二條 P0 資安線先接到同一張讀回板", + "subtitle": "這張板讀取後端彙整的只讀快照,並附上 Wazuh 正式只讀路由、controlled apply preflight、owner-review readback 與 allowlisted check-mode dry-run 的公開安全彙總;它不保存原始 Wazuh 載荷、不啟動掃描、不送訊息、不改主機。", "statusLabel": "讀回狀態", "statusDetail": "讀回成功只代表 IwoooS 能看見目前的證據邊界;runtime 寫入、主動回應、掃描、重啟、Nginx reload、workflow 修改與機密操作仍全部關閉。", "laneStatusLabel": "目前狀態", @@ -20450,6 +20450,7 @@ "wazuh_owner_evidence_preflight": "負責人證據預檢已就緒,尚未授權執行期動作", "wazuh_runtime_controlled_apply_preflight": "受控執行預檢已就緒,執行期閘門仍關閉", "wazuh_runtime_gate_owner_review": "owner-review packet 已接受為 review readiness,執行期閘門仍關閉", + "wazuh_allowlisted_check_mode_dry_run": "allowlisted check-mode dry-run 已 staged,執行期閘門仍關閉", "wazuh_dashboard_api": "API 連線退化,禁止顯示綠燈", "kali_intake": "部分執行期健康已整合,仍待完整驗收", "alert_readability": "格式合約已就緒,尚未有實發收件證據", @@ -20486,6 +20487,10 @@ "label": "Owner review", "detail": "review packet 已接受為 readback readiness;不代表 live query、active response 或 host write。" }, + "allowlistedDryRun": { + "label": "Dry-run staged", + "detail": "allowlisted check-mode dry-run refs 已讀回;不代表 live Wazuh query、active response 或 host write。" + }, "ownerAccepted": { "label": "負責人驗收", "detail": "收到 / 接受都必須由正式 owner response 證明。" @@ -20524,6 +20529,10 @@ "title": "Wazuh Runtime Gate Owner Review", "body": "owner-review decision、target selector、diff、check-mode / dry-run evidence、rollback、verifier 與 writeback 已讀回;它只代表 review readiness,不查 live Wazuh、不開 active response、不寫主機。" }, + "wazuh_allowlisted_check_mode_dry_run": { + "title": "Wazuh Allowlisted Check-Mode Dry-Run", + "body": "allowlisted public aliases、check-mode plan、dry-run evidence refs、redaction attestation、post dry-run verifier、rollback revalidation 與 KM / PlayBook writeback 已讀回;它仍不查 live Wazuh、不保存 raw output、不寫主機。" + }, "wazuh_dashboard_api": { "title": "Wazuh Dashboard API", "body": "API connection / API version 還要補 readback;index pattern 通過不能宣稱 Wazuh 全綠。" diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index bf4e81682..54bf3f28e 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -348,6 +348,7 @@ type RuntimeSecurityReadbackSummaryItem = { | 'metadataGate' | 'controlledApplyPreflight' | 'ownerReview' + | 'allowlistedDryRun' | 'ownerAccepted' | 'kaliRuntime' | 'runtimeGate' @@ -8247,6 +8248,7 @@ const runtimeSecurityLaneStatusKeys = new Set 0 ? 'steady' : 'locked', }, + { + key: 'allowlistedDryRun', + value: summary ? String(summary.wazuh_allowlisted_check_mode_dry_run_packet_accepted_count) : '...', + icon: FileCheck2, + tone: summary && summary.wazuh_allowlisted_check_mode_dry_run_packet_accepted_count > 0 ? 'steady' : 'locked', + }, { key: 'ownerAccepted', value: summary ? `${summary.owner_response_accepted_count}/${summary.owner_response_received_count}` : '...', diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index acf8d3419..0e704ced4 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -106,6 +106,7 @@ export interface IwoooSRuntimeSecurityReadbackLane { | 'wazuh_owner_evidence_preflight' | 'wazuh_runtime_controlled_apply_preflight' | 'wazuh_runtime_gate_owner_review' + | 'wazuh_allowlisted_check_mode_dry_run' | 'wazuh_dashboard_api' | 'kali_intake' | 'alert_readability' @@ -183,6 +184,14 @@ export interface IwoooSRuntimeSecurityReadbackResponse { wazuh_runtime_owner_review_packet_review_ready_count: number wazuh_runtime_owner_review_packet_accepted_count: number wazuh_runtime_owner_review_runtime_gate_count: number + wazuh_allowlisted_check_mode_dry_run_target_selector_count: number + wazuh_allowlisted_check_mode_dry_run_check_mode_plan_count: number + wazuh_allowlisted_check_mode_dry_run_evidence_ref_count: number + wazuh_allowlisted_check_mode_dry_run_result_ref_count: number + wazuh_allowlisted_check_mode_dry_run_packet_received_count: number + wazuh_allowlisted_check_mode_dry_run_packet_accepted_count: number + wazuh_allowlisted_check_mode_dry_run_post_verifier_count: number + wazuh_allowlisted_check_mode_dry_run_runtime_gate_count: number kali_active_scan_authorized_count: number kali_execute_authorized_count: number kali_finding_envelope_accepted_count: number @@ -420,6 +429,70 @@ export interface IwoooSWazuhRuntimeGateOwnerReviewReadbackResponse { no_false_green_rules: string[] } +export interface IwoooSWazuhAllowlistedCheckModeDryRunItem { + item_id: + | 'allowlisted_target_selector' + | 'check_mode_plan' + | 'dry_run_evidence' + | 'redaction_attestation' + | 'post_dry_run_verifier' + | 'rollback_and_writeback' + title: string + state_key: string + accepted: boolean + required_fields: string[] + next_gate: string +} + +export interface IwoooSWazuhAllowlistedCheckModeDryRunResponse { + schema_version: 'iwooos_wazuh_allowlisted_check_mode_dry_run_readback_v1' + source_schema_version: 'wazuh_allowlisted_check_mode_dry_run_v1' + status: string + mode: string + source_refs: string[] + dry_run_packet_validation_endpoint: string + dry_run_packet_validation_mode: string + summary: { + expected_scope_alias_count: number + allowlisted_target_selector_count: number + check_mode_plan_count: number + dry_run_evidence_ref_count: number + dry_run_result_ref_count: number + dry_run_redaction_attestation_count: number + post_dry_run_verifier_count: number + rollback_revalidation_count: number + km_playbook_writeback_ready_count: number + dry_run_packet_received_count: number + dry_run_packet_review_ready_count: number + dry_run_packet_accepted_count: number + dry_run_packet_supplement_required_count: number + dry_run_packet_quarantined_count: number + dry_run_runtime_action_rejected_count: number + forbidden_payload_count: number + forbidden_action_count: number + runtime_gate_count: number + wazuh_api_live_query_authorized_count: number + wazuh_active_response_authorized_count: number + host_write_authorized_count: number + secret_value_collection_allowed_count: number + } + target_selectors: Array<{ + node_alias: string + scope: string + selector_kind: string + check_mode_allowed: boolean + runtime_write_allowed: boolean + }> + required_dry_run_fields: string[] + dry_run_items: IwoooSWazuhAllowlistedCheckModeDryRunItem[] + outcome_lanes: string[] + forbidden_payloads: string[] + forbidden_actions: string[] + boundary_markers: string[] + boundaries: Record + no_false_green_rules: string[] +} + export interface IwoooSWazuhManagedHostCoverageHost { node_id: string role: string @@ -783,6 +856,11 @@ export const apiClient = { return handleResponse(res) }, + async getIwoooSWazuhAllowlistedCheckModeDryRun() { + const res = await fetch(`${API_BASE_URL}/iwooos/wazuh-allowlisted-check-mode-dry-run`, { cache: 'no-store' }) + return handleResponse(res) + }, + async getIwoooSWazuhManagedHostCoverage() { const res = await fetch(`${API_BASE_URL}/iwooos/wazuh-managed-host-coverage`, { cache: 'no-store' }) return handleResponse(res) diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index acce1723b..78f1ead67 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -48710,3 +48710,39 @@ production browser smoke: **本段驗證目標**: - JSON parse、`py_compile`、focused GitHub gate API tests、Delivery Workbench test、`git diff --check`。 - 此段只關閉內部 MCP / RAG / KM / PlayBook / LOG governance gap;GitHub executable channel 仍維持 freeze / external unblock。 +## 2026-06-28 — 19:09 Wazuh allowlisted check-mode dry-run 本地完成 + +**時間與來源**: +- 2026-06-28 18:20-19:09 Asia/Taipei。 +- 來源:feature branch `codex/wazuh-manager-registry-accepted-readback-20260628`,接續 production 已讀回的 Wazuh manager registry accepted、controlled apply preflight 與 runtime gate owner-review readback。 + +**完成內容**: +- 新增 `docs/security/wazuh-allowlisted-check-mode-dry-run.snapshot.json`,把 6 個公開 alias target selector、check-mode plan、dry-run evidence refs、dry-run result refs、redaction attestation、post dry-run verifier、rollback revalidation 與 KM / PlayBook writeback 收成可讀回 contract。 +- 新增 `GET /api/v1/iwooos/wazuh-allowlisted-check-mode-dry-run`,只回傳公開安全 readback;不查 live Wazuh、不讀主機、不保存 raw output、不讀或回傳 secret。 +- 新增 `POST /api/v1/iwooos/wazuh-allowlisted-check-mode-dry-run/validate-dry-run-packet`,只做單次 redacted dry-run packet no-persist validation,分流 accepted / supplement / quarantined / rejected runtime action;POST 不更新總帳。 +- `runtime-security-readback` 納入第 12 條 P0 lane:`source_snapshot_count=13`、`p0_lane_count=12`,並新增 allowlisted dry-run counters;dry-run accepted readback 為 `1`,runtime gate 仍為 `0`。 +- `/zh-TW/iwooos` 新增 allowlisted dry-run summary 與 lane status;`en` / `zh-TW` messages 依現有 IwoooS mirror guard 同步新增文案。 + +**本地驗證結果**: +- `python3 -m json.tool docs/security/wazuh-allowlisted-check-mode-dry-run.snapshot.json >/dev/null`:通過。 +- `python3 -m json.tool apps/web/messages/zh-TW.json >/dev/null`、`python3 -m json.tool apps/web/messages/en.json >/dev/null`:通過。 +- `python3 -m py_compile apps/api/src/services/iwooos_wazuh_allowlisted_check_mode_dry_run.py apps/api/src/services/iwooos_runtime_security_readback.py apps/api/src/api/v1/iwooos.py`:通過。 +- `DATABASE_URL=sqlite:///test.db PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_iwooos_runtime_security_readback.py -q`:`21 passed`。 +- `python3.11 -m ruff check apps/api/src/services/iwooos_wazuh_allowlisted_check_mode_dry_run.py apps/api/src/services/iwooos_runtime_security_readback.py apps/api/src/api/v1/iwooos.py apps/api/tests/test_iwooos_runtime_security_readback.py`:通過。 +- `python3 scripts/security/wazuh-readonly-route-boundary-guard.py --root .`:`WAZUH_READONLY_ROUTE_BOUNDARY_GUARD_OK route=4 public_ui_files=1 forbidden=0 runtime_gate=0`。 +- `python3 scripts/security/iwooos-frontend-display-redaction-guard.py --root .`:`IWOOOS_FRONTEND_DISPLAY_REDACTION_GUARD_OK`。 +- `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 +- `pnpm --filter @awoooi/web exec tsc --noEmit --incremental false`:通過。 +- `git diff --check`:通過。 + +**仍維持 0 / false**: +- `runtime_gate_count=0`、`wazuh_api_live_query_authorized_count=0`、`wazuh_active_response_authorized_count=0`、`host_write_authorized_count=0`、`secret_value_collection_allowed_count=0`。 +- `runtime_execution_authorized=false`、`payload_persisted=false`、`raw_wazuh_payload_storage_allowed=false`、`raw_output_storage_allowed=false`、`not_authorization=true`。 + +**未做**: +- 沒有查 live Wazuh API、沒有 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime 寫操作。 +- 沒有讀 secret 明文、沒有讀 `.env`、沒有讀 raw sessions / SQLite / auth。 +- 沒有使用 GitHub API / gh / GitHub Actions;沒有 force push。 + +**下一個 P0**: +- commit / push 到 Gitea main,等待 Gitea CD / code-review,部署後驗證 `GET /api/v1/iwooos/wazuh-allowlisted-check-mode-dry-run`、POST redacted sample、`GET /api/v1/iwooos/runtime-security-readback` 與 `/zh-TW/iwooos` desktop / mobile。 diff --git a/docs/security/wazuh-allowlisted-check-mode-dry-run.snapshot.json b/docs/security/wazuh-allowlisted-check-mode-dry-run.snapshot.json new file mode 100644 index 000000000..154b93fb1 --- /dev/null +++ b/docs/security/wazuh-allowlisted-check-mode-dry-run.snapshot.json @@ -0,0 +1,237 @@ +{ + "schema_version": "wazuh_allowlisted_check_mode_dry_run_v1", + "generated_at": "2026-06-28T19:10:00+08:00", + "status": "allowlisted_check_mode_dry_run_staged_no_runtime_action", + "mode": "committed_dry_run_readback_no_live_wazuh_no_secret_collection", + "summary": { + "expected_scope_alias_count": 6, + "allowlisted_target_selector_count": 6, + "check_mode_plan_count": 1, + "dry_run_evidence_ref_count": 1, + "dry_run_result_ref_count": 1, + "dry_run_redaction_attestation_count": 1, + "post_dry_run_verifier_count": 1, + "rollback_revalidation_count": 1, + "km_playbook_writeback_ready_count": 1, + "dry_run_packet_received_count": 1, + "dry_run_packet_review_ready_count": 1, + "dry_run_packet_accepted_count": 1, + "dry_run_packet_supplement_required_count": 0, + "dry_run_packet_quarantined_count": 0, + "dry_run_runtime_action_rejected_count": 0, + "forbidden_payload_count": 18, + "forbidden_action_count": 20, + "runtime_gate_count": 0, + "wazuh_api_live_query_authorized_count": 0, + "wazuh_active_response_authorized_count": 0, + "host_write_authorized_count": 0, + "secret_value_collection_allowed_count": 0 + }, + "target_selectors": [ + { + "node_alias": "managed_core_node_a", + "scope": "wazuh_manager_registry_accepted_alias", + "selector_kind": "public_alias_only", + "check_mode_allowed": true, + "runtime_write_allowed": false + }, + { + "node_alias": "managed_core_node_b", + "scope": "wazuh_manager_registry_accepted_alias", + "selector_kind": "public_alias_only", + "check_mode_allowed": true, + "runtime_write_allowed": false + }, + { + "node_alias": "managed_core_node_c", + "scope": "wazuh_manager_registry_accepted_alias", + "selector_kind": "public_alias_only", + "check_mode_allowed": true, + "runtime_write_allowed": false + }, + { + "node_alias": "managed_edge_node_a", + "scope": "wazuh_manager_registry_accepted_alias", + "selector_kind": "public_alias_only", + "check_mode_allowed": true, + "runtime_write_allowed": false + }, + { + "node_alias": "managed_edge_node_b", + "scope": "wazuh_manager_registry_accepted_alias", + "selector_kind": "public_alias_only", + "check_mode_allowed": true, + "runtime_write_allowed": false + }, + { + "node_alias": "managed_lab_node_a", + "scope": "wazuh_manager_registry_accepted_alias", + "selector_kind": "public_alias_only", + "check_mode_allowed": true, + "runtime_write_allowed": false + } + ], + "required_dry_run_fields": [ + "dry_run_intent", + "target_selector_aliases", + "check_mode_plan_ref", + "dry_run_evidence_ref", + "dry_run_result_ref", + "dry_run_result_state", + "dry_run_redaction_attestation", + "post_dry_run_verifier_ref", + "rollback_revalidation_ref", + "km_playbook_writeback_ref", + "followup_owner", + "audit_receipt_ref", + "runtime_boundary_ack", + "live_wazuh_query_boundary_ack", + "host_write_boundary_ack", + "secret_boundary_ack" + ], + "dry_run_items": [ + { + "item_id": "allowlisted_target_selector", + "title": "Allowlisted public-alias target selector", + "state_key": "allowlisted_target_selector_accepted", + "accepted": true, + "required_fields": [ + "target_selector_aliases" + ], + "next_gate": "post-dry-run verifier readback before any runtime gate change" + }, + { + "item_id": "check_mode_plan", + "title": "Check-mode plan reference", + "state_key": "check_mode_plan_accepted", + "accepted": true, + "required_fields": [ + "check_mode_plan_ref" + ], + "next_gate": "check-mode plan must remain redacted refs only" + }, + { + "item_id": "dry_run_evidence", + "title": "Dry-run evidence and result refs", + "state_key": "dry_run_evidence_accepted", + "accepted": true, + "required_fields": [ + "dry_run_evidence_ref", + "dry_run_result_ref", + "dry_run_result_state" + ], + "next_gate": "dry-run evidence cannot include raw host output" + }, + { + "item_id": "redaction_attestation", + "title": "Redaction attestation", + "state_key": "redaction_attestation_accepted", + "accepted": true, + "required_fields": [ + "dry_run_redaction_attestation", + "secret_boundary_ack", + "live_wazuh_query_boundary_ack" + ], + "next_gate": "raw output, secrets, live query and host writes remain rejected" + }, + { + "item_id": "post_dry_run_verifier", + "title": "Post dry-run verifier", + "state_key": "post_dry_run_verifier_accepted", + "accepted": true, + "required_fields": [ + "post_dry_run_verifier_ref" + ], + "next_gate": "verifier readback must pass before runtime gate review can continue" + }, + { + "item_id": "rollback_and_writeback", + "title": "Rollback revalidation and KM / PlayBook writeback", + "state_key": "rollback_writeback_accepted", + "accepted": true, + "required_fields": [ + "rollback_revalidation_ref", + "km_playbook_writeback_ref", + "audit_receipt_ref" + ], + "next_gate": "writeback receipt is required after verifier readback" + } + ], + "outcome_lanes": [ + "accepted_for_allowlisted_check_mode_dry_run_readback_only", + "request_allowlisted_check_mode_dry_run_supplement", + "request_target_selector_fix", + "request_dry_run_intent_fix", + "request_boundary_ack_fix", + "quarantine_sensitive_payload", + "reject_runtime_action_request" + ], + "forbidden_payloads": [ + "secret_value", + "token_value", + "private_key", + "cookie", + "session", + "authorization_header", + "client.keys", + "raw_wazuh_payload", + "raw_agent_identity", + "raw_hostname", + "internal_ip", + "full_cli_output", + "full_journal", + "raw_dashboard_request", + "unredacted_screenshot", + "private_namespace", + "raw_env_file", + "raw_runtime_volume" + ], + "forbidden_actions": [ + "wazuh_api_live_query", + "wazuh_active_response", + "wazuh_agent_restart", + "wazuh_agent_reenroll", + "wazuh_manager_restart", + "host_write", + "systemd_restart", + "docker_restart", + "nginx_reload", + "firewall_change", + "kali_active_scan", + "credentialed_scan", + "exploit_attempt", + "secret_rotation", + "k8s_apply", + "argocd_sync", + "database_migration", + "force_push", + "repo_ref_delete", + "workflow_trigger" + ], + "execution_boundaries": { + "active_scan_authorized": false, + "alertmanager_reload_authorized": false, + "auto_block_authorized": false, + "credentialed_scan_authorized": false, + "firewall_change_authorized": false, + "host_write_authorized": false, + "kali_execute_authorized": false, + "kali_scan_authorized": false, + "nginx_reload_authorized": false, + "production_write_authorized": false, + "runtime_execution_authorized": false, + "runtime_gate_open": false, + "secret_value_collection_allowed": false, + "telegram_send_authorized": false, + "wazuh_active_response_authorized": false, + "wazuh_api_live_query_authorized": false, + "not_authorization": true + }, + "no_false_green_rules": [ + "Allowlisted check-mode dry-run staged does not open runtime gate.", + "Dry-run evidence refs must be redacted and cannot include raw host output.", + "Target selectors are public aliases only and do not authorize host writes.", + "Dry-run readback does not authorize live Wazuh queries or active response.", + "Post dry-run verifier and KM / PlayBook writeback must pass before any future runtime gate review continues." + ] +}