first commit
This commit is contained in:
+227
@@ -0,0 +1,227 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .config import Config
|
||||
from .eve import EVEWatcher
|
||||
from .policy import PolicyEngine
|
||||
from .routeros import RouterOSClient
|
||||
from .state import RuntimeStats
|
||||
from .store import AlertStore
|
||||
from .tap import TapDevice
|
||||
from .tzsp import TZSPReceiver
|
||||
from .webui import WebServer
|
||||
|
||||
|
||||
def _routeros_target(cfg: Config) -> tuple[str, int]:
|
||||
parsed = urlparse(cfg.routeros_url)
|
||||
host = parsed.hostname or cfg.routeros_url
|
||||
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
||||
return host, port
|
||||
|
||||
|
||||
def main() -> int:
|
||||
cfg = Config.from_env()
|
||||
stop_event = threading.Event()
|
||||
stats = RuntimeStats()
|
||||
started_at = datetime.now(timezone.utc)
|
||||
started_monotonic = time.monotonic()
|
||||
|
||||
os.makedirs(os.path.dirname(cfg.eve_path) or ".", exist_ok=True)
|
||||
os.makedirs(os.path.dirname(cfg.db_path) or ".", exist_ok=True)
|
||||
|
||||
store = AlertStore(cfg.db_path)
|
||||
purged = store.purge_older_than(cfg.alert_retention_days)
|
||||
if purged:
|
||||
print(f"[db] purged {purged} old alerts", flush=True)
|
||||
|
||||
tap = TapDevice(cfg.tap_name, cfg.tap_mtu)
|
||||
try:
|
||||
tap.open()
|
||||
except Exception as exc:
|
||||
print(f"[fatal] cannot create TAP {cfg.tap_name}: {exc}", file=sys.stderr, flush=True)
|
||||
print("[fatal] container needs /dev/net/tun and NET_ADMIN capability", file=sys.stderr, flush=True)
|
||||
store.close()
|
||||
return 2
|
||||
|
||||
print(f"[tap] {cfg.tap_name} is up, mtu={cfg.tap_mtu}", flush=True)
|
||||
|
||||
suricata_cmd = [
|
||||
"suricata",
|
||||
"-c", cfg.suricata_config,
|
||||
f"--af-packet={cfg.tap_name}",
|
||||
"-l", os.path.dirname(cfg.eve_path) or "/var/log/suricata",
|
||||
"--user", "suricata",
|
||||
"--group", "suricata",
|
||||
"--set", f"vars.address-groups.HOME_NET={cfg.suricata_home_net}",
|
||||
]
|
||||
|
||||
test_cmd = ["suricata", "-T", "-c", cfg.suricata_config, "--set", f"vars.address-groups.HOME_NET={cfg.suricata_home_net}"]
|
||||
print("[suricata] validating configuration", flush=True)
|
||||
test = subprocess.run(test_cmd, check=False)
|
||||
if test.returncode != 0:
|
||||
print(f"[fatal] suricata configuration test failed with rc={test.returncode}", file=sys.stderr, flush=True)
|
||||
tap.close()
|
||||
store.close()
|
||||
return test.returncode or 3
|
||||
|
||||
print("[suricata] starting IDS process", flush=True)
|
||||
suricata = subprocess.Popen(suricata_cmd)
|
||||
with open("/run/suricata.pid", "w", encoding="ascii") as pid_file:
|
||||
pid_file.write(str(suricata.pid))
|
||||
|
||||
policy = PolicyEngine(
|
||||
cfg.auto_block,
|
||||
cfg.auto_block_max_severity,
|
||||
cfg.monitored_networks,
|
||||
cfg.never_block,
|
||||
)
|
||||
routeros = RouterOSClient(
|
||||
cfg.routeros_url,
|
||||
cfg.routeros_user,
|
||||
cfg.routeros_password,
|
||||
cfg.routeros_verify_tls,
|
||||
cfg.routeros_address_list,
|
||||
cfg.routeros_http_timeout,
|
||||
)
|
||||
|
||||
receiver = TZSPReceiver(cfg.tzsp_bind, cfg.tzsp_port, tap.write, stats, stop_event)
|
||||
watcher = EVEWatcher(cfg.eve_path, store, policy, routeros, cfg.block_timeout, stats, stop_event)
|
||||
routeros_host, routeros_port = _routeros_target(cfg)
|
||||
|
||||
def health() -> dict:
|
||||
suricata_up = suricata.poll() is None
|
||||
tzsp_up = receiver.is_alive() and receiver.sock is not None
|
||||
tap_up = tap.fd is not None and os.path.exists(f"/sys/class/net/{cfg.tap_name}")
|
||||
eve_up = watcher.is_alive()
|
||||
routeros_status = "configured" if routeros.configured else "disabled"
|
||||
core_up = suricata_up and tzsp_up and tap_up and eve_up
|
||||
routeros_required_ok = (not cfg.auto_block) or routeros.configured
|
||||
operational = core_up and routeros_required_ok
|
||||
|
||||
return {
|
||||
"status": "ok" if operational else "degraded",
|
||||
"mode": "full",
|
||||
"dev_mode": False,
|
||||
"operational": operational,
|
||||
"started_at": started_at.isoformat(),
|
||||
"uptime_seconds": round(time.monotonic() - started_monotonic, 1),
|
||||
"suricata_running": suricata_up,
|
||||
"suricata_pid": suricata.pid,
|
||||
"auto_block": cfg.auto_block,
|
||||
"routeros_configured": routeros.configured,
|
||||
"services": {
|
||||
"web": {
|
||||
"name": "Web UI / API",
|
||||
"status": "up",
|
||||
"details": f"Listening on TCP {cfg.web_bind}:{cfg.web_port}",
|
||||
},
|
||||
"tzsp": {
|
||||
"name": "TZSP receiver",
|
||||
"status": "up" if tzsp_up else "down",
|
||||
"details": f"Listening on UDP {cfg.tzsp_bind}:{cfg.tzsp_port}",
|
||||
},
|
||||
"tap": {
|
||||
"name": "TAP interface",
|
||||
"status": "up" if tap_up else "down",
|
||||
"details": f"{cfg.tap_name}, MTU {cfg.tap_mtu}",
|
||||
},
|
||||
"suricata": {
|
||||
"name": "Suricata IDS",
|
||||
"status": "up" if suricata_up else "down",
|
||||
"details": f"PID {suricata.pid}" if suricata_up else f"Process exited with code {suricata.poll()}",
|
||||
},
|
||||
"eve": {
|
||||
"name": "EVE JSON watcher",
|
||||
"status": "up" if eve_up else "down",
|
||||
"details": cfg.eve_path,
|
||||
},
|
||||
"routeros": {
|
||||
"name": "RouterOS REST integration",
|
||||
"status": routeros_status,
|
||||
"details": f"{cfg.routeros_url}; auto-block={'enabled' if cfg.auto_block else 'disabled'}",
|
||||
},
|
||||
},
|
||||
"ports": [
|
||||
{
|
||||
"name": "Web UI / API",
|
||||
"direction": "listen",
|
||||
"protocol": "TCP",
|
||||
"address": cfg.web_bind,
|
||||
"port": cfg.web_port,
|
||||
"status": "up",
|
||||
},
|
||||
{
|
||||
"name": "TZSP receiver",
|
||||
"direction": "listen",
|
||||
"protocol": "UDP",
|
||||
"address": cfg.tzsp_bind,
|
||||
"port": cfg.tzsp_port,
|
||||
"status": "up" if tzsp_up else "down",
|
||||
},
|
||||
{
|
||||
"name": "RouterOS REST API",
|
||||
"direction": "outbound",
|
||||
"protocol": "TCP",
|
||||
"address": routeros_host,
|
||||
"port": routeros_port,
|
||||
"status": routeros_status,
|
||||
},
|
||||
],
|
||||
"runtime": stats.snapshot(),
|
||||
}
|
||||
|
||||
web = WebServer(cfg, store, health)
|
||||
|
||||
def request_stop(_signum=None, _frame=None):
|
||||
stop_event.set()
|
||||
|
||||
signal.signal(signal.SIGTERM, request_stop)
|
||||
signal.signal(signal.SIGINT, request_stop)
|
||||
|
||||
receiver.start()
|
||||
watcher.start()
|
||||
web.start()
|
||||
|
||||
rc = 0
|
||||
try:
|
||||
while not stop_event.is_set():
|
||||
suricata_rc = suricata.poll()
|
||||
if suricata_rc is not None:
|
||||
print(f"[fatal] Suricata exited with rc={suricata_rc}", file=sys.stderr, flush=True)
|
||||
rc = suricata_rc or 4
|
||||
break
|
||||
time.sleep(0.5)
|
||||
finally:
|
||||
stop_event.set()
|
||||
receiver.close()
|
||||
try:
|
||||
web.stop()
|
||||
except Exception:
|
||||
pass
|
||||
if suricata.poll() is None:
|
||||
suricata.terminate()
|
||||
try:
|
||||
suricata.wait(timeout=8)
|
||||
except subprocess.TimeoutExpired:
|
||||
suricata.kill()
|
||||
suricata.wait(timeout=3)
|
||||
try:
|
||||
os.remove("/run/suricata.pid")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
tap.close()
|
||||
store.close()
|
||||
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user