from __future__ import annotations import httpx import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from src.api.v1.iwooos import router def _client() -> TestClient: app = FastAPI() app.include_router(router) return TestClient(app) def test_iwooos_wazuh_compat_route_returns_disabled_boundary_by_default(monkeypatch: pytest.MonkeyPatch): 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) monkeypatch.delenv("IWOOOS_WAZUH_EXPECTED_MIN_AGENT_COUNT", raising=False) response = _client().get("/api/iwooos/wazuh") assert response.status_code == 200 data = response.json() assert data["schema_version"] == "iwooos_wazuh_readonly_status_v1" assert data["status"] == "disabled_waiting_iwooos_wazuh_owner_gate" assert data["configured"] is False assert data["summary"]["runtime_gate_count"] == 0 assert data["summary"]["agent_visibility_no_false_green_count"] == 1 assert data["boundaries"]["active_response_authorized"] is False assert data["boundaries"]["host_write_authorized"] is False assert data["boundaries"]["raw_wazuh_payload_storage_allowed"] is False assert data["boundaries"]["internal_ip_public_display_allowed"] is False def test_iwooos_wazuh_v1_route_rejects_missing_server_side_env(monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("IWOOOS_WAZUH_READONLY_ENABLED", "true") monkeypatch.setenv("WAZUH_API_BASE_URL", "") monkeypatch.setenv("WAZUH_API_USERNAME", "") monkeypatch.setenv("WAZUH_API_PASSWORD", "") monkeypatch.delenv("IWOOOS_WAZUH_EXPECTED_MIN_AGENT_COUNT", raising=False) response = _client().get("/api/v1/iwooos/wazuh") assert response.status_code == 503 data = response.json() assert data["status"] == "misconfigured_missing_server_side_wazuh_env" assert data["configured"] is False assert data["summary"]["runtime_gate_count"] == 0 def test_iwooos_wazuh_rejects_non_https_base_url(monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("IWOOOS_WAZUH_READONLY_ENABLED", "true") monkeypatch.setenv("WAZUH_API_BASE_URL", "http://wazuh.example.test:55000") monkeypatch.setenv("WAZUH_API_USERNAME", "readonly") monkeypatch.setenv("WAZUH_API_PASSWORD", "placeholder") monkeypatch.delenv("IWOOOS_WAZUH_EXPECTED_MIN_AGENT_COUNT", raising=False) response = _client().get("/api/iwooos/wazuh") assert response.status_code == 503 data = response.json() assert data["status"] == "misconfigured_missing_server_side_wazuh_env" assert data["boundaries"]["secret_value_collection_allowed"] is False def test_iwooos_wazuh_live_response_is_metadata_only(monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("IWOOOS_WAZUH_READONLY_ENABLED", "true") monkeypatch.setenv("WAZUH_API_BASE_URL", "https://wazuh.example.test:55000") monkeypatch.setenv("WAZUH_API_USERNAME", "readonly") monkeypatch.setenv("WAZUH_API_PASSWORD", "placeholder") monkeypatch.delenv("IWOOOS_WAZUH_EXPECTED_MIN_AGENT_COUNT", raising=False) def handler(request: httpx.Request) -> httpx.Response: if request.url.path == "/security/user/authenticate": return httpx.Response(200, json={"data": {"token": "token-value"}}) if request.url.path == "/agents/summary/status": return httpx.Response( 200, json={"data": {"connection": {"total": 2, "active": 1, "disconnected": 1, "pending": 0}}}, ) if request.url.path == "/agents": return httpx.Response( 200, json={ "data": { "affected_items": [ { "id": "001", "name": "host-110-private-name", "ip": "192.168.0.110", "status": "active", "os": {"platform": "linux"}, "lastKeepAlive": "2026-06-24T13:00:00Z", } ] } }, ) return httpx.Response(404) transport = httpx.MockTransport(handler) original_async_client = httpx.AsyncClient def client_factory(*args, **kwargs): kwargs["transport"] = transport return original_async_client(*args, **kwargs) monkeypatch.setattr(httpx, "AsyncClient", client_factory) response = _client().get("/api/iwooos/wazuh") assert response.status_code == 200 data = response.json() assert data["status"] == "readonly_metadata_available" assert data["configured"] is True assert data["summary"]["agent_total"] == 2 assert data["summary"]["agent_registry_empty_count"] == 0 assert data["summary"]["agent_below_expected_minimum_count"] == 0 assert data["summary"]["agent_visibility_no_false_green_count"] == 1 assert data["summary"]["runtime_gate_count"] == 0 assert data["agents"] == [ { "alias": "agent-01", "status": "active", "os": "linux", "last_seen_present": True, } ] assert "host-110-private-name" not in response.text assert "192.168.0.110" not in response.text assert "token-value" not in response.text def test_iwooos_wazuh_marks_empty_agent_registry_as_degraded(monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("IWOOOS_WAZUH_READONLY_ENABLED", "true") monkeypatch.setenv("WAZUH_API_BASE_URL", "https://wazuh.example.test:55000") monkeypatch.setenv("WAZUH_API_USERNAME", "readonly") monkeypatch.setenv("WAZUH_API_PASSWORD", "placeholder") monkeypatch.delenv("IWOOOS_WAZUH_EXPECTED_MIN_AGENT_COUNT", raising=False) def handler(request: httpx.Request) -> httpx.Response: if request.url.path == "/security/user/authenticate": return httpx.Response(200, json={"data": {"token": "token-value"}}) if request.url.path == "/agents/summary/status": return httpx.Response( 200, json={"data": {"connection": {"total": 0, "active": 0, "disconnected": 0, "pending": 0}}}, ) if request.url.path == "/agents": return httpx.Response(200, json={"data": {"affected_items": []}}) return httpx.Response(404) transport = httpx.MockTransport(handler) original_async_client = httpx.AsyncClient def client_factory(*args, **kwargs): kwargs["transport"] = transport return original_async_client(*args, **kwargs) monkeypatch.setattr(httpx, "AsyncClient", client_factory) response = _client().get("/api/iwooos/wazuh") assert response.status_code == 200 data = response.json() assert data["status"] == "wazuh_agent_registry_empty" assert data["summary"]["agent_total"] == 0 assert data["summary"]["agent_registry_empty_count"] == 1 assert data["summary"]["agent_below_expected_minimum_count"] == 0 assert data["summary"]["agent_visibility_no_false_green_count"] == 1 assert data["summary"]["runtime_gate_count"] == 0 assert data["agents"] == [] assert "token-value" not in response.text def test_iwooos_wazuh_marks_agent_count_below_expected_as_degraded(monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("IWOOOS_WAZUH_READONLY_ENABLED", "true") monkeypatch.setenv("WAZUH_API_BASE_URL", "https://wazuh.example.test:55000") monkeypatch.setenv("WAZUH_API_USERNAME", "readonly") monkeypatch.setenv("WAZUH_API_PASSWORD", "placeholder") monkeypatch.setenv("IWOOOS_WAZUH_EXPECTED_MIN_AGENT_COUNT", "2") def handler(request: httpx.Request) -> httpx.Response: if request.url.path == "/security/user/authenticate": return httpx.Response(200, json={"data": {"token": "token-value"}}) if request.url.path == "/agents/summary/status": return httpx.Response( 200, json={"data": {"connection": {"total": 1, "active": 1, "disconnected": 0, "pending": 0}}}, ) if request.url.path == "/agents": return httpx.Response( 200, json={ "data": { "affected_items": [ { "id": "001", "name": "private-host-name", "ip": "192.168.0.110", "status": "active", "os": {"platform": "linux"}, "lastKeepAlive": "2026-06-24T13:00:00Z", } ] } }, ) return httpx.Response(404) transport = httpx.MockTransport(handler) original_async_client = httpx.AsyncClient def client_factory(*args, **kwargs): kwargs["transport"] = transport return original_async_client(*args, **kwargs) monkeypatch.setattr(httpx, "AsyncClient", client_factory) response = _client().get("/api/iwooos/wazuh") assert response.status_code == 200 data = response.json() assert data["status"] == "wazuh_agent_registry_below_expected" assert data["summary"]["expected_min_agent_count"] == 2 assert data["summary"]["agent_total"] == 1 assert data["summary"]["agent_registry_empty_count"] == 0 assert data["summary"]["agent_below_expected_minimum_count"] == 1 assert data["summary"]["runtime_gate_count"] == 0 assert "private-host-name" not in response.text assert "192.168.0.110" not in response.text assert "token-value" not in response.text