Compare commits

...

15 Commits

Author SHA1 Message Date
Your Name
3c5ca49cbf feat(iwooos): add wazuh allowlisted dry-run gate 2026-06-28 19:13:51 +08:00
Your Name
19bc2d059b fix(agents): refresh runtime control deploy readback 2026-06-28 19:09:26 +08:00
Your Name
27b96f0450 Merge remote-tracking branch 'gitea-ssh/main' into codex/github-redacted-evidence-validator-20260627 2026-06-28 19:04:00 +08:00
Your Name
da4d8d22e8 docs(recovery): record failclosed cron P3 readback [skip ci] 2026-06-28 19:03:40 +08:00
Your Name
f5b8e7b3b0 Merge remote-tracking branch 'gitea-ssh/main' into codex/github-redacted-evidence-validator-20260627 2026-06-28 19:02:56 +08:00
Your Name
b556c43d31 fix(agents): trigger runtime control plane deploy 2026-06-28 19:02:28 +08:00
Your Name
3b3fbef634 Revert "fix(recovery): remove immutable failclosed cron source"
This reverts commit ba444193a0.
2026-06-28 19:02:06 +08:00
Your Name
6840c3578b Revert "fix(recovery): reopen controlled cd lane source"
This reverts commit f4a7c01eef.
2026-06-28 19:02:06 +08:00
Your Name
22a249aa7a Merge remote-tracking branch 'gitea-ssh/main' into codex/github-redacted-evidence-validator-20260627 2026-06-28 19:01:42 +08:00
Your Name
af45811e87 fix(agents): format runtime control API test
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m44s
Code Review / ai-code-review (push) Successful in 9s
CD Pipeline / build-and-deploy (push) Successful in 5m55s
CD Pipeline / post-deploy-checks (push) Successful in 1m32s
2026-06-28 18:54:40 +08:00
Your Name
061adec533 feat(agents): add deploy control plane loop readback 2026-06-28 18:54:40 +08:00
Your Name
f4a7c01eef fix(recovery): reopen controlled cd lane source 2026-06-28 18:54:40 +08:00
Your Name
ba444193a0 fix(recovery): remove immutable failclosed cron source 2026-06-28 18:54:39 +08:00
Your Name
17d25839dc docs(logbook): record wazuh owner review readback [skip ci] 2026-06-28 18:52:24 +08:00
Your Name
f05d9f2a33 docs(logbook): record wazuh registry production readback [skip ci] 2026-06-28 18:50:30 +08:00
13 changed files with 1736 additions and 16 deletions

View File

@@ -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],

View File

