From c0b597573ffc6d1327be92ddd777425a0319b2c1 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 15 Jul 2026 10:44:58 +0800 Subject: [PATCH] fix(iwooos): project canonical siem receipts --- .../iwooos_security_asset_control_plane.py | 75 ++++++++++++++++- ...est_iwooos_security_asset_control_plane.py | 83 ++++++++++++++++--- 2 files changed, 144 insertions(+), 14 deletions(-) diff --git a/apps/api/src/services/iwooos_security_asset_control_plane.py b/apps/api/src/services/iwooos_security_asset_control_plane.py index b317dc7fd..826b801de 100644 --- a/apps/api/src/services/iwooos_security_asset_control_plane.py +++ b/apps/api/src/services/iwooos_security_asset_control_plane.py @@ -309,9 +309,13 @@ def _compliance_rows(rows: Iterable[Any]) -> list[dict[str, Any]]: def _automation_stage_rows( - rows: Iterable[Any], accepted_ai_trace_count: int + rows: Iterable[Any], + accepted_ai_trace_count: int, + *, + runtime_receipt_counts: dict[str, int] | None = None, ) -> list[dict[str, Any]]: source = list(rows) + external_receipts = runtime_receipt_counts or {} stages: list[dict[str, Any]] = [] for stage_id, label, operation_types in _AI_LOOP_STAGES: receipt_count = sum( @@ -320,6 +324,10 @@ def _automation_stage_rows( if str(_value(row, "operation_type", "")) in operation_types and str(_value(row, "status", "")) in {"success", "dry_run"} ) + receipt_count = max( + receipt_count, + max(0, int(external_receipts.get(stage_id, 0) or 0)), + ) if stage_id == "decide": receipt_count += accepted_ai_trace_count stages.append( @@ -809,8 +817,17 @@ def build_iwooos_security_asset_control_plane( compliance_unknown = sum(row["unknown_count"] for row in compliance) accepted_ai_trace_count = int(_value(ai_trace_row, "accepted_trace_count", 0) or 0) + runtime_receipt_counts = { + "correlate": int( + _value(siem_row, "correlated_controlled_route_count_24h", 0) or 0 + ), + "verify": int(_value(siem_row, "independent_verifier_pass_count_24h", 0) or 0), + "learn": int(_value(siem_row, "learning_writeback_count_24h", 0) or 0), + } automation_stages = _automation_stage_rows( - automation_source, accepted_ai_trace_count + automation_source, + accepted_ai_trace_count, + runtime_receipt_counts=runtime_receipt_counts, ) observed_automation_stages = sum( 1 for stage in automation_stages if stage["status"] == "observed" @@ -882,6 +899,9 @@ def build_iwooos_security_asset_control_plane( "normalized_event_count_24h": int( _value(siem_row, "normalized_event_count_24h", 0) or 0 ), + "correlated_controlled_route_count_24h": runtime_receipt_counts["correlate"], + "independent_verifier_pass_count_24h": runtime_receipt_counts["verify"], + "learning_writeback_count_24h": runtime_receipt_counts["learn"], "mean_time_to_resolve_minutes_24h": ( round(float(_value(siem_row, "mttr_minutes_24h")), 1) if _value(siem_row, "mttr_minutes_24h") is not None @@ -1103,14 +1123,18 @@ def build_iwooos_security_asset_control_plane( ) ) missing_ai_stages = len(_AI_LOOP_STAGES) - observed_automation_stages - if missing_ai_stages > 0 or verified_receipts == 0: + if ( + missing_ai_stages > 0 + or verified_receipts == 0 + or not same_run_closed_loop_proven + ): work_items.append( _work_item( "AIA-P0-008-01", "P0", "ai_security_automation", "完成 AI Agent 同 run 受控閉環", - max(1, missing_ai_stages), + max(1, missing_ai_stages + (0 if same_run_closed_loop_proven else 1)), "同 trace 串接 decision、check、bounded apply、verifier、rollback/no-write 與 learning。", ) ) @@ -1927,6 +1951,49 @@ class IwoooSSecurityAssetControlPlaneService: WHERE project_id = 'awoooi' AND received_at >= NOW() - INTERVAL '24 hours') AS normalized_event_count_24h, + (SELECT count(*) FROM automation_operation_log route + WHERE route.operation_type = 'ansible_candidate_matched' + AND route.status IN ('success', 'dry_run') + AND route.created_at >= NOW() - INTERVAL '24 hours' + AND COALESCE( + NULLIF(route.incident_id::text, ''), + NULLIF(route.input ->> 'incident_id', '') + ) IS NOT NULL + AND NULLIF(route.input ->> 'catalog_id', '') IS NOT NULL) + AS correlated_controlled_route_count_24h, + (SELECT count(*) FROM incident_evidence verifier + WHERE verifier.collected_at >= NOW() - INTERVAL '24 hours' + AND verifier.verification_result = 'success' + AND NULLIF( + verifier.post_execution_state ->> 'automation_run_id', + '' + ) IS NOT NULL + AND NULLIF( + verifier.post_execution_state ->> 'apply_op_id', + '' + ) IS NOT NULL + AND verifier.post_execution_state + ->> 'all_postconditions_passed' = 'true' + AND verifier.post_execution_state + ->> 'executor_returncode_trusted' = 'false') + AS independent_verifier_pass_count_24h, + (SELECT count(*) FROM automation_operation_log learning + WHERE learning.operation_type + = 'ansible_learning_writeback_recorded' + AND learning.status = 'success' + AND learning.created_at >= NOW() - INTERVAL '24 hours' + AND NULLIF( + learning.input ->> 'automation_run_id', + '' + ) IS NOT NULL + AND NULLIF( + learning.input ->> 'apply_op_id', + '' + ) IS NOT NULL + AND learning.output ->> 'learning_recorded' = 'true' + AND learning.output + ->> 'repository_readback_verified' = 'true') + AS learning_writeback_count_24h, (SELECT avg(EXTRACT(EPOCH FROM (resolved_at - created_at)) / 60.0) FROM incidents WHERE project_id = 'awoooi' diff --git a/apps/api/tests/test_iwooos_security_asset_control_plane.py b/apps/api/tests/test_iwooos_security_asset_control_plane.py index c1250852d..f121075f2 100644 --- a/apps/api/tests/test_iwooos_security_asset_control_plane.py +++ b/apps/api/tests/test_iwooos_security_asset_control_plane.py @@ -31,6 +31,8 @@ def _ready_payload( *, discovery_status: str = "success", discovery_error: str | None = None, + automation_rows: list[SimpleNamespace] | None = None, + runtime_receipt_counts: dict[str, int] | None = None, ) -> dict: now = datetime(2026, 7, 11, 4, 0, tzinfo=UTC) discovery = _row( @@ -79,16 +81,25 @@ def _ready_payload( "encryption_at_rest", ) ] - automation = [ - _row(operation_type="asset_discovered", status="success", cnt=1), - _row(operation_type="rule_matched", status="success", cnt=3), - _row(operation_type="rule_created", status="success", cnt=2), - _row(operation_type="ansible_check_mode_executed", status="dry_run", cnt=2), - _row(operation_type="ansible_apply_executed", status="success", cnt=1), - _row(operation_type="remediation_verified", status="success", cnt=1), - _row(operation_type="km_created", status="success", cnt=1), - _row(operation_type="ansible_apply_executed", status="failed", cnt=1), - ] + automation = ( + automation_rows + if automation_rows is not None + else [ + _row(operation_type="asset_discovered", status="success", cnt=1), + _row(operation_type="rule_matched", status="success", cnt=3), + _row(operation_type="rule_created", status="success", cnt=2), + _row( + operation_type="ansible_check_mode_executed", + status="dry_run", + cnt=2, + ), + _row(operation_type="ansible_apply_executed", status="success", cnt=1), + _row(operation_type="remediation_verified", status="success", cnt=1), + _row(operation_type="km_created", status="success", cnt=1), + _row(operation_type="ansible_apply_executed", status="failed", cnt=1), + ] + ) + runtime_counts = runtime_receipt_counts or {} return build_iwooos_security_asset_control_plane( discovery_row=discovery, inventory_rows=inventory, @@ -108,6 +119,9 @@ def _ready_payload( resolved_incident_count_24h=3, decision_chain_count_24h=5, normalized_event_count_24h=18, + correlated_controlled_route_count_24h=runtime_counts.get("correlate", 0), + independent_verifier_pass_count_24h=runtime_counts.get("verify", 0), + learning_writeback_count_24h=runtime_counts.get("learn", 0), mttr_minutes_24h=14.25, ), audit_row=_row( @@ -188,6 +202,9 @@ def test_builds_live_asset_siem_audit_and_ai_control_plane_without_false_closure assert any( item["work_item_id"] == "AIA-P1-001-01" for item in payload["work_items"] ) + assert any( + item["work_item_id"] == "AIA-P0-008-01" for item in payload["work_items"] + ) public_text = json.dumps(payload, ensure_ascii=False) assert "192.168." not in public_text @@ -197,6 +214,41 @@ def test_builds_live_asset_siem_audit_and_ai_control_plane_without_false_closure assert 'secret_value_collection_allowed": true' not in public_text.lower() +def test_projects_canonical_runtime_receipts_without_false_same_run_closure() -> None: + payload = _ready_payload( + automation_rows=[ + _row(operation_type="asset_discovered", status="success", cnt=1), + _row(operation_type="rule_matched", status="success", cnt=2), + _row(operation_type="rule_created", status="success", cnt=2), + _row( + operation_type="ansible_check_mode_executed", + status="dry_run", + cnt=2, + ), + _row(operation_type="ansible_apply_executed", status="success", cnt=1), + _row(operation_type="remediation_verified", status="success", cnt=2), + _row(operation_type="km_created", status="success", cnt=2), + ], + runtime_receipt_counts={"correlate": 7, "verify": 5, "learn": 4}, + ) + + ai_stages = { + stage["stage_id"]: stage for stage in payload["ai_automation"]["stages"] + } + assert ai_stages["correlate"]["receipt_count_24h"] == 7 + assert ai_stages["verify"]["receipt_count_24h"] == 5 + assert ai_stages["learn"]["receipt_count_24h"] == 4 + assert payload["summary"]["verified_remediation_receipt_count_24h"] == 5 + assert payload["summary"]["siem_stage_coverage_percent"] == 100 + assert payload["summary"]["ai_loop_stage_coverage_percent"] == 100 + assert payload["completion"]["strict_runtime_closure_percent"] == 0 + assert payload["ai_automation"]["same_run_closed_loop_proven"] is False + + work_items = {item["work_item_id"]: item for item in payload["work_items"]} + assert "AIA-P0-007-01" not in work_items + assert work_items["AIA-P0-008-01"]["gap_count"] == 1 + + def test_discovery_failure_exposes_only_allowlisted_collector_ids() -> None: raw_error = ( "RuntimeError: asset_collector_failures=domain_tls_inventory," @@ -687,6 +739,17 @@ def test_service_bounds_source_concurrency_for_database_budget(monkeypatch) -> N assert "digest_pinned" in inventory_query assert "live_tls_probe_evidenced" in inventory_query assert "live_tls_healthy" in inventory_query + siem_query = next( + statement for statement in statements if "FROM alert_rule_catalog" in statement + ) + assert "correlated_controlled_route_count_24h" in siem_query + assert "ansible_candidate_matched" in siem_query + assert "independent_verifier_pass_count_24h" in siem_query + assert "all_postconditions_passed" in siem_query + assert "executor_returncode_trusted" in siem_query + assert "learning_writeback_count_24h" in siem_query + assert "ansible_learning_writeback_recorded" in siem_query + assert "repository_readback_verified" in siem_query def test_public_api_returns_live_aggregate(monkeypatch) -> None: