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 .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) 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']}", }, "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) 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())