()
| 7086 | _ndBusy(btn, true, 'Listening…'); |
| 7087 | out.classList.remove('hidden'); |
| 7088 | out.innerHTML = '<p class="text-sm text-gray-400">Passively capturing TLS/QUIC handshakes on the segment…</p>'; |
| 7089 | try { |
| 7090 | _tlsFillIfaces(); |
| 7091 | const qs = '?seconds=' + encodeURIComponent(secs) + (iface ? '&interface=' + encodeURIComponent(iface) : '') + (noquic ? '&no_quic=1' : ''); |
| 7092 | const d = await fetchAPI('/api/net/tls-watch' + qs); |
| 7093 | if (!d || d.success === false) { |
| 7094 | const msg = (d && d.error) || 'failed'; |
| 7095 | let extra = ''; |
| 7096 | if (d && d.missing_tool === 'tcpdump') extra = ' <button onclick="installNetTool(\'tcpdump\', this, runTlsWatch)" class="ml-2 underline text-cyan-400">Install tcpdump</button>'; |
| 7097 | out.innerHTML = '<p class="text-sm text-red-400">Error: ' + escapeHtml(msg) + extra + '</p>'; |
| 7098 | return; |
| 7099 | } |
| 7100 | const [cls, label] = _TLS_VERDICT_STYLE[d.verdict] || _TLS_VERDICT_STYLE.unknown; |
| 7101 | let html = `<div class="mb-2 px-3 py-2 rounded border ${cls} text-sm">${label}</div>`; |
| 7102 | html += `<p class="text-xs text-gray-500 mb-2">Interface: ${escapeHtml(d.interface || '—')} · ${d.seconds}s window · ${d.tls || 0} TLS · ${d.quic || 0} QUIC handshake(s)</p>`; |
| 7103 | const S = d.sessions || []; |
| 7104 | if (!S.length) { |
| 7105 | html += '<p class="text-sm text-gray-400">No TLS/QUIC handshakes observed. On a switched network you need a SPAN/mirror port to see other hosts.</p>'; |
| 7106 | } else { |
| 7107 | html += '<table class="min-w-full text-xs text-gray-300 whitespace-nowrap"><thead>' + |
| 7108 | '<tr class="text-left text-gray-500"><th class="px-2 py-1">Proto</th><th class="px-2 py-1">Endpoints</th><th class="px-2 py-1">SNI</th><th class="px-2 py-1">ALPN</th><th class="px-2 py-1">JA4</th><th class="px-2 py-1">Negotiated</th></tr></thead><tbody>'; |
| 7109 | S.forEach(s => { |
| 7110 | const alpn = (s.alpn || []).join(',') || '—'; |
| 7111 | const neg = s.negotiated_version ? (_tlsVerName(s.negotiated_version) + (s.cipher ? ' ' + s.cipher : '')) : '—'; |
| 7112 | const bad = (s.findings || []).some(f => f.severity === 'high'); |
| 7113 | const dup = (s.count > 1) ? ` <span class="text-slate-500" title="${s.count} duplicate connections collapsed">×${s.count}</span>` : ''; |
| 7114 | html += `<tr class="border-t border-slate-800 align-top"> |
| 7115 | <td class="px-2 py-1 uppercase ${s.proto === 'quic' ? 'text-cyan-300' : 'text-gray-400'}">${escapeHtml(s.proto)}</td> |
| 7116 | <td class="px-2 py-1 font-mono text-gray-400">${escapeHtml(s.src)} → ${escapeHtml(s.dst)}${dup}</td> |
| 7117 | <td class="px-2 py-1 ${bad ? 'text-red-300' : 'text-gray-200'}">${escapeHtml(s.sni || '—')}</td> |
| 7118 | <td class="px-2 py-1 text-gray-400">${escapeHtml(alpn)}</td> |
| 7119 | <td class="px-2 py-1 font-mono text-gray-300" title="${escapeHtml(s.ja4_r || '')}">${escapeHtml(s.ja4 || '—')}</td> |
| 7120 | <td class="px-2 py-1 text-gray-400">${escapeHtml(neg)}</td> |
| 7121 | </tr>`; |
| 7122 | (s.findings || []).forEach(f => { |
| 7123 | html += `<tr class="border-0"><td></td><td colspan="5" class="px-2 pb-1 text-xs ${_TLS_SEV_COLOR[f.severity] || 'text-gray-400'}">└ ${escapeHtml(f.severity)} ${escapeHtml(f.code)}: ${escapeHtml(f.message)}</td></tr>`; |
| 7124 | }); |
| 7125 | }); |
| 7126 | html += '</tbody></table>'; |
| 7127 | } |
| 7128 | out.innerHTML = html; |
| 7129 | } catch (e) { |
| 7130 | out.innerHTML = '<p class="text-sm text-red-400">Failed: ' + escapeHtml(e.message) + '</p>'; |
| 7131 | } finally { |
| 7132 | _ndBusy(btn, false); |
| 7133 | } |
| 7134 | } |
| 7135 | |
| 7136 | // ---- SSH Watch (passive regreSSHion / Terrapin observer) ------------------- |
| 7137 | const _SSH_VERDICT_STYLE = { |
| 7138 | clean: ['bg-green-950/40 border-green-900 text-green-400', '✓ No SSH posture issues — modern algorithms, no regreSSHion pattern'], |
| 7139 | suspicious: ['bg-amber-950/50 border-amber-800 text-amber-300', '⚠ SSH exposure — regreSSHion posture, Terrapin, or weak algorithms; review the findings'], |
| 7140 | unknown: ['bg-slate-800 border-slate-700 text-slate-400', '— Could not determine'], |
| 7141 | }; |
| 7142 | const _SSH_SEV_COLOR = { high: 'text-red-300', warn: 'text-amber-300', notice: 'text-gray-300', info: 'text-gray-500' }; |
| 7143 | function _sshFillIfaces() { |
| 7144 | const sel = document.getElementById('ssh-iface'); |
| 7145 | if (!sel || sel.dataset.filled === '1') return Promise.resolve(); |
nothing calls this directly
no test coverage detected