Compare commits
15 Commits
codex/gith
...
codex/wazu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c5ca49cbf | ||
|
|
19bc2d059b | ||
|
|
27b96f0450 | ||
|
|
da4d8d22e8 | ||
|
|
f5b8e7b3b0 | ||
|
|
b556c43d31 | ||
|
|
3b3fbef634 | ||
|
|
6840c3578b | ||
|
|
22a249aa7a | ||
|
|
af45811e87 | ||
|
|
061adec533 | ||
|
|
f4a7c01eef | ||
|
|
ba444193a0 | ||
|
|
17d25839dc | ||
|
|
f05d9f2a33 |
@@ -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],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 已授權",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 全綠。"
|
||||
|
||||
@@ -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 全綠。"
|
||||
|
||||
@@ -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}` : '...',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 stub:authority / 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 source;authority / 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 load:load5/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 200,schema `iwooos_wazuh_manager_registry_reviewer_validation_readback_v1`,狀態 `manager_registry_accepted_readback_committed_no_runtime_no_secret_collection`。
|
||||
- Readback counters:owner 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-persist,POST 後 GET 總帳仍維持 `manager_registry_accepted_count=6`、runtime gate `0`。
|
||||
- Production `GET /api/v1/iwooos/runtime-security-readback` HTTP 200,schema `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 200,target 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 200,owner-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 readback:manager 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 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。
|
||||
|
||||
237
docs/security/wazuh-allowlisted-check-mode-dry-run.snapshot.json
Normal file
237
docs/security/wazuh-allowlisted-check-mode-dry-run.snapshot.json
Normal 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."
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user