()
| 7822 | `<tr class="border-t border-slate-800"><td class="px-3 py-1.5 text-gray-500">Neighbours</td><td class="px-3 py-1.5">${d.neighbor_count != null ? d.neighbor_count : '—'}</td></tr>` + |
| 7823 | '</tbody></table>'; |
| 7824 | if (d.impersonators && d.impersonators.length) { |
| 7825 | html += '<p class="text-xs text-gray-400 mt-2">One MAC answering for many IPs:</p><ul class="text-xs text-gray-400 list-disc pl-5">' + |
| 7826 | d.impersonators.map(i => { |
| 7827 | const ips = i.ips || []; |
| 7828 | return '<li class="font-mono">' + escapeHtml(i.mac) + ' → ' + escapeHtml(ips.slice(0, 6).join(', ')) + (ips.length > 6 ? '…' : '') + '</li>'; |
| 7829 | }).join('') + '</ul>'; |
| 7830 | } |
| 7831 | if (d.reasons && d.reasons.length) { |
| 7832 | html += '<ul class="text-xs text-gray-400 mt-2 list-disc pl-5">' + |
| 7833 | d.reasons.map(r => '<li>' + escapeHtml(r) + '</li>').join('') + '</ul>'; |
| 7834 | } |
| 7835 | out.innerHTML = html; |
| 7836 | } catch (e) { |
| 7837 | out.innerHTML = '<p class="text-sm text-red-400">Failed: ' + escapeHtml(e.message) + '</p>'; |
| 7838 | } finally { |
| 7839 | _ndBusy(btn, false); |
| 7840 | } |
| 7841 | } |
| 7842 | |
| 7843 | // ---- DHCP Guardian card (rogue DHCP + starvation, detection-only) ---------- |
| 7844 | const _DHCP_VERDICT_STYLE = { |
| 7845 | clean: ['bg-green-950/40 border-green-900 text-green-400', '✓ No rogue DHCP server or starvation detected'], |
| 7846 | suspicious: ['bg-amber-950/50 border-amber-800 text-amber-300', '⚠ DHCP anomaly — review below'], |
| 7847 | rogue: ['bg-red-950/60 border-red-800 text-red-300', '🛑 Rogue / fake DHCP server detected'], |
| 7848 | starvation: ['bg-red-950/60 border-red-800 text-red-300', '🛑 DHCP starvation (pool-exhaustion) detected'], |
| 7849 | unknown: ['bg-slate-800 border-slate-700 text-slate-400', '— Could not determine'], |
| 7850 | }; |
| 7851 | function _dhcpFillIfaces() { |
| 7852 | const sel = document.getElementById('dhcp-iface'); |
| 7853 | if (!sel || sel.dataset.filled === '1') return Promise.resolve(); |
| 7854 | return fetchAPI('/api/net/interfaces').then(x => { |
| 7855 | (x.interfaces || []).forEach(i => { |
| 7856 | const o = document.createElement('option'); |
| 7857 | o.value = i.name; |
| 7858 | const tag = i.type === 'wifi' ? ' (WiFi)' : i.type === 'ethernet' ? ' (LAN)' : (i.type ? ' (' + i.type + ')' : ''); |
| 7859 | o.textContent = i.name + tag; |
| 7860 | sel.appendChild(o); |
| 7861 | }); |
| 7862 | sel.dataset.filled = '1'; |
| 7863 | }).catch(() => {}); |
| 7864 | } |
| 7865 | async function runDhcpGuardian() { |
| 7866 | const out = document.getElementById('dhcp-results'); |
| 7867 | if (!out) return; |
| 7868 | const btn = (typeof event !== 'undefined' && event && event.target) ? event.target : null; |
| 7869 | const ifaceSel = document.getElementById('dhcp-iface'); |
| 7870 | const iface = ifaceSel && ifaceSel.value ? ifaceSel.value : ''; |
| 7871 | _ndBusy(btn, true, 'Scanning…'); |
| 7872 | out.classList.remove('hidden'); |
| 7873 | out.innerHTML = '<p class="text-sm text-gray-400">Provoking DHCP offers and watching for a DISCOVER flood… (a few seconds)</p>'; |
| 7874 | try { |
| 7875 | _dhcpFillIfaces(); |
| 7876 | const d = await fetchAPI('/api/net/dhcp-guardian' + (iface ? '?interface=' + encodeURIComponent(iface) : '')); |
| 7877 | if (!d || d.success === false) { |
| 7878 | out.innerHTML = '<p class="text-sm text-red-400">Error: ' + escapeHtml((d && d.error) || 'failed') + '</p>'; |
| 7879 | return; |
no test coverage detected