diff --git a/web/routes/network.py b/web/routes/network.py index c973201..e50985c 100644 --- a/web/routes/network.py +++ b/web/routes/network.py @@ -1005,34 +1005,18 @@ def arp_spoof_fix(): @network_bp.route('/wifi/scan', methods=['POST']) @login_required def wifi_scan(): - """Scan for nearby WiFi networks using iwlist or nmcli.""" + """Scan for nearby WiFi networks using iw dev scan.""" results = [] try: - # Try nmcli first (most reliable) — nmcli works as normal user - ok, out = _run('nmcli -t -f SSID,BSSID,CHAN,FREQ,SIGNAL,SECURITY,MODE dev wifi list --rescan yes', timeout=20) - if ok and out.strip(): - for line in out.strip().split('\n'): - parts = _parse_nmcli_line(line) - if len(parts) >= 7: - results.append({ - 'ssid': parts[0] or '(Hidden)', - 'bssid': parts[1], - 'channel': parts[2], - 'frequency': parts[3], - 'signal': parts[4], - 'security': parts[5], - 'mode': parts[6], - }) + iface = _get_wireless_interface() + if not iface: + return jsonify({'ok': False, 'error': 'No wireless interface found'}) + + r = _run_root(['iw', 'dev', iface, 'scan'], timeout=20) + if r['ok']: + results = _parse_iw_scan(r['stdout']) else: - # Fallback to iwlist (needs root) - iface = _get_wireless_interface() - if not iface: - return jsonify({'ok': False, 'error': 'No wireless interface found'}) - r = _run_root(['iwlist', iface, 'scanning'], timeout=20) - if r['ok']: - results = _parse_iwlist(r['stdout']) - else: - return jsonify({'ok': False, 'error': r.get('stderr', 'WiFi scan failed')}) + return jsonify({'ok': False, 'error': r.get('stderr', 'WiFi scan failed')}) except Exception as e: return jsonify({'ok': False, 'error': str(e)}) @@ -1045,24 +1029,28 @@ def ssid_map(): """Build an SSID map showing all access points, their BSSIDs, channels, and signal strength. Groups by SSID to show all APs broadcasting the same network name.""" try: - ok, out = _run('nmcli -t -f SSID,BSSID,CHAN,SIGNAL,SECURITY dev wifi list --rescan yes', timeout=20) - networks = {} - if ok and out.strip(): - for line in out.strip().split('\n'): - parts = _parse_nmcli_line(line) - if len(parts) >= 5: - ssid = parts[0] or '(Hidden)' - entry = { - 'bssid': parts[1], - 'channel': parts[2], - 'signal': parts[3], - 'security': parts[4], - } - if ssid not in networks: - networks[ssid] = {'ssid': ssid, 'aps': [], 'security': entry['security']} - networks[ssid]['aps'].append(entry) + iface = _get_wireless_interface() + if not iface: + return jsonify({'ok': False, 'error': 'No wireless interface found'}) + + r = _run_root(['iw', 'dev', iface, 'scan'], timeout=20) + if not r['ok']: + return jsonify({'ok': False, 'error': r.get('stderr', 'WiFi scan failed')}) + + scan_results = _parse_iw_scan(r['stdout']) + networks = {} + for net in scan_results: + ssid = net.get('ssid', '(Hidden)') or '(Hidden)' + entry = { + 'bssid': net.get('bssid', ''), + 'channel': net.get('channel', ''), + 'signal': net.get('signal', ''), + 'security': net.get('security', ''), + } + if ssid not in networks: + networks[ssid] = {'ssid': ssid, 'aps': [], 'security': entry['security']} + networks[ssid]['aps'].append(entry) - # Sort by signal strength (strongest first) ssid_list = sorted(networks.values(), key=lambda x: max(int(a.get('signal', '0') or '0') for a in x['aps']), reverse=True) return jsonify({'ok': True, 'ssids': ssid_list, 'total_ssids': len(ssid_list), 'total_aps': sum(len(s['aps']) for s in ssid_list)}) @@ -1123,37 +1111,48 @@ def _get_wireless_interface(): return None -def _parse_iwlist(output): - """Parse iwlist scanning output into structured data.""" - import re +def _parse_iw_scan(output): + """Parse 'iw dev scan' output into structured data.""" networks = [] - current = {} + current = None for line in output.split('\n'): - line = line.strip() - if 'Cell' in line and 'Address:' in line: + line_s = line.strip() + # New BSS block + m = re.match(r'^BSS\s+([\da-fA-F:]+)', line) + if m: if current: networks.append(current) - m = re.search(r'Address:\s*([\da-fA-F:]+)', line) - current = {'bssid': m.group(1) if m else '', 'ssid': '', 'channel': '', 'signal': '', 'security': '', 'mode': ''} - elif 'ESSID:' in line: - m = re.search(r'ESSID:"(.+?)"', line) - current['ssid'] = m.group(1) if m else '(Hidden)' - elif 'Channel:' in line: - m = re.search(r'Channel:(\d+)', line) + current = {'bssid': m.group(1).upper(), 'ssid': '', 'channel': '', + 'frequency': '', 'signal': '', 'security': 'Open', 'mode': ''} + continue + if current is None: + continue + if line_s.startswith('SSID:'): + current['ssid'] = line_s[5:].strip() or '(Hidden)' + elif line_s.startswith('freq:'): + current['frequency'] = line_s[5:].strip() + elif line_s.startswith('signal:'): + m = re.search(r'(-?[\d.]+)', line_s) + if m: + current['signal'] = str(int(float(m.group(1)))) + elif line_s.startswith('DS Parameter set: channel'): + m = re.search(r'channel\s+(\d+)', line_s) if m: current['channel'] = m.group(1) - elif 'Signal level' in line: - m = re.search(r'Signal level[=:](-?\d+)', line) - if m: - current['signal'] = m.group(1) - elif 'Encryption key:' in line: - current['security'] = 'Open' if 'off' in line else 'Encrypted' - elif 'WPA' in line or 'WPA2' in line: - current['security'] = 'WPA2' if 'WPA2' in line else 'WPA' - elif 'Mode:' in line: - m = re.search(r'Mode:(\S+)', line) - if m: - current['mode'] = m.group(1) + elif 'primary channel:' in line_s: + m = re.search(r'primary channel:\s*(\d+)', line_s) + if m and not current['channel']: + current['channel'] = m.group(1) + elif 'RSN:' in line_s or 'WPA:' in line_s: + if 'RSN:' in line_s: + current['security'] = 'WPA2' + elif current['security'] == 'Open': + current['security'] = 'WPA' + elif 'BSS Load:' in line_s or 'capability:' in line_s: + if 'ESS' in line_s: + current['mode'] = 'Infra' + elif 'IBSS' in line_s: + current['mode'] = 'Ad-Hoc' if current: networks.append(current) return networks diff --git a/web/templates/network.html b/web/templates/network.html index 0785c72..b53c222 100644 --- a/web/templates/network.html +++ b/web/templates/network.html @@ -129,7 +129,7 @@ Deauth Attack Pineapple / Evil Twin MITM Proxy - WiFi Audit + WiFi Audit