@@ -10,12 +10,13 @@ KM, and Telegram receipts are present.
from __future__ import annotations
from collections.abc import Iterable, Mapping
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from sqlalchemy import text
from src.core.config import settings
from src.core.logging import get_logger
from sqlalchemy import text
from src.db.base import get_db_context
from src.services.report_generation_service import (
DAILY_REPORT_HOUR_TAIPEI,
@@ -28,7 +29,7 @@ from src.services.report_generation_service import (
_SCHEMA_VERSION = "ai_agent_autonomous_runtime_control_v1"
_RUNTIME_AUTHORITY = "current_owner_directive_controlled_ai_automation"
_DEPLOY_READBACK_MARKER = "p2_416_d1n_autonomous_runtime_control_prod_readback_v2"
_DEPLOY_ATTEMPT_NOTE = "cd_3673_retry_after_host_pressure_gate_fix"
_DEPLOY_ATTEMPT_NOTE = "cd_internal_control_plane_readback_retry_20260628_2"
_LIVE_READBACK_SCHEMA_VERSION = "ai_agent_autonomous_runtime_receipt_readback_v1"
_DEFAULT_PROJECT_ID = "awoooi"
_DEFAULT_LOOKBACK_HOURS = 24
@@ -54,8 +55,8 @@ def _utc_iso(value: Any) -> str | None:
return None
if isinstance(value, datetime):
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc).isoformat()
value = value.replace(tzinfo=UTC)
return value.astimezone(UTC).isoformat()
return str(value)
@@ -229,6 +230,188 @@ def _latest_flow_closure(
}
def classify_deploy_control_plane_observation(
*,
run_status: str,
is_latest_deploy_intent: bool,
active_task_container_count: int,
production_marker_hit: bool,
latest_flow_closed: bool,
runner_capacity_ok: bool,
runner_forbidden_label_count: int,
) -> dict[str, Any]:
"""Classify CD/run noise into an internal PlayBook decision."""
normalized_status = str(run_status or "unknown").strip().lower()
has_active_task = active_task_container_count > 0
runner_lane_safe = runner_capacity_ok and runner_forbidden_label_count == 0
production_truth_ok = production_marker_hit and latest_flow_closed
if not is_latest_deploy_intent:
classification = "superseded_run_skip"
action = "skip_cd_work_and_attach_to_superseded_intent"
elif production_truth_ok and normalized_status == "success":
classification = "deploy_succeeded_marker_hit"
action = "close_deploy_intent_and_write_receipts"
elif normalized_status == "running" and has_active_task and runner_lane_safe:
classification = "running_with_controlled_task"
action = "continue_observing_without_restarting_runner"
elif normalized_status == "running" and not has_active_task and production_truth_ok:
classification = "running_no_container_stale_ui"
action = "treat_gitea_spinner_as_stale_and_keep_production_truth"
elif normalized_status == "failure" and production_truth_ok:
classification = "failed_run_superseded_by_marker_hit"
action = "record_non_blocking_failure_and_keep_current_marker"
elif normalized_status == "failure":
classification = "real_failure_requires_playbook_repair"
action = "open_cd_repair_playbook_with_target_selector_and_verifier"
elif not runner_lane_safe:
classification = "runner_lane_guardrail_violation"
action = "fail_closed_runner_lane_and_open_repair_playbook"
else:
classification = "waiting_for_controlled_observation"
action = "wait_for_mcp_observation_or_deploy_intent_update"
return {
"schema_version": "ai_agent_deploy_control_plane_decision_v1",
"classification": classification,
"action": action,
"inputs": {
"run_status": normalized_status,
"is_latest_deploy_intent": is_latest_deploy_intent,
"active_task_container_count": max(0, active_task_container_count),
"production_marker_hit": production_marker_hit,
"latest_flow_closed": latest_flow_closed,
"runner_capacity_ok": runner_capacity_ok,
"runner_forbidden_label_count": max(0, runner_forbidden_label_count),
},
"internal_writeback": {
"mcp_event_type": "deploy_run_observation",
"rag_context_required": True,
"km_writeback_required": True,
"playbook_route_required": True,
"log_projection_required": True,
"telegram_receipt_required": classification in {
"deploy_succeeded_marker_hit",
"real_failure_requires_playbook_repair",
"runner_lane_guardrail_violation",
},
},
"safety_boundary": {
"reads_raw_sessions": False,
"reads_secret_values": False,
"opens_legacy_runner": False,
"uses_force_push": False,
"writes_runtime_state": classification in {
"deploy_succeeded_marker_hit",
"real_failure_requires_playbook_repair",
"runner_lane_guardrail_violation",
},
},
}
def _control_plane_integration() -> dict[str, Any]:
classifier_examples = [
classify_deploy_control_plane_observation(
run_status="success",
is_latest_deploy_intent=True,
active_task_container_count=0,
production_marker_hit=True,
latest_flow_closed=True,
runner_capacity_ok=True,
runner_forbidden_label_count=0,
),
classify_deploy_control_plane_observation(
run_status="running",
is_latest_deploy_intent=True,
active_task_container_count=0,
production_marker_hit=True,
latest_flow_closed=True,
runner_capacity_ok=True,
runner_forbidden_label_count=0,
),
classify_deploy_control_plane_observation(
run_status="failure",
is_latest_deploy_intent=True,
active_task_container_count=0,
production_marker_hit=False,
latest_flow_closed=False,
runner_capacity_ok=True,
runner_forbidden_label_count=0,
),
]
return {
"schema_version": "ai_agent_autonomous_runtime_internal_loop_v1",
"status": "mcp_rag_km_playbook_log_control_loop_declared",
"purpose": (
"把 Gitea run、runner lane、production marker、browser smoke 與 executor receipt "
"先收斂成內部事件,再由 PlayBook decision 推進或跳過。"
),
"mcp_sensors": [
{
"sensor_id": "gitea_actions_run_observer",
"normalized_event": "RunObservation",
"raw_secret_access_allowed": False,
},
{
"sensor_id": "controlled_runner_lane_observer",
"normalized_event": "RunnerLaneState",
"raw_runner_token_access_allowed": False,
},
{
"sensor_id": "production_marker_observer",
"normalized_event": "ProductionTruthSnapshot",
"raw_session_access_allowed": False,
},
{
"sensor_id": "browser_smoke_observer",
"normalized_event": "FrontendTruthSnapshot",
"raw_conversation_access_allowed": False,
},
],
"rag_context_queries": [
"runner_pressure_buildkit_stockplatform_collision",
"controlled_cd_lane_capacity_label_guardrails",
"autonomous_runtime_marker_receipt_contract",
],
"playbook_decision_classes": [
"deploy_succeeded_marker_hit",
"running_with_controlled_task",
"running_no_container_stale_ui",
"superseded_run_skip",
"failed_run_superseded_by_marker_hit",
"real_failure_requires_playbook_repair",
"runner_lane_guardrail_violation",
],
"km_writeback_contract": {
"knowledge_entry_path_type": "deploy_control_plane_decision:<deploy_intent_id>",
"required_refs": [
"deploy_intent_id",
"target_sha",
"gitea_run_id",
"production_marker",
"latest_flow_closure",
"runner_lane_state",
],
"stores_raw_logs": False,
"stores_secret_values": False,
},
"log_projection_contract": {
"timeline_event_type": "ai_agent_deploy_control_plane_decision",
"logbook_projection": "summary_only_after_verifier",
"raw_html_or_long_log_allowed": False,
},
"classifier_examples": classifier_examples,
"rollups": {
"mcp_sensor_count": 4,
"rag_context_query_count": 3,
"playbook_decision_class_count": 7,
"classifier_example_count": len(classifier_examples),
},
}
def build_runtime_receipt_readback_from_rows(
*,
project_id: str = _DEFAULT_PROJECT_ID,
@@ -483,9 +666,10 @@ def build_ai_agent_autonomous_runtime_control() -> dict[str, Any]:
"new_behavior": "用 Telegram Gateway 實送報告與 actionable receipt不直接暴露 Bot API",
},
]
control_plane_integration = _control_plane_integration()
payload = {
"schema_version": _SCHEMA_VERSION,
"generated_at": datetime.now(timezone.utc).isoformat(),
"generated_at": datetime.now(UTC).isoformat(),
"program_status": {
"current_task_id": "P2-416-D1N",
"status": "current_directive_control_plane_active",
@@ -565,6 +749,7 @@ def build_ai_agent_autonomous_runtime_control() -> dict[str, Any]:
"telegram_receipt_or_alert",
],
},
"control_plane_integration": control_plane_integration,
"legacy_policy_overrides": legacy_overrides,
"hard_blockers": hard_blockers,
"visibility_contract": {
@@ -589,6 +774,10 @@ def build_ai_agent_autonomous_runtime_control() -> dict[str, Any]:
1 for item in executor_receipts if item["writes_runtime_state"]
),
"legacy_policy_overridden_count": len(legacy_overrides),
"mcp_sensor_count": control_plane_integration["rollups"]["mcp_sensor_count"],
"rag_context_query_count": control_plane_integration["rollups"]["rag_context_query_count"],
"playbook_decision_class_count": control_plane_integration["rollups"]["playbook_decision_class_count"],
"deploy_control_classifier_example_count": control_plane_integration["rollups"]["classifier_example_count"],
},
}
_attach_runtime_receipt_readback(

View File

@@ -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 readbackruntime 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 已授權",
],
}

