(interfaces = [])
| 8919 | html += `<p class="text-xs uppercase text-gray-400 mt-4 mb-1">All MACs observed (${obs.length})</p>` + |
| 8920 | '<table class="min-w-full text-xs text-gray-300 whitespace-nowrap"><thead>' + |
| 8921 | '<tr class="text-left text-gray-500"><th class="px-2 py-1">MAC</th><th class="px-2 py-1">Type</th><th class="px-2 py-1">Vendor</th><th class="px-2 py-1">IP(s)</th></tr>' + |
| 8922 | '</thead><tbody>' + |
| 8923 | obs.map(c => { |
| 8924 | const [lbl, cls] = KLASS[c.klass] || [c.klass, 'text-gray-400']; |
| 8925 | const ips = (c.ips || []); |
| 8926 | return `<tr class="border-t border-slate-800"> |
| 8927 | <td class="px-2 py-1 font-mono ${c.klass === 'spoofed_vendor_oui' ? 'text-red-300' : ''}">${escapeHtml(c.mac)}</td> |
| 8928 | <td class="px-2 py-1 ${cls}">${escapeHtml(lbl)}</td> |
| 8929 | <td class="px-2 py-1 text-gray-400">${escapeHtml(c.vendor || '—')}</td> |
| 8930 | <td class="px-2 py-1 font-mono">${escapeHtml(ips.slice(0, 4).join(', ')) || '—'}${ips.length > 4 ? '…' : ''}</td> |
| 8931 | </tr>`; |
| 8932 | }).join('') + |
| 8933 | '</tbody></table>'; |
| 8934 | } |
| 8935 | // Reasons narrative |
| 8936 | if (d.reasons && d.reasons.length) { |
| 8937 | html += '<ul class="text-xs text-gray-400 mt-3 list-disc pl-5">' + |
| 8938 | d.reasons.map(x => '<li>' + escapeHtml(x) + '</li>').join('') + '</ul>'; |
| 8939 | } |
| 8940 | if (!d.oui_db) { |
| 8941 | html += '<p class="text-xs text-gray-600 mt-2">OUI vendor database not installed — spoof detection uses the built-in seed only. Install arp-scan for full vendor coverage.</p>'; |
| 8942 | } |
| 8943 | out.innerHTML = html; |
| 8944 | } catch (e) { |
| 8945 | out.innerHTML = '<p class="text-sm text-red-400">Failed: ' + escapeHtml(e.message) + '</p>'; |
| 8946 | } finally { |
| 8947 | _ndBusy(btn, false); |
| 8948 | } |
| 8949 | } |
| 8950 | async function macWatchReset() { |
| 8951 | try { |
| 8952 | await postAPI('/api/net/mac-watch-reset', {}); |
| 8953 | addConsoleMessage('MAC Watch history cleared', 'info'); |
| 8954 | const out = document.getElementById('macwatch-results'); |
| 8955 | if (out) { out.classList.remove('hidden'); out.innerHTML = '<p class="text-sm text-gray-400">History cleared — run a scan to rebuild it.</p>'; } |
| 8956 | } catch (e) { |
| 8957 | addConsoleMessage('Failed to reset MAC Watch history: ' + e.message, 'error'); |
| 8958 | } |
| 8959 | } |
| 8960 | function exportMacWatchCsv() { |
| 8961 | const d = _ndLastMacWatch; |
| 8962 | const TYPE = { spoofed_vendor_oui: 'Spoofed', universal: 'Vendor', |
| 8963 | randomized: 'Randomized', virtual_laa: 'Virtual/VM' }; |
| 8964 | _ndDownloadCsv('mac_watch' + (d && d.interface ? '_' + d.interface : ''), |
| 8965 | ['MAC', 'Type', 'Vendor', 'IPs', 'Flag', 'Note'], |
| 8966 | ((d && d.observed_macs) || []).map(c => { |
| 8967 | const ips = c.ips || []; |
| 8968 | const flag = c.klass === 'spoofed_vendor_oui' ? 'SPOOFED' |
| 8969 | : (ips.length > 1 ? 'CLONE (multiple IPs)' : ''); |
| 8970 | return [c.mac, TYPE[c.klass] || c.klass, c.vendor || '', |
| 8971 | ips.join(' '), flag, c.note || '']; |
| 8972 | })); |
no test coverage detected