from __future__ import annotations import os from dataclasses import dataclass def _bool(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 _int(name: str, default: int) -> int: value = os.getenv(name) if value is None or not value.strip(): return default return int(value) def _choice(name: str, default: str, allowed: set[str]) -> str: value = (os.getenv(name, default) or default).strip().lower() return value if value in allowed else default def _float(name: str, default: float) -> float: value = os.getenv(name) if value is None or not value.strip(): return default return float(value) @dataclass(frozen=True) class Config: tzsp_bind: str tzsp_port: int tap_name: str tap_mtu: int suricata_config: str suricata_output_config: str suricata_home_net: str suricata_local_rules: str suricata_extra_rules_glob: str suricata_custom_rules: str suricata_threshold_config: str suricata_persist_lib_dir: str update_rules_on_start: bool rule_update_interval_hours: int web_bind: str web_port: int db_path: str eve_path: str suricata_log_max_mb: int forensic_pcap_mode: str forensic_pcap_window_seconds: int forensic_pcap_memory_mb: int forensic_pcap_max_files: int forensic_pcap_max_total_mb: int alert_retention_days: int alert_max_severity: int alert_dedup_window_seconds: int alert_ignore_sids: str alert_ignore_categories: str auto_block: bool auto_block_max_severity: int monitored_networks: str never_block: str block_timeout: str routeros_url: str routeros_user: str routeros_password: str routeros_verify_tls: bool routeros_address_list: str routeros_http_timeout: int admin_token: str admin_username: str admin_password: str session_hours: int session_cookie_secure: bool analytics_snapshot_interval_seconds: int redis_url: str redis_managed: bool redis_data_dir: str redis_port: int redis_maxmemory_mb: int redis_snapshot_seconds: int redis_aof: bool traffic_retention_hours: int traffic_max_events: int traffic_memory_events: int websocket_queue_size: int live_flow_update_seconds: float ndr_enabled: bool ndr_correlation_window_seconds: int behavior_min_observations: int ndr_auto_block: bool ndr_auto_block_risk: int routeros_inventory_interval_seconds: int notify_webhook_url: str notify_min_risk: int notify_timeout_seconds: int @classmethod def from_env(cls) -> "Config": return cls( tzsp_bind=os.getenv("TZSP_BIND", "0.0.0.0"), tzsp_port=_int("TZSP_PORT", 37008), tap_name=os.getenv("TAP_NAME", "suritap0"), tap_mtu=_int("TAP_MTU", 9000), suricata_config=os.getenv("SURICATA_CONFIG", "/etc/suricata/suricata.yaml"), suricata_output_config=os.getenv( "SURICATA_OUTPUT_CONFIG", "/opt/ids/suricata/ids-output.yaml" ), suricata_home_net=os.getenv( "SURICATA_HOME_NET", "[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]", ), suricata_local_rules=os.getenv( "SURICATA_LOCAL_RULES", "/data/suricata/local.rules" ), suricata_extra_rules_glob=os.getenv( "SURICATA_EXTRA_RULES_GLOB", "/data/suricata/*.rules" ), suricata_custom_rules=os.getenv( "SURICATA_CUSTOM_RULES", "/data/suricata/custom.rules" ), suricata_threshold_config=os.getenv( "SURICATA_THRESHOLD_CONFIG", "/data/suricata/threshold.config" ), suricata_persist_lib_dir=os.getenv( "SURICATA_PERSIST_LIB_DIR", "/data/lib/suricata" ), update_rules_on_start=_bool("UPDATE_RULES_ON_START", False), rule_update_interval_hours=max(0, _int("RULE_UPDATE_INTERVAL_HOURS", 24)), web_bind=os.getenv("WEB_BIND", "0.0.0.0"), web_port=_int("WEB_PORT", 8080), db_path=os.getenv("DB_PATH", "/data/ids.db"), eve_path=os.getenv("EVE_PATH", "/data/logs/suricata/eve.json"), suricata_log_max_mb=_int("SURICATA_LOG_MAX_MB", 512), forensic_pcap_mode=_choice("FORENSIC_PCAP_MODE", "blocks", {"blocks", "alerts", "all", "off"}), forensic_pcap_window_seconds=max(5, _int("FORENSIC_PCAP_WINDOW_SECONDS", 60)), forensic_pcap_memory_mb=max(1, _int("FORENSIC_PCAP_MEMORY_MB", 64)), forensic_pcap_max_files=max(1, _int("FORENSIC_PCAP_MAX_FILES", 32)), forensic_pcap_max_total_mb=max(1, _int("FORENSIC_PCAP_MAX_TOTAL_MB", 512)), alert_retention_days=_int("ALERT_RETENTION_DAYS", 14), # Suricata severity uses 1 as the most important value. Keeping # 1-2 by default removes low-priority informational noise from the # incident database while raw EVE remains available on disk. alert_max_severity=_int("ALERT_MAX_SEVERITY", 2), alert_dedup_window_seconds=_int("ALERT_DEDUP_WINDOW_SECONDS", 300), alert_ignore_sids=os.getenv("ALERT_IGNORE_SIDS", "1000001"), alert_ignore_categories=os.getenv("ALERT_IGNORE_CATEGORIES", ""), auto_block=_bool("AUTO_BLOCK", False), auto_block_max_severity=_int("AUTO_BLOCK_MAX_SEVERITY", 1), monitored_networks=os.getenv("MONITORED_NETWORKS", "192.168.0.0/16,10.0.0.0/8,172.16.0.0/12"), never_block=os.getenv("NEVER_BLOCK", ""), block_timeout=os.getenv("BLOCK_TIMEOUT", "1h"), routeros_url=os.getenv("ROUTEROS_URL", "https://172.31.255.1").rstrip("/"), routeros_user=os.getenv("ROUTEROS_USER", "suricata-api"), routeros_password=os.getenv("ROUTEROS_PASSWORD", "CHANGE_ME"), routeros_verify_tls=_bool("ROUTEROS_VERIFY_TLS", False), routeros_address_list=os.getenv("ROUTEROS_ADDRESS_LIST", "IDS-BLOCK"), routeros_http_timeout=_int("ROUTEROS_HTTP_TIMEOUT", 5), admin_token=os.getenv("ADMIN_TOKEN", ""), admin_username=os.getenv("ADMIN_USERNAME", "admin").strip() or "admin", admin_password=os.getenv("ADMIN_PASSWORD", ""), session_hours=max(1, _int("SESSION_HOURS", 168)), session_cookie_secure=_bool("SESSION_COOKIE_SECURE", False), analytics_snapshot_interval_seconds=max( 15, _int("ANALYTICS_SNAPSHOT_INTERVAL_SECONDS", 60) ), redis_url=os.getenv("REDIS_URL", "redis://127.0.0.1:6379/0"), redis_managed=_bool("REDIS_MANAGED", True), redis_data_dir=os.getenv("REDIS_DATA_DIR", "/data/redis"), redis_port=_int("REDIS_PORT", 6379), # Managed Redis is the sole traffic-history store. Do not evict by # count/memory; retention time is the authoritative bound. redis_maxmemory_mb=0, redis_snapshot_seconds=_int("REDIS_SNAPSHOT_SECONDS", 1800), redis_aof=_bool("REDIS_AOF", True), traffic_retention_hours=_int("TRAFFIC_RETENTION_HOURS", 24), traffic_max_events=0, traffic_memory_events=0, websocket_queue_size=_int("WEBSOCKET_QUEUE_SIZE", 512), live_flow_update_seconds=_float("LIVE_FLOW_UPDATE_SECONDS", 2.0), ndr_enabled=_bool("NDR_ENABLED", True), ndr_correlation_window_seconds=max(300, _int("NDR_CORRELATION_WINDOW_SECONDS", 1800)), behavior_min_observations=max(10, _int("BEHAVIOR_MIN_OBSERVATIONS", 50)), ndr_auto_block=_bool("NDR_AUTO_BLOCK", False), ndr_auto_block_risk=max(70, min(100, _int("NDR_AUTO_BLOCK_RISK", 92))), routeros_inventory_interval_seconds=max(60, _int("ROUTEROS_INVENTORY_INTERVAL_SECONDS", 300)), notify_webhook_url=os.getenv("NOTIFY_WEBHOOK_URL", "").strip(), notify_min_risk=max(1, min(100, _int("NOTIFY_MIN_RISK", 80))), notify_timeout_seconds=max(1, min(30, _int("NOTIFY_TIMEOUT_SECONDS", 5))), ) def public_dict(self) -> dict: return { "tzsp_bind": self.tzsp_bind, "tzsp_port": self.tzsp_port, "tap_name": self.tap_name, "tap_mtu": self.tap_mtu, "suricata_home_net": self.suricata_home_net, "suricata_extra_rules_glob": self.suricata_extra_rules_glob, "web_port": self.web_port, "rule_update_interval_hours": self.rule_update_interval_hours, "alert_retention_days": self.alert_retention_days, "suricata_log_max_mb": self.suricata_log_max_mb, "forensic_pcap_mode": self.forensic_pcap_mode, "forensic_pcap_window_seconds": self.forensic_pcap_window_seconds, "forensic_pcap_max_files": self.forensic_pcap_max_files, "forensic_pcap_max_total_mb": self.forensic_pcap_max_total_mb, "alert_max_severity": self.alert_max_severity, "alert_dedup_window_seconds": self.alert_dedup_window_seconds, "alert_ignore_sids": self.alert_ignore_sids, "alert_ignore_categories": self.alert_ignore_categories, "auto_block": self.auto_block, "auto_block_max_severity": self.auto_block_max_severity, "monitored_networks": self.monitored_networks, "never_block": self.never_block, "block_timeout": self.block_timeout, "routeros_url": self.routeros_url, "routeros_user": self.routeros_user, "routeros_verify_tls": self.routeros_verify_tls, "routeros_address_list": self.routeros_address_list, "auth_enabled": bool(self.admin_password or self.admin_token), "admin_username": self.admin_username, "session_hours": self.session_hours, "analytics_snapshot_interval_seconds": self.analytics_snapshot_interval_seconds, "traffic_retention_hours": self.traffic_retention_hours, "redis_managed": self.redis_managed, "redis_snapshot_seconds": self.redis_snapshot_seconds, "redis_aof": self.redis_aof, "traffic_max_events": self.traffic_max_events, "traffic_memory_events": self.traffic_memory_events, "live_flow_update_seconds": self.live_flow_update_seconds, "ndr_enabled": self.ndr_enabled, "ndr_correlation_window_seconds": self.ndr_correlation_window_seconds, "behavior_min_observations": self.behavior_min_observations, "ndr_auto_block": self.ndr_auto_block, "ndr_auto_block_risk": self.ndr_auto_block_risk, "routeros_inventory_interval_seconds": self.routeros_inventory_interval_seconds, "notify_webhook_enabled": bool(self.notify_webhook_url), "notify_min_risk": self.notify_min_risk, }