from __future__ import annotations import os import signal import threading import time from datetime import datetime, timezone from urllib.parse import urlparse from .config import Config 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": 1000001, "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) def health() -> dict: 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, "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", }, "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) def request_stop(_signum=None, _frame=None) -> None: stop_event.set() signal.signal(signal.SIGTERM, request_stop) signal.signal(signal.SIGINT, request_stop) 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: store.close() return 0 if __name__ == "__main__": raise SystemExit(main())