mirror of
https://github.com/smittix/intercept.git
synced 2026-04-25 07:10:00 -07:00
Backend: - New utils/wifi/ package with models, scanner, parsers, channel analyzer - Quick Scan mode using system tools (nmcli, iw, iwlist, airport) - Deep Scan mode using airodump-ng with monitor mode - Hidden SSID correlation engine - Channel utilization analysis with recommendations - v2 API endpoints at /wifi/v2/* with SSE streaming - TSCM integration updated to use new scanner (backwards compatible) Frontend: - WiFi mode controller (wifi.js) with dual-mode support - Channel utilization chart component (channel-chart.js) - Updated wifi.html template with scan mode tabs and export Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
206 lines
5.9 KiB
Python
206 lines
5.9 KiB
Python
r"""
|
|
Parser for NetworkManager nmcli output.
|
|
|
|
Example output from 'nmcli -t -f BSSID,SSID,MODE,CHAN,FREQ,RATE,SIGNAL,SECURITY device wifi list':
|
|
00\:11\:22\:33\:44\:55:MyWiFi:Infra:6:2437 MHz:130 Mbit/s:75:WPA2
|
|
00\:11\:22\:33\:44\:66::Infra:11:2462 MHz:54 Mbit/s:60:WPA2
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from ..models import WiFiObservation
|
|
from ..constants import (
|
|
SECURITY_OPEN,
|
|
SECURITY_WEP,
|
|
SECURITY_WPA,
|
|
SECURITY_WPA2,
|
|
SECURITY_WPA3,
|
|
SECURITY_WPA_WPA2,
|
|
SECURITY_WPA2_WPA3,
|
|
SECURITY_ENTERPRISE,
|
|
SECURITY_UNKNOWN,
|
|
CIPHER_CCMP,
|
|
CIPHER_TKIP,
|
|
CIPHER_UNKNOWN,
|
|
AUTH_PSK,
|
|
AUTH_SAE,
|
|
AUTH_EAP,
|
|
AUTH_OPEN,
|
|
AUTH_UNKNOWN,
|
|
get_channel_from_frequency,
|
|
get_band_from_frequency,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def parse_nmcli_scan(output: str) -> list[WiFiObservation]:
|
|
"""
|
|
Parse nmcli terse output.
|
|
|
|
Args:
|
|
output: Raw output from nmcli with -t flag.
|
|
|
|
Returns:
|
|
List of WiFiObservation objects.
|
|
"""
|
|
observations = []
|
|
|
|
for line in output.strip().split('\n'):
|
|
if not line:
|
|
continue
|
|
|
|
obs = _parse_nmcli_line(line)
|
|
if obs:
|
|
observations.append(obs)
|
|
|
|
return observations
|
|
|
|
|
|
def _parse_nmcli_line(line: str) -> Optional[WiFiObservation]:
|
|
"""Parse a single line of nmcli terse output."""
|
|
try:
|
|
# nmcli terse format uses : as delimiter but escapes colons in values with \:
|
|
# Need to carefully split
|
|
parts = _split_nmcli_line(line)
|
|
|
|
if len(parts) < 8:
|
|
return None
|
|
|
|
# BSSID,SSID,MODE,CHAN,FREQ,RATE,SIGNAL,SECURITY
|
|
bssid = parts[0].upper()
|
|
ssid = parts[1] if parts[1] else None
|
|
# mode = parts[2] # 'Infra' or 'Ad-Hoc'
|
|
channel_str = parts[3]
|
|
freq_str = parts[4]
|
|
# rate_str = parts[5] # e.g., '130 Mbit/s'
|
|
signal_str = parts[6]
|
|
security_str = parts[7] if len(parts) > 7 else ''
|
|
|
|
# Parse channel
|
|
channel = int(channel_str) if channel_str.isdigit() else None
|
|
|
|
# Parse frequency (e.g., "2437 MHz")
|
|
freq_match = re.match(r'(\d+)', freq_str)
|
|
frequency_mhz = int(freq_match.group(1)) if freq_match else None
|
|
|
|
# If no channel, derive from frequency
|
|
if not channel and frequency_mhz:
|
|
channel = get_channel_from_frequency(frequency_mhz)
|
|
|
|
# Parse signal strength (nmcli gives percentage 0-100)
|
|
# Convert to approximate dBm: -100 + (signal * 0.5)
|
|
# More accurate: signal 100 = -30 dBm, signal 0 = -100 dBm
|
|
rssi = None
|
|
if signal_str.isdigit():
|
|
signal_pct = int(signal_str)
|
|
rssi = int(-100 + (signal_pct * 0.7)) # Rough conversion
|
|
|
|
# Parse security
|
|
security, cipher, auth = _parse_nmcli_security(security_str)
|
|
|
|
return WiFiObservation(
|
|
timestamp=datetime.now(),
|
|
bssid=bssid,
|
|
essid=ssid,
|
|
channel=channel,
|
|
frequency_mhz=frequency_mhz,
|
|
rssi=rssi,
|
|
security=security,
|
|
cipher=cipher,
|
|
auth=auth,
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Failed to parse nmcli line: {line!r} - {e}")
|
|
return None
|
|
|
|
|
|
def _split_nmcli_line(line: str) -> list[str]:
|
|
"""Split nmcli terse line handling escaped colons."""
|
|
parts = []
|
|
current = []
|
|
i = 0
|
|
|
|
while i < len(line):
|
|
if line[i] == '\\' and i + 1 < len(line) and line[i + 1] == ':':
|
|
# Escaped colon - add literal colon
|
|
current.append(':')
|
|
i += 2
|
|
elif line[i] == ':':
|
|
# Field delimiter
|
|
parts.append(''.join(current))
|
|
current = []
|
|
i += 1
|
|
else:
|
|
current.append(line[i])
|
|
i += 1
|
|
|
|
# Add last field
|
|
parts.append(''.join(current))
|
|
|
|
return parts
|
|
|
|
|
|
def _parse_nmcli_security(security_str: str) -> tuple[str, str, str]:
|
|
"""
|
|
Parse nmcli security string.
|
|
|
|
Examples:
|
|
'WPA2' -> (WPA2, CCMP, PSK)
|
|
'WPA1 WPA2' -> (WPA/WPA2, CCMP, PSK)
|
|
'WPA3' -> (WPA3, CCMP, SAE)
|
|
'802.1X' -> (Enterprise, CCMP, EAP)
|
|
'WEP' -> (WEP, WEP, OPEN)
|
|
'' or '--' -> (Open, None, Open)
|
|
"""
|
|
if not security_str or security_str == '--':
|
|
return SECURITY_OPEN, CIPHER_UNKNOWN, AUTH_OPEN
|
|
|
|
security_upper = security_str.upper()
|
|
|
|
# Determine security type
|
|
security = SECURITY_UNKNOWN
|
|
|
|
if '802.1X' in security_upper:
|
|
security = SECURITY_ENTERPRISE
|
|
elif 'WPA3' in security_upper:
|
|
if 'WPA2' in security_upper:
|
|
security = SECURITY_WPA2_WPA3
|
|
else:
|
|
security = SECURITY_WPA3
|
|
elif 'WPA2' in security_upper:
|
|
if 'WPA1' in security_upper or security_upper.count('WPA') > 1:
|
|
security = SECURITY_WPA_WPA2
|
|
else:
|
|
security = SECURITY_WPA2
|
|
elif 'WPA' in security_upper:
|
|
security = SECURITY_WPA
|
|
elif 'WEP' in security_upper:
|
|
security = SECURITY_WEP
|
|
|
|
# Determine cipher (assume CCMP for WPA2+)
|
|
cipher = CIPHER_UNKNOWN
|
|
if security in (SECURITY_WPA2, SECURITY_WPA3, SECURITY_WPA2_WPA3, SECURITY_ENTERPRISE):
|
|
cipher = CIPHER_CCMP
|
|
elif security == SECURITY_WPA or security == SECURITY_WPA_WPA2:
|
|
cipher = CIPHER_TKIP # Often TKIP for mixed mode
|
|
|
|
# Determine auth
|
|
auth = AUTH_UNKNOWN
|
|
if security == SECURITY_ENTERPRISE or '802.1X' in security_upper:
|
|
auth = AUTH_EAP
|
|
elif security == SECURITY_WPA3:
|
|
auth = AUTH_SAE
|
|
elif security in (SECURITY_WPA, SECURITY_WPA2, SECURITY_WPA_WPA2, SECURITY_WPA2_WPA3):
|
|
auth = AUTH_PSK
|
|
elif security == SECURITY_OPEN:
|
|
auth = AUTH_OPEN
|
|
|
|
return security, cipher, auth
|