from __future__ import annotations import os import signal import threading import time from datetime import datetime, timezone from pathlib import Path 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 .metrics import PrometheusMetrics 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, "redis": { "managed": False, "available": False, "running": False, "ready": False, "pid": None, "port": cfg.redis_port, "restarts": 0, "data_dir": cfg.redis_data_dir, "maxmemory_mb": 0, "snapshot_seconds": cfg.redis_snapshot_seconds, "aof": cfg.redis_aof, "persistence": "disabled in web-only development mode", "last_error": "", }, "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", }, "redis": { "name": "Managed Redis", "status": "disabled", "details": "Redis is disabled in web-only development 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(), } version_path = Path(__file__).resolve().parents[1] / "VERSION" try: app_version = version_path.read_text(encoding="ascii").strip() or "unknown" except OSError: app_version = "unknown" prometheus_metrics = PrometheusMetrics( stats, version=app_version, mode="web-only-development", started_at=started_at, state_provider=lambda: { "components": {"web": True}, "features": { "auto_block": False, "ndr": False, "notifications": False, "routeros": False, "redis_managed": False, "forensic_pcap": False, }, }, event_bus=event_bus, live_pipeline=live_pipeline, ) 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, metrics_provider=prometheus_metrics.render, healthcheck_provider=lambda: {"status": "ok" if store.ping() else "degraded", "operational": store.ping()}, ) 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())