feat(iwooos): flag empty Wazuh agent registry
This commit is contained in:
@@ -27,9 +27,17 @@ def _wazuh_env() -> dict[str, str]:
|
||||
"base_url": os.getenv("WAZUH_API_BASE_URL", "").strip(),
|
||||
"username": os.getenv("WAZUH_API_USERNAME", "").strip(),
|
||||
"password": os.getenv("WAZUH_API_PASSWORD", "").strip(),
|
||||
"expected_min_agent_count": os.getenv("IWOOOS_WAZUH_EXPECTED_MIN_AGENT_COUNT", "").strip(),
|
||||
}
|
||||
|
||||
|
||||
def _expected_min_agent_count(value: str) -> int:
|
||||
try:
|
||||
return max(0, int(value))
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def _https_url(value: str) -> str | None:
|
||||
parsed = urlparse(value)
|
||||
if parsed.scheme != "https" or not parsed.netloc:
|
||||
@@ -54,6 +62,10 @@ def _boundary_response(status_text: str, http_status: int = 200) -> JSONResponse
|
||||
"active_response_authorized_count": 0,
|
||||
"host_write_authorized_count": 0,
|
||||
"runtime_gate_count": 0,
|
||||
"expected_min_agent_count": _expected_min_agent_count(_wazuh_env()["expected_min_agent_count"]),
|
||||
"agent_registry_empty_count": 0,
|
||||
"agent_below_expected_minimum_count": 0,
|
||||
"agent_visibility_no_false_green_count": 1,
|
||||
},
|
||||
"boundaries": _boundaries(),
|
||||
},
|
||||
@@ -82,6 +94,18 @@ def _redacted_agent(agent: dict[str, Any], index: int) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _int_or_default(value: Any, default: int) -> int:
|
||||
return value if isinstance(value, int) else default
|
||||
|
||||
|
||||
def _agent_visibility_status(agent_total: int, expected_min_agent_count: int) -> str:
|
||||
if agent_total <= 0:
|
||||
return "wazuh_agent_registry_empty"
|
||||
if expected_min_agent_count > 0 and agent_total < expected_min_agent_count:
|
||||
return "wazuh_agent_registry_below_expected"
|
||||
return "readonly_metadata_available"
|
||||
|
||||
|
||||
async def _fetch_json(client: httpx.AsyncClient, url: str, headers: dict[str, str]) -> dict[str, Any]:
|
||||
response = await client.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
@@ -128,20 +152,31 @@ async def _wazuh_readonly_status() -> JSONResponse:
|
||||
affected_items = ((agents_payload.get("data") or {}).get("affected_items") or [])
|
||||
if not isinstance(affected_items, list):
|
||||
affected_items = []
|
||||
expected_min_agent_count = _expected_min_agent_count(env["expected_min_agent_count"])
|
||||
agent_total = _int_or_default(connection.get("total"), len(affected_items))
|
||||
agent_active = _int_or_default(connection.get("active"), 0)
|
||||
agent_disconnected = _int_or_default(connection.get("disconnected"), 0)
|
||||
agent_pending = _int_or_default(connection.get("pending"), 0)
|
||||
agent_registry_empty = agent_total <= 0
|
||||
agent_below_expected = expected_min_agent_count > 0 and agent_total < expected_min_agent_count
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"schema_version": "iwooos_wazuh_readonly_status_v1",
|
||||
"status": "readonly_metadata_available",
|
||||
"status": _agent_visibility_status(agent_total, expected_min_agent_count),
|
||||
"mode": "metadata_only_no_active_response_no_raw_payload",
|
||||
"configured": True,
|
||||
"summary": {
|
||||
"wazuh_platform_reported_count": 1,
|
||||
"readonly_api_enabled_count": 1,
|
||||
"agent_total": connection.get("total", len(affected_items)),
|
||||
"agent_active": connection.get("active", 0),
|
||||
"agent_disconnected": connection.get("disconnected", 0),
|
||||
"agent_pending": connection.get("pending", 0),
|
||||
"agent_total": agent_total,
|
||||
"agent_active": agent_active,
|
||||
"agent_disconnected": agent_disconnected,
|
||||
"agent_pending": agent_pending,
|
||||
"expected_min_agent_count": expected_min_agent_count,
|
||||
"agent_registry_empty_count": 1 if agent_registry_empty else 0,
|
||||
"agent_below_expected_minimum_count": 1 if agent_below_expected else 0,
|
||||
"agent_visibility_no_false_green_count": 1,
|
||||
"wazuh_manager_query_accepted_count": 0,
|
||||
"wazuh_event_accepted_count": 0,
|
||||
"host_forensics_accepted_count": 0,
|
||||
|
||||
@@ -19,6 +19,7 @@ def test_iwooos_wazuh_compat_route_returns_disabled_boundary_by_default(monkeypa
|
||||
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")
|
||||
|
||||
@@ -28,6 +29,7 @@ def test_iwooos_wazuh_compat_route_returns_disabled_boundary_by_default(monkeypa
|
||||
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
|
||||
@@ -39,6 +41,7 @@ def test_iwooos_wazuh_v1_route_rejects_missing_server_side_env(monkeypatch: pyte
|
||||
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")
|
||||
|
||||
@@ -54,6 +57,7 @@ def test_iwooos_wazuh_rejects_non_https_base_url(monkeypatch: pytest.MonkeyPatch
|
||||
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")
|
||||
|
||||
@@ -68,6 +72,7 @@ def test_iwooos_wazuh_live_response_is_metadata_only(monkeypatch: pytest.MonkeyP
|
||||
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":
|
||||
@@ -113,6 +118,9 @@ def test_iwooos_wazuh_live_response_is_metadata_only(monkeypatch: pytest.MonkeyP
|
||||
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"] == [
|
||||
{
|
||||
@@ -125,3 +133,104 @@ def test_iwooos_wazuh_live_response_is_metadata_only(monkeypatch: pytest.MonkeyP
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user