Files
routeros-suricata-tzsp/app/eve.py
T
2026-08-14 11:33:01 +02:00

129 lines
4.3 KiB
Python

from __future__ import annotations
import json
import os
import threading
import time
from typing import Any
from .policy import PolicyEngine
from .routeros import RouterOSClient
from .state import RuntimeStats
from .store import AlertStore
from .tuning import AlertTuner
class EVEWatcher(threading.Thread):
def __init__(
self,
path: str,
store: AlertStore,
tuner: AlertTuner,
policy: PolicyEngine,
routeros: RouterOSClient,
block_timeout: str,
dedup_window_seconds: int,
stats: RuntimeStats,
stop_event: threading.Event,
) -> None:
super().__init__(name="eve-watcher", daemon=True)
self.path = path
self.store = store
self.tuner = tuner
self.policy = policy
self.routeros = routeros
self.block_timeout = block_timeout
self.dedup_window_seconds = max(0, int(dedup_window_seconds))
self.stats = stats
self.stop_event = stop_event
self._initial_seek_done = False
def run(self) -> None:
while not self.stop_event.is_set():
if not os.path.exists(self.path):
time.sleep(0.5)
continue
try:
self._follow_file()
except OSError as exc:
print(f"[eve] file error: {exc}", flush=True)
time.sleep(1.0)
def _follow_file(self) -> None:
with open(self.path, "r", encoding="utf-8", errors="replace") as handle:
# Ignore historical EVE only on the first attach. After rotation or
# truncation read the replacement file from byte 0 so alerts that
# arrived during the hand-off are not skipped.
if not self._initial_seek_done:
handle.seek(0, os.SEEK_END)
self._initial_seek_done = True
inode = os.fstat(handle.fileno()).st_ino
print(f"[eve] following {self.path}", flush=True)
while not self.stop_event.is_set():
line = handle.readline()
if line:
self._process_line(line)
continue
try:
stat = os.stat(self.path)
if stat.st_ino != inode or stat.st_size < handle.tell():
return
except FileNotFoundError:
return
time.sleep(0.2)
def _process_line(self, line: str) -> None:
try:
event: dict[str, Any] = json.loads(line)
except json.JSONDecodeError:
self.stats.inc("eve_parse_errors")
return
self.stats.inc("eve_events")
event_type = event.get("event_type")
if event_type == "stats":
raw_stats = event.get("stats")
if isinstance(raw_stats, dict):
self.stats.update_suricata(raw_stats, str(event.get("timestamp") or ""))
return
if event_type != "alert":
return
self.stats.inc("eve_alerts")
self.stats.stamp("last_alert_at")
tuning = self.tuner.evaluate(event)
if not tuning.keep:
self.stats.inc("alerts_filtered")
key = f"alerts_filtered_{tuning.reason}"
self.stats.inc(key)
return
duplicate_id = self.store.find_recent_duplicate(event, self.dedup_window_seconds)
if duplicate_id is not None:
self.store.bump_duplicate(duplicate_id, event)
self.stats.inc("alerts_deduplicated")
return
decision = self.policy.evaluate(event)
blocked = False
reason = decision.reason
if decision.should_block and decision.target:
self.stats.inc("block_attempts")
alert = event.get("alert") or {}
sid = alert.get("signature_id", "unknown")
signature = str(alert.get("signature", "Suricata alert"))
result = self.routeros.block_ip(
decision.target,
self.block_timeout,
f"Suricata SID {sid}: {signature}",
)
blocked = result.success
reason = result.message if result.success else f"{decision.reason}; {result.message}"
self.stats.inc("block_success" if result.success else "block_errors")
self.store.insert_alert(event, blocked, decision.target, reason)