Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions watchdogs/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
54 changes: 50 additions & 4 deletions watchdogs/loot_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
<ssid>_<bssid>.pcap – real pcap (from start_handshake_serial)
<ssid>_<bssid>.hccapx – hashcat format (from start_handshake_serial)
<ssid>_<bssid>.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
"""
Expand All @@ -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.

Expand Down Expand Up @@ -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()
Expand All @@ -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()):
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand Down