View File

@@ -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

View File

@@ -1,6 +1,7 @@
from src.services.ai_agent_autonomous_runtime_control import (
build_ai_agent_autonomous_runtime_control,
build_runtime_receipt_readback_from_rows,
classify_deploy_control_plane_observation,
)
@@ -15,7 +16,7 @@ def test_ai_agent_autonomous_runtime_control_uses_current_owner_directive():
"p2_416_d1n_autonomous_runtime_control_prod_readback_v2"
)
assert data["program_status"]["deploy_attempt_note"] == (
"cd_3673_retry_after_host_pressure_gate_fix"
"cd_internal_control_plane_readback_retry_20260628_2"
)
assert data["program_status"]["legacy_no_send_no_live_rules_overridden"] is True
assert data["program_status"]["implementation_completion_percent"] == 88
@@ -58,6 +59,57 @@ def test_ai_agent_autonomous_runtime_control_exposes_reports_and_executor_receip
assert data["runtime_receipt_readback"]["db_read_status"] == "not_queried"
def test_ai_agent_autonomous_runtime_control_exposes_internal_control_loop():
data = build_ai_agent_autonomous_runtime_control()
integration = data["control_plane_integration"]
assert integration["schema_version"] == "ai_agent_autonomous_runtime_internal_loop_v1"
assert integration["status"] == "mcp_rag_km_playbook_log_control_loop_declared"
assert {sensor["normalized_event"] for sensor in integration["mcp_sensors"]} == {
"RunObservation",
"RunnerLaneState",
"ProductionTruthSnapshot",
"FrontendTruthSnapshot",
}
assert "controlled_cd_lane_capacity_label_guardrails" in integration["rag_context_queries"]
assert "running_no_container_stale_ui" in integration["playbook_decision_classes"]
assert integration["km_writeback_contract"]["stores_raw_logs"] is False
assert integration["km_writeback_contract"]["stores_secret_values"] is False
assert integration["log_projection_contract"]["raw_html_or_long_log_allowed"] is False
assert data["rollups"]["mcp_sensor_count"] == 4
assert data["rollups"]["playbook_decision_class_count"] == 7
def test_deploy_control_plane_classifier_separates_stale_spinner_from_real_failure():
stale = classify_deploy_control_plane_observation(
run_status="running",
is_latest_deploy_intent=True,
active_task_container_count=0,
production_marker_hit=True,
latest_flow_closed=True,
runner_capacity_ok=True,
runner_forbidden_label_count=0,
)
assert stale["classification"] == "running_no_container_stale_ui"
assert stale["action"] == "treat_gitea_spinner_as_stale_and_keep_production_truth"
assert stale["safety_boundary"]["writes_runtime_state"] is False
assert stale["internal_writeback"]["km_writeback_required"] is True
failure = classify_deploy_control_plane_observation(
run_status="failure",
is_latest_deploy_intent=True,
active_task_container_count=0,
production_marker_hit=False,
latest_flow_closed=False,
runner_capacity_ok=True,
runner_forbidden_label_count=0,
)
assert failure["classification"] == "real_failure_requires_playbook_repair"
assert failure["action"] == "open_cd_repair_playbook_with_target_selector_and_verifier"
assert failure["safety_boundary"]["opens_legacy_runner"] is False
assert failure["internal_writeback"]["playbook_route_required"] is True
def test_ai_agent_autonomous_runtime_control_keeps_hard_blockers_and_redaction():
data = build_ai_agent_autonomous_runtime_control()

