Spaces:
Sleeping
Sleeping
| import json | |
| import sqlite3 | |
| from pathlib import Path | |
| from scanner.domain.models import ScanReport | |
| class MonitorStore: | |
| """SQLite-backed store for scan history, URL tracking, and alerts.""" | |
| def __init__(self, db_path: str | Path | None = None): | |
| self.db_path = Path(db_path or Path.home() / ".pis" / "monitor.db") | |
| self.db_path.parent.mkdir(parents=True, exist_ok=True) | |
| self._conn: sqlite3.Connection | None = None | |
| self._init_db() | |
| def conn(self) -> sqlite3.Connection: | |
| if self._conn is None: | |
| self._conn = sqlite3.connect(str(self.db_path)) | |
| self._conn.row_factory = sqlite3.Row | |
| return self._conn | |
| def _init_db(self): | |
| c = self.conn | |
| c.executescript(""" | |
| CREATE TABLE IF NOT EXISTS monitored_urls ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| url TEXT UNIQUE NOT NULL, | |
| label TEXT, | |
| interval_hours REAL NOT NULL DEFAULT 6, | |
| last_scan_at TEXT, | |
| last_risk_score INTEGER DEFAULT 0, | |
| last_risk_category TEXT DEFAULT 'none', | |
| highest_risk_score INTEGER DEFAULT 0, | |
| total_scans INTEGER DEFAULT 0, | |
| alert_on_change BOOLEAN DEFAULT 1, | |
| alert_webhook TEXT, | |
| alert_slack TEXT, | |
| alert_email TEXT, | |
| enabled BOOLEAN DEFAULT 1, | |
| created_at TEXT DEFAULT (datetime('now')), | |
| tags TEXT DEFAULT '[]' | |
| ); | |
| CREATE TABLE IF NOT EXISTS scan_history ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| url_id INTEGER NOT NULL, | |
| risk_score INTEGER NOT NULL, | |
| risk_category TEXT NOT NULL, | |
| total_findings INTEGER DEFAULT 0, | |
| findings_json TEXT DEFAULT '[]', | |
| scan_time_ms INTEGER DEFAULT 0, | |
| scanned_at TEXT DEFAULT (datetime('now')), | |
| diff_summary TEXT, | |
| FOREIGN KEY (url_id) REFERENCES monitored_urls(id) | |
| ); | |
| CREATE TABLE IF NOT EXISTS alerts ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| url_id INTEGER NOT NULL, | |
| alert_type TEXT NOT NULL, | |
| severity TEXT NOT NULL, | |
| message TEXT NOT NULL, | |
| delivered BOOLEAN DEFAULT 0, | |
| created_at TEXT DEFAULT (datetime('now')), | |
| delivered_at TEXT, | |
| FOREIGN KEY (url_id) REFERENCES monitored_urls(id) | |
| ); | |
| CREATE INDEX IF NOT EXISTS idx_scan_history_url ON scan_history(url_id, scanned_at DESC); | |
| CREATE INDEX IF NOT EXISTS idx_alerts_pending ON alerts(delivered, created_at); | |
| """) | |
| def add_url( | |
| self, url: str, interval_hours: float = 6, label: str = "", webhook: str = "", tags: list[str] | None = None | |
| ) -> int: | |
| c = self.conn | |
| c.execute( | |
| "INSERT OR IGNORE INTO monitored_urls (url, label, interval_hours, alert_webhook, tags) VALUES (?, ?, ?, ?, ?)", | |
| (url, label, interval_hours, webhook, json.dumps(tags or [])), | |
| ) | |
| self.conn.commit() | |
| row = c.execute("SELECT id FROM monitored_urls WHERE url = ?", (url,)).fetchone() | |
| return row["id"] if row else -1 | |
| def remove_url(self, url: str): | |
| self.conn.execute("DELETE FROM monitored_urls WHERE url = ?", (url,)) | |
| self.conn.commit() | |
| def get_urls(self, enabled_only: bool = True) -> list[dict]: | |
| q = "SELECT * FROM monitored_urls" | |
| if enabled_only: | |
| q += " WHERE enabled = 1" | |
| rows = self.conn.execute(q).fetchall() | |
| return [dict(r) for r in rows] | |
| def get_url(self, url_id: int) -> dict | None: | |
| row = self.conn.execute("SELECT * FROM monitored_urls WHERE id = ?", (url_id,)).fetchone() | |
| return dict(row) if row else None | |
| def record_scan(self, url_id: int, report: ScanReport) -> int: | |
| diff = "" | |
| prev = self.conn.execute( | |
| "SELECT risk_score, findings_json FROM scan_history WHERE url_id = ? ORDER BY scanned_at DESC LIMIT 1", | |
| (url_id,), | |
| ).fetchone() | |
| if prev: | |
| prev_score = prev["risk_score"] | |
| delta = report.risk_score - prev_score | |
| if delta != 0: | |
| diff = f"Score changed from {prev_score} to {report.risk_score} ({'+' if delta > 0 else ''}{delta})" | |
| cursor = self.conn.cursor() | |
| cursor.execute( | |
| """INSERT INTO scan_history (url_id, risk_score, risk_category, total_findings, | |
| findings_json, scan_time_ms, diff_summary) VALUES (?, ?, ?, ?, ?, ?, ?)""", | |
| ( | |
| url_id, | |
| report.risk_score, | |
| report.risk_category, | |
| report.total_findings, | |
| json.dumps([f.model_dump(mode="json") for f in report.findings]), | |
| report.scan_time_ms, | |
| diff, | |
| ), | |
| ) | |
| cursor.execute( | |
| """UPDATE monitored_urls SET last_scan_at = datetime('now'), | |
| last_risk_score = ?, last_risk_category = ?, | |
| total_scans = total_scans + 1, | |
| highest_risk_score = MAX(highest_risk_score, ?) | |
| WHERE id = ?""", | |
| (report.risk_score, report.risk_category, report.risk_score, url_id), | |
| ) | |
| self.conn.commit() | |
| return cursor.lastrowid or 0 | |
| def get_history(self, url_id: int, limit: int = 50) -> list[dict]: | |
| rows = self.conn.execute( | |
| "SELECT * FROM scan_history WHERE url_id = ? ORDER BY scanned_at DESC LIMIT ?", | |
| (url_id, limit), | |
| ).fetchall() | |
| return [dict(r) for r in rows] | |
| def get_recent_scans(self, hours: int = 24) -> list[dict]: | |
| rows = self.conn.execute( | |
| """SELECT u.url, u.label, h.risk_score, h.risk_category, h.total_findings, h.scanned_at | |
| FROM scan_history h JOIN monitored_urls u ON h.url_id = u.id | |
| WHERE h.scanned_at >= datetime('now', ?) | |
| ORDER BY h.scanned_at DESC""", | |
| (f"-{hours} hours",), | |
| ).fetchall() | |
| return [dict(r) for r in rows] | |
| def create_alert(self, url_id: int, alert_type: str, severity: str, message: str) -> int: | |
| cursor = self.conn.cursor() | |
| cursor.execute( | |
| "INSERT INTO alerts (url_id, alert_type, severity, message) VALUES (?, ?, ?, ?)", | |
| (url_id, alert_type, severity, message), | |
| ) | |
| self.conn.commit() | |
| return cursor.lastrowid or 0 | |
| def get_pending_alerts(self) -> list[dict]: | |
| rows = self.conn.execute( | |
| "SELECT a.*, u.url, u.alert_webhook, u.alert_slack FROM alerts a " | |
| "JOIN monitored_urls u ON a.url_id = u.id WHERE a.delivered = 0 ORDER BY a.created_at" | |
| ).fetchall() | |
| return [dict(r) for r in rows] | |
| def mark_alert_delivered(self, alert_id: int): | |
| self.conn.execute( | |
| "UPDATE alerts SET delivered = 1, delivered_at = datetime('now') WHERE id = ?", | |
| (alert_id,), | |
| ) | |
| self.conn.commit() | |
| def close(self): | |
| if self._conn: | |
| self._conn.close() | |
| self._conn = None | |