diff --git a/apps/api/src/api/v1/iwooos.py b/apps/api/src/api/v1/iwooos.py index 998f53aa7..86c4e5997 100644 --- a/apps/api/src/api/v1/iwooos.py +++ b/apps/api/src/api/v1/iwooos.py @@ -37,6 +37,7 @@ from src.services.iwooos_wazuh_managed_host_coverage import ( ) from src.services.iwooos_wazuh_manager_registry_reviewer_validation import ( load_latest_iwooos_wazuh_manager_registry_reviewer_validation, + validate_iwooos_wazuh_manager_registry_acceptance_evidence as validate_wazuh_manager_registry_acceptance_evidence_payload, validate_iwooos_wazuh_manager_registry_owner_export as validate_wazuh_manager_registry_owner_export_payload, ) from src.services.iwooos_wazuh_owner_evidence_preflight import ( @@ -211,6 +212,40 @@ async def validate_iwooos_wazuh_manager_registry_owner_export(owner_export: dict ) from exc +@router.post( + "/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-manager-registry-acceptance", + response_model=dict[str, Any], + summary="驗證 Wazuh manager registry accepted 脫敏 evidence packet", + description=( + "針對單次 owner / reviewer 提供的 redacted Wazuh manager registry acceptance evidence " + "packet 進行 no-persist review readiness validation,回傳 accepted-for-review / needs supplement / " + "quarantined / rejected runtime action 分流。此端點不保存 payload、不查 Wazuh API、不讀主機、" + "不重新註冊 agent、不重啟服務、不讀或回傳機密明文、不啟用主動回應、不改 Nginx / Docker / " + "K8s / firewall,也不更新 manager registry accepted 總帳。" + ), +) +async def validate_iwooos_wazuh_manager_registry_acceptance_evidence( + acceptance_evidence: dict[str, Any], +) -> dict[str, Any]: + """回傳單次 Wazuh manager registry accepted evidence 的公開安全驗證結果。""" + try: + payload = await asyncio.to_thread( + validate_wazuh_manager_registry_acceptance_evidence_payload, + acceptance_evidence, + ) + 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 manager registry acceptance evidence 驗證器無效:{exc}", + ) from exc + + @router.get( "/api/v1/iwooos/runtime-security-readback", response_model=dict[str, Any], diff --git a/apps/api/src/services/iwooos_wazuh_manager_registry_reviewer_validation.py b/apps/api/src/services/iwooos_wazuh_manager_registry_reviewer_validation.py index 766fa1051..0e9b025e2 100644 --- a/apps/api/src/services/iwooos_wazuh_manager_registry_reviewer_validation.py +++ b/apps/api/src/services/iwooos_wazuh_manager_registry_reviewer_validation.py @@ -111,6 +111,24 @@ _ACCOUNTABILITY_REQUIRED_FIELDS = { "postcheck_plan", } +_ACCEPTANCE_EVIDENCE_REQUIRED_FIELDS = { + "acceptance_reviewer_role", + "acceptance_decision", + "acceptance_decision_reason", + "accepted_scope_aliases", + "accepted_agent_total", + "accepted_agent_active", + "accepted_agent_disconnected", + "accepted_agent_never_connected", + "manager_registry_acceptance_evidence_refs", + "post_enable_readback_ref", + "product_scope_parity_ref", + "reviewed_at", + "followup_owner", + "rollback_owner", + "post_acceptance_verifier_plan", +} + def load_latest_iwooos_wazuh_manager_registry_reviewer_validation( security_dir: Path | None = None, @@ -137,6 +155,15 @@ def load_latest_iwooos_wazuh_manager_registry_reviewer_validation( "reviewer_validation_failed_count": _int(summary.get("reviewer_validation_failed_count")), "reviewer_validation_quarantined_count": _int(summary.get("reviewer_validation_quarantined_count")), "manager_registry_accepted_count": _int(summary.get("manager_registry_accepted_count")), + "manager_registry_acceptance_intake_endpoint_available_count": _int( + summary.get("manager_registry_acceptance_intake_endpoint_available_count") + ), + "manager_registry_acceptance_evidence_received_count": _int( + summary.get("manager_registry_acceptance_evidence_received_count") + ), + "manager_registry_acceptance_evidence_review_ready_count": _int( + summary.get("manager_registry_acceptance_evidence_review_ready_count") + ), "post_enable_readback_passed_count": _int(summary.get("post_enable_readback_passed_count")), "runtime_gate_count": _int(summary.get("runtime_gate_count")), "host_write_authorized_count": _int(summary.get("host_write_authorized_count")), @@ -157,6 +184,10 @@ def load_latest_iwooos_wazuh_manager_registry_reviewer_validation( "/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export" ), "owner_export_validation_mode": "no_persist_validation_no_runtime_action", + "manager_registry_acceptance_validation_endpoint": ( + "/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-manager-registry-acceptance" + ), + "manager_registry_acceptance_validation_mode": "no_persist_acceptance_evidence_review_no_runtime_action", "summary": merged_summary, "expected_scope_aliases": _strings(snapshot.get("expected_scope_aliases")), "reviewer_validation_checks": _checks(snapshot.get("reviewer_validation_checks")), @@ -267,6 +298,9 @@ def _boundary_markers(summary: dict[str, int]) -> list[str]: f"wazuh_manager_registry_reviewer_validation_passed_count={summary['reviewer_validation_passed_count']}", f"wazuh_manager_registry_reviewer_validation_quarantined_count={summary['reviewer_validation_quarantined_count']}", f"wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count={summary['manager_registry_accepted_count']}", + "wazuh_manager_registry_acceptance_validation_api_available=true", + f"wazuh_manager_registry_acceptance_evidence_received_count={summary['manager_registry_acceptance_evidence_received_count']}", + f"wazuh_manager_registry_acceptance_evidence_review_ready_count={summary['manager_registry_acceptance_evidence_review_ready_count']}", f"wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count={summary['post_enable_readback_passed_count']}", f"wazuh_manager_registry_reviewer_validation_runtime_gate_count={summary['runtime_gate_count']}", "wazuh_api_live_query_authorized=false", @@ -299,7 +333,24 @@ def _require_boundaries(payload: dict[str, Any]) -> None: failed = _int(summary.get("reviewer_validation_failed_count")) quarantined = _int(summary.get("reviewer_validation_quarantined_count")) post_enable = _int(summary.get("post_enable_readback_passed_count")) - if any(value < 0 for value in (received, accepted, ready, passed, failed, quarantined, post_enable)): + acceptance_endpoint = _int(summary.get("manager_registry_acceptance_intake_endpoint_available_count")) + acceptance_received = _int(summary.get("manager_registry_acceptance_evidence_received_count")) + acceptance_ready = _int(summary.get("manager_registry_acceptance_evidence_review_ready_count")) + if any( + value < 0 + for value in ( + received, + accepted, + ready, + passed, + failed, + quarantined, + post_enable, + acceptance_endpoint, + acceptance_received, + acceptance_ready, + ) + ): raise ValueError("Wazuh manager registry reviewer validation counters 不得為負數") if accepted > received: raise ValueError("owner_registry_export_accepted_count 不得大於 received_count") @@ -309,6 +360,14 @@ def _require_boundaries(payload: dict[str, Any]) -> None: raise ValueError("reviewer_validation_passed_count 不得大於 accepted_count") if post_enable > passed: raise ValueError("post_enable_readback_passed_count 不得大於 reviewer_validation_passed_count") + if acceptance_endpoint > 1: + raise ValueError("manager_registry_acceptance_intake_endpoint_available_count 不得大於 1") + if acceptance_ready > acceptance_received: + raise ValueError( + "manager_registry_acceptance_evidence_review_ready_count 不得大於 received_count" + ) + if acceptance_ready: + raise ValueError("manager_registry_acceptance_evidence_review_ready_count 目前不得在全域讀回中自動上修") if failed and passed: raise ValueError("reviewer_validation_failed_count 與 passed_count 不得同時為正") if quarantined and accepted: @@ -477,6 +536,124 @@ def validate_iwooos_wazuh_manager_registry_owner_export( return _validation_result(contract, outcome, findings, evidence_status) +def validate_iwooos_wazuh_manager_registry_acceptance_evidence( + acceptance_evidence: dict[str, Any], + security_dir: Path | None = None, +) -> dict[str, Any]: + """Validate one redacted manager-registry acceptance packet without changing runtime truth.""" + contract = load_latest_iwooos_wazuh_manager_registry_reviewer_validation(security_dir) + snapshot = _load_snapshot(security_dir or _DEFAULT_SECURITY_DIR) + expected_aliases = set(_strings(snapshot.get("expected_scope_aliases"))) + + findings: list[dict[str, Any]] = [] + if not isinstance(acceptance_evidence, dict): + findings.append( + _finding( + "MRA-01", + "blocker", + "request_manager_registry_acceptance_evidence_supplement", + "manager registry acceptance evidence 必須是 JSON object。", + [], + ) + ) + return _acceptance_validation_result(contract, "request_manager_registry_acceptance_evidence_supplement", findings) + + sensitive_hits = _collect_sensitive_hits(acceptance_evidence) + if sensitive_hits: + findings.append( + _finding( + "MRA-04", + "critical", + "quarantine_sensitive_payload", + "acceptance evidence 含禁止內容或疑似未脫敏內容,已進隔離分流;回應不回傳原始值。", + [hit["path"] for hit in sensitive_hits[:12]], + {"categories": sorted({hit["category"] for hit in sensitive_hits})}, + ) + ) + return _acceptance_validation_result(contract, "quarantine_sensitive_payload", findings) + + runtime_hits = _collect_runtime_action_hits(acceptance_evidence) + if runtime_hits: + findings.append( + _finding( + "MRA-05", + "critical", + "reject_runtime_action_request", + "acceptance evidence 夾帶 runtime action request;此 validator 只允許脫敏 acceptance refs,不授權 active response、restart、host write 或掃描。", + runtime_hits[:12], + ) + ) + return _acceptance_validation_result(contract, "reject_runtime_action_request", findings) + + missing_fields = [ + field for field in sorted(_ACCEPTANCE_EVIDENCE_REQUIRED_FIELDS) if not _present(acceptance_evidence.get(field)) + ] + if missing_fields: + findings.append( + _finding( + "MRA-01", + "blocker", + "request_manager_registry_acceptance_evidence_supplement", + "manager registry acceptance evidence 欄位不足,需要補齊後再驗證。", + missing_fields, + ) + ) + + count_issue = _validate_acceptance_counts(acceptance_evidence) + if count_issue: + findings.append( + _finding( + "MRA-02", + "blocker", + "request_manager_registry_acceptance_counts_fix", + count_issue, + [ + "accepted_agent_total", + "accepted_agent_active", + "accepted_agent_disconnected", + "accepted_agent_never_connected", + ], + ) + ) + + alias_issue = _validate_aliases(acceptance_evidence.get("accepted_scope_aliases"), expected_aliases) + if alias_issue: + findings.append( + _finding( + "MRA-03", + "blocker", + "request_manager_registry_acceptance_scope_parity_fix", + alias_issue, + ["accepted_scope_aliases"], + ) + ) + + decision = acceptance_evidence.get("acceptance_decision") + if decision != "accept_manager_registry_evidence_for_review": + findings.append( + _finding( + "MRA-06", + "blocker", + "request_manager_registry_acceptance_decision_fix", + "acceptance_decision 必須明確為 accept_manager_registry_evidence_for_review;不可宣稱已更新 manager registry accepted 總帳。", + ["acceptance_decision"], + ) + ) + + outcome = _first_blocking_lane(findings) or "accepted_for_manager_registry_acceptance_review_only" + if outcome == "accepted_for_manager_registry_acceptance_review_only": + findings.append( + _finding( + "MRA-07", + "info", + "manager_registry_acceptance_evidence_review_ready", + "acceptance evidence 已通過 no-persist review readiness;仍不更新全域 manager_registry_accepted_count,不開 runtime gate。", + ["manager_registry_acceptance_evidence_refs"], + ) + ) + return _acceptance_validation_result(contract, outcome, findings) + + def _validation_result( contract: dict[str, Any], outcome_lane: str, @@ -543,6 +720,70 @@ def _validation_result( } +def _acceptance_validation_result( + contract: dict[str, Any], + outcome_lane: str, + findings: list[dict[str, Any]], +) -> dict[str, Any]: + accepted = outcome_lane == "accepted_for_manager_registry_acceptance_review_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_manager_registry_acceptance_evidence_validation_result_v1", + "contract_schema_version": contract["schema_version"], + "status": outcome_lane, + "mode": "no_persist_acceptance_evidence_review_no_runtime_no_secret_collection", + "outcome_lane": outcome_lane, + "accepted_for_manager_registry_acceptance_review_only": accepted, + "quarantined": quarantined, + "runtime_action_rejected": rejected_runtime, + "summary": { + "manager_registry_acceptance_evidence_received_count": 1, + "manager_registry_acceptance_evidence_review_ready_count": 1 if accepted else 0, + "manager_registry_acceptance_evidence_supplement_required_count": 1 if supplement_required else 0, + "manager_registry_acceptance_evidence_quarantined_count": 1 if quarantined else 0, + "manager_registry_acceptance_runtime_action_rejected_count": 1 if rejected_runtime else 0, + "manager_registry_accepted_count": 0, + "runtime_gate_count": 0, + "host_write_authorized_count": 0, + "active_response_authorized_count": 0, + "secret_value_collection_allowed_count": 0, + "finding_count": len(findings), + }, + "validation_findings": findings, + "boundary_markers": [ + "wazuh_manager_registry_acceptance_validation_received_count=1", + f"wazuh_manager_registry_acceptance_validation_review_ready_count={1 if accepted else 0}", + f"wazuh_manager_registry_acceptance_validation_quarantined_count={1 if quarantined else 0}", + "wazuh_manager_registry_acceptance_validation_manager_registry_accepted_count=0", + "wazuh_manager_registry_acceptance_validation_runtime_gate_count=0", + "wazuh_manager_registry_acceptance_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_agent_reenroll_authorized": False, + "wazuh_agent_restart_authorized": False, + "wazuh_manager_restart_authorized": False, + "wazuh_active_response_authorized": False, + "host_write_authorized": False, + "secret_value_collection_allowed": False, + "raw_wazuh_payload_storage_allowed": False, + "kali_active_scan_authorized": False, + "runtime_execution_authorized": False, + "manager_registry_accepted_updated": False, + "not_authorization": True, + }, + "next_gate": "commit_manager_registry_accepted_readback_evidence" if accepted else "manager_registry_acceptance_fix_and_resubmit", + } + + def _finding( check_id: str, severity: str, @@ -614,6 +855,29 @@ def _validate_counts(owner_export: dict[str, Any]) -> str | None: return None +def _validate_acceptance_counts(acceptance_evidence: dict[str, Any]) -> str | None: + fields = [ + "accepted_agent_total", + "accepted_agent_active", + "accepted_agent_disconnected", + "accepted_agent_never_connected", + ] + counts = {field: _int_or_none(acceptance_evidence.get(field)) for field in fields} + if any(value is None for value in counts.values()): + return "accepted agent count 欄位必須是非負整數。" + if any(value is not None and value < 0 for value in counts.values()): + return "accepted agent count 欄位不得為負數。" + total = counts["accepted_agent_total"] + active = counts["accepted_agent_active"] + disconnected = counts["accepted_agent_disconnected"] + never_connected = counts["accepted_agent_never_connected"] + if total is None or active is None or disconnected is None or never_connected is None: + return "accepted agent count 欄位必須是非負整數。" + if total < active + disconnected + never_connected: + return "accepted_agent_total 不得小於 active + disconnected + never_connected。" + return None + + 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): @@ -709,6 +973,10 @@ def _first_blocking_lane(findings: list[dict[str, Any]]) -> str | None: "request_dashboard_api_repair_postcheck", "request_readonly_credential_metadata", "request_owner_accountability_supplement", + "request_manager_registry_acceptance_evidence_supplement", + "request_manager_registry_acceptance_counts_fix", + "request_manager_registry_acceptance_scope_parity_fix", + "request_manager_registry_acceptance_decision_fix", ): if any(item.get("lane") == lane for item in findings): return lane diff --git a/apps/api/tests/test_iwooos_wazuh_manager_registry_reviewer_validation.py b/apps/api/tests/test_iwooos_wazuh_manager_registry_reviewer_validation.py index dc607c3d4..2f8c8bfff 100644 --- a/apps/api/tests/test_iwooos_wazuh_manager_registry_reviewer_validation.py +++ b/apps/api/tests/test_iwooos_wazuh_manager_registry_reviewer_validation.py @@ -6,6 +6,7 @@ from fastapi.testclient import TestClient from src.api.v1.iwooos import router from src.services.iwooos_wazuh_manager_registry_reviewer_validation import ( load_latest_iwooos_wazuh_manager_registry_reviewer_validation, + validate_iwooos_wazuh_manager_registry_acceptance_evidence, validate_iwooos_wazuh_manager_registry_owner_export, ) @@ -78,18 +79,42 @@ def _valid_owner_export() -> dict: } +def _valid_acceptance_evidence() -> dict: + return { + "acceptance_reviewer_role": "IwoooS reviewer", + "acceptance_decision": "accept_manager_registry_evidence_for_review", + "acceptance_decision_reason": "脫敏 manager registry evidence 與六個公開別名 scope parity 一致,可進 commit review。", + "accepted_scope_aliases": EXPECTED_ALIASES, + "accepted_agent_total": 6, + "accepted_agent_active": 2, + "accepted_agent_disconnected": 3, + "accepted_agent_never_connected": 1, + "manager_registry_acceptance_evidence_refs": [ + "evidence-ref-registry-summary", + "evidence-ref-scope-parity", + "evidence-ref-post-enable-readback", + ], + "post_enable_readback_ref": "evidence-ref-post-enable-readback", + "product_scope_parity_ref": "evidence-ref-scope-parity", + "reviewed_at": "2026-06-27T22:10:00+08:00", + "followup_owner": "IwoooS reviewer", + "rollback_owner": "IwoooS runtime owner", + "post_acceptance_verifier_plan": "commit_readback_without_runtime_action", + } + + def test_iwooos_wazuh_manager_registry_reviewer_validation_contract_has_passed_redacted_export() -> None: payload = load_latest_iwooos_wazuh_manager_registry_reviewer_validation() assert payload["schema_version"] == "iwooos_wazuh_manager_registry_reviewer_validation_readback_v1" assert payload["source_schema_version"] == "wazuh_manager_registry_reviewer_validation_v1" - assert payload["status"] == "post_enable_iwooos_readback_passed_no_runtime_no_secret_collection" - assert payload["mode"] == "committed_post_enable_iwooos_readback_passed_no_runtime_no_secret_collection" + assert payload["status"] == "manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection" + assert payload["mode"] == "committed_manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection" assert payload["summary"]["expected_scope_alias_count"] == 6 assert payload["summary"]["required_owner_field_count"] == 28 assert payload["summary"]["per_host_required_field_count"] == 9 - assert payload["summary"]["reviewer_validation_check_count"] == 10 - assert payload["summary"]["outcome_lane_count"] == 14 + assert payload["summary"]["reviewer_validation_check_count"] == 11 + assert payload["summary"]["outcome_lane_count"] == 17 assert payload["summary"]["evidence_slot_count"] == 6 assert payload["summary"]["forbidden_payload_count"] == 27 assert payload["summary"]["owner_registry_export_received_count"] == 1 @@ -99,6 +124,9 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_contract_has_passed_r assert payload["summary"]["post_enable_readback_passed_count"] == 1 assert payload["summary"]["reviewer_validation_quarantined_count"] == 0 assert payload["summary"]["manager_registry_accepted_count"] == 0 + assert payload["summary"]["manager_registry_acceptance_intake_endpoint_available_count"] == 1 + assert payload["summary"]["manager_registry_acceptance_evidence_received_count"] == 0 + assert payload["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 0 assert payload["summary"]["runtime_gate_count"] == 0 @@ -132,8 +160,11 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe() assert data["summary"]["reviewer_validation_passed_count"] == 1 assert data["summary"]["post_enable_readback_passed_count"] == 1 assert data["summary"]["manager_registry_accepted_count"] == 0 + assert data["summary"]["manager_registry_acceptance_intake_endpoint_available_count"] == 1 + assert data["summary"]["manager_registry_acceptance_evidence_received_count"] == 0 + assert data["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 0 assert data["summary"]["runtime_gate_count"] == 0 - assert len(data["reviewer_validation_checks"]) == 10 + assert len(data["reviewer_validation_checks"]) == 11 assert len(data["evidence_slots"]) == 6 assert any( marker == "wazuh_manager_registry_reviewer_validation_owner_registry_export_received_count=1" @@ -155,6 +186,18 @@ def test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe() marker == "wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0" for marker in data["boundary_markers"] ) + assert any( + marker == "wazuh_manager_registry_acceptance_validation_api_available=true" + for marker in data["boundary_markers"] + ) + assert any( + marker == "wazuh_manager_registry_acceptance_evidence_received_count=0" + for marker in data["boundary_markers"] + ) + assert any( + marker == "wazuh_manager_registry_acceptance_evidence_review_ready_count=0" + for marker in data["boundary_markers"] + ) assert any( marker == "wazuh_manager_registry_reviewer_validation_runtime_gate_count=0" for marker in data["boundary_markers"] @@ -251,3 +294,82 @@ def test_iwooos_wazuh_manager_registry_owner_export_validation_rejects_runtime_a assert payload["summary"]["owner_registry_export_accepted_count"] == 0 assert payload["summary"]["runtime_gate_count"] == 0 assert any(finding["check_id"] == "RV-09" for finding in payload["validation_findings"]) + + +def test_iwooos_wazuh_manager_registry_acceptance_evidence_validation_accepts_redacted_packet_for_review_only() -> None: + payload = validate_iwooos_wazuh_manager_registry_acceptance_evidence(_valid_acceptance_evidence()) + + assert payload["schema_version"] == "iwooos_wazuh_manager_registry_acceptance_evidence_validation_result_v1" + assert payload["status"] == "accepted_for_manager_registry_acceptance_review_only" + assert payload["mode"] == "no_persist_acceptance_evidence_review_no_runtime_no_secret_collection" + assert payload["accepted_for_manager_registry_acceptance_review_only"] is True + assert payload["summary"]["manager_registry_acceptance_evidence_received_count"] == 1 + assert payload["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 1 + assert payload["summary"]["manager_registry_accepted_count"] == 0 + assert payload["summary"]["runtime_gate_count"] == 0 + assert payload["boundaries"]["payload_persisted"] is False + assert payload["boundaries"]["manager_registry_accepted_updated"] is False + assert payload["boundaries"]["runtime_execution_authorized"] is False + assert payload["next_gate"] == "commit_manager_registry_accepted_readback_evidence" + + +def test_iwooos_wazuh_manager_registry_acceptance_evidence_api_does_not_persist_or_open_runtime() -> None: + client = _client() + response = client.post( + "/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-manager-registry-acceptance", + json=_valid_acceptance_evidence(), + ) + + assert response.status_code == 200 + result = response.json() + assert result["status"] == "accepted_for_manager_registry_acceptance_review_only" + assert result["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 1 + assert result["summary"]["manager_registry_accepted_count"] == 0 + assert result["summary"]["runtime_gate_count"] == 0 + + readback = client.get("/api/v1/iwooos/wazuh-manager-registry-reviewer-validation").json() + assert readback["summary"]["manager_registry_acceptance_evidence_received_count"] == 0 + assert readback["summary"]["manager_registry_acceptance_evidence_review_ready_count"] == 0 + assert readback["summary"]["manager_registry_accepted_count"] == 0 + assert readback["summary"]["runtime_gate_count"] == 0 + + +def test_iwooos_wazuh_manager_registry_acceptance_evidence_validation_requests_missing_fields() -> None: + candidate = _valid_acceptance_evidence() + candidate.pop("product_scope_parity_ref") + + payload = validate_iwooos_wazuh_manager_registry_acceptance_evidence(candidate) + + assert payload["status"] == "request_manager_registry_acceptance_evidence_supplement" + assert payload["accepted_for_manager_registry_acceptance_review_only"] is False + assert payload["summary"]["manager_registry_acceptance_evidence_supplement_required_count"] == 1 + assert payload["summary"]["manager_registry_accepted_count"] == 0 + assert any("product_scope_parity_ref" in finding["field_paths"] for finding in payload["validation_findings"]) + + +def test_iwooos_wazuh_manager_registry_acceptance_evidence_validation_quarantines_sensitive_payload() -> None: + candidate = _valid_acceptance_evidence() + candidate["post_enable_readback_ref"] = "post enable accidentally included 10.250.250.250" + + payload = validate_iwooos_wazuh_manager_registry_acceptance_evidence(candidate) + + assert payload["status"] == "quarantine_sensitive_payload" + assert payload["quarantined"] is True + assert payload["summary"]["manager_registry_acceptance_evidence_quarantined_count"] == 1 + assert payload["summary"]["manager_registry_accepted_count"] == 0 + assert "10.250.250.250" not in str(payload) + assert any(finding["check_id"] == "MRA-04" for finding in payload["validation_findings"]) + + +def test_iwooos_wazuh_manager_registry_acceptance_evidence_validation_rejects_runtime_action_request() -> None: + candidate = _valid_acceptance_evidence() + candidate["requested_actions"] = ["wazuh_active_response"] + + payload = validate_iwooos_wazuh_manager_registry_acceptance_evidence(candidate) + + assert payload["status"] == "reject_runtime_action_request" + assert payload["runtime_action_rejected"] is True + assert payload["summary"]["manager_registry_acceptance_runtime_action_rejected_count"] == 1 + assert payload["summary"]["manager_registry_accepted_count"] == 0 + assert payload["summary"]["runtime_gate_count"] == 0 + assert any(finding["check_id"] == "MRA-05" for finding in payload["validation_findings"]) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 9f3cee304..acfa3f44f 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -20841,6 +20841,8 @@ "loadingBoundary": "正在讀取 Wazuh manager registry reviewer validation API", "validationEndpointLabel": "脫敏 owner export 驗證端點", "validationModeLabel": "驗證模式", + "acceptanceEndpointLabel": "manager accepted evidence 驗證端點", + "acceptanceModeLabel": "accepted evidence 模式", "slotReceivedLabel": "已收件", "slotAcceptedLabel": "已接受", "slotNextGateLabel": "下一關", @@ -20862,7 +20864,7 @@ }, "checks": { "label": "Reviewer checks", - "detail": "10 個檢查固定欄位、算術、矩陣、Dashboard API 與停止線。" + "detail": "11 個檢查固定欄位、算術、矩陣、Dashboard API、post-enable 與 acceptance 停止線。" }, "slots": { "label": "Evidence slots", @@ -20876,6 +20878,18 @@ "label": "Post-enable", "detail": "正式 API 與前台已讀回 reviewer passed;這不是 live Wazuh 查詢授權。" }, + "acceptanceApi": { + "label": "Acceptance API", + "detail": "manager registry accepted evidence 可 no-persist 收件驗證,但不寫總帳。" + }, + "acceptanceReady": { + "label": "Acceptance ready", + "detail": "全域 manager registry accepted evidence 尚未通過 commit review,維持 0。" + }, + "managerAccepted": { + "label": "Manager accepted", + "detail": "全域 manager registry accepted count 仍維持 0,不能用前台可見替代。" + }, "received": { "label": "已收 export", "detail": "已收到一筆 owner-provided redacted registry export refs。" diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 9f3cee304..acfa3f44f 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -20841,6 +20841,8 @@ "loadingBoundary": "正在讀取 Wazuh manager registry reviewer validation API", "validationEndpointLabel": "脫敏 owner export 驗證端點", "validationModeLabel": "驗證模式", + "acceptanceEndpointLabel": "manager accepted evidence 驗證端點", + "acceptanceModeLabel": "accepted evidence 模式", "slotReceivedLabel": "已收件", "slotAcceptedLabel": "已接受", "slotNextGateLabel": "下一關", @@ -20862,7 +20864,7 @@ }, "checks": { "label": "Reviewer checks", - "detail": "10 個檢查固定欄位、算術、矩陣、Dashboard API 與停止線。" + "detail": "11 個檢查固定欄位、算術、矩陣、Dashboard API、post-enable 與 acceptance 停止線。" }, "slots": { "label": "Evidence slots", @@ -20876,6 +20878,18 @@ "label": "Post-enable", "detail": "正式 API 與前台已讀回 reviewer passed;這不是 live Wazuh 查詢授權。" }, + "acceptanceApi": { + "label": "Acceptance API", + "detail": "manager registry accepted evidence 可 no-persist 收件驗證,但不寫總帳。" + }, + "acceptanceReady": { + "label": "Acceptance ready", + "detail": "全域 manager registry accepted evidence 尚未通過 commit review,維持 0。" + }, + "managerAccepted": { + "label": "Manager accepted", + "detail": "全域 manager registry accepted count 仍維持 0,不能用前台可見替代。" + }, "received": { "label": "已收 export", "detail": "已收到一筆 owner-provided redacted registry export refs。" diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index c6b2380c4..f9e7eeb81 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -16,7 +16,9 @@ import { Code2, Database, Clock3, + ClipboardList, FileText, + FileCheck2, FileWarning, GitBranch, ListChecks, @@ -26,6 +28,7 @@ import { Route, SearchCheck, Server, + ShieldAlert, ShieldCheck, Workflow, } from 'lucide-react' @@ -2488,6 +2491,9 @@ const wazuhManagerRegistryReviewerValidationBoundaries = [ 'wazuh_manager_registry_reviewer_validation_quarantined_count=0', 'wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0', 'wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count=1', + 'wazuh_manager_registry_acceptance_validation_api_available=true', + 'wazuh_manager_registry_acceptance_evidence_received_count=0', + 'wazuh_manager_registry_acceptance_evidence_review_ready_count=0', 'wazuh_manager_registry_reviewer_validation_runtime_gate_count=0', 'wazuh_api_live_query_authorized=false', 'wazuh_agent_reenroll_authorized=false', @@ -9828,6 +9834,24 @@ function IwoooSWazuhManagerRegistryReviewerValidationBoard() { icon: SearchCheck, tone: summary?.post_enable_readback_passed_count ? 'steady' : 'locked', }, + { + key: 'acceptanceApi', + value: summary ? String(summary.manager_registry_acceptance_intake_endpoint_available_count) : loading ? '...' : '1', + icon: FileCheck2, + tone: summary?.manager_registry_acceptance_intake_endpoint_available_count ? 'steady' : 'locked', + }, + { + key: 'acceptanceReady', + value: summary ? String(summary.manager_registry_acceptance_evidence_review_ready_count) : loading ? '...' : '0', + icon: ClipboardList, + tone: 'locked', + }, + { + key: 'managerAccepted', + value: summary ? String(summary.manager_registry_accepted_count) : loading ? '...' : '0', + icon: ShieldAlert, + tone: 'locked', + }, { key: 'received', value: summary ? String(summary.owner_registry_export_received_count) : loading ? '...' : '1', @@ -9855,6 +9879,10 @@ function IwoooSWazuhManagerRegistryReviewerValidationBoard() { const validationEndpoint = data?.owner_export_validation_endpoint ?? '/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export' const validationMode = data?.owner_export_validation_mode ?? 'no_persist_validation_no_runtime_action' + const acceptanceEndpoint = data?.manager_registry_acceptance_validation_endpoint + ?? '/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-manager-registry-acceptance' + const acceptanceMode = data?.manager_registry_acceptance_validation_mode + ?? 'no_persist_acceptance_evidence_review_no_runtime_action' const evidenceSlots = data?.evidence_slots ?? [] const visibleChecks = data?.reviewer_validation_checks?.slice(0, 4) ?? [] const statusText = loading ? t('status.loading') : failed ? t('status.failed') : t('status.ready') @@ -9883,6 +9911,8 @@ function IwoooSWazuhManagerRegistryReviewerValidationBoard() {
{t('validationEndpointLabel')}:{validationEndpoint} {t('validationModeLabel')}:{validationMode} + {t('acceptanceEndpointLabel')}:{acceptanceEndpoint} + {t('acceptanceModeLabel')}:{acceptanceMode}
diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 5a3f9b08d..fae6345e3 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -343,6 +343,8 @@ export interface IwoooSWazuhManagerRegistryReviewerValidationResponse { source_refs: string[] owner_export_validation_endpoint: string owner_export_validation_mode: string + manager_registry_acceptance_validation_endpoint: string + manager_registry_acceptance_validation_mode: string summary: { expected_scope_alias_count: number required_owner_field_count: number @@ -359,6 +361,9 @@ export interface IwoooSWazuhManagerRegistryReviewerValidationResponse { reviewer_validation_failed_count: number reviewer_validation_quarantined_count: number manager_registry_accepted_count: number + manager_registry_acceptance_intake_endpoint_available_count: number + manager_registry_acceptance_evidence_received_count: number + manager_registry_acceptance_evidence_review_ready_count: number post_enable_readback_passed_count: number runtime_gate_count: number host_write_authorized_count: number diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 512351396..18d833d62 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,35 @@ +## 2026-06-27 — 22:51 AwoooP controlled automation copy guard 進 main + +**背景**:上一段已把正式 AwoooP Approvals / Runs / Work Items / Alerts HTML payload 中殘留的舊 manual gate 語意清零;本段不是再做文案文件,而是把防回歸規則寫成 repo guard,避免 `待人工決策`、`阻塞與人工閘門`、`人工接手`、`manual gate`、`owner review` 等語意再次回到低 / 中 / 高風險流程。 + +**完成內容**: +- 新增 `scripts/security/awooop-controlled-automation-copy-guard.py`,read-only 掃描 AwoooP routes 與 zh-TW / en messages,禁止舊 manual / default-human gate 字串回流。 +- guard 要求正式 copy 保留 controlled automation 新語意:`待 AI 受控決策`、`阻塞與 AI 受控隊列`、`AI 處置包與工作項`、`受控執行邊界`、`受控授權閘門`、`controlled gate`、`controlled review`。 +- guard 同時鎖定 `/zh-TW/awooop/alerts` 必須存在並導向 `#ai-alert-card-delivery-readback`,避免 Alerts smoke 退回 404。 +- `scripts/security/security-mirror-progress-guard.py` 已串接 AwoooP controlled automation copy guard。 +- 新增 `apps/api/tests/test_awooop_controlled_automation_copy_guard.py`,把 guard 納入 pytest 防線。 + +**本地驗證結果**: +- `python3 scripts/security/awooop-controlled-automation-copy-guard.py --root .`:`AWOOOP_CONTROLLED_AUTOMATION_COPY_GUARD_OK`。 +- `python3 -m py_compile scripts/security/awooop-controlled-automation-copy-guard.py scripts/security/security-mirror-progress-guard.py apps/api/tests/test_awooop_controlled_automation_copy_guard.py`:通過。 +- `DATABASE_URL=sqlite:///test.db PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_awooop_controlled_automation_copy_guard.py -q`:`1 passed`。 +- `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 +- `git diff --check`:通過。 + +**Gitea / CI 狀態**: +- guard commit:`219fc3835 test(awooop): guard controlled automation copy`。 +- rebase 當下 `gitea-ssh/main=0e2e8057c` 已包含 `219fc3835`;本 LOGBOOK commit 會再推進 main head。 +- `code-review.yaml #3710` 因較新的 main push 被取消;最新 head `0040a595a` 的 `code-review.yaml #3712` 成功,`ansible-lint.yml #3711` 成功。 +- `cd.yaml #3709` 是 `219fc3835` 被後續 push supersede 後的舊 run;本段 guard / test 變更不在 CD path filter 內,不需要新的 deploy marker 才能生效於 repo 防線。 + +**production readback 邊界**: +- 產品 runtime 仍由上一段 deploy marker `f98aaa8ee chore(cd): deploy a2733fd [skip ci]` 覆蓋的 AwoooP copy 清理提供。 +- 重新讀回 Approvals / Runs / Work Items / Alerts:HTTP `200`,舊詞 hit count 皆為 `0`;Alerts final URL 仍回 `/zh-TW/awooop/runs`。 +- 本段未做 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime 寫操作,未讀 secret,未 force push。 + +**下一步**: +- 後續若再改 AwoooP / IwoooS approval 類 copy,必須先跑 `python3 scripts/security/awooop-controlled-automation-copy-guard.py --root .` 與 `python3 scripts/security/security-mirror-progress-guard.py --root .`,避免把 manual gate 當作 low / medium / high 的預設終局。 + ## 2026-06-27 — 22:28 AwoooP manual gate payload residue 正式清零 **背景**:正式 `/zh-TW/awooop/approvals`、Runs、Work Items 的 HTML payload 仍因 Next / i18n namespace 序列化帶出 IwoooS 舊文案 `人工閘門`,每頁各命中 32 次;`/zh-TW/awooop/alerts` 也回 404。這會讓 AwoooP 看起來仍把人工 gate 當作 low / medium / high 的預設終局。 diff --git a/docs/security/wazuh-manager-registry-reviewer-validation.snapshot.json b/docs/security/wazuh-manager-registry-reviewer-validation.snapshot.json index c9ff32a0b..ce9a9bf7f 100644 --- a/docs/security/wazuh-manager-registry-reviewer-validation.snapshot.json +++ b/docs/security/wazuh-manager-registry-reviewer-validation.snapshot.json @@ -150,11 +150,12 @@ "firewall_change", "nginx_reload" ], - "generated_at": "2026-06-27T21:45:00+08:00", - "mode": "committed_post_enable_iwooos_readback_passed_no_runtime_no_secret_collection", + "generated_at": "2026-06-27T22:10:00+08:00", + "mode": "committed_manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection", "no_false_green_rules": [ "reviewer validation passed 只代表脫敏 owner export refs 通過 no-persist 驗證。", "post-enable IwoooS readback passed 只代表 production API / 前台已讀回 reviewer passed,不代表 live Wazuh 查詢或 runtime action。", + "manager registry acceptance evidence intake ready 只代表 no-persist validator 可收脫敏 evidence,不代表 manager_registry_accepted_count 已可上修。", "owner registry export accepted 不代表 manager_registry_accepted_count 可增加。", "Dashboard 可見、index pattern 三綠勾、HTTP 200 或 transport observed 不可替代 manager registry counts。", "reviewer accepted 只可更新只讀 posture;active response、agent restart、reenroll、host write、secret rotation 或掃描仍需獨立 runtime gate。" @@ -173,7 +174,10 @@ "ready_for_reviewer_validation", "accepted_for_readonly_posture_only", "post_enable_iwooos_readback_passed", - "manager_registry_acceptance_evidence_review" + "manager_registry_acceptance_evidence_review", + "manager_registry_acceptance_evidence_intake_ready", + "accepted_for_manager_registry_acceptance_review_only", + "commit_manager_registry_accepted_readback_evidence" ], "per_host_required_fields": [ "node_alias", @@ -276,6 +280,12 @@ "failure_lane": "post_enable_iwooos_readback_passed_no_runtime", "required_evidence": "production API 與前台 smoke 已讀回 reviewer passed;此讀回只更新 read-only posture,不查 live Wazuh、不保存 raw payload、不開 runtime gate。", "title": "Post-enable IwoooS readback 已讀回但不開 runtime" + }, + { + "check_id": "RV-11", + "failure_lane": "waiting_manager_registry_acceptance_evidence", + "required_evidence": "新增 no-persist acceptance evidence validator;只有 reviewer packet 通過後才可進 commit review,global manager_registry_accepted_count 仍維持 0。", + "title": "Manager registry acceptance evidence 可收件但不自動上修" } ], "schema_version": "wazuh_manager_registry_reviewer_validation_v1", @@ -284,7 +294,7 @@ "docs/security/wazuh-agent-visibility-owner-evidence-preflight.snapshot.json", "docs/security/wazuh-managed-host-coverage-gate.snapshot.json" ], - "status": "post_enable_iwooos_readback_passed_no_runtime_no_secret_collection", + "status": "manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection", "summary": { "active_response_authorized_count": 0, "evidence_slot_count": 6, @@ -292,14 +302,17 @@ "forbidden_action_count": 11, "forbidden_payload_count": 27, "host_write_authorized_count": 0, + "manager_registry_acceptance_evidence_received_count": 0, + "manager_registry_acceptance_evidence_review_ready_count": 0, + "manager_registry_acceptance_intake_endpoint_available_count": 1, "manager_registry_accepted_count": 0, - "outcome_lane_count": 14, + "outcome_lane_count": 17, "owner_registry_export_accepted_count": 1, "owner_registry_export_received_count": 1, "per_host_required_field_count": 9, "post_enable_readback_passed_count": 1, "required_owner_field_count": 28, - "reviewer_validation_check_count": 10, + "reviewer_validation_check_count": 11, "reviewer_validation_failed_count": 0, "reviewer_validation_passed_count": 1, "reviewer_validation_quarantined_count": 0, diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 22513b1ce..a7dae459a 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -29583,6 +29583,9 @@ def validate(root: Path) -> None: "wazuh_manager_registry_reviewer_validation_passed_count=1", "wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count=1", "wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0", + "wazuh_manager_registry_acceptance_validation_api_available=true", + "wazuh_manager_registry_acceptance_evidence_received_count=0", + "wazuh_manager_registry_acceptance_evidence_review_ready_count=0", "wazuh_manager_registry_reviewer_validation_runtime_gate_count=0", ]: assert_text_contains("iwooos_frontend_product_text.wazuh_managed_host_coverage", frontend_product_text, expected) @@ -29602,17 +29605,25 @@ def validate(root: Path) -> None: "wazuh_manager_registry_reviewer_validation_v1", "iwooos_wazuh_manager_registry_reviewer_validation_readback_v1", "/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-owner-export", + "/api/v1/iwooos/wazuh-manager-registry-reviewer-validation/validate-manager-registry-acceptance", "iwooos_wazuh_manager_registry_owner_export_validation_result_v1", + "iwooos_wazuh_manager_registry_acceptance_evidence_validation_result_v1", "validate_iwooos_wazuh_manager_registry_owner_export", + "validate_iwooos_wazuh_manager_registry_acceptance_evidence", "test_iwooos_wazuh_manager_registry_reviewer_validation_api_is_public_safe", "test_iwooos_wazuh_manager_registry_owner_export_validation_accepts_redacted_payload", "test_iwooos_wazuh_manager_registry_owner_export_validation_quarantines_sensitive_payload", "test_iwooos_wazuh_manager_registry_owner_export_validation_rejects_runtime_action_request", + "test_iwooos_wazuh_manager_registry_acceptance_evidence_validation_accepts_redacted_packet_for_review_only", + "test_iwooos_wazuh_manager_registry_acceptance_evidence_api_does_not_persist_or_open_runtime", "wazuh_manager_registry_reviewer_validation_owner_registry_export_received_count=1", "wazuh_manager_registry_reviewer_validation_owner_registry_export_accepted_count=1", "wazuh_manager_registry_reviewer_validation_passed_count=1", "wazuh_manager_registry_reviewer_validation_post_enable_readback_passed_count=1", "wazuh_manager_registry_reviewer_validation_manager_registry_accepted_count=0", + "wazuh_manager_registry_acceptance_validation_api_available=true", + "wazuh_manager_registry_acceptance_evidence_received_count=0", + "wazuh_manager_registry_acceptance_evidence_review_ready_count=0", "wazuh_manager_registry_reviewer_validation_runtime_gate_count=0", ]: assert_text_contains( diff --git a/scripts/security/wazuh-manager-registry-reviewer-validation.py b/scripts/security/wazuh-manager-registry-reviewer-validation.py index ac4388199..2e4f95927 100644 --- a/scripts/security/wazuh-manager-registry-reviewer-validation.py +++ b/scripts/security/wazuh-manager-registry-reviewer-validation.py @@ -132,6 +132,12 @@ REVIEWER_VALIDATION_CHECKS = [ "required_evidence": "production API 與前台 smoke 已讀回 reviewer passed;此讀回只更新 read-only posture,不查 live Wazuh、不保存 raw payload、不開 runtime gate。", "failure_lane": "post_enable_iwooos_readback_passed_no_runtime", }, + { + "check_id": "RV-11", + "title": "Manager registry acceptance evidence 可收件但不自動上修", + "required_evidence": "新增 no-persist acceptance evidence validator;只有 reviewer packet 通過後才可進 commit review,global manager_registry_accepted_count 仍維持 0。", + "failure_lane": "waiting_manager_registry_acceptance_evidence", + }, ] OUTCOME_LANES = [ @@ -149,6 +155,9 @@ OUTCOME_LANES = [ "accepted_for_readonly_posture_only", "post_enable_iwooos_readback_passed", "manager_registry_acceptance_evidence_review", + "manager_registry_acceptance_evidence_intake_ready", + "accepted_for_manager_registry_acceptance_review_only", + "commit_manager_registry_accepted_readback_evidence", ] EVIDENCE_SLOTS = [ @@ -293,8 +302,8 @@ def build_snapshot(generated_at: str) -> dict[str, Any]: return { "schema_version": SCHEMA_VERSION, "generated_at": generated_at, - "status": "post_enable_iwooos_readback_passed_no_runtime_no_secret_collection", - "mode": "committed_post_enable_iwooos_readback_passed_no_runtime_no_secret_collection", + "status": "manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection", + "mode": "committed_manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection", "scope": "wazuh_manager_registry_owner_export_reviewer_validation", "source_refs": [ "docs/security/wazuh-agent-visibility-owner-evidence-preflight.snapshot.json", @@ -316,6 +325,9 @@ def build_snapshot(generated_at: str) -> dict[str, Any]: "reviewer_validation_failed_count": 0, "reviewer_validation_quarantined_count": 0, "manager_registry_accepted_count": 0, + "manager_registry_acceptance_intake_endpoint_available_count": 1, + "manager_registry_acceptance_evidence_received_count": 0, + "manager_registry_acceptance_evidence_review_ready_count": 0, "post_enable_readback_passed_count": 1, "runtime_gate_count": 0, "host_write_authorized_count": 0, @@ -357,6 +369,7 @@ def build_snapshot(generated_at: str) -> dict[str, Any]: "no_false_green_rules": [ "reviewer validation passed 只代表脫敏 owner export refs 通過 no-persist 驗證。", "post-enable IwoooS readback passed 只代表 production API / 前台已讀回 reviewer passed,不代表 live Wazuh 查詢或 runtime action。", + "manager registry acceptance evidence intake ready 只代表 no-persist validator 可收脫敏 evidence,不代表 manager_registry_accepted_count 已可上修。", "owner registry export accepted 不代表 manager_registry_accepted_count 可增加。", "Dashboard 可見、index pattern 三綠勾、HTTP 200 或 transport observed 不可替代 manager registry counts。", "reviewer accepted 只可更新只讀 posture;active response、agent restart、reenroll、host write、secret rotation 或掃描仍需獨立 runtime gate。", @@ -367,11 +380,15 @@ def build_snapshot(generated_at: str) -> dict[str, Any]: def validate(root: Path) -> None: snapshot = load_json(root / SNAPSHOT_PATH) assert_equal("schema_version", snapshot.get("schema_version"), SCHEMA_VERSION) - assert_equal("status", snapshot.get("status"), "post_enable_iwooos_readback_passed_no_runtime_no_secret_collection") + assert_equal( + "status", + snapshot.get("status"), + "manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection", + ) assert_equal( "mode", snapshot.get("mode"), - "committed_post_enable_iwooos_readback_passed_no_runtime_no_secret_collection", + "committed_manager_registry_acceptance_evidence_intake_ready_no_runtime_no_secret_collection", ) assert_equal("scope", snapshot.get("scope"), "wazuh_manager_registry_owner_export_reviewer_validation") assert_equal("expected_scope_aliases", snapshot.get("expected_scope_aliases"), EXPECTED_SCOPE_ALIASES) @@ -401,12 +418,15 @@ def validate(root: Path) -> None: "reviewer_validation_ready_count", "reviewer_validation_passed_count", "post_enable_readback_passed_count", + "manager_registry_acceptance_intake_endpoint_available_count", ]: assert_equal(f"summary.{key}", summary.get(key), 1) for key in [ "reviewer_validation_failed_count", "reviewer_validation_quarantined_count", "manager_registry_accepted_count", + "manager_registry_acceptance_evidence_received_count", + "manager_registry_acceptance_evidence_review_ready_count", "runtime_gate_count", "host_write_authorized_count", "active_response_authorized_count", @@ -465,6 +485,7 @@ def main() -> None: f"received={summary['owner_registry_export_received_count']} " f"accepted={summary['owner_registry_export_accepted_count']} " f"post_enable={summary['post_enable_readback_passed_count']} " + f"acceptance_endpoint={summary['manager_registry_acceptance_intake_endpoint_available_count']} " f"runtime_gate={summary['runtime_gate_count']}" )