Specs stay at repo root (cross-VM). Move deploy and code into logical projects with README per domain, updated manifest.yaml, and symlinks at legacy paths for VM122 backward compatibility.
238 lines
7.3 KiB
Python
238 lines
7.3 KiB
Python
"""Wazuh SOC — dados para Audit Overview (tenant VM104)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
WAZUH_TENANT_ID = 2
|
|
WAZUH_API_URL = os.getenv("WAZUH_API_URL", "https://10.10.10.104:55000/")
|
|
WAZUH_MIN_LEVEL = int(os.getenv("WAZUH_MIN_TICKET_LEVEL", "10"))
|
|
|
|
|
|
def _now() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def _parse_payload(raw: str | dict) -> dict:
|
|
if isinstance(raw, dict):
|
|
return raw
|
|
try:
|
|
return json.loads(raw or "{}")
|
|
except json.JSONDecodeError:
|
|
return {}
|
|
|
|
|
|
def wazuh_api_status() -> dict:
|
|
try:
|
|
with httpx.Client(timeout=5.0, verify=False) as client:
|
|
response = client.get(WAZUH_API_URL)
|
|
online = response.status_code in (200, 401)
|
|
return {
|
|
"reachable": True,
|
|
"http_status": response.status_code,
|
|
"api_online": online,
|
|
}
|
|
except Exception as exc:
|
|
return {"reachable": False, "http_status": None, "api_online": False, "error": str(exc)}
|
|
|
|
|
|
def _parse_alert_row(row: sqlite3.Row) -> dict:
|
|
payload = _parse_payload(row["payload"])
|
|
data = payload.get("data") or {}
|
|
level = int(data.get("level") or 0)
|
|
return {
|
|
"id": row["id"],
|
|
"event_type": row["event_type"],
|
|
"created_at": row["created_at"],
|
|
"level": level,
|
|
"rule_id": data.get("rule_id"),
|
|
"description": (data.get("description") or "").strip(),
|
|
"agent": (data.get("agent") or payload.get("domain") or "—").strip(),
|
|
"agent_ip": data.get("agent_ip"),
|
|
"srcip": data.get("srcip"),
|
|
"session_id": payload.get("session_id"),
|
|
"severity": _level_severity(level),
|
|
}
|
|
|
|
|
|
def _level_severity(level: int) -> str:
|
|
if level >= 12:
|
|
return "critical"
|
|
if level >= WAZUH_MIN_LEVEL:
|
|
return "high"
|
|
if level >= 7:
|
|
return "medium"
|
|
return "low"
|
|
|
|
|
|
def list_wazuh_alerts(conn: sqlite3.Connection, limit: int = 200) -> list[dict]:
|
|
rows = conn.execute(
|
|
"""
|
|
SELECT id, event_type, payload, created_at
|
|
FROM webhook_events
|
|
WHERE source = 'wazuh'
|
|
ORDER BY id DESC
|
|
LIMIT ?
|
|
""",
|
|
(limit,),
|
|
).fetchall()
|
|
return [_parse_alert_row(r) for r in rows]
|
|
|
|
|
|
def _in_hours(iso: str | None, hours: int) -> bool:
|
|
if not iso:
|
|
return False
|
|
try:
|
|
ts = datetime.fromisoformat(iso.replace("Z", "+00:00"))
|
|
if ts.tzinfo is None:
|
|
ts = ts.replace(tzinfo=timezone.utc)
|
|
return datetime.now(timezone.utc) - ts <= timedelta(hours=hours)
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def _build_agents(alerts: list[dict]) -> list[dict]:
|
|
agents: dict[str, dict] = {}
|
|
for alert in alerts:
|
|
name = alert["agent"] or "—"
|
|
entry = agents.setdefault(
|
|
name,
|
|
{
|
|
"agent": name,
|
|
"agent_ip": alert.get("agent_ip"),
|
|
"alerts_count": 0,
|
|
"max_level": 0,
|
|
"last_seen": alert["created_at"],
|
|
},
|
|
)
|
|
entry["alerts_count"] += 1
|
|
entry["max_level"] = max(entry["max_level"], alert["level"])
|
|
if alert["created_at"] > entry["last_seen"]:
|
|
entry["last_seen"] = alert["created_at"]
|
|
if alert.get("agent_ip"):
|
|
entry["agent_ip"] = alert["agent_ip"]
|
|
return sorted(agents.values(), key=lambda a: (-a["max_level"], -a["alerts_count"]))
|
|
|
|
|
|
def _overall_status(alerts: list[dict], api_online: bool, open_tickets: int) -> str:
|
|
recent_24h = [a for a in alerts if _in_hours(a["created_at"], 24)]
|
|
max_level_24h = max((a["level"] for a in recent_24h), default=0)
|
|
if max_level_24h >= 12 or open_tickets > 0:
|
|
return "critical"
|
|
if any(a["level"] >= WAZUH_MIN_LEVEL for a in recent_24h):
|
|
return "degraded"
|
|
if alerts and api_online:
|
|
return "healthy"
|
|
if api_online:
|
|
return "healthy"
|
|
if alerts:
|
|
return "degraded"
|
|
return "unknown"
|
|
|
|
|
|
def wazuh_tenant_overview(
|
|
conn: sqlite3.Connection,
|
|
tenant_id: int,
|
|
name: str,
|
|
ip: str,
|
|
) -> dict:
|
|
alerts = list_wazuh_alerts(conn, 200)
|
|
agents = _build_agents(alerts)
|
|
api = wazuh_api_status()
|
|
open_tickets = conn.execute(
|
|
"SELECT COUNT(*) c FROM tickets WHERE tenant_id = ? AND status NOT IN ('closed', 'resolved')",
|
|
(tenant_id,),
|
|
).fetchone()["c"]
|
|
alerts_24h = sum(1 for a in alerts if _in_hours(a["created_at"], 24))
|
|
alerts_high = sum(1 for a in alerts if a["level"] >= WAZUH_MIN_LEVEL)
|
|
status = _overall_status(alerts, api.get("api_online", False), open_tickets)
|
|
last_alert = alerts[0]["created_at"] if alerts else None
|
|
top_issues = [
|
|
{
|
|
"domain": a["agent"],
|
|
"check_id": f"L{a['level']}",
|
|
"status": a["severity"],
|
|
"message": a["description"] or a["event_type"],
|
|
"at": a["created_at"],
|
|
}
|
|
for a in alerts[:5]
|
|
]
|
|
return {
|
|
"tenant_id": tenant_id,
|
|
"name": name,
|
|
"ip": ip,
|
|
"kind": "wazuh_soc",
|
|
"status": status,
|
|
"api_online": api.get("api_online", False),
|
|
"http_status": api.get("http_status"),
|
|
"alerts_total": len(alerts),
|
|
"alerts_24h": alerts_24h,
|
|
"alerts_high": alerts_high,
|
|
"agents_count": len(agents),
|
|
"open_tickets": open_tickets,
|
|
"min_ticket_level": WAZUH_MIN_LEVEL,
|
|
"domains_count": 0,
|
|
"last_audit_at": last_alert,
|
|
"last_alert_at": last_alert,
|
|
"score": {
|
|
"pass": len(agents),
|
|
"warn": alerts_high,
|
|
"fail": open_tickets,
|
|
"total": max(len(alerts), 1),
|
|
},
|
|
"top_issues": top_issues,
|
|
}
|
|
|
|
|
|
def wazuh_tenant_details(
|
|
conn: sqlite3.Connection,
|
|
tenant_id: int,
|
|
name: str,
|
|
ip: str,
|
|
) -> dict:
|
|
alerts = list_wazuh_alerts(conn, 100)
|
|
agents = _build_agents(alerts)
|
|
api = wazuh_api_status()
|
|
tickets = conn.execute(
|
|
"""
|
|
SELECT id, subject, status, created_at, session_id
|
|
FROM tickets WHERE tenant_id = ?
|
|
ORDER BY id DESC LIMIT 50
|
|
""",
|
|
(tenant_id,),
|
|
).fetchall()
|
|
ticket_rows = [dict(r) for r in tickets]
|
|
open_tickets = sum(1 for t in ticket_rows if t["status"] not in ("closed", "resolved"))
|
|
alerts_24h = [a for a in alerts if _in_hours(a["created_at"], 24)]
|
|
alerts_7d = [a for a in alerts if _in_hours(a["created_at"], 168)]
|
|
level_10_plus = sum(1 for a in alerts if a["level"] >= WAZUH_MIN_LEVEL)
|
|
level_12_plus = sum(1 for a in alerts if a["level"] >= 12)
|
|
return {
|
|
"tenant_id": tenant_id,
|
|
"name": name,
|
|
"ip": ip,
|
|
"kind": "wazuh_soc",
|
|
"generated_at": _now(),
|
|
"api": api,
|
|
"min_ticket_level": WAZUH_MIN_LEVEL,
|
|
"summary": {
|
|
"alerts_total": len(alerts),
|
|
"alerts_24h": len(alerts_24h),
|
|
"alerts_7d": len(alerts_7d),
|
|
"agents_total": len(agents),
|
|
"level_10_plus": level_10_plus,
|
|
"level_12_plus": level_12_plus,
|
|
"open_tickets": open_tickets,
|
|
"api_online": api.get("api_online", False),
|
|
},
|
|
"agents": agents,
|
|
"alerts": alerts,
|
|
"tickets": ticket_rows,
|
|
"domains": [],
|
|
}
|