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:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user