()
| 5390 | } else if (x.type === 'handshake_harvest') { |
| 5391 | title = '🤝 Handshake capture (deauth-and-capture)'; |
| 5392 | body = `<div>Client <span class="font-mono">${_wifidefMacLink(x.station)}</span> was deauthed then completed a 4-way handshake${x.bssid ? ` with <span class="font-mono">${_wifidefMacLink(x.bssid)}</span>` : ''} — forced-reconnect capture.</div>`; |
| 5393 | } else if (x.type === 'pnl_leak') { |
| 5394 | title = '📢 PNL leak (saved-network list)'; |
| 5395 | const ss = (x.ssids || []); |
| 5396 | body = `<div>Client <span class="font-mono">${_wifidefMacLink(x.station)}</span> is broadcasting its saved networks — evil-twin/KARMA bait.</div>` |
| 5397 | + (ss.length ? `<div class="text-[11px] text-gray-400">${ss.slice(0, 8).map(_esc).join(', ')}${ss.length > 8 ? '…' : ''}</div>` : ''); |
| 5398 | } |
| 5399 | return `<div class="glass rounded-xl p-4 ${border}"><div class="font-semibold text-sm mb-1">${title}</div><div class="text-sm text-gray-300">${body}</div><div class="text-[11px] text-gray-500 mt-1">${x.detail || ''}${body.indexOf('wifiPivotFromDefense') >= 0 ? ' <span class="text-gray-600">· click a MAC to inspect it in Signal Intelligence</span>' : ''}</div></div>`; |
| 5400 | }).join(''); |
| 5401 | } |
| 5402 | // Counts |
| 5403 | const c = d.counts || {}; |
| 5404 | let chips = [['frames', d.frames], ['deauth', c.deauth], ['auth', c.auth], ['beacons', c.beacon], ['probe-req', c.probe_req], ['probe-resp', c.probe_resp]] |
| 5405 | .map(([k, v]) => `<span class="px-3 py-1 rounded bg-slate-800 border border-slate-700"><b>${v || 0}</b> <span class="text-gray-400">${k}</span></span>`).join(''); |
| 5406 | // Airspace density vs the flood threshold — lets the user calibrate. |
| 5407 | const a = d.airspace; |
| 5408 | if (a) { |
| 5409 | const near = a.ssids >= a.beacon_ssid_threshold; |
| 5410 | chips += `<span class="px-3 py-1 rounded bg-slate-800 border ${near ? 'border-red-600/60 text-red-300' : 'border-slate-700'}" title="Distinct SSIDs / BSSIDs heard this capture, vs the beacon-flood threshold. Raise the threshold above your normal density.">` |
| 5411 | + `<b>${a.ssids}</b> SSIDs · <b>${a.bssids}</b> BSSIDs <span class="text-gray-500">(flood ≥ ${a.beacon_ssid_threshold})</span></span>`; |
| 5412 | } |
| 5413 | document.getElementById('wifidef-counts').innerHTML = chips; |
| 5414 | // AP table |
| 5415 | document.getElementById('wifidef-ap-count').textContent = `(${(d.aps || []).length})`; |
| 5416 | const body = document.getElementById('wifidef-ap-tbody'); |
| 5417 | const trusted = null; |
| 5418 | if (!d.aps || !d.aps.length) body.innerHTML = '<tr><td colspan="5" class="py-4 text-center text-gray-500">No beacons captured (wrong channel? try “hop”).</td></tr>'; |
| 5419 | else body.innerHTML = d.aps.map(a => `<tr class="border-b border-slate-800/50 cursor-pointer hover:bg-slate-800/40" title="Open in Signal Intelligence" onclick="wifiPivotFromDefense('${a.bssid}','${encodeURIComponent(a.ssid || '').replace(/'/g, '%27')}', false)"> |
| 5420 | <td class="py-1 pr-2">${a.ssid || '<span class=\'text-gray-500 italic\'>hidden</span>'}</td> |
| 5421 | <td class="py-1 pr-2 font-mono text-[11px]">${a.bssid}</td> |
| 5422 | <td class="py-1 pr-2">${a.channel == null ? '—' : a.channel}</td> |
| 5423 | <td class="py-1 pr-2">${a.rssi == null ? '—' : a.rssi + ' dBm'}</td> |
| 5424 | <td class="py-1 pr-2">${a.beacons}</td></tr>`).join(''); |
| 5425 | } |
| 5426 | |
| 5427 | // ESP32 attack-tool correlation, folded inline into the WIDS scan: the fused |
| 5428 | // verdict is computed from the SAME capture the WIDS just rendered (no second |
| 5429 | // Wi-Fi scan) plus a cached BLE overlay + LAN inventory (+ opt-in SubGHz), and |
| 5430 | // shown above the detections. Backend: POST /api/wifidef/halehound. |
| 5431 | const _WIFIDEF_HH = { |
| 5432 | confirmed: ['CONFIRMED', 'bg-red-600 text-white'], |
| 5433 | likely: ['LIKELY', 'bg-red-500 text-white'], |
| 5434 | possible: ['POSSIBLE', 'bg-amber-500 text-slate-900'], |
| 5435 | trace: ['trace', 'bg-slate-600 text-slate-200'], |
| 5436 | none: ['clear', 'bg-emerald-600 text-white'], |
| 5437 | }; |
| 5438 | // Fold the ESP32 fusion into the main WIDS scan: reuse the just-captured WIDS |
| 5439 | // data (no second Wi-Fi capture), a cached BLE snapshot (refreshed on a slow |
| 5440 | // timer, not every scan — kind on a Pi Zero and on the continuous loop), and an |
| 5441 | // opt-in SubGHz sweep on manual scans only. Renders inline above the detections. |
| 5442 | async function _wifidefFusion(opts) { |
| 5443 | opts = opts || {}; |
| 5444 | const box = document.getElementById('wifidef-hh-inline'); |
| 5445 | const vb = document.getElementById('wifidef-hh-verdict'); |
| 5446 | const body = document.getElementById('wifidef-hh-body'); |
| 5447 | if (!box || !_wifidef.data) return; |
| 5448 | box.classList.remove('hidden'); |
no test coverage detected