Files
intercept/utils/wifi/parsers/iw.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

234 lines
7.1 KiB
Python

"""
Parser for Linux iw scan output.
Example output from 'iw dev wlan0 scan':
BSS 00:11:22:33:44:55(on wlan0)
TSF: 12345678901234 usec (0d, 03:25:45)
freq: 2437
beacon interval: 100 TUs
capability: ESS Privacy ShortSlotTime (0x0411)
signal: -65.00 dBm
last seen: 100 ms ago
SSID: MyWiFi
Supported rates: 1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0
DS Parameter set: channel 6
RSN: * Version: 1
* Group cipher: CCMP
* Pairwise ciphers: CCMP
* Authentication suites: PSK
* Capabilities: 16-PTKSA-RC 1-GTKSA-RC (0x000c)
"""
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_UNKNOWN,
CIPHER_CCMP,
CIPHER_TKIP,
CIPHER_GCMP,
CIPHER_WEP,
CIPHER_UNKNOWN,
AUTH_PSK,
AUTH_SAE,
AUTH_EAP,
AUTH_OWE,
AUTH_OPEN,
AUTH_UNKNOWN,
WIDTH_20_MHZ,
WIDTH_40_MHZ,
WIDTH_80_MHZ,
WIDTH_160_MHZ,
get_channel_from_frequency,
)
logger = logging.getLogger(__name__)
def parse_iw_scan(output: str) -> list[WiFiObservation]:
"""
Parse iw scan output.
Args:
output: Raw output from 'iw dev <interface> scan'.
Returns:
List of WiFiObservation objects.
"""
observations = []
current_block = []
for line in output.split('\n'):
if line.startswith('BSS '):
# Start of new BSS entry
if current_block:
obs = _parse_iw_block(current_block)
if obs:
observations.append(obs)
current_block = [line]
elif current_block:
current_block.append(line)
# Parse last block
if current_block:
obs = _parse_iw_block(current_block)
if obs:
observations.append(obs)
return observations
def _parse_iw_block(lines: list[str]) -> Optional[WiFiObservation]:
"""Parse a single BSS block from iw output."""
try:
# First line: BSS 00:11:22:33:44:55(on wlan0) -- associated
first_line = lines[0]
bssid_match = re.match(r'BSS ([0-9a-fA-F:]{17})', first_line)
if not bssid_match:
return None
bssid = bssid_match.group(1).upper()
# Parse remaining fields
ssid = None
frequency_mhz = None
channel = None
rssi = None
width = WIDTH_20_MHZ
has_privacy = False
has_rsn = False
has_wpa = False
cipher = CIPHER_UNKNOWN
auth = AUTH_UNKNOWN
i = 1
while i < len(lines):
line = lines[i].strip()
if line.startswith('freq:'):
freq_match = re.search(r'freq:\s*(\d+)', line)
if freq_match:
frequency_mhz = int(freq_match.group(1))
channel = get_channel_from_frequency(frequency_mhz)
elif line.startswith('signal:'):
signal_match = re.search(r'signal:\s*(-?\d+\.?\d*)', line)
if signal_match:
rssi = int(float(signal_match.group(1)))
elif line.startswith('SSID:'):
ssid_match = re.match(r'SSID:\s*(.*)', line)
if ssid_match:
ssid = ssid_match.group(1).strip()
if not ssid or ssid == '\\x00' * len(ssid):
ssid = None
elif line.startswith('DS Parameter set:'):
chan_match = re.search(r'channel\s*(\d+)', line)
if chan_match:
channel = int(chan_match.group(1))
elif line.startswith('capability:'):
if 'Privacy' in line:
has_privacy = True
elif line.startswith('RSN:') or line.startswith('WPA:'):
is_rsn = line.startswith('RSN:')
if is_rsn:
has_rsn = True
else:
has_wpa = True
# Parse the RSN/WPA block
i += 1
while i < len(lines) and lines[i].startswith('\t\t'):
subline = lines[i].strip()
if 'Group cipher:' in subline or 'Pairwise ciphers:' in subline:
if 'CCMP' in subline:
cipher = CIPHER_CCMP
elif 'TKIP' in subline:
cipher = CIPHER_TKIP
elif 'GCMP' in subline:
cipher = CIPHER_GCMP
elif 'Authentication suites:' in subline:
if 'SAE' in subline:
auth = AUTH_SAE
elif 'PSK' in subline:
auth = AUTH_PSK
elif 'IEEE 802.1X' in subline or 'EAP' in subline:
auth = AUTH_EAP
elif 'OWE' in subline:
auth = AUTH_OWE
i += 1
continue
elif 'HT operation:' in line or 'VHT operation:' in line or 'HE operation:' in line:
# Parse width from subsequent lines
i += 1
while i < len(lines) and lines[i].startswith('\t\t'):
subline = lines[i].strip()
if 'channel width:' in subline.lower():
if '160' in subline:
width = WIDTH_160_MHZ
elif '80' in subline:
width = WIDTH_80_MHZ
elif '40' in subline:
width = WIDTH_40_MHZ
i += 1
continue
i += 1
# Determine security type
security = SECURITY_OPEN
if has_rsn and has_wpa:
security = SECURITY_WPA_WPA2
elif has_rsn:
if auth == AUTH_SAE:
security = SECURITY_WPA3
else:
security = SECURITY_WPA2
elif has_wpa:
security = SECURITY_WPA
elif has_privacy:
security = SECURITY_WEP
cipher = CIPHER_WEP
if auth == AUTH_UNKNOWN:
if security == SECURITY_OPEN:
auth = AUTH_OPEN
elif security in (SECURITY_WPA, SECURITY_WPA2, SECURITY_WPA_WPA2):
auth = AUTH_PSK
return WiFiObservation(
timestamp=datetime.now(),
bssid=bssid,
essid=ssid,
channel=channel,
frequency_mhz=frequency_mhz,
rssi=rssi,
security=security,
cipher=cipher,
auth=auth,
width=width,
)
except Exception as e:
logger.debug(f"Failed to parse iw block: {e}")
return None