98 lines
3.1 KiB
Python
98 lines
3.1 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
|
|
|
|
|
|
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)
|