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 class EVEWatcher(threading.Thread): def __init__( self, path: str, store: AlertStore, policy: PolicyEngine, routeros: RouterOSClient, block_timeout: str, stats: RuntimeStats, stop_event: threading.Event, ) -> None: super().__init__(name="eve-watcher", daemon=True) self.path = path self.store = store self.policy = policy self.routeros = routeros self.block_timeout = block_timeout self.stats = stats self.stop_event = stop_event 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: handle.seek(0, os.SEEK_END) 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") if event.get("event_type") != "alert": return self.stats.inc("eve_alerts") self.stats.stamp("last_alert_at") 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)