()
| 6962 | return `<tr class="border-t border-slate-800"> |
| 6963 | <td class="px-2 py-1 font-mono ${rogue ? 'text-red-300' : 'text-gray-200'}">${escapeHtml(s.src)}${s.broadcast ? ' <span class="text-amber-300">bcast</span>' : ''}</td> |
| 6964 | <td class="px-2 py-1 text-gray-400">${escapeHtml((s.modes || []).join(', '))}</td> |
| 6965 | <td class="px-2 py-1 ${stCls}">${strata}${s.kod ? ' KoD' : ''}</td> |
| 6966 | <td class="px-2 py-1 font-mono ${bigOff ? 'text-red-300' : 'text-gray-400'}">${off}</td> |
| 6967 | <td class="px-2 py-1 font-mono text-gray-400">${escapeHtml((s.refids || []).join(', ') || '—')}</td> |
| 6968 | <td class="px-2 py-1">${rogue ? '<span class="text-red-300">ROGUE</span>' : '<span class="text-gray-500">trusted</span>'}</td> |
| 6969 | </tr>`; |
| 6970 | }).join('') + '</tbody></table>'; |
| 6971 | } |
| 6972 | if (d.reasons && d.reasons.length) { |
| 6973 | html += '<ul class="text-xs text-gray-400 mt-2 list-disc pl-5">' + |
| 6974 | d.reasons.map(r => '<li>' + escapeHtml(r) + '</li>').join('') + '</ul>'; |
| 6975 | } |
| 6976 | if (d.advisories && d.advisories.length) { |
| 6977 | html += '<ul class="text-xs text-cyan-400/80 mt-2 list-disc pl-5">' + |
| 6978 | d.advisories.map(a => '<li>' + escapeHtml(a) + '</li>').join('') + '</ul>'; |
| 6979 | } |
| 6980 | out.innerHTML = html; |
| 6981 | } catch (e) { |
| 6982 | out.innerHTML = '<p class="text-sm text-red-400">Failed: ' + escapeHtml(e.message) + '</p>'; |
| 6983 | } finally { |
| 6984 | _ndBusy(btn, false); |
| 6985 | } |
| 6986 | } |
| 6987 | async function ntpTrustBaseline() { |
| 6988 | try { |
| 6989 | await postAPI('/api/net/ntp-baseline', { action: 'reset' }); |
| 6990 | addConsoleMessage('NTP baseline reset — re-learning current time source(s)', 'info'); |
| 6991 | await runNtpWatch(); |
| 6992 | } catch (e) { |
| 6993 | addConsoleMessage('Failed to reset NTP baseline: ' + e.message, 'error'); |
| 6994 | } |
| 6995 | } |
| 6996 | |
| 6997 | // ---- ICMP Watch (passive ICMP-redirect / L3 route injection) --------------- |
| 6998 | const _ICMP_VERDICT_STYLE = { |
| 6999 | clean: ['bg-green-950/40 border-green-900 text-green-400', '✓ No ICMP redirects / injections / floods detected'], |
| 7000 | anomaly: ['bg-amber-950/50 border-amber-800 text-amber-300', '⚠ ICMP anomaly — a redirect/IRDP from a known gateway (verify)'], |
| 7001 | recon: ['bg-amber-950/50 border-amber-800 text-amber-300', '⚠ ICMP reconnaissance — timestamp / address-mask / info requests'], |
| 7002 | tunnel: ['bg-amber-950/50 border-amber-800 text-amber-300', '⚠ ICMP tunnelling — oversized echo payloads (covert channel / exfil)'], |
| 7003 | flood: ['bg-red-950/60 border-red-800 text-red-300', '🛑 ICMP flood — ping-flood / smurf DoS'], |
| 7004 | 'rogue-irdp': ['bg-red-950/60 border-red-800 text-red-300', '🛑 Rogue IRDP router advertisement — gateway injection'], |
| 7005 | redirect: ['bg-red-950/60 border-red-800 text-red-300', '🛑 ICMP Redirect — Layer-3 man-in-the-middle (route injection)'], |
| 7006 | unknown: ['bg-slate-800 border-slate-700 text-slate-400', '— Could not determine'], |
| 7007 | }; |
| 7008 | function _icmpFillIfaces() { |
| 7009 | const sel = document.getElementById('icmp-iface'); |
| 7010 | if (!sel || sel.dataset.filled === '1') return Promise.resolve(); |
| 7011 | return fetchAPI('/api/net/interfaces').then(x => { |
| 7012 | (x.interfaces || []).forEach(i => { |
| 7013 | const o = document.createElement('option'); |
| 7014 | o.value = i.name; |
| 7015 | const tag = i.type === 'wifi' ? ' (WiFi)' : i.type === 'ethernet' ? ' (LAN)' : (i.type ? ' (' + i.type + ')' : ''); |
| 7016 | o.textContent = i.name + tag; |
| 7017 | sel.appendChild(o); |
| 7018 | }); |
| 7019 | sel.dataset.filled = '1'; |
| 7020 | }).catch(() => {}); |
| 7021 | } |
no test coverage detected