View File

@@ -6,7 +6,6 @@ from src.services.ai_agent_autonomous_runtime_control import (
build_ai_agent_autonomous_runtime_control,
)
_PUBLIC_FORBIDDEN_TERMS = [
"工作視窗",
"對話內容",
@@ -68,7 +67,7 @@ def test_get_ai_agent_autonomous_runtime_control_api(monkeypatch):
"p2_416_d1n_autonomous_runtime_control_prod_readback_v2"
)
assert data["program_status"]["deploy_attempt_note"] == (
"cd_3673_retry_after_host_pressure_gate_fix"
"cd_internal_control_plane_readback_retry_20260628_2"
)
assert data["current_policy"]["owner_review_required_for_low_medium_high"] is False
assert data["report_delivery"]["status"] == "telegram_gateway_delivery_enabled"
@@ -77,6 +76,11 @@ def test_get_ai_agent_autonomous_runtime_control_api(monkeypatch):
"ai_agent_autonomous_runtime_receipt_readback_v1"
)
assert data["runtime_receipt_readback"]["db_read_status"] == "not_queried"
assert data["control_plane_integration"]["status"] == (
"mcp_rag_km_playbook_log_control_loop_declared"
)
assert data["rollups"]["mcp_sensor_count"] == 4
assert data["rollups"]["deploy_control_classifier_example_count"] == 3
def test_get_ai_agent_autonomous_runtime_control_api_redacts_public_terms(monkeypatch):

