Files
awoooi/apps/api/src/services/iwooos_wazuh_readonly_status.py
Your Name 9778cc22fc
Some checks failed
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / tests (push) Successful in 1m38s
CD Pipeline / build-and-deploy (push) Successful in 4m22s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
feat(iwooos): surface Wazuh live route in runtime readback
2026-06-26 23:32:39 +08:00

195 lines
7.4 KiB
Python

"""
IwoooS Wazuh read-only metadata status.
This module returns public-safe aggregate metadata only. It never stores raw
Wazuh payloads, never exposes agent identity or LAN topology, and never
authorizes active response or host writes.
"""
from __future__ import annotations
import os
from base64 import b64encode
from dataclasses import dataclass
from typing import Any
from urllib.parse import urljoin, urlparse
import httpx
REQUEST_TIMEOUT_SECONDS = 5.0
@dataclass(frozen=True)
class WazuhReadonlyStatus:
payload: dict[str, Any]
http_status: int = 200
def wazuh_env() -> dict[str, str]:
return {
"enabled": os.getenv("IWOOOS_WAZUH_READONLY_ENABLED", "").strip().lower(),
"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:
return None
return value.rstrip("/") + "/"
def boundaries() -> dict[str, bool]:
return {
"active_response_authorized": False,
"host_write_authorized": False,
"secret_value_collection_allowed": False,
"raw_wazuh_payload_storage_allowed": False,
"agent_identity_public_display_allowed": False,
"internal_ip_public_display_allowed": False,
"not_authorization": True,
}
def boundary_status(status_text: str, http_status: int = 200) -> WazuhReadonlyStatus:
return WazuhReadonlyStatus(
http_status=http_status,
payload={
"schema_version": "iwooos_wazuh_readonly_status_v1",
"status": status_text,
"mode": "metadata_only_no_active_response_no_raw_payload",
"configured": False,
"summary": {
"wazuh_platform_reported_count": 1,
"readonly_api_enabled_count": 0,
"wazuh_manager_query_accepted_count": 0,
"wazuh_event_accepted_count": 0,
"host_forensics_accepted_count": 0,
"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(),
},
)
def redacted_agent(agent: dict[str, Any], index: int) -> dict[str, Any]:
os_info = agent.get("os") if isinstance(agent.get("os"), dict) else {}
return {
"alias": f"agent-{index + 1:02d}",
"status": agent.get("status", "unknown"),
"os": os_info.get("platform") or os_info.get("name") or "unknown",
"last_seen_present": bool(agent.get("lastKeepAlive")),
}
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_minimum: int) -> str:
if agent_total <= 0:
return "wazuh_agent_registry_empty"
if expected_minimum > 0 and agent_total < expected_minimum:
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()
payload = response.json()
return payload if isinstance(payload, dict) else {}
async def load_iwooos_wazuh_readonly_status() -> WazuhReadonlyStatus:
env = wazuh_env()
if env["enabled"] != "true":
return boundary_status("disabled_waiting_iwooos_wazuh_owner_gate")
base_url = https_url(env["base_url"])
if not base_url or not env["username"] or not env["password"]:
return boundary_status("misconfigured_missing_server_side_wazuh_env", 503)
try:
auth_header = b64encode(f"{env['username']}:{env['password']}".encode("utf-8")).decode("ascii")
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT_SECONDS) as client:
auth = await fetch_json(
client,
urljoin(base_url, "security/user/authenticate"),
{"Authorization": f"Basic {auth_header}"},
)
token = (auth.get("data") or {}).get("token")
if not token:
return boundary_status("wazuh_auth_token_missing", 502)
bearer_headers = {"Authorization": f"Bearer {token}"}
status_payload = await fetch_json(
client,
urljoin(base_url, "agents/summary/status"),
bearer_headers,
)
agents_payload = await fetch_json(
client,
urljoin(base_url, "agents?limit=100&select=id,status,os.name,os.platform,lastKeepAlive"),
bearer_headers,
)
except (httpx.HTTPError, ValueError):
return boundary_status("wazuh_readonly_metadata_unavailable", 502)
connection = ((status_payload.get("data") or {}).get("connection") or {})
affected_items = ((agents_payload.get("data") or {}).get("affected_items") or [])
if not isinstance(affected_items, list):
affected_items = []
expected_minimum = 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_minimum > 0 and agent_total < expected_minimum
return WazuhReadonlyStatus(
payload={
"schema_version": "iwooos_wazuh_readonly_status_v1",
"status": agent_visibility_status(agent_total, expected_minimum),
"mode": "metadata_only_no_active_response_no_raw_payload",
"configured": True,
"summary": {
"wazuh_platform_reported_count": 1,
"readonly_api_enabled_count": 1,
"agent_total": agent_total,
"agent_active": agent_active,
"agent_disconnected": agent_disconnected,
"agent_pending": agent_pending,
"expected_min_agent_count": expected_minimum,
"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,
"active_response_authorized_count": 0,
"host_write_authorized_count": 0,
"runtime_gate_count": 0,
},
"agents": [redacted_agent(agent, index) for index, agent in enumerate(affected_items[:20])],
"boundaries": boundaries(),
},
)