({ btnId, resultsId, targetId, endpoint, label, summaryKey })
| 10298 | d.reasons.map(r => '<li>' + escapeHtml(r) + '</li>').join('') + '</ul>'; |
| 10299 | } |
| 10300 | (d.advisories || []).forEach(a => { |
| 10301 | html += `<div class="mt-2 px-3 py-2 rounded border text-xs text-gray-400 border-slate-700 bg-slate-900/40">${escapeHtml(a)}</div>`; |
| 10302 | }); |
| 10303 | out.innerHTML = html; |
| 10304 | } catch (e) { |
| 10305 | out.innerHTML = '<p class="text-sm text-red-400">Failed: ' + escapeHtml(e.message) + '</p>'; |
| 10306 | } finally { |
| 10307 | _ndBusy(btn, false); |
| 10308 | } |
| 10309 | } |
| 10310 | async function eigrpTrustBaseline() { |
| 10311 | try { |
| 10312 | await postAPI('/api/net/eigrp-baseline', { action: 'reset' }); |
| 10313 | addConsoleMessage('EIGRP baseline reset — re-learning current routers & prefixes', 'info'); |
| 10314 | await runEigrpWatch(); |
| 10315 | } catch (e) { |
| 10316 | addConsoleMessage('Failed to reset EIGRP baseline: ' + e.message, 'error'); |
| 10317 | } |
| 10318 | } |
| 10319 | |
| 10320 | // ---- FHRP Watch (passive HSRP/VRRP/GLBP/CARP hijack scanner) ---------------- |
| 10321 | const _FHRP_VERDICT_STYLE = { |
| 10322 | clean: ['bg-green-950/40 border-green-900 text-green-400', '✓ FHRP groups/speakers/GLBP forwarders match the trusted baseline'], |
| 10323 | 'weak-auth': ['bg-amber-950/50 border-amber-800 text-amber-300', '⚠ Weak / no FHRP authentication — a forged hello can win the election'], |
| 10324 | 'glbp-weight-skew': ['bg-amber-950/50 border-amber-800 text-amber-300', '⚠ GLBP forwarder weight changed — load-share shift steers which hosts route through an AVF'], |
| 10325 | 'priority-change': ['bg-amber-950/50 border-amber-800 text-amber-300', '⚠ A known FHRP speaker raised its priority — possible pre-takeover'], |
| 10326 | 'rogue-speaker': ['bg-amber-950/50 border-amber-800 text-amber-300', '⚠ Unexpected FHRP speaker on the segment — not in the baseline'], |
| 10327 | 'glbp-avf-hijack': ['bg-red-950/60 border-red-800 text-red-300', '🛑 GLBP AVF HIJACK — a forwarder (vMAC) seized: stealth per-slice MITM while the gateway election looks healthy'], |
| 10328 | hijack: ['bg-red-950/60 border-red-800 text-red-300', '🛑 FHRP HIJACK — a new speaker is winning the virtual-gateway election'], |
| 10329 | unknown: ['bg-slate-800 border-slate-700 text-slate-400', '— Could not determine'], |
| 10330 | }; |
| 10331 | function _fhrpFillIfaces() { |
| 10332 | const sel = document.getElementById('fhrp-iface'); |
| 10333 | if (!sel || sel.dataset.filled === '1') return Promise.resolve(); |
| 10334 | return fetchAPI('/api/net/interfaces').then(x => { |
| 10335 | (x.interfaces || []).forEach(i => { |
| 10336 | const o = document.createElement('option'); |
| 10337 | o.value = i.name; |
| 10338 | const tag = i.type === 'wifi' ? ' (WiFi)' : i.type === 'ethernet' ? ' (LAN)' : (i.type ? ' (' + i.type + ')' : ''); |
| 10339 | o.textContent = i.name + tag; |
| 10340 | sel.appendChild(o); |
| 10341 | }); |
| 10342 | sel.dataset.filled = '1'; |
| 10343 | }).catch(() => {}); |
| 10344 | } |
| 10345 | async function runFhrpWatch() { |
| 10346 | const out = document.getElementById('fhrp-results'); |
| 10347 | if (!out) return; |
no test coverage detected