"""Persistent storage for dashboard metrics, threat logs, and scan history.""" from __future__ import annotations import json import os import sqlite3 import threading from datetime import datetime, timedelta from typing import Any, Dict, List, Optional from ayn_antivirus.constants import ( DASHBOARD_MAX_THREATS_DISPLAY, DASHBOARD_METRIC_RETENTION_HOURS, DASHBOARD_SCAN_HISTORY_DAYS, DEFAULT_DASHBOARD_DB_PATH, ) class DashboardStore: """SQLite-backed store for all dashboard data. Parameters ---------- db_path: Path to the SQLite database file. Created automatically if it does not exist. """ def __init__(self, db_path: str = DEFAULT_DASHBOARD_DB_PATH) -> None: os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True) self.db_path = db_path self._lock = threading.RLock() self.conn = sqlite3.connect(db_path, check_same_thread=False) self.conn.row_factory = sqlite3.Row self.conn.execute("PRAGMA journal_mode=WAL") self.conn.execute("PRAGMA synchronous=NORMAL") self._create_tables() # ------------------------------------------------------------------ # Schema # ------------------------------------------------------------------ def _create_tables(self) -> None: self.conn.executescript(""" CREATE TABLE IF NOT EXISTS metrics ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL DEFAULT (datetime('now')), cpu_percent REAL DEFAULT 0, mem_percent REAL DEFAULT 0, mem_used INTEGER DEFAULT 0, mem_total INTEGER DEFAULT 0, disk_usage_json TEXT DEFAULT '[]', load_avg_json TEXT DEFAULT '[]', net_connections INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS threat_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL DEFAULT (datetime('now')), file_path TEXT, threat_name TEXT NOT NULL, threat_type TEXT NOT NULL, severity TEXT NOT NULL, detector TEXT, file_hash TEXT, action_taken TEXT DEFAULT 'detected', details TEXT ); CREATE TABLE IF NOT EXISTS scan_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL DEFAULT (datetime('now')), scan_type TEXT NOT NULL, scan_path TEXT, files_scanned INTEGER DEFAULT 0, files_skipped INTEGER DEFAULT 0, threats_found INTEGER DEFAULT 0, duration_seconds REAL DEFAULT 0, status TEXT DEFAULT 'completed' ); CREATE TABLE IF NOT EXISTS signature_updates ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL DEFAULT (datetime('now')), feed_name TEXT NOT NULL, hashes_added INTEGER DEFAULT 0, ips_added INTEGER DEFAULT 0, domains_added INTEGER DEFAULT 0, urls_added INTEGER DEFAULT 0, status TEXT DEFAULT 'success', details TEXT ); CREATE TABLE IF NOT EXISTS activity_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL DEFAULT (datetime('now')), level TEXT NOT NULL DEFAULT 'INFO', source TEXT, message TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_metrics_ts ON metrics(timestamp); CREATE INDEX IF NOT EXISTS idx_threats_ts ON threat_log(timestamp); CREATE INDEX IF NOT EXISTS idx_threats_severity ON threat_log(severity); CREATE INDEX IF NOT EXISTS idx_scans_ts ON scan_history(timestamp); CREATE INDEX IF NOT EXISTS idx_sigs_ts ON signature_updates(timestamp); CREATE INDEX IF NOT EXISTS idx_activity_ts ON activity_log(timestamp); """) self.conn.commit() # ------------------------------------------------------------------ # Metrics # ------------------------------------------------------------------ def record_metric( self, cpu: float, mem_pct: float, mem_used: int, mem_total: int, disk_usage: list, load_avg: list, net_conns: int, ) -> None: with self._lock: self.conn.execute( "INSERT INTO metrics " "(cpu_percent, mem_percent, mem_used, mem_total, " "disk_usage_json, load_avg_json, net_connections) " "VALUES (?,?,?,?,?,?,?)", (cpu, mem_pct, mem_used, mem_total, json.dumps(disk_usage), json.dumps(load_avg), net_conns), ) self.conn.commit() def get_latest_metrics(self) -> Optional[Dict[str, Any]]: with self._lock: row = self.conn.execute( "SELECT * FROM metrics ORDER BY id DESC LIMIT 1" ).fetchone() if not row: return None d = dict(row) d["disk_usage"] = json.loads(d.pop("disk_usage_json", "[]")) d["load_avg"] = json.loads(d.pop("load_avg_json", "[]")) return d def get_metrics_history(self, hours: int = 1) -> List[Dict[str, Any]]: cutoff = (datetime.utcnow() - timedelta(hours=hours)).strftime("%Y-%m-%d %H:%M:%S") with self._lock: rows = self.conn.execute( "SELECT * FROM metrics WHERE timestamp >= ? ORDER BY timestamp", (cutoff,), ).fetchall() result: List[Dict[str, Any]] = [] for r in rows: d = dict(r) d["disk_usage"] = json.loads(d.pop("disk_usage_json", "[]")) d["load_avg"] = json.loads(d.pop("load_avg_json", "[]")) result.append(d) return result # ------------------------------------------------------------------ # Threats # ------------------------------------------------------------------ def record_threat( self, file_path: str, threat_name: str, threat_type: str, severity: str, detector: str = "", file_hash: str = "", action: str = "detected", details: str = "", ) -> None: with self._lock: self.conn.execute( "INSERT INTO threat_log " "(file_path, threat_name, threat_type, severity, " "detector, file_hash, action_taken, details) " "VALUES (?,?,?,?,?,?,?,?)", (file_path, threat_name, threat_type, severity, detector, file_hash, action, details), ) self.conn.commit() def get_recent_threats( self, limit: int = DASHBOARD_MAX_THREATS_DISPLAY, ) -> List[Dict[str, Any]]: with self._lock: rows = self.conn.execute( "SELECT * FROM threat_log ORDER BY id DESC LIMIT ?", (limit,) ).fetchall() return [dict(r) for r in rows] def get_threat_stats(self) -> Dict[str, Any]: with self._lock: total = self.conn.execute( "SELECT COUNT(*) FROM threat_log" ).fetchone()[0] by_severity: Dict[str, int] = {} for row in self.conn.execute( "SELECT severity, COUNT(*) as cnt FROM threat_log GROUP BY severity" ): by_severity[row[0]] = row[1] cutoff_24h = (datetime.utcnow() - timedelta(hours=24)).strftime("%Y-%m-%d %H:%M:%S") cutoff_7d = (datetime.utcnow() - timedelta(days=7)).strftime("%Y-%m-%d %H:%M:%S") last_24h = self.conn.execute( "SELECT COUNT(*) FROM threat_log WHERE timestamp >= ?", (cutoff_24h,), ).fetchone()[0] last_7d = self.conn.execute( "SELECT COUNT(*) FROM threat_log WHERE timestamp >= ?", (cutoff_7d,), ).fetchone()[0] return { "total": total, "by_severity": by_severity, "last_24h": last_24h, "last_7d": last_7d, } # ------------------------------------------------------------------ # Scans # ------------------------------------------------------------------ def record_scan( self, scan_type: str, scan_path: str, files_scanned: int, files_skipped: int, threats_found: int, duration: float, status: str = "completed", ) -> None: with self._lock: self.conn.execute( "INSERT INTO scan_history " "(scan_type, scan_path, files_scanned, files_skipped, " "threats_found, duration_seconds, status) " "VALUES (?,?,?,?,?,?,?)", (scan_type, scan_path, files_scanned, files_skipped, threats_found, duration, status), ) self.conn.commit() def get_recent_scans(self, limit: int = 30) -> List[Dict[str, Any]]: with self._lock: rows = self.conn.execute( "SELECT * FROM scan_history ORDER BY id DESC LIMIT ?", (limit,) ).fetchall() return [dict(r) for r in rows] def get_scan_chart_data( self, days: int = DASHBOARD_SCAN_HISTORY_DAYS, ) -> List[Dict[str, Any]]: cutoff = (datetime.utcnow() - timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S") with self._lock: rows = self.conn.execute( "SELECT DATE(timestamp) as day, " "COUNT(*) as scans, " "SUM(threats_found) as threats, " "SUM(files_scanned) as files " "FROM scan_history WHERE timestamp >= ? " "GROUP BY DATE(timestamp) ORDER BY day", (cutoff,), ).fetchall() return [dict(r) for r in rows] # ------------------------------------------------------------------ # Signature Updates # ------------------------------------------------------------------ def record_sig_update( self, feed_name: str, hashes: int = 0, ips: int = 0, domains: int = 0, urls: int = 0, status: str = "success", details: str = "", ) -> None: with self._lock: self.conn.execute( "INSERT INTO signature_updates " "(feed_name, hashes_added, ips_added, domains_added, " "urls_added, status, details) " "VALUES (?,?,?,?,?,?,?)", (feed_name, hashes, ips, domains, urls, status, details), ) self.conn.commit() def get_recent_sig_updates(self, limit: int = 20) -> List[Dict[str, Any]]: with self._lock: rows = self.conn.execute( "SELECT * FROM signature_updates ORDER BY id DESC LIMIT ?", (limit,), ).fetchall() return [dict(r) for r in rows] def get_sig_stats(self) -> Dict[str, Any]: """Return signature stats from the actual signatures database.""" result = { "total_hashes": 0, "total_ips": 0, "total_domains": 0, "total_urls": 0, "last_update": None, } # Try to read live counts from the signatures DB sig_db_path = self.db_path.replace("dashboard.db", "signatures.db") try: import sqlite3 as _sql sdb = _sql.connect(sig_db_path) sdb.row_factory = _sql.Row for tbl, key in [("threats", "total_hashes"), ("ioc_ips", "total_ips"), ("ioc_domains", "total_domains"), ("ioc_urls", "total_urls")]: try: result[key] = sdb.execute(f"SELECT COUNT(*) FROM {tbl}").fetchone()[0] except Exception: pass try: ts = sdb.execute("SELECT MAX(added_date) FROM threats").fetchone()[0] result["last_update"] = ts except Exception: pass sdb.close() except Exception: # Fallback to dashboard update log with self._lock: row = self.conn.execute( "SELECT SUM(hashes_added), SUM(ips_added), " "SUM(domains_added), SUM(urls_added) FROM signature_updates" ).fetchone() result["total_hashes"] = row[0] or 0 result["total_ips"] = row[1] or 0 result["total_domains"] = row[2] or 0 result["total_urls"] = row[3] or 0 lu = self.conn.execute( "SELECT MAX(timestamp) FROM signature_updates" ).fetchone()[0] result["last_update"] = lu return result # ------------------------------------------------------------------ # Activity Log # ------------------------------------------------------------------ def log_activity( self, message: str, level: str = "INFO", source: str = "system", ) -> None: with self._lock: self.conn.execute( "INSERT INTO activity_log (level, source, message) VALUES (?,?,?)", (level, source, message), ) self.conn.commit() def get_recent_logs(self, limit: int = 20) -> List[Dict[str, Any]]: with self._lock: rows = self.conn.execute( "SELECT * FROM activity_log ORDER BY id DESC LIMIT ?", (limit,) ).fetchall() return [dict(r) for r in rows] # ------------------------------------------------------------------ # Cleanup # ------------------------------------------------------------------ def cleanup_old_metrics( self, hours: int = DASHBOARD_METRIC_RETENTION_HOURS, ) -> None: cutoff = (datetime.utcnow() - timedelta(hours=hours)).strftime("%Y-%m-%d %H:%M:%S") with self._lock: self.conn.execute("DELETE FROM metrics WHERE timestamp < ?", (cutoff,)) self.conn.commit() def close(self) -> None: self.conn.close()