From 3ac7305954b81978ddd1982e940a01905df24fe5 Mon Sep 17 00:00:00 2001 From: Username Date: Thu, 25 Dec 2025 19:47:51 +0100 Subject: [PATCH] proxywatchd: persist MITM certificate stats across restarts Add save_state/load_state to MITMCertStats for JSON persistence. Stats saved periodically (5min) and at shutdown, loaded at startup. --- proxywatchd.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/proxywatchd.py b/proxywatchd.py index b3977fd..ba7b503 100644 --- a/proxywatchd.py +++ b/proxywatchd.py @@ -765,6 +765,44 @@ class MITMCertStats(object): 'recent': list(self.recent_certs[-20:]) } + def save_state(self, filepath): + """Save MITM stats to JSON file for persistence.""" + import json + with self.lock: + state = { + 'certs': self.certs, + 'by_org': self.by_org, + 'by_issuer': self.by_issuer, + 'by_proxy': self.by_proxy, + 'total_count': self.total_count, + 'recent_certs': self.recent_certs[-50:], + } + try: + with open(filepath, 'w') as f: + json.dump(state, f) + except Exception as e: + _log('failed to save MITM state: %s' % str(e), 'warn') + + def load_state(self, filepath): + """Load MITM stats from JSON file.""" + import json + try: + with open(filepath, 'r') as f: + state = json.load(f) + with self.lock: + self.certs = state.get('certs', {}) + self.by_org = state.get('by_org', {}) + self.by_issuer = state.get('by_issuer', {}) + self.by_proxy = state.get('by_proxy', {}) + self.total_count = state.get('total_count', 0) + self.recent_certs = state.get('recent_certs', []) + _log('restored MITM state: %d certs, %d detections' % ( + len(self.certs), self.total_count), 'info') + except IOError: + pass # File doesn't exist yet, start fresh + except Exception as e: + _log('failed to load MITM state: %s' % str(e), 'warn') + def extract_cert_info(cert_der): """Extract certificate information from DER-encoded certificate. @@ -1574,6 +1612,13 @@ class Proxywatchd(): except Exception as e: _log('failed to save final session state: %s' % str(e), 'warn') + # Save MITM certificate stats + try: + mitm_cert_stats.save_state(self.mitm_state_file) + _log('MITM cert state saved', 'watchd') + except Exception as e: + _log('failed to save MITM state: %s' % str(e), 'warn') + def _prep_db(self): self.mysqlite = mysqlite.mysqlite(config.watchd.database, str) def _close_db(self): @@ -2128,6 +2173,11 @@ class Proxywatchd(): self.stats.load_state(saved_state) self._close_db() + + # Load MITM certificate state (same directory as database) + db_dir = os.path.dirname(config.watchd.database) or '.' + self.mitm_state_file = os.path.join(db_dir, 'mitm_certs.json') + mitm_cert_stats.load_state(self.mitm_state_file) _log('database: %d total proxies, %d due for testing' % (total, due), 'watchd') # Initialize Tor connection pool @@ -2211,6 +2261,12 @@ class Proxywatchd(): except Exception as e: _log('failed to save session state: %s' % str(e), 'warn') + # Save MITM certificate stats periodically + try: + mitm_cert_stats.save_state(self.mitm_state_file) + except Exception as e: + _log('failed to save MITM state: %s' % str(e), 'warn') + # Hourly stats snapshot now = time.time() if not hasattr(self, '_last_snapshot'):