()
| 8119 | sel.appendChild(o); |
| 8120 | }); |
| 8121 | sel.dataset.filled = '1'; |
| 8122 | }).catch(() => {}); |
| 8123 | } |
| 8124 | async function runTelnetWatch() { |
| 8125 | const out = document.getElementById('telnet-results'); |
| 8126 | if (!out) return; |
| 8127 | const btn = (typeof event !== 'undefined' && event && event.target) ? event.target : null; |
| 8128 | const ifaceSel = document.getElementById('telnet-iface'); |
| 8129 | const iface = ifaceSel && ifaceSel.value ? ifaceSel.value : ''; |
| 8130 | const secsEl = document.getElementById('telnet-secs'); |
| 8131 | const secs = secsEl && secsEl.value ? secsEl.value : '12'; |
| 8132 | _ndBusy(btn, true, 'Listening…'); |
| 8133 | out.classList.remove('hidden'); |
| 8134 | out.innerHTML = '<p class="text-sm text-gray-400">Passively capturing Telnet on the segment…</p>'; |
| 8135 | try { |
| 8136 | _telnetFillIfaces(); |
| 8137 | const qs = '?seconds=' + encodeURIComponent(secs) + (iface ? '&interface=' + encodeURIComponent(iface) : ''); |
| 8138 | const d = await fetchAPI('/api/net/telnet-watch' + qs); |
| 8139 | if (!d || d.success === false) { |
| 8140 | const msg = (d && d.error) || 'failed'; |
| 8141 | let extra = ''; |
| 8142 | if (d && d.missing_tool === 'tcpdump') extra = ' <button onclick="installNetTool(\'tcpdump\', this, runTelnetWatch)" class="ml-2 underline text-cyan-400">Install tcpdump</button>'; |
| 8143 | out.innerHTML = '<p class="text-sm text-red-400">Error: ' + escapeHtml(msg) + extra + '</p>'; |
| 8144 | return; |
| 8145 | } |
| 8146 | const [cls, label] = _TELNET_VERDICT_STYLE[d.verdict] || _TELNET_VERDICT_STYLE.unknown; |
| 8147 | let html = `<div class="mb-2 px-3 py-2 rounded border ${cls} text-sm">${label}</div>`; |
| 8148 | html += `<p class="text-xs text-gray-500 mb-2">Interface: ${escapeHtml(d.interface || '—')} · ${d.seconds}s window · ${d.count || 0} flow(s) · ${d.findings_total || 0} finding(s)</p>`; |
| 8149 | const S = d.sessions || []; |
| 8150 | if (!S.length) { |
| 8151 | html += '<p class="text-sm text-gray-400">No Telnet sessions observed. On a switched network you need a SPAN/mirror port to see other hosts.</p>'; |
| 8152 | } else { |
| 8153 | html += '<table class="min-w-full text-xs text-gray-300 whitespace-nowrap"><thead>' + |
| 8154 | '<tr class="text-left text-gray-500"><th class="px-2 py-1">Client</th><th class="px-2 py-1">Server</th><th class="px-2 py-1">Findings</th></tr></thead><tbody>'; |
| 8155 | S.forEach(s => { |
| 8156 | const bad = (s.findings || []).some(f => f.severity === 'critical' || f.severity === 'high'); |
| 8157 | html += `<tr class="border-t border-slate-800 align-top"> |
| 8158 | <td class="px-2 py-1 font-mono ${bad ? 'text-red-300' : 'text-gray-400'}">${escapeHtml(s.client || '—')}</td> |
| 8159 | <td class="px-2 py-1 font-mono text-gray-400">${escapeHtml(s.server || '—')}</td> |
| 8160 | <td class="px-2 py-1 text-gray-400">${(s.findings || []).length}</td> |
| 8161 | </tr>`; |
| 8162 | (s.findings || []).forEach(f => { |
| 8163 | html += `<tr class="border-0"><td></td><td colspan="2" class="px-2 pb-1 text-xs ${_TELNET_SEV_COLOR[f.severity] || 'text-gray-400'}">└ ${escapeHtml(f.severity)} ${escapeHtml(f.code)}: ${escapeHtml(f.desc || '')}</td></tr>`; |
| 8164 | }); |
| 8165 | }); |
| 8166 | html += '</tbody></table>'; |
| 8167 | } |
| 8168 | out.innerHTML = html; |
| 8169 | } catch (e) { |
| 8170 | out.innerHTML = '<p class="text-sm text-red-400">Failed: ' + escapeHtml(e.message) + '</p>'; |
| 8171 | } finally { |
| 8172 | _ndBusy(btn, false); |
| 8173 | } |
| 8174 | } |
| 8175 | |
| 8176 | // ---- IGMP Watch (passive multicast security scanner) ----------------------- |
| 8177 | const _IGMP_VERDICT_STYLE = { |
| 8178 | clean: ['bg-green-950/40 border-green-900 text-green-400', '✓ No IGMP / multicast anomalies detected'], |
no test coverage detected