first commit
This commit is contained in:
+144
@@ -0,0 +1,144 @@
|
||||
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
|
||||
Reference in New Issue
Block a user