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
195 lines
7.4 KiB
Python
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(),
|
|
},
|
|
)
|