Files
intercept/utils/wifi/parsers/nmcli.py
Smittix 9515f5fd7a Add unified WiFi scanning module with dual-mode architecture
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>
2026-01-21 22:06:16 +00:00

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