worked poc
This commit is contained in:
+114
-12
@@ -4,18 +4,23 @@ import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .config import Config
|
||||
from .eve import EVEWatcher
|
||||
from .maintenance import storage_info
|
||||
from .policy import PolicyEngine
|
||||
from .routeros import RouterOSClient
|
||||
from .rules import RuleManager
|
||||
from .state import RuntimeStats
|
||||
from .store import AlertStore
|
||||
from .tap import TapDevice
|
||||
from .tuning import AlertTuner
|
||||
from .tzsp import TZSPReceiver
|
||||
from .webui import WebServer
|
||||
|
||||
@@ -27,6 +32,29 @@ def _routeros_target(cfg: Config) -> tuple[str, int]:
|
||||
return host, port
|
||||
|
||||
|
||||
def _ensure_suricata_state(cfg: Config) -> None:
|
||||
for path in (cfg.suricata_custom_rules, cfg.suricata_threshold_config):
|
||||
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(path).touch(exist_ok=True)
|
||||
|
||||
|
||||
def _suricata_common_args(cfg: Config, log_dir: str) -> list[str]:
|
||||
return [
|
||||
"-c",
|
||||
cfg.suricata_config,
|
||||
"-l",
|
||||
log_dir,
|
||||
# Suricata exposes one additive -s signature path; use its supported
|
||||
# globbing so all persisted local/custom .rules files are loaded.
|
||||
"-s",
|
||||
cfg.suricata_extra_rules_glob,
|
||||
"--set",
|
||||
f"vars.address-groups.HOME_NET={cfg.suricata_home_net}",
|
||||
"--set",
|
||||
f"threshold-file={cfg.suricata_threshold_config}",
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
cfg = Config.from_env()
|
||||
stop_event = threading.Event()
|
||||
@@ -36,8 +64,12 @@ def main() -> int:
|
||||
|
||||
os.makedirs(os.path.dirname(cfg.eve_path) or ".", exist_ok=True)
|
||||
os.makedirs(os.path.dirname(cfg.db_path) or ".", exist_ok=True)
|
||||
_ensure_suricata_state(cfg)
|
||||
|
||||
store = AlertStore(cfg.db_path)
|
||||
purged_tests = store.purge_builtin_test_incidents()
|
||||
if purged_tests:
|
||||
print(f"[db] removed {purged_tests} legacy pipeline-test incidents", flush=True)
|
||||
purged = store.purge_older_than(cfg.alert_retention_days)
|
||||
if purged:
|
||||
print(f"[db] purged {purged} old alerts", flush=True)
|
||||
@@ -53,21 +85,31 @@ def main() -> int:
|
||||
|
||||
print(f"[tap] {cfg.tap_name} is up, mtu={cfg.tap_mtu}", flush=True)
|
||||
|
||||
log_dir = os.path.dirname(cfg.eve_path) or "/var/log/suricata"
|
||||
suricata_cmd = [
|
||||
"suricata",
|
||||
"-c", cfg.suricata_config,
|
||||
*_suricata_common_args(cfg, log_dir),
|
||||
f"--af-packet={cfg.tap_name}",
|
||||
"-l", os.path.dirname(cfg.eve_path) or "/var/log/suricata",
|
||||
"--user", "suricata",
|
||||
"--group", "suricata",
|
||||
"--set", f"vars.address-groups.HOME_NET={cfg.suricata_home_net}",
|
||||
"--user",
|
||||
"suricata",
|
||||
"--group",
|
||||
"suricata",
|
||||
# Debian's default unix-command socket is directly under /var/run,
|
||||
# which is not writable after Suricata drops privileges.
|
||||
"--set",
|
||||
"unix-command.filename=suricata/suricata-command.socket",
|
||||
]
|
||||
|
||||
test_cmd = ["suricata", "-T", "-c", cfg.suricata_config, "--set", f"vars.address-groups.HOME_NET={cfg.suricata_home_net}"]
|
||||
print("[suricata] validating configuration", flush=True)
|
||||
test = subprocess.run(test_cmd, check=False)
|
||||
print("[suricata] validating configuration and managed rules", flush=True)
|
||||
with tempfile.TemporaryDirectory(prefix="suricata-config-test-") as test_log_dir:
|
||||
test_cmd = ["suricata", "-T", *_suricata_common_args(cfg, test_log_dir)]
|
||||
test = subprocess.run(test_cmd, check=False)
|
||||
if test.returncode != 0:
|
||||
print(f"[fatal] suricata configuration test failed with rc={test.returncode}", file=sys.stderr, flush=True)
|
||||
print(
|
||||
f"[fatal] suricata configuration test failed with rc={test.returncode}",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
tap.close()
|
||||
store.close()
|
||||
return test.returncode or 3
|
||||
@@ -77,6 +119,11 @@ def main() -> int:
|
||||
with open("/run/suricata.pid", "w", encoding="ascii") as pid_file:
|
||||
pid_file.write(str(suricata.pid))
|
||||
|
||||
tuner = AlertTuner(
|
||||
cfg.alert_max_severity,
|
||||
cfg.alert_ignore_sids,
|
||||
cfg.alert_ignore_categories,
|
||||
)
|
||||
policy = PolicyEngine(
|
||||
cfg.auto_block,
|
||||
cfg.auto_block_max_severity,
|
||||
@@ -93,7 +140,22 @@ def main() -> int:
|
||||
)
|
||||
|
||||
receiver = TZSPReceiver(cfg.tzsp_bind, cfg.tzsp_port, tap.write, stats, stop_event)
|
||||
watcher = EVEWatcher(cfg.eve_path, store, policy, routeros, cfg.block_timeout, stats, stop_event)
|
||||
watcher = EVEWatcher(
|
||||
cfg.eve_path,
|
||||
store,
|
||||
tuner,
|
||||
policy,
|
||||
routeros,
|
||||
cfg.block_timeout,
|
||||
cfg.alert_dedup_window_seconds,
|
||||
stats,
|
||||
stop_event,
|
||||
)
|
||||
rule_manager = RuleManager(
|
||||
cfg,
|
||||
pid_provider=lambda: suricata.pid if suricata.poll() is None else None,
|
||||
suricata_available=True,
|
||||
)
|
||||
routeros_host, routeros_port = _routeros_target(cfg)
|
||||
|
||||
def health() -> dict:
|
||||
@@ -102,7 +164,10 @@ def main() -> int:
|
||||
tap_up = tap.fd is not None and os.path.exists(f"/sys/class/net/{cfg.tap_name}")
|
||||
eve_up = watcher.is_alive()
|
||||
routeros_status = "configured" if routeros.configured else "disabled"
|
||||
core_up = suricata_up and tzsp_up and tap_up and eve_up
|
||||
db = store.database_info()
|
||||
storage = storage_info(cfg.db_path, cfg.eve_path)
|
||||
rules = rule_manager.status()
|
||||
core_up = suricata_up and tzsp_up and tap_up and eve_up and db["ok"]
|
||||
routeros_required_ok = (not cfg.auto_block) or routeros.configured
|
||||
operational = core_up and routeros_required_ok
|
||||
|
||||
@@ -117,6 +182,9 @@ def main() -> int:
|
||||
"suricata_pid": suricata.pid,
|
||||
"auto_block": cfg.auto_block,
|
||||
"routeros_configured": routeros.configured,
|
||||
"database": db,
|
||||
"storage": storage,
|
||||
"rules": rules,
|
||||
"services": {
|
||||
"web": {
|
||||
"name": "Web UI / API",
|
||||
@@ -143,6 +211,21 @@ def main() -> int:
|
||||
"status": "up" if eve_up else "down",
|
||||
"details": cfg.eve_path,
|
||||
},
|
||||
"database": {
|
||||
"name": "SQLite database",
|
||||
"status": "up" if db["ok"] else "down",
|
||||
"details": f"{db['path']}; {db['rows']} incidents; WAL={db['journal_mode']}",
|
||||
},
|
||||
"storage": {
|
||||
"name": "Persistent storage",
|
||||
"status": "up" if storage["free_bytes"] > 0 else "down",
|
||||
"details": f"{storage['path']}; {storage['used_percent']}% used",
|
||||
},
|
||||
"rules": {
|
||||
"name": "Managed rules",
|
||||
"status": "up" if rules["available"] else "disabled",
|
||||
"details": f"{rules.get('builtin_rule_count', 0)} built-in; {rules['custom_rule_count']} custom; {rules['threshold_entry_count']} threshold/suppress entries",
|
||||
},
|
||||
"routeros": {
|
||||
"name": "RouterOS REST integration",
|
||||
"status": routeros_status,
|
||||
@@ -178,7 +261,25 @@ def main() -> int:
|
||||
"runtime": stats.snapshot(),
|
||||
}
|
||||
|
||||
web = WebServer(cfg, store, health)
|
||||
web = WebServer(cfg, store, health, stats=stats, rule_manager=rule_manager)
|
||||
|
||||
def housekeeping() -> None:
|
||||
interval_seconds = max(0, cfg.rule_update_interval_hours) * 3600
|
||||
next_rule_update = time.monotonic() + interval_seconds if interval_seconds else None
|
||||
while not stop_event.wait(3600):
|
||||
try:
|
||||
removed = store.purge_older_than(cfg.alert_retention_days)
|
||||
if removed:
|
||||
print(f"[db] purged {removed} expired incidents", flush=True)
|
||||
except Exception as exc:
|
||||
print(f"[housekeeping] alert retention failed: {exc}", file=sys.stderr, flush=True)
|
||||
if next_rule_update is not None and time.monotonic() >= next_rule_update:
|
||||
result = rule_manager.update_vendor_rules()
|
||||
stream = sys.stdout if result.ok else sys.stderr
|
||||
print(f"[rules] scheduled update: {result.message}", file=stream, flush=True)
|
||||
next_rule_update = time.monotonic() + interval_seconds
|
||||
|
||||
housekeeping_thread = threading.Thread(target=housekeeping, name="housekeeping", daemon=True)
|
||||
|
||||
def request_stop(_signum=None, _frame=None):
|
||||
stop_event.set()
|
||||
@@ -188,6 +289,7 @@ def main() -> int:
|
||||
|
||||
receiver.start()
|
||||
watcher.start()
|
||||
housekeeping_thread.start()
|
||||
web.start()
|
||||
|
||||
rc = 0
|
||||
|
||||
Reference in New Issue
Block a user