Fix WiFi scanner to use iw instead of nmcli, fix WiFi Audit link

WiFi scanner was returning empty results because nmcli reports wlan0 as
unavailable. Switched to iw dev scan via daemon. Fixed WiFi Audit link
from /wifi-audit to /wifi/ to match blueprint url_prefix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
SsSnake
2026-03-24 07:41:36 -07:00
parent b200527e73
commit 98a643abf3
2 changed files with 67 additions and 68 deletions

View File

@@ -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 <iface> 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

View File

@@ -129,7 +129,7 @@
<a href="/deauth" class="btn btn-sm" style="text-align:center;text-decoration:none;border:1px solid var(--danger,#f55);color:var(--danger,#f55)">Deauth Attack</a>
<a href="/pineapple" class="btn btn-sm" style="text-align:center;text-decoration:none;border:1px solid var(--danger,#f55);color:var(--danger,#f55)">Pineapple / Evil Twin</a>
<a href="/mitm-proxy" class="btn btn-sm" style="text-align:center;text-decoration:none;border:1px solid var(--danger,#f55);color:var(--danger,#f55)">MITM Proxy</a>
<a href="/wifi-audit" class="btn btn-sm" style="text-align:center;text-decoration:none;border:1px solid var(--danger,#f55);color:var(--danger,#f55)">WiFi Audit</a>
<a href="/wifi/" class="btn btn-sm" style="text-align:center;text-decoration:none;border:1px solid var(--danger,#f55);color:var(--danger,#f55)">WiFi Audit</a>
</div>
</div>
</div>