from __future__ import annotations import json import os import sqlite3 import threading from datetime import datetime, timedelta, timezone from typing import Any class AlertStore: def __init__(self, path: str) -> None: self.path = path os.makedirs(os.path.dirname(path) or ".", exist_ok=True) self._lock = threading.Lock() self._conn = sqlite3.connect(path, check_same_thread=False) self._conn.row_factory = sqlite3.Row self._init_schema() def _init_schema(self) -> None: with self._lock: self._conn.executescript( """ PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; CREATE TABLE IF NOT EXISTS alerts ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, flow_id TEXT, src_ip TEXT, src_port INTEGER, dest_ip TEXT, dest_port INTEGER, proto TEXT, signature_id INTEGER, signature TEXT, category TEXT, severity INTEGER, action TEXT, blocked INTEGER NOT NULL DEFAULT 0, block_target TEXT, block_reason TEXT, raw_json TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_alerts_timestamp ON alerts(timestamp DESC); CREATE INDEX IF NOT EXISTS idx_alerts_signature_id ON alerts(signature_id); CREATE INDEX IF NOT EXISTS idx_alerts_blocked ON alerts(blocked); """ ) self._conn.commit() def insert_alert( self, event: dict[str, Any], blocked: bool, block_target: str | None, block_reason: str, ) -> int: alert = event.get("alert") or {} values = ( str(event.get("timestamp") or datetime.now(timezone.utc).isoformat()), str(event.get("flow_id") or ""), event.get("src_ip"), event.get("src_port"), event.get("dest_ip"), event.get("dest_port"), event.get("proto"), _as_int(alert.get("signature_id")), alert.get("signature"), alert.get("category"), _as_int(alert.get("severity")), alert.get("action"), 1 if blocked else 0, block_target, block_reason, json.dumps(event, ensure_ascii=False, separators=(",", ":")), ) with self._lock: cursor = self._conn.execute( """ INSERT INTO alerts ( timestamp, flow_id, src_ip, src_port, dest_ip, dest_port, proto, signature_id, signature, category, severity, action, blocked, block_target, block_reason, raw_json ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, values, ) self._conn.commit() return int(cursor.lastrowid) def recent(self, limit: int = 100) -> list[dict[str, Any]]: limit = min(max(int(limit), 1), 500) with self._lock: rows = self._conn.execute( """ SELECT id, timestamp, src_ip, src_port, dest_ip, dest_port, proto, signature_id, signature, category, severity, action, blocked, block_target, block_reason FROM alerts ORDER BY id DESC LIMIT ? """, (limit,), ).fetchall() result = [] for row in rows: item = dict(row) item["blocked"] = bool(item["blocked"]) result.append(item) return result def summary(self) -> dict[str, Any]: with self._lock: total = self._conn.execute("SELECT COUNT(*) FROM alerts").fetchone()[0] blocked = self._conn.execute("SELECT COUNT(*) FROM alerts WHERE blocked=1").fetchone()[0] sev = self._conn.execute( "SELECT severity, COUNT(*) AS count FROM alerts GROUP BY severity ORDER BY severity" ).fetchall() return { "total_alerts": int(total), "blocked_alerts": int(blocked), "by_severity": {str(row["severity"]): int(row["count"]) for row in sev}, } def purge_older_than(self, days: int) -> int: if days <= 0: return 0 cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat() with self._lock: cursor = self._conn.execute("DELETE FROM alerts WHERE timestamp < ?", (cutoff,)) self._conn.commit() return int(cursor.rowcount) def close(self) -> None: with self._lock: self._conn.close() def _as_int(value: Any) -> int | None: if value is None or value == "": return None try: return int(value) except (TypeError, ValueError): return None