""" 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 ..constants import ( AUTH_EAP, AUTH_OPEN, AUTH_OWE, AUTH_PSK, AUTH_SAE, AUTH_UNKNOWN, CIPHER_CCMP, CIPHER_GCMP, CIPHER_TKIP, CIPHER_UNKNOWN, CIPHER_WEP, SECURITY_OPEN, SECURITY_WEP, SECURITY_WPA, SECURITY_WPA2, SECURITY_WPA3, SECURITY_WPA_WPA2, WIDTH_20_MHZ, WIDTH_40_MHZ, WIDTH_80_MHZ, WIDTH_160_MHZ, get_channel_from_frequency, ) from ..models import WiFiObservation logger = logging.getLogger(__name__) def parse_iw_scan(output: str) -> list[WiFiObservation]: """ Parse iw scan output. Args: output: Raw output from 'iw dev 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]) -> WiFiObservation | None: """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