from __future__ import annotations from dataclasses import dataclass from typing import Any @dataclass(frozen=True) class TuningDecision: keep: bool reason: str class AlertTuner: """Small second-stage noise filter for the dashboard/incident database. It intentionally does not replace Suricata threshold.config. The latter is the right place for sensor-level suppressions and thresholds. This filter is a final guardrail so low-priority or explicitly ignored alerts do not flood SQLite and the UI. """ def __init__( self, max_severity: int, ignore_sids: str = "", ignore_categories: str = "", ) -> None: self.max_severity = max(0, int(max_severity)) self.ignore_sids = _parse_int_set(ignore_sids) self.ignore_categories = { value.strip().casefold() for value in (ignore_categories or "").split(",") if value.strip() } def evaluate(self, event: dict[str, Any]) -> TuningDecision: alert = event.get("alert") or {} sid = _as_int(alert.get("signature_id")) severity = _as_int(alert.get("severity")) category = str(alert.get("category") or "").strip() if sid is not None and sid in self.ignore_sids: return TuningDecision(False, "ignored_sid") if category and category.casefold() in self.ignore_categories: return TuningDecision(False, "ignored_category") if self.max_severity > 0: if severity is None: return TuningDecision(False, "invalid_severity") if severity > self.max_severity: return TuningDecision(False, "low_priority") return TuningDecision(True, "accepted") def _parse_int_set(value: str) -> set[int]: result: set[int] = set() for item in (value or "").split(","): item = item.strip() if not item: continue try: result.add(int(item)) except ValueError: continue return result def _as_int(value: Any) -> int | None: try: return int(value) except (TypeError, ValueError): return None