Files
routeros-suricata-tzsp/app/dev_web.py
T
2026-08-15 18:29:36 +02:00

255 lines
8.2 KiB
Python

from __future__ import annotations
import os
import signal
import threading
import time
from datetime import datetime, timezone
from urllib.parse import urlparse
from .analytics_cache import AnalyticsSnapshotCache
from .config import Config
from .live import EventBus, LiveEventPipeline, TrafficHistory
from .maintenance import storage_info
from .rules import RuleManager
from .state import RuntimeStats
from .store import AlertStore
from .webui import WebServer
def _bool_env(name: str, default: bool = False) -> bool:
value = os.getenv(name)
if value is None:
return default
return value.strip().lower() in {"1", "true", "yes", "on"}
def _seed_demo_alert(store: AlertStore) -> None:
if store.summary()["total_alerts"]:
return
event = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"flow_id": "dev-demo",
"src_ip": "192.168.100.10",
"src_port": 51515,
"dest_ip": "203.0.113.10",
"dest_port": 443,
"proto": "TCP",
"alert": {
"signature_id": 1001999,
"signature": "DEV MODE SAMPLE ALERT",
"category": "Development/Test",
"severity": 2,
"action": "allowed",
},
}
store.insert_alert(event, False, None, "development sample")
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.db_path) or ".", exist_ok=True)
store = AlertStore(cfg.db_path)
if _bool_env("DEV_SEED_DATA", False):
_seed_demo_alert(store)
routeros_host, routeros_port = _routeros_target(cfg)
rule_manager = RuleManager(cfg, pid_provider=lambda: None, suricata_available=False)
event_bus = EventBus(
history_size=5000,
subscriber_queue_size=cfg.websocket_queue_size,
)
traffic_history = TrafficHistory(
"",
cfg.traffic_retention_hours,
200000,
5000,
allow_memory_fallback=True,
)
live_pipeline = LiveEventPipeline(event_bus, traffic_history)
analytics_cache = AnalyticsSnapshotCache(
store,
traffic_history,
stop_event,
cfg.analytics_snapshot_interval_seconds,
)
if _bool_env("DEV_SEED_DATA", False):
now_ms = int(time.time() * 1000)
live_pipeline.publish({
"id": "dev-flow",
"timestamp": datetime.now(timezone.utc).isoformat(),
"ts_ms": now_ms,
"type": "flow",
"flow_id": "dev-flow",
"src_ip": "192.168.100.10",
"src_port": 51515,
"dest_ip": "203.0.113.10",
"dest_port": 443,
"proto": "TCP",
"app_proto": "tls",
"direction": "outbound",
"bytes": 8192,
"packets": 12,
"flow_state": "established",
})
def health() -> dict:
db = store.database_info()
storage = storage_info(cfg.db_path, cfg.eve_path)
rules = rule_manager.status()
return {
"status": "development",
"mode": "web-only-development",
"dev_mode": True,
"operational": True,
"started_at": started_at.isoformat(),
"uptime_seconds": round(time.monotonic() - started_monotonic, 1),
"suricata_running": False,
"suricata_pid": None,
"auto_block": False,
"routeros_configured": False,
"database": db,
"storage": storage,
"rules": rules,
"services": {
"web": {
"name": "Web UI / API",
"status": "up",
"details": f"Development server listening on {cfg.web_bind}:{cfg.web_port}",
},
"tzsp": {
"name": "TZSP receiver",
"status": "disabled",
"details": "Disabled in web-only development mode",
},
"tap": {
"name": "TAP interface",
"status": "disabled",
"details": f"{cfg.tap_name} is not created in development mode",
},
"suricata": {
"name": "Suricata IDS",
"status": "disabled",
"details": "Suricata is not started in web-only development mode",
},
"eve": {
"name": "EVE JSON watcher",
"status": "disabled",
"details": "EVE watcher is not started in web-only development mode",
},
"database": {
"name": "SQLite database",
"status": "up",
"details": f"{db['path']}; {db['rows']} incidents; WAL={db['journal_mode']}",
},
"traffic_history": {
"name": "Live traffic history",
"status": "up",
"details": "Bounded RAM history in web-only development mode",
},
"analytics_cache": {
"name": "Persistent dashboard summaries",
"status": "up",
"details": f"Redis snapshots for 15m/1h/6h/24h every {cfg.analytics_snapshot_interval_seconds}s",
},
"storage": {
"name": "Persistent storage",
"status": "up",
"details": f"{storage['path']}; {storage['used_percent']}% used",
},
"rules": {
"name": "Managed rules",
"status": "disabled",
"details": "Editors are visible, but Suricata validation/reload requires full mode",
},
"routeros": {
"name": "RouterOS REST integration",
"status": "disabled",
"details": "RouterOS integration and auto-blocking are disabled in development mode",
},
},
"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": "disabled",
},
{
"name": "RouterOS REST API",
"direction": "outbound",
"protocol": "TCP",
"address": routeros_host,
"port": routeros_port,
"status": "disabled",
},
],
"runtime": stats.snapshot(),
}
web = WebServer(
cfg,
store,
health,
stats=stats,
rule_manager=rule_manager,
traffic_history=traffic_history,
event_bus=event_bus,
live_pipeline=live_pipeline,
analytics_cache=analytics_cache,
)
def request_stop(_signum=None, _frame=None) -> None:
stop_event.set()
signal.signal(signal.SIGTERM, request_stop)
signal.signal(signal.SIGINT, request_stop)
live_pipeline.start()
analytics_cache.start()
web.start()
print(f"[dev] web-only mode active at http://{cfg.web_bind}:{cfg.web_port}", flush=True)
try:
while not stop_event.is_set():
time.sleep(0.25)
except KeyboardInterrupt:
stop_event.set()
finally:
try:
web.stop()
finally:
live_pipeline.stop()
analytics_cache.stop()
store.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())