Add WIFICANARY plugin - passive WiFi rogue-AP detection

Periodic iw-scan-based detection of the 6 heuristics that don't need
monitor-mode hardware (see issue #1789): pwnagotchi/Pineapple signatures,
evil-twin/open clones, baseline-AP-absent-with-clone, security downgrades,
and duplicate-SSID/different-vendor - all evaluated against a user-curated
trusted-AP baseline (WIFICANARY_trusted_aps). A detection creates a
flagged Devices entry even for BSSIDs that never associate, per the
addendum on the same issue.

- WIFICANARY_TRUSTED_SECURITY is multi-select: an observed encryption
  exactly matching any selected value is accepted; otherwise it's flagged
  if weaker than the strongest selected value (deliberate - comparing
  against the weakest would make multi-select pointless, since anything
  at/above the weakest would silently pass regardless of the rest of the
  selection).
- Added a "known device turned rogue" motor: escalate_known_devices()
  cross-references each detection's BSSID against the Devices table via
  the new DeviceInstance.getAllByMacs(). This covers the BSSID-identity
  half of the issue #1789 addendum's motor 10; the deauth/probe-source-MAC
  half still needs monitor-mode data this plugin doesn't have.
- Vendor is deliberately not looked up by this plugin - any device it
  creates gets devVendor filled in for free by core's own vendor_update
  plugin on its next pass.

43 wificanary unit tests + 10 DeviceInstance.getAllByMacs() tests, all
test_plugin_conventions.py checks pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011meLPKCzVpdZyAUfv5U6mm
This commit is contained in:
Mauricio CamayoandClaude Sonnet 5 committed 2026-09-23 15:56:47 -05:00
1 parent cd1d0ed11e
commit d0a3a5416b
7 files changed
+2129 -1

No files matched your search

+13
View File
@@ -104,6 +104,19 @@ class DeviceInstance:
SELECT * FROM Devices WHERE devMac = ?
""", (mac,))
def getAllByMacs(self, macs):
"""Return every Devices row whose devMac is in `macs`, as a dict keyed
by lowercased devMac - one query for a batch of MACs instead of one
`getByMac()` call per MAC, for a caller that needs to cross-reference
several MACs against known devices in a single pass (e.g. WIFICANARY's
known-device-turned-rogue check)."""
macs = [m for m in dict.fromkeys(macs) if m]
if not macs:
return {}
placeholders = ",".join("?" for _ in macs)
rows = self._fetchall(f"SELECT * FROM Devices WHERE devMac IN ({placeholders})", tuple(macs))
return {row["devMac"].lower(): row for row in rows}
def exists(self, devGUID):
row = self._fetchone("""
SELECT COUNT(*) as count FROM Devices WHERE devGUID = ?