diff --git a/watchdogs/app.py b/watchdogs/app.py index 4815b68..93e4c26 100644 --- a/watchdogs/app.py +++ b/watchdogs/app.py @@ -23,7 +23,7 @@ from .serial_manager import SerialManager, detect_esp32_port from .gps_manager import GpsManager -from .loot_manager import LootManager +from .loot_manager import LootManager, is_portal_form_line from .app_state import AppState, Network from .network_manager import NetworkManager from .coastline import COASTLINES @@ -3455,14 +3455,18 @@ def _handle_serial_line(self, line: str): # --- Portal / Evil Twin capture parsing --- if self.state.portal_running or self.state.evil_twin_running: - is_form = ("received post" in sl or "form submission" in sl) + is_form = is_portal_form_line(s) is_client = ("client connected" in sl or "client count" in sl) if is_form or is_client: tag = "EP" if self.state.portal_running else "ET" if self.state.portal_running and self.loot: - self.loot.save_portal_event(s) + self.loot.save_portal_activity(s) + if is_form: + self.loot.save_portal_event(s) if self.state.evil_twin_running and self.loot: - self.loot.save_evil_twin_event(s) + self.loot.save_evil_twin_activity(s) + if is_form: + self.loot.save_evil_twin_event(s) if is_form: # URL-decode captured data for display try: @@ -7443,6 +7447,8 @@ def _load_all_captures(self): raw_line = raw_line.strip() if not raw_line: continue + if not is_portal_form_line(raw_line): + continue decoded = unquote_plus(raw_line) fields = self._parse_post_fields(decoded) if fields: diff --git a/watchdogs/loot_manager.py b/watchdogs/loot_manager.py index 5596276..1fd7206 100644 --- a/watchdogs/loot_manager.py +++ b/watchdogs/loot_manager.py @@ -11,7 +11,9 @@ _.pcap – real pcap (from start_handshake_serial) _.hccapx – hashcat format (from start_handshake_serial) _.22000 – hc22000 hash (auto-generated from complete hccapx) + portal_events.log – portal client/form activity portal_passwords.log – portal form submissions + evil_twin_events.log – evil twin client/form activity evil_twin_capture.log – evil twin captured data attacks.log – attack start/stop events """ @@ -33,6 +35,21 @@ log = logging.getLogger(__name__) +LOOT_DB_VERSION = 2 + +_PORTAL_FORM_MARKERS = ( + "received post", + "form submission", + "received form data", +) + + +def is_portal_form_line(line: str) -> bool: + """Return True when an ESP32 portal/twin log line contains form data.""" + sl = line.lower() + return any(marker in sl for marker in _PORTAL_FORM_MARKERS) + + def _fsync_file(fh) -> None: """Flush Python buffer + OS page cache to physical disk. @@ -204,9 +221,12 @@ def _load_or_build_db(self) -> dict: try: with open(self._db_path, "r", encoding="utf-8") as fh: data = json.load(fh) - if isinstance(data, dict) and "version" in data and "sessions" in data: + if (isinstance(data, dict) + and data.get("version") == LOOT_DB_VERSION + and "sessions" in data): log.info("Loot DB loaded: %d sessions", len(data.get("sessions", {}))) return data + log.info("Loot DB version changed, rebuilding") except (json.JSONDecodeError, OSError) as exc: log.warning("Loot DB corrupted (%s), rebuilding", exc) return self._rebuild_db() @@ -217,7 +237,7 @@ def _rebuild_db(self) -> dict: Also generates .22000 files retroactively for any .hccapx that doesn't already have a corresponding .22000 file. """ - db: dict = {"version": 1, "sessions": {}, "totals": {}} + db: dict = {"version": LOOT_DB_VERSION, "sessions": {}, "totals": {}} if not self._base.is_dir(): return db for entry in sorted(self._base.iterdir()): @@ -265,13 +285,19 @@ def _scan_session_dir(self, session_path: Path) -> dict: pw_file = session_path / "portal_passwords.log" if pw_file.is_file(): try: - counts["passwords"] = sum(1 for _ in open(pw_file, encoding="utf-8")) + counts["passwords"] = sum( + 1 for line in open(pw_file, encoding="utf-8") + if is_portal_form_line(line) + ) except OSError: pass et_file = session_path / "evil_twin_capture.log" if et_file.is_file(): try: - counts["et_captures"] = sum(1 for _ in open(et_file, encoding="utf-8")) + counts["et_captures"] = sum( + 1 for line in open(et_file, encoding="utf-8") + if is_portal_form_line(line) + ) except OSError: pass mc_nodes_file = session_path / "meshcore_nodes.csv" @@ -1083,11 +1109,21 @@ def save_sniffer_probes(self, probes: List[ProbeEntry]) -> None: # Portal # ------------------------------------------------------------------ + def save_portal_activity(self, line: str) -> None: + """Append a portal activity line without affecting credential counts.""" + if not self._session_active: + return + filepath = self._session / "portal_events.log" + ts = datetime.now().strftime("%H:%M:%S") + _sync_append(filepath, f"[{ts}] {line}\n") + def save_portal_event(self, line: str) -> None: """Append a portal password/form submission line. fsync'd — this is captured-credential data we can't afford to lose on power failure.""" if not self._session_active: return + if not is_portal_form_line(line): + return filepath = self._session / "portal_passwords.log" ts = datetime.now().strftime("%H:%M:%S") _sync_append(filepath, f"[{ts}] {line}\n") @@ -1097,10 +1133,20 @@ def save_portal_event(self, line: str) -> None: # Evil Twin # ------------------------------------------------------------------ + def save_evil_twin_activity(self, line: str) -> None: + """Append an evil twin activity line without affecting capture counts.""" + if not self._session_active: + return + filepath = self._session / "evil_twin_events.log" + ts = datetime.now().strftime("%H:%M:%S") + _sync_append(filepath, f"[{ts}] {line}\n") + def save_evil_twin_event(self, line: str) -> None: """Append an evil twin capture line. fsync'd — captured credentials.""" if not self._session_active: return + if not is_portal_form_line(line): + return filepath = self._session / "evil_twin_capture.log" ts = datetime.now().strftime("%H:%M:%S") _sync_append(filepath, f"[{ts}] {line}\n")