145 lines
5.1 KiB
Python
145 lines
5.1 KiB
Python
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
|