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']) @network_bp.route('/wifi/scan', methods=['POST'])
@login_required @login_required
def wifi_scan(): def wifi_scan():
"""Scan for nearby WiFi networks using iwlist or nmcli.""" """Scan for nearby WiFi networks using iw dev scan."""
results = [] results = []
try: try:
# Try nmcli first (most reliable) — nmcli works as normal user iface = _get_wireless_interface()
ok, out = _run('nmcli -t -f SSID,BSSID,CHAN,FREQ,SIGNAL,SECURITY,MODE dev wifi list --rescan yes', timeout=20) if not iface:
if ok and out.strip(): return jsonify({'ok': False, 'error': 'No wireless interface found'})
for line in out.strip().split('\n'):
parts = _parse_nmcli_line(line) r = _run_root(['iw', 'dev', iface, 'scan'], timeout=20)
if len(parts) >= 7: if r['ok']:
results.append({ results = _parse_iw_scan(r['stdout'])
'ssid': parts[0] or '(Hidden)',
'bssid': parts[1],
'channel': parts[2],
'frequency': parts[3],
'signal': parts[4],
'security': parts[5],
'mode': parts[6],
})
else: else:
# Fallback to iwlist (needs root) return jsonify({'ok': False, 'error': r.get('stderr', 'WiFi scan failed')})
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')})
except Exception as e: except Exception as e:
return jsonify({'ok': False, 'error': str(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. """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.""" Groups by SSID to show all APs broadcasting the same network name."""
try: try:
ok, out = _run('nmcli -t -f SSID,BSSID,CHAN,SIGNAL,SECURITY dev wifi list --rescan yes', timeout=20) iface = _get_wireless_interface()
networks = {} if not iface:
if ok and out.strip(): return jsonify({'ok': False, 'error': 'No wireless interface found'})
for line in out.strip().split('\n'):
parts = _parse_nmcli_line(line) r = _run_root(['iw', 'dev', iface, 'scan'], timeout=20)
if len(parts) >= 5: if not r['ok']:
ssid = parts[0] or '(Hidden)' return jsonify({'ok': False, 'error': r.get('stderr', 'WiFi scan failed')})
entry = {
'bssid': parts[1], scan_results = _parse_iw_scan(r['stdout'])
'channel': parts[2], networks = {}
'signal': parts[3], for net in scan_results:
'security': parts[4], ssid = net.get('ssid', '(Hidden)') or '(Hidden)'
} entry = {
if ssid not in networks: 'bssid': net.get('bssid', ''),
networks[ssid] = {'ssid': ssid, 'aps': [], 'security': entry['security']} 'channel': net.get('channel', ''),
networks[ssid]['aps'].append(entry) '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) 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), return jsonify({'ok': True, 'ssids': ssid_list, 'total_ssids': len(ssid_list),
'total_aps': sum(len(s['aps']) for s in ssid_list)}) 'total_aps': sum(len(s['aps']) for s in ssid_list)})
@@ -1123,37 +1111,48 @@ def _get_wireless_interface():
return None return None
def _parse_iwlist(output): def _parse_iw_scan(output):
"""Parse iwlist scanning output into structured data.""" """Parse 'iw dev <iface> scan' output into structured data."""
import re
networks = [] networks = []
current = {} current = None
for line in output.split('\n'): for line in output.split('\n'):
line = line.strip() line_s = line.strip()
if 'Cell' in line and 'Address:' in line: # New BSS block
m = re.match(r'^BSS\s+([\da-fA-F:]+)', line)
if m:
if current: if current:
networks.append(current) networks.append(current)
m = re.search(r'Address:\s*([\da-fA-F:]+)', line) current = {'bssid': m.group(1).upper(), 'ssid': '', 'channel': '',
current = {'bssid': m.group(1) if m else '', 'ssid': '', 'channel': '', 'signal': '', 'security': '', 'mode': ''} 'frequency': '', 'signal': '', 'security': 'Open', 'mode': ''}
elif 'ESSID:' in line: continue
m = re.search(r'ESSID:"(.+?)"', line) if current is None:
current['ssid'] = m.group(1) if m else '(Hidden)' continue
elif 'Channel:' in line: if line_s.startswith('SSID:'):
m = re.search(r'Channel:(\d+)', line) 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: if m:
current['channel'] = m.group(1) current['channel'] = m.group(1)
elif 'Signal level' in line: elif 'primary channel:' in line_s:
m = re.search(r'Signal level[=:](-?\d+)', line) m = re.search(r'primary channel:\s*(\d+)', line_s)
if m: if m and not current['channel']:
current['signal'] = m.group(1) current['channel'] = m.group(1)
elif 'Encryption key:' in line: elif 'RSN:' in line_s or 'WPA:' in line_s:
current['security'] = 'Open' if 'off' in line else 'Encrypted' if 'RSN:' in line_s:
elif 'WPA' in line or 'WPA2' in line: current['security'] = 'WPA2'
current['security'] = 'WPA2' if 'WPA2' in line else 'WPA' elif current['security'] == 'Open':
elif 'Mode:' in line: current['security'] = 'WPA'
m = re.search(r'Mode:(\S+)', line) elif 'BSS Load:' in line_s or 'capability:' in line_s:
if m: if 'ESS' in line_s:
current['mode'] = m.group(1) current['mode'] = 'Infra'
elif 'IBSS' in line_s:
current['mode'] = 'Ad-Hoc'
if current: if current:
networks.append(current) networks.append(current)
return networks 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="/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="/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="/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> </div>
</div> </div>