View File

@@ -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

View File

@@ -20429,8 +20429,8 @@
},
"runtimeSecurityReadback": {
"eyebrow": "IwoooS Runtime 資安讀回",
"title": "十條 P0 資安線先接到同一張讀回板",
"subtitle": "這張板讀取後端彙整的只讀快照,並附上 Wazuh 正式只讀路由、controlled apply preflightowner-review readback 的公開安全彙總;它不保存原始 Wazuh 載荷、不啟動掃描、不送訊息、不改主機。",
"title": "十條 P0 資安線先接到同一張讀回板",
"subtitle": "這張板讀取後端彙整的只讀快照,並附上 Wazuh 正式只讀路由、controlled apply preflightowner-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 還要補 readbackindex pattern 通過不能宣稱 Wazuh 全綠。"

View File

@@ -20429,8 +20429,8 @@
},
"runtimeSecurityReadback": {
"eyebrow": "IwoooS Runtime 資安讀回",
"title": "十條 P0 資安線先接到同一張讀回板",
"subtitle": "這張板讀取後端彙整的只讀快照,並附上 Wazuh 正式只讀路由、controlled apply preflightowner-review readback 的公開安全彙總;它不保存原始 Wazuh 載荷、不啟動掃描、不送訊息、不改主機。",
"title": "十條 P0 資安線先接到同一張讀回板",
"subtitle": "這張板讀取後端彙整的只讀快照,並附上 Wazuh 正式只讀路由、controlled apply preflightowner-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 還要補 readbackindex pattern 通過不能宣稱 Wazuh 全綠。"

View File

@@ -348,6 +348,7 @@ type RuntimeSecurityReadbackSummaryItem = {
| 'metadataGate'
| 'controlledApplyPreflight'
| 'ownerReview'
| 'allowlistedDryRun'
| 'ownerAccepted'
| 'kaliRuntime'
| 'runtimeGate'
@@ -8247,6 +8248,7 @@ const runtimeSecurityLaneStatusKeys = new Set<IwoooSRuntimeSecurityReadbackRespo
'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',
@@ -8336,6 +8338,12 @@ function IwoooSRuntimeSecurityReadbackBoard() {
icon: ClipboardCheck,
tone: summary && summary.wazuh_runtime_owner_review_packet_accepted_count > 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}` : '...',

View File

@@ -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<string, boolean>
no_false_green_rules: string[]
}
export interface IwoooSWazuhManagedHostCoverageHost {
node_id: string
role: string
@@ -783,6 +856,11 @@ export const apiClient = {
return handleResponse<IwoooSWazuhRuntimeGateOwnerReviewReadbackResponse>(res)
},
async getIwoooSWazuhAllowlistedCheckModeDryRun() {
const res = await fetch(`${API_BASE_URL}/iwooos/wazuh-allowlisted-check-mode-dry-run`, { cache: 'no-store' })
return handleResponse<IwoooSWazuhAllowlistedCheckModeDryRunResponse>(res)
},
async getIwoooSWazuhManagedHostCoverage() {
const res = await fetch(`${API_BASE_URL}/iwooos/wazuh-managed-host-coverage`, { cache: 'no-store' })
return handleResponse<IwoooSWazuhManagedHostCoverageResponse>(res)

View File

@@ -1,3 +1,49 @@
## 2026-06-28 — 18:58 110 fail-closed authority cron immutable 修復與 P3 讀回
**背景**`1fdbc96a9` 曾把 fail-closed authority source / timer source 刪除live 110 也被改成 disabled stubauthority / canonical SHA `03bbf87d...``/etc/cron.d/awoooi-runner-failclosed-authority` 內容為 `No cron jobs are defined``awoooi-cd-lane-drain.service active/enabled`。後續 `f4d1b99da``6840c3578` 已 revert 該方向;`5c540460c` / `3b3fbef63` 補上 cron 檔 immutable 覆寫修復。
**完成內容**
- Source of truth 維持 fail-closed authority`scripts/reboot-recovery/enforce-110-runner-failclosed.sh` SHA `e8febeda83a0d3c8357a9d946f64de486dfea8d97f1c0bf597d9adc352d98681`
- live 110 已安裝 hotfix sourceauthority / canonical SHA 皆為 `e8febeda83a0d3c8357a9d946f64de486dfea8d97f1c0bf597d9adc352d98681`
- cron line 讀回:`* * * * * root /usr/local/lib/awoooi/enforce-110-runner-failclosed.authority.sh --apply >>/var/log/awoooi-runner-failclosed-authority-cron.log 2>&1`
- `awoooi-runner-failclosed-authority.timer``awoooi-runner-failclosed-enforcer.timer` 讀回 `active/enabled``awoooi-cd-lane-drain.service inactive/masked`job container `0`lane process `0`
**驗證結果**
- P3 release gate`PASS=38 WARN=3 BLOCKED=0`Result `P3_RELEASE_WITH_CAUTION`
- full cold-start 子 gate`PASS=92 WARN=1 BLOCKED=0`
- 110 loadload5/core `0.470833`、load15/core `0.431667`runner/CD guardrails `CD_LANE_GUARDRAILS_OK 1``BAD_RUNNER_GUARDRAILS 0``NO_ACTIVE_JOB_CONTAINERS`
- Actions 頁最新 running / 舊 run 仍是 `1fdbc96a9``#3846/#3847`,未看到 `5c540460c` 或後續 `[skip ci]` commit 觸發新 push workflow。
- 110 port 22 exec channel 有 30 秒級延遲;後續 SSH verifier 應使用較寬 timeout不得用 5 秒 server-alive 直接判死。
**邊界**:沒有讀 secret / runner token / raw session / SQLite / auth / `.env`;沒有重啟 Docker / Nginx / firewall / K3s / DB沒有 force push沒有打開 legacy runner 或 controlled drain lane。
## 2026-06-28 — 18:50 AI Agent deploy control plane 內部迴圈
**完成內容**
- `agent-autonomous-runtime-control` 新增 `control_plane_integration` readback將 Gitea run、controlled runner lane、production marker 與 browser smoke 轉成 MCP sensors、RAG context、PlayBook decision class、KM writeback contract 與 log projection contract。
- 新增 `classify_deploy_control_plane_observation()`,把 superseded run、production marker hit、controlled task running、stale Gitea spinner、real failure 與 runner lane guardrail violation 分流成 AI PlayBook action而不是重開 legacy runner 或回到人工判讀。
- API rollups 增加 `mcp_sensor_count``rag_context_query_count``playbook_decision_class_count``deploy_control_classifier_example_count`,讓正式 readback 可直接看出內部控制迴圈資產是否存在。
**驗證結果**
- `DATABASE_URL=sqlite:///test.db PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_ai_agent_autonomous_runtime_control.py apps/api/tests/test_ai_agent_autonomous_runtime_control_api.py -q``8 passed`
- `python3 -m py_compile apps/api/src/services/ai_agent_autonomous_runtime_control.py apps/api/tests/test_ai_agent_autonomous_runtime_control.py apps/api/tests/test_ai_agent_autonomous_runtime_control_api.py`:通過。
**邊界**:沒有讀 raw sessions / secret / runner token沒有開 legacy runner沒有 force push沒有直接寫 runtime只新增 readback 與分類器 contract。
## 2026-06-28 — 18:49 IwoooS Wazuh manager registry accepted 與 controlled apply preflight production readback
**完成內容**
- Production `GET /api/v1/iwooos/wazuh-manager-registry-reviewer-validation` HTTP 200schema `iwooos_wazuh_manager_registry_reviewer_validation_readback_v1`,狀態 `manager_registry_accepted_readback_committed_no_runtime_no_secret_collection`
- Readback countersowner export received / accepted / reviewer passed / post-enable readback / acceptance evidence received / acceptance ready 皆 `1``manager_registry_accepted_count=6`runtime gate、host write、active response、secret value collection 仍全 `0`
- Production `POST /validate-owner-export` valid redacted sample 回 `accepted_for_readonly_posture_only``POST /validate-manager-registry-acceptance` valid redacted sample 回 `accepted_for_manager_registry_acceptance_review_only`;兩個 POST 皆 no-persistPOST 後 GET 總帳仍維持 `manager_registry_accepted_count=6`、runtime gate `0`
- Production `GET /api/v1/iwooos/runtime-security-readback` HTTP 200schema `iwooos_runtime_security_readback_v1`,讀回 `wazuh_manager_registry_accepted_count=6``runtime_gate_count=0`
- Production `GET /api/v1/iwooos/wazuh-runtime-controlled-apply-preflight` HTTP 200target selector / source-of-truth diff / check-mode / dry-run / rollback / post-apply verifier / KM PlayBook writeback 皆 `1`redacted controlled-apply packet POST 回 `accepted_for_controlled_apply_preflight_review_only`POST 後 GET counters 不被 payload 改寫。
- Production `GET /api/v1/iwooos/wazuh-runtime-gate-owner-review-readback` HTTP 200owner-review packet received / review ready / accepted 皆 `1`、supplement `0`redacted owner-review packet POST 回 `accepted_for_runtime_gate_owner_review_readback_only`POST 後 GET counters 不被 payload 改寫。
- Runtime-security 總板同步讀回 `wazuh_runtime_apply_preflight_ready_count=1``wazuh_runtime_owner_review_packet_accepted_count=1``wazuh_live_metadata_gate_owner_accepted_count=1``wazuh_live_metadata_gate_live_query_authorized_count=0``runtime_gate_count=0`
- Production `/zh-TW/iwooos` desktop / mobile browser readbackmanager registry reviewer validation board 可見 `Reviewer passed=1``Post-enable=1``Acceptance ready=1``Manager accepted=6``執行期=0`console error `0`、水平溢出 `0`、敏感 pattern hit `0`
**邊界**:沒有讀 secret / raw Wazuh payload / raw session / SQLite / auth沒有查 live Wazuh沒有 active response、agent restart、host write、K8s secret patch、Nginx、firewall、DB、GitHub 或 force push。
## 2026-06-28 — 18:40 IwoooS Wazuh live metadata readiness production readback
**完成內容**
@@ -48664,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 gapGitHub 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 actionPOST 不更新總帳。
- `runtime-security-readback` 納入第 12 條 P0 lane`source_snapshot_count=13``p0_lane_count=12`,並新增 allowlisted dry-run countersdry-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。

View File

@@ -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."
]
}