Lightweight wrapper around the Pushover HTTP API.
| 14 | |
| 15 | |
| 16 | class PushoverService: |
| 17 | """Lightweight wrapper around the Pushover HTTP API.""" |
| 18 | |
| 19 | def __init__(self, shared_data): |
| 20 | self.shared_data = shared_data |
| 21 | self._lock = threading.Lock() |
| 22 | # Tracks already-notified items to avoid spamming |
| 23 | self._notified_devices = set() # set of IPs ever notified (persisted across restarts via DB load) |
| 24 | self._offline_devices = set() # IPs that went offline this session (for back-online detection) |
| 25 | self._last_notified_vuln_count = 0 # last count we sent a vuln alert for |
| 26 | self._notified_creds = 0 # last known cred count |
| 27 | self._last_send_ts = 0.0 # rate-limit: min 2 s between sends |
| 28 | self._startup_ts = time.time() # suppress device notifications shortly after restart |
| 29 | self._startup_grace_s = 90 # seconds to wait before sending device alerts |
| 30 | self._load_known_state_from_db() |
| 31 | |
| 32 | # ------------------------------------------------------------------ |
| 33 | # DB state loader — prevents restart-triggered false notifications |
| 34 | # ------------------------------------------------------------------ |
| 35 | |
| 36 | def _load_known_state_from_db(self): |
| 37 | """Pre-populate _notified_devices and _last_notified_vuln_count from the DB |
| 38 | so that a restart does not re-notify about already-known devices/vulns.""" |
| 39 | try: |
| 40 | db = getattr(self.shared_data, 'db', None) |
| 41 | if db is None: |
| 42 | return |
| 43 | with db.get_connection() as conn: |
| 44 | cursor = conn.cursor() |
| 45 | # Load all known IPs |
| 46 | cursor.execute("SELECT ip FROM hosts WHERE ip IS NOT NULL AND ip != ''") |
| 47 | rows = cursor.fetchall() |
| 48 | with self._lock: |
| 49 | for row in rows: |
| 50 | # sqlite3.Row supports index access but has no .get() |
| 51 | ip = row[0] if row else '' |
| 52 | if ip: |
| 53 | self._notified_devices.add(ip) |
| 54 | # Load current vuln count as baseline (so we only alert on genuinely new ones) |
| 55 | cursor.execute( |
| 56 | "SELECT COUNT(*) FROM hosts " |
| 57 | "WHERE vulnerabilities IS NOT NULL AND vulnerabilities != '' AND vulnerabilities != 'None'" |
| 58 | ) |
| 59 | row = cursor.fetchone() |
| 60 | baseline = row[0] if row else 0 |
| 61 | with self._lock: |
| 62 | self._last_notified_vuln_count = baseline |
| 63 | # Load credential baseline count |
| 64 | try: |
| 65 | cursor.execute("SELECT COUNT(*) FROM hosts WHERE credentials IS NOT NULL AND credentials != '' AND credentials != 'None'") |
| 66 | cred_row = cursor.fetchone() |
| 67 | cred_baseline = cred_row[0] if cred_row else 0 |
| 68 | with self._lock: |
| 69 | self._notified_creds = cred_baseline |
| 70 | except Exception: |
| 71 | pass # credentials column may not exist |
| 72 | logger.debug( |
| 73 | f"Pushover: loaded {len(self._notified_devices)} known IPs, " |
no outgoing calls