This commit is contained in:
Mateusz Gruszczyński
2026-08-17 10:07:19 +02:00
parent 074d17be89
commit cc3c446c8e
29 changed files with 627 additions and 105 deletions
+77 -9
View File
@@ -34,6 +34,40 @@ from .tzsp_rust import RustTZSPReceiver
from .webui import WebServer
def _process_rss_mib(pid: int | None) -> float | None:
if not pid:
return None
try:
for line in Path(f"/proc/{int(pid)}/status").read_text(encoding="ascii", errors="replace").splitlines():
if line.startswith("VmRSS:"):
parts = line.split()
return round(int(parts[1]) / 1024.0, 1)
except (OSError, ValueError, IndexError):
pass
return None
def _cgroup_memory_mib() -> dict[str, float | None]:
def read_number(path: str) -> int | None:
try:
raw = Path(path).read_text(encoding="ascii").strip()
if not raw or raw == "max":
return None
return int(raw)
except (OSError, ValueError):
return None
current = read_number("/sys/fs/cgroup/memory.current")
limit = read_number("/sys/fs/cgroup/memory.max")
if current is None:
current = read_number("/sys/fs/cgroup/memory/memory.usage_in_bytes")
limit = read_number("/sys/fs/cgroup/memory/memory.limit_in_bytes")
return {
"current_mib": round(current / (1024 * 1024), 1) if current is not None else None,
"limit_mib": round(limit / (1024 * 1024), 1) if limit is not None else None,
}
def _routeros_target(cfg: Config) -> tuple[str, int]:
parsed = urlparse(cfg.routeros_url)
host = parsed.hostname or cfg.routeros_url
@@ -240,8 +274,8 @@ def main() -> int:
traffic_history = TrafficHistory(
cfg.redis_url,
cfg.traffic_retention_hours,
0,
0,
cfg.traffic_max_events,
cfg.traffic_memory_events,
require_redis=True,
allow_memory_fallback=False,
archive_store=store,
@@ -282,6 +316,26 @@ def main() -> int:
)
routeros_host, routeros_port = _routeros_target(cfg)
def healthcheck() -> dict:
# Docker calls this frequently. Keep it strictly O(1): no rule-file
# scans, archive counts, directory walks or dashboard aggregates.
suricata_up = suricata.poll() is None
tzsp_up = receiver.is_alive()
tap_up = os.path.exists(f"/sys/class/net/{cfg.tap_name}")
eve_up = watcher.is_alive()
db_up = store.ping()
routeros_required_ok = (not cfg.auto_block) or routeros.configured
operational = suricata_up and tzsp_up and tap_up and eve_up and db_up and routeros_required_ok
return {
"status": "ok" if operational else "degraded",
"operational": operational,
"suricata_running": suricata_up,
"tzsp_running": tzsp_up,
"tap_up": tap_up,
"eve_running": eve_up,
"database_ok": db_up,
}
def health() -> dict:
suricata_up = suricata.poll() is None
tzsp_up = receiver.is_alive()
@@ -294,6 +348,10 @@ def main() -> int:
runtime = stats.snapshot()
receiver_status = receiver.status()
redis_status = redis_supervisor.status()
history_status = traffic_history.status()
ndr_status = ndr_analyzer.status()
ndr_summary = store.ndr_summary()
notifier_status = notifier.status()
suri_stats = runtime.get("suricata") or {}
kernel_packets = int(suri_stats.get("capture.kernel_packets", 0) or 0)
kernel_drops = int(suri_stats.get("capture.kernel_drops", 0) or 0)
@@ -316,6 +374,13 @@ def main() -> int:
core_up = suricata_up and tzsp_up and tap_up and eve_up and db["ok"]
routeros_required_ok = (not cfg.auto_block) or routeros.configured
operational = core_up and routeros_required_ok
process_memory = {
"python_rss_mib": _process_rss_mib(os.getpid()),
"rust_rss_mib": _process_rss_mib(receiver.pid),
"suricata_rss_mib": _process_rss_mib(suricata.pid if suricata_up else None),
"redis_rss_mib": _process_rss_mib(redis_status.get("pid")),
"cgroup": _cgroup_memory_mib(),
}
return {
"status": "ok" if operational else "degraded",
@@ -328,11 +393,13 @@ def main() -> int:
"suricata_pid": suricata.pid,
"auto_block": cfg.auto_block,
"routeros_configured": routeros.configured,
"ndr": {**ndr_analyzer.status(), **store.ndr_summary()},
"ndr": {**ndr_status, **ndr_summary},
"database": db,
"storage": storage,
"rules": rules,
"redis": redis_status,
"memory": process_memory,
"traffic_history": history_status,
"services": {
"web": {
"name": "Web UI / API",
@@ -387,7 +454,7 @@ def main() -> int:
},
"traffic_history": {
"name": "Live traffic history",
"status": "up" if traffic_history.status().get("redis_ok") else "degraded",
"status": "up" if history_status.get("redis_ok") else "degraded",
"details": f"Redis ingest buffer -> SQLite archive; retention={cfg.traffic_retention_hours}h",
},
"analytics_cache": {
@@ -397,13 +464,13 @@ def main() -> int:
},
"ndr": {
"name": "MikroSuricata NDR correlation",
"status": "up" if ndr_analyzer.status().get("running") else "disabled" if not cfg.ndr_enabled else "degraded",
"details": f"assets={store.ndr_summary()['assets']}; incidents={store.ndr_summary()['incidents']}; IOC={store.ndr_summary()['enabled_iocs']}; queue={ndr_analyzer.status()['queue']}",
"status": "up" if ndr_status.get("running") else "disabled" if not cfg.ndr_enabled else "degraded",
"details": f"assets={ndr_summary['assets']}; incidents={ndr_summary['incidents']}; IOC={ndr_summary['enabled_iocs']}; queue={ndr_status['queue']}",
},
"notifications": {
"name": "High-risk webhook notifications",
"status": "up" if notifier.status().get("running") else "disabled" if not notifier.enabled else "degraded",
"details": f"min risk={cfg.notify_min_risk}; sent={notifier.status()['sent']}; failed={notifier.status()['failed']}; queue={notifier.status()['queue']}",
"status": "up" if notifier_status.get("running") else "disabled" if not notifier.enabled else "degraded",
"details": f"min risk={cfg.notify_min_risk}; sent={notifier_status['sent']}; failed={notifier_status['failed']}; queue={notifier_status['queue']}",
},
"redis": {
"name": "Managed Redis",
@@ -455,7 +522,7 @@ def main() -> int:
"status": routeros_status,
},
],
"runtime": stats.snapshot(),
"runtime": runtime,
}
version_path = Path(__file__).resolve().parents[1] / "VERSION"
@@ -514,6 +581,7 @@ def main() -> int:
forensic_pcap=forensic_pcap,
traffic_source=receiver,
metrics_provider=prometheus_metrics.render,
healthcheck_provider=healthcheck,
)
def housekeeping() -> None: