Files
RMM-Hunter/internal/web/templates/index.html
T
Evan Hosinski adcad167df Add support for dynamic hosts file management, browser opening, and new favicon handling in web server. Add elimination workflow UI enhancements with better state management and design.
TODO: Test elimination per finding type in web view

Figure out where RustDesk registry persistence is located. The installer is aware of it somehow
2025-10-12 20:02:49 -04:00

652 lines
36 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RMM Hunter Web UI</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<style>
:root{
--bg:#0b0f0c; --bg2:#111612; --panel:#0f1511; --accent:#17e46e; --accent2:#0eea5a; --muted:#a7b5a9; --text:#e6f4ea; --danger:#ff5c7a; --warn:#ffd166;
}
*{box-sizing:border-box}
body{margin:0;font-family:Inter,system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:linear-gradient(180deg,var(--bg),#070a08);color:var(--text);min-height:100vh;display:flex;flex-direction:column}
/* Custom scrollbar styling */
*::-webkit-scrollbar{width:12px;height:12px}
*::-webkit-scrollbar-track{background:#050805;border-radius:10px}
*::-webkit-scrollbar-thumb{background:#124b2b;border-radius:10px;border:2px solid #050805}
*::-webkit-scrollbar-thumb:hover{background:#17e46e}
*::-webkit-scrollbar-corner{background:#050805}
/* Firefox scrollbar */
*{scrollbar-width:thin;scrollbar-color:#124b2b #050805}
a{color:var(--accent)}
header{position:sticky;top:0;z-index:10;backdrop-filter:blur(12px);background:rgba(11,15,12,.95);border-bottom:1px solid rgba(23,228,110,.15);box-shadow:0 4px 20px rgba(0,0,0,.3)}
.nav{max-width:1400px;margin:0 auto;display:flex;align-items:center;gap:8px;padding:12px 24px}
.brand{display:flex;align-items:center;gap:12px;font-weight:700;letter-spacing:.5px;font-size:16px;color:var(--text)}
.brand img{width:40px;height:40px;object-fit:contain;filter:drop-shadow(0 0 8px rgba(23,228,110,.3))}
.spacer{flex:1}
.nav a.btn{display:inline-flex;align-items:center;padding:10px 18px;border:none;border-radius:6px;color:var(--text);text-decoration:none;transition:all .2s;font-weight:500;font-size:14px;position:relative;overflow:hidden}
.nav a.btn::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(135deg,rgba(23,228,110,.1),rgba(23,228,110,.05));opacity:0;transition:opacity .2s}
.nav a.btn:hover::before{opacity:1}
.nav a.btn:hover{background:rgba(23,228,110,.08);transform:translateY(-1px)}
.nav a.primary{background:linear-gradient(135deg,#17e46e,#0eea5a);color:#000;font-weight:600;box-shadow:0 4px 12px rgba(23,228,110,.25)}
.nav a.primary:hover{box-shadow:0 6px 20px rgba(23,228,110,.4);transform:translateY(-2px)}
.nav a.danger{background:rgba(255,92,122,.1);color:#ff5c7a}
.nav a.danger:hover{background:rgba(255,92,122,.2)}
main{max-width:1100px;margin:20px auto;padding:0 16px;flex:1;width:100%}
main.full-width{max-width:none;padding:0 20px}
.card{background:var(--panel);border:1px solid #133422;border-radius:12px;padding:16px;margin-bottom:16px;box-shadow:0 8px 24px rgba(0,0,0,.25);overflow-wrap:break-word;word-wrap:break-word;word-break:break-word;overflow:hidden}
h1,h2{margin:10px 0}
.muted{color:var(--muted)}
.grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));min-height:400px}
.pill{display:inline-flex;align-items:center;gap:6px;background:rgba(23,228,110,.08);border:1px solid rgba(23,228,110,.2);border-radius:6px;padding:6px 12px;font-size:12px;color:var(--accent);font-weight:500}
.log{background:#050805;border:1px solid #0c2819;border-radius:10px;padding:10px;height:calc(100vh - 400px);min-height:400px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:12px;color:#b7f6c8;white-space:pre-wrap;word-wrap:break-word}
.actions{display:flex;gap:10px;flex-wrap:wrap}
.btn{cursor:pointer;user-select:none;display:inline-flex;align-items:center;gap:8px;padding:10px 14px;border-radius:10px;border:1px solid #124b2b;background:#0e1a13;color:var(--text)}
.btn:hover{filter:brightness(1.08)}
.btn.primary{background:linear-gradient(180deg,#0e351e,#0a2a18);border-color:#1d7e4a}
.danger{color:#fff;border-color:#4d121b;background:#1a0e10}
.tag{display:inline-block;padding:6px 12px;border-radius:6px;border:1px solid rgba(23,228,110,.3);background:rgba(23,228,110,.08);color:var(--accent);font-weight:500;font-size:13px}
.typing{overflow:hidden;white-space:nowrap;display:inline-block;border-right:2px solid var(--accent);animation:blink 0.7s step-end infinite}
@keyframes blink{0%,100%{border-color:var(--accent)}50%{border-color:transparent}}
.hidden{display:none}
footer{padding:20px 16px;color:var(--muted);text-align:center;font-size:13px;border-top:1px solid #0d2015;margin-top:auto}
.footer-icon{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:8px;background:#0e1a13;border:1px solid #124b2b;color:var(--accent);transition:all 0.2s ease;text-decoration:none}
.footer-icon:hover{background:#103e24;border-color:#1d7e4a;color:var(--accent2);transform:translateY(-2px)}
.tooltip{border-bottom:1px dotted var(--muted);cursor:help}
.elim-layout{display:grid;grid-template-columns:280px 1fr 320px;gap:16px;height:calc(100vh - 200px);min-height:600px}
.elim-tree{background:var(--panel);border:1px solid #133422;border-radius:12px;padding:12px;overflow-y:auto}
.elim-center{background:var(--panel);border:1px solid #133422;border-radius:12px;padding:20px;overflow-y:auto;display:flex;flex-direction:column}
.elim-wiki{background:var(--panel);border:1px solid #133422;border-radius:12px;padding:16px;overflow-y:auto}
.tree-node{cursor:pointer;padding:8px 10px;border-radius:6px;margin:4px 0;user-select:none;font-weight:600;color:var(--text);border:1px solid transparent}
.tree-node:hover{background:#0e1a13;border-color:#124b2b}
.tree-node.expanded{background:#0e1a13;border-color:#124b2b}
.tree-child{cursor:pointer;padding:6px 10px 6px 12px;border-radius:6px;margin:2px 0;font-size:13px;color:var(--muted);border:1px solid transparent;display:flex;align-items:center;gap:8px;transition:all 0.15s}
.tree-child:hover{background:#0a2a18;color:var(--text);border-color:#124b2b}
.tree-child.selected{background:#103e24;color:var(--accent);border-color:#1d7e4a}
.tree-child::before{content:'→';color:var(--accent);opacity:0;transition:opacity 0.15s}
.tree-child:hover::before,.tree-child.selected::before{opacity:1}
.detail-field{margin:12px 0;padding:10px;background:#050805;border:1px solid #0c2819;border-radius:8px}
.detail-label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px}
.detail-value{color:var(--text);font-size:14px;word-break:break-all}
.order-badge{display:inline-block;padding:6px 14px;border-radius:6px;font-size:11px;font-weight:600;margin-bottom:12px;width:fit-content}
.order-1{background:#1a0e10;color:#ff5c7a;border:1px solid #4d121b}
.order-2{background:#1a1510;color:#ffd166;border:1px solid #4d3b12}
.order-3{background:#0e1a13;color:#17e46e;border:1px solid #124b2b}
.wiki-section{margin-bottom:20px}
.wiki-title{font-weight:600;color:var(--accent);margin-bottom:8px;font-size:15px}
.wiki-text{color:var(--muted);font-size:13px;line-height:1.6}
.wiki-list{margin:8px 0;padding-left:20px;color:var(--muted);font-size:13px}
.wiki-list li{margin:4px 0}
.elim-btn{width:fit-content;align-self:center;margin-top:auto;padding:16px 32px;background:rgba(255,92,122,.08);border:1px solid rgba(255,92,122,.3);color:#ff5c7a;border-radius:8px;cursor:pointer;font-weight:600;font-size:15px;transition:all 0.25s;position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;gap:10px}
.elim-btn::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(135deg,rgba(255,92,122,.15),rgba(255,92,122,.05));opacity:0;transition:opacity 0.25s}
.elim-btn:hover{background:rgba(255,92,122,.15);border-color:#ff5c7a;transform:translateY(-2px);box-shadow:0 6px 20px rgba(255,92,122,.3)}
.elim-btn:hover::before{opacity:1}
.elim-btn:active{transform:translateY(0);box-shadow:0 2px 8px rgba(255,92,122,.2)}
.elim-btn:disabled{opacity:0.4;cursor:not-allowed;transform:none}
.elim-btn-icon{font-size:18px}
.empty-state{text-align:center;padding:40px;color:var(--muted)}
/* Splash Screen */
#splash{position:fixed;top:0;left:0;right:0;bottom:0;background:radial-gradient(circle at center,#0f1511,#0b0f0c,#000);z-index:9999;display:flex;flex-direction:column;align-items:center;justify-content:center;opacity:1;transition:opacity 0.5s ease-out}
#splash.fade-out{opacity:0;pointer-events:none}
.splash-logo{width:180px;height:180px;margin-bottom:30px;animation:logoFloat 3s ease-in-out infinite;filter:drop-shadow(0 0 40px rgba(23,228,110,.6))}
@keyframes logoFloat{0%,100%{transform:translateY(0px)}50%{transform:translateY(-15px)}}
.splash-title{font-size:32px;font-weight:700;color:var(--accent);margin-bottom:12px;letter-spacing:2px;text-shadow:0 0 20px rgba(23,228,110,.5)}
.splash-subtitle{font-size:14px;color:var(--muted);letter-spacing:1px;margin-bottom:40px}
.splash-loader{width:200px;height:3px;background:rgba(23,228,110,.1);border-radius:3px;overflow:hidden;position:relative}
.splash-loader::after{content:'';position:absolute;top:0;left:0;height:100%;width:40%;background:linear-gradient(90deg,transparent,var(--accent),transparent);animation:loading 1.5s ease-in-out infinite}
@keyframes loading{0%{left:-40%}100%{left:100%}}
</style>
</head>
<body>
<!-- Splash Screen -->
<div id="splash">
<img src="/logo" alt="RMM Hunter" class="splash-logo">
<div class="splash-title">RMM HUNTER</div>
<div class="splash-subtitle">POWERED BY KRAKENTECH</div>
<div class="splash-loader"></div>
</div>
<header>
<div class="nav">
<div class="brand"><img src="/logo" alt="RMM Hunter"><span>RMM Hunter</span></div>
<span class="pill">KrakenTech LLC</span>
<div class="spacer"></div>
<a href="#hunt" class="btn primary">Hunt</a>
<a href="#previous" class="btn">Previous Hunts</a>
<a href="#eliminate" class="btn">Eliminate</a>
<a id="quitBtn" class="btn danger">Quit</a>
</div>
</header>
<main>
<section id="huntSection" class="card">
<h2>Hunt</h2>
<p class="muted">Click Start Hunt to scan for Remote Monitoring & Management (RMM) software. Progress logs will appear below in real-time.</p>
<div class="actions">
<button id="startHunt" class="btn primary">Start Hunt</button>
<span id="huntTag" class="tag hidden"></span>
</div>
<div id="log" class="log" aria-live="polite"></div>
</section>
<section id="previousSection" class="card">
<h2>Use Previous Hunt</h2>
<div id="hunts" class="grid"></div>
</section>
<section id="reportSection" class="card hidden">
<h2>Report</h2>
<div id="reportHeader"></div>
<div id="reportBody"></div>
<div class="actions">
<a href="#eliminate" class="btn primary">Proceed to Eliminate</a>
</div>
</section>
<section id="eliminateSection" class="hidden">
<h2 style="margin:0 0 16px 0">Eliminate Detected Items</h2>
<div id="elimContent" class="elim-layout">
<div class="elim-tree" id="elimTree">
<div class="empty-state">Load a report first</div>
</div>
<div class="elim-center" id="elimCenter">
<div class="empty-state">Select an item from the tree</div>
</div>
<div class="elim-wiki" id="elimWiki">
<div class="empty-state">Item details will appear here</div>
</div>
</div>
</section>
</main>
<footer>
<div style="display:flex;flex-direction:column;align-items:center;gap:12px">
<div style="font-weight:600;font-size:15px;color:var(--text)">KrakenTech LLC</div>
<div style="font-size:13px;color:var(--muted)">RMM Hunter Web Interface</div>
<div style="display:flex;gap:16px;align-items:center">
<a href="https://github.com/KrakenTech-LLC/RMM-Hunter" target="_blank" rel="noopener" class="footer-icon" title="View on GitHub">
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
</a>
<a href="https://krakensec.tech" target="_blank" rel="noopener" class="footer-icon" title="Visit KrakenTech Website">
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0ZM5.78 8.75a9.64 9.64 0 0 0 1.363 4.177c.255.426.542.832.857 1.215.245-.296.551-.705.857-1.215A9.64 9.64 0 0 0 10.22 8.75Zm4.44-1.5a9.64 9.64 0 0 0-1.363-4.177c-.307-.51-.612-.919-.857-1.215a9.927 9.927 0 0 0-.857 1.215A9.64 9.64 0 0 0 5.78 7.25Zm-5.944 1.5H1.543a6.507 6.507 0 0 0 4.666 5.5c-.123-.181-.24-.365-.352-.552-.715-1.192-1.437-2.874-1.581-4.948Zm-2.733-1.5h2.733c.144-2.074.866-3.756 1.58-4.948.12-.197.237-.381.353-.552a6.507 6.507 0 0 0-4.666 5.5Zm10.181 1.5c-.144 2.074-.866 3.756-1.58 4.948-.12.197-.237.381-.353.552a6.507 6.507 0 0 0 4.666-5.5Zm2.733-1.5a6.507 6.507 0 0 0-4.666-5.5c.123.181.24.365.353.552.714 1.192 1.436 2.874 1.58 4.948Z"/>
</svg>
</a>
<a href="mailto:ehosinski@krakensec.tech" class="footer-icon" title="Contact Us">
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
<path d="M.05 3.555A2 2 0 0 1 2 2h12a2 2 0 0 1 1.95 1.555L8 8.414.05 3.555ZM0 4.697v7.104l5.803-3.558L0 4.697ZM6.761 8.83l-6.57 4.027A2 2 0 0 0 2 14h12a2 2 0 0 0 1.808-1.144l-6.57-4.027L8 9.586l-1.239-.757Zm3.436-.586L16 11.801V4.697l-5.803 3.546Z"/>
</svg>
</a>
</div>
</div>
</footer>
<script>
(function(){
// Splash screen
window.addEventListener('load', function() {
setTimeout(function() {
const splash = document.getElementById('splash');
splash.classList.add('fade-out');
setTimeout(function() {
splash.style.display = 'none';
}, 500);
}, 1500); // Show splash for 1.5 seconds
});
const logEl = document.getElementById('log');
const huntsEl = document.getElementById('hunts');
const huntTag = document.getElementById('huntTag');
const reportSection = document.getElementById('reportSection');
const reportHeader = document.getElementById('reportHeader');
const reportBody = document.getElementById('reportBody');
const huntSection = document.getElementById('huntSection');
const previousSection = document.getElementById('previousSection');
const eliminateSection = document.getElementById('eliminateSection');
function route(){
const h = location.hash || '#hunt';
const mainEl = document.querySelector('main');
huntSection.classList.toggle('hidden', h!=='#hunt');
previousSection.classList.toggle('hidden', h!=='#previous');
reportSection.classList.toggle('hidden', h!=='#report');
eliminateSection.classList.toggle('hidden', h!=='#eliminate');
// Make eliminate page full-width
mainEl.classList.toggle('full-width', h==='#eliminate');
// If navigating to eliminate without a loaded report, show helpful message
if (h === '#eliminate' && !currentReport) {
elimTree.innerHTML = '<div class="empty-state">No report loaded.<br><br>Please run a hunt or load a previous report first.</div>';
elimCenter.innerHTML = '<div class="empty-state">Load a report to see elimination options</div>';
elimWiki.innerHTML = '<div class="empty-state">Select an item to see details</div>';
}
}
window.addEventListener('hashchange', route); route();
async function listHunts(){
huntsEl.innerHTML = '<div class="muted">Loading...</div>';
const r = await fetch('/api/hunts');
const data = await r.json();
if(!Array.isArray(data)||data.length===0){
huntsEl.innerHTML = '<div class="muted">No previous hunts found yet.</div>';
return;
}
huntsEl.innerHTML = '';
for(const it of data){
const card = document.createElement('div');
card.className='card';
card.innerHTML = `<div style="display:flex;flex-direction:column;gap:8px">
<strong>${it.reportName||it.file}</strong>
<span class="muted">Generated: ${it.generatedAt||'unknown'}</span>
<div class="actions"><button class="btn" data-file="${encodeURIComponent(it.file)}">Open Report</button></div>
</div>`;
card.querySelector('button').addEventListener('click',()=>openReport(it.file));
huntsEl.appendChild(card);
}
}
async function openReport(file){
const r = await fetch('/api/report?file='+encodeURIComponent(file));
const data = await r.json();
renderReport(data);
location.hash = '#report';
}
function renderReport(rep){
reportHeader.innerHTML = '';
reportBody.innerHTML = '';
const risk = rep.riskRating||{};
reportHeader.innerHTML = `<div class="pill">Risk: <strong style="color:${riskColor(risk.rating)}">${risk.rating||'N/A'}</strong> (${(risk.score??'-')}/10)</div>
<div class="muted">${escapeHTML(rep.riskRating?.summary||'')}</div>`;
const f = rep.findings||rep||{};
console.log('Report data:', rep);
console.log('Findings:', f);
console.log('Binaries:', f.binaries);
const blocks = [
{id:'processes', title:'Processes', arr:f.processes, info:'Programs currently running on your computer.'},
{id:'services', title:'Services', arr:f.services, info:'Background components that start with Windows and run without windows.'},
{id:'connections', title:'Outbound Connections', arr:f.outboundConnections, info:'Network connections from your computer to the internet.'},
{id:'autoruns', title:'AutoRuns (Registry/Startup)', arr:f.autoRuns||f.autoruns, info:'Items that automatically start when Windows starts (often stored in the registry).'},
{id:'tasks', title:'Scheduled Tasks', arr:f.scheduledTasks, info:'Actions scheduled by Windows Task Scheduler.'},
{id:'binaries', title:'Binaries', arr:f.binaries, info:'Executable files found on disk.'},
{id:'directories', title:'Directories', arr:f.directories, info:'Folders where related files were found.'}
];
for(const b of blocks){
const wrap = document.createElement('div');
wrap.className='card';
const count = Array.isArray(b.arr)?b.arr.length:0;
wrap.innerHTML = `<h3>${b.title} <span class="tag">${count}</span> <span class="tooltip" title="${b.info}">ⓘ</span></h3>`;
if(count===0){
wrap.innerHTML += '<div class="muted">No items found.</div>';
} else {
const list = document.createElement('div'); list.style.display='grid'; list.style.gap='8px';
for(const item of b.arr){
console.log(`Rendering ${b.id}:`, item);
list.appendChild(renderItem(b.id,item));
}
wrap.appendChild(list);
}
reportBody.appendChild(wrap);
}
}
function renderItem(kind, item){
const d = document.createElement('div'); d.className='card'; d.style.margin='0';
const esc = escapeHTML;
if(kind==='processes'){
d.innerHTML = `<strong>${esc(item.name||'')}</strong><div class="muted">PID: ${item.pid} • Path: ${esc(item.path||'')}</div>`;
} else if(kind==='services'){
d.innerHTML = `<strong>${esc(item.displayName||item.name||'')}</strong><div class="muted">Start: ${esc(item.startType||'')} • Bin: ${esc(item.binaryPathName||'')}</div>`;
} else if(kind==='connections'){
d.innerHTML = `<strong>${esc(item.process||'')}</strong><div class="muted">${esc(item.localAddr||'')}${esc(item.remoteAddr||'')} (${esc(item.remoteHost||'')})</div>`;
} else if(kind==='autoruns'){
d.innerHTML = `<strong>${esc(item.name||item.imageName||item.entry||'')}</strong><div class="muted">${esc(item.location||item.type||'')}${esc(item.command||item.imagePath||item.launchString||item.launch_string||'')}</div>`;
} else if(kind==='tasks'){
d.innerHTML = `<strong>${esc(item.name||'')}</strong><div class="muted">State: ${esc(item.state||'')} • Next: ${esc(item.nextRun||'')}</div>`;
} else if(kind==='binaries'){
const path = item.path || item.Path || (typeof item === 'string' ? item : '');
d.innerHTML = `<div class="muted" style="font-size:13px">📄 Binary</div><strong>${esc(path)}</strong>`;
} else if(kind==='directories'){
const path = item.path || item.Path || (typeof item === 'string' ? item : '');
d.innerHTML = `<div class="muted" style="font-size:13px">📁 Directory</div><strong>${esc(path)}</strong>`;
}
return d;
}
function escapeHTML(s){ return (s||'').toString().replace(/[&<>"']/g, c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c])); }
function riskColor(r){ if(!r) return '#5a6'; r=(r+"").toLowerCase(); if(r==='high')return '#ff5c7a'; if(r==='medium')return '#ffd166'; if(r==='low')return '#17e46e'; return '#5a6'; }
// Typing animation function
function typeText(element, text, speed = 50) {
element.textContent = '';
element.classList.add('typing');
let i = 0;
return new Promise((resolve) => {
const interval = setInterval(() => {
if (i < text.length) {
element.textContent += text.charAt(i);
i++;
} else {
clearInterval(interval);
element.classList.remove('typing');
resolve();
}
}, speed);
});
}
// Hunt flow
document.getElementById('startHunt').addEventListener('click', async ()=>{
logEl.textContent=''; huntTag.classList.add('hidden');
let ws;
try{ ws = new WebSocket((location.protocol==='https:'?'wss':'ws')+'://'+location.host+'/ws/hunt');
ws.onmessage = ev => { logEl.textContent += ev.data + '\n'; logEl.scrollTop = logEl.scrollHeight; };
}catch(e){ console.error(e); }
const r = await fetch('/api/hunt/start',{method:'POST'});
const data = await r.json();
if(data && data.reportName){
huntTag.classList.remove('hidden');
await typeText(huntTag, 'Report: '+data.reportName+'.json', 30);
// Load report
const rep = await (await fetch('/api/report?file='+encodeURIComponent(data.reportName+'.json'))).json();
renderReport(rep); location.hash = '#report';
}
if(ws) ws.close();
listHunts();
});
document.getElementById('quitBtn').addEventListener('click', async ()=>{
await fetch('/api/quit',{method:'POST'});
});
listHunts();
// Elimination interface
let currentReport = null;
const elimTree = document.getElementById('elimTree');
const elimCenter = document.getElementById('elimCenter');
const elimWiki = document.getElementById('elimWiki');
// Order of operations for safe elimination
const eliminationOrder = {
connections: { order: 1, label: 'STEP 1: CRITICAL', color: 'order-1' },
processes: { order: 2, label: 'STEP 2: HIGH PRIORITY', color: 'order-2' },
services: { order: 2, label: 'STEP 2: HIGH PRIORITY', color: 'order-2' },
tasks: { order: 2, label: 'STEP 2: HIGH PRIORITY', color: 'order-2' },
autoruns: { order: 2, label: 'STEP 2: HIGH PRIORITY', color: 'order-2' },
binaries: { order: 3, label: 'STEP 3: CLEANUP', color: 'order-3' },
directories: { order: 3, label: 'STEP 3: CLEANUP', color: 'order-3' }
};
const wikiContent = {
connections: {
title: 'Outbound Network Connections',
description: 'Active network connections from your computer to remote servers. RMM software maintains persistent connections to allow remote access.',
whatItMeans: 'These are live communication channels between your computer and external servers. Blocking them prevents the RMM software from receiving commands or sending data.',
fields: {
localAddr: 'Your computer\'s IP address and port number',
remoteAddr: 'The remote server\'s IP address and port',
remoteHost: 'The domain name or hostname of the remote server',
state: 'Connection status (ESTABLISHED means actively connected)',
pid: 'Process ID of the program using this connection',
process: 'Name of the program maintaining this connection'
},
action: 'Creates a Windows Firewall rule to block all outbound traffic to the remote host. This immediately severs the connection and prevents reconnection.',
why: 'Always eliminate connections FIRST to prevent the RMM software from detecting removal attempts or receiving commands during cleanup.'
},
processes: {
title: 'Running Processes',
description: 'Programs currently executing in your computer\'s memory. RMM software runs as background processes to maintain functionality.',
whatItMeans: 'These are active programs running right now. They consume system resources and can perform actions on your computer.',
fields: {
name: 'The executable filename of the process',
pid: 'Unique Process ID assigned by Windows',
ppid: 'Parent Process ID (the process that started this one)',
parent: 'Name of the parent process',
path: 'Full file path to the executable on disk',
args: 'Command-line arguments passed to the process',
created: 'When this process was started'
},
action: 'Terminates the process immediately using its Process ID. This stops the program from running.',
why: 'Kill processes BEFORE deleting binaries. A running process locks its executable file, preventing deletion. Processes can also restart services or recreate files.'
},
services: {
title: 'Windows Services',
description: 'Background programs that start automatically with Windows and run without user interaction. RMM software often installs as a service for persistence.',
whatItMeans: 'Services are special programs that Windows manages. They start automatically and run in the background, even when no user is logged in.',
fields: {
name: 'Internal service name used by Windows',
displayName: 'User-friendly name shown in Services manager',
serviceType: 'How the service runs (own process vs shared)',
startType: 'When the service starts (Automatic, Manual, Disabled)',
binaryPathName: 'Full path to the service executable',
serviceStartName: 'Account the service runs under',
description: 'What the service claims to do'
},
action: 'Stops the service if running, then deletes it from the Windows Service Control Manager. This prevents it from starting again.',
why: 'Stop services BEFORE deleting their binaries. Services can restart processes and maintain persistence even after process termination.'
},
tasks: {
title: 'Scheduled Tasks',
description: 'Automated actions scheduled to run at specific times or events. RMM software uses scheduled tasks to restart itself or maintain persistence.',
whatItMeans: 'These are automated jobs that Windows runs on a schedule. They can restart programs, run scripts, or perform maintenance.',
fields: {
name: 'Name of the scheduled task',
author: 'Who created the task',
state: 'Current status (Ready, Running, Disabled)',
enabled: 'Whether the task is active',
path: 'Location in Task Scheduler hierarchy',
nextRun: 'When the task will execute next',
lastRun: 'When the task last executed',
lastResult: 'Exit code from last execution'
},
action: 'Disables the task and then deletes it from Windows Task Scheduler. This prevents scheduled execution.',
why: 'Remove scheduled tasks BEFORE binaries. Tasks can automatically restart processes or services, undoing your cleanup efforts.'
},
autoruns: {
title: 'AutoRun Entries (Startup Items)',
description: 'Registry entries and startup folders that cause programs to run automatically when Windows starts or a user logs in.',
whatItMeans: 'These are configuration entries that tell Windows to automatically start certain programs. They ensure the RMM software runs every time you boot your computer.',
fields: {
type: 'Category of autorun (Registry, Startup Folder, etc.)',
location: 'Specific registry key or folder path',
entry: 'Name of the autorun entry',
imagePath: 'Path to the executable that will run',
imageName: 'Filename of the executable',
launchString: 'Full command that will be executed',
arguments: 'Command-line parameters',
md5: 'MD5 hash of the executable (for verification)',
sha1: 'SHA1 hash of the executable',
sha256: 'SHA256 hash of the executable'
},
action: 'Removes the registry entry or startup folder item. This prevents the program from starting automatically.',
why: 'Delete autorun entries BEFORE binaries to prevent automatic restart on next boot. However, do this AFTER killing processes to avoid immediate restart attempts.'
},
binaries: {
title: 'Binary Files (Executables)',
description: 'Executable files (.exe, .dll) stored on your hard drive. These are the actual program files for the RMM software.',
whatItMeans: 'These are the program files themselves. Deleting them removes the software from your computer permanently.',
fields: {
path: 'Full file path to the executable on disk'
},
action: 'Permanently deletes the file from your hard drive. This cannot be undone without a backup.',
why: 'Delete binaries LAST. You must first kill all processes using them, stop services that reference them, and remove scheduled tasks that execute them. A file in use cannot be deleted.'
},
directories: {
title: 'Installation Directories',
description: 'Folders containing RMM software files, configuration, logs, and data. These are the installation directories.',
whatItMeans: 'These are folders that contain all the files related to the RMM software, including executables, configuration files, and data.',
fields: {
path: 'Full path to the directory'
},
action: 'Recursively deletes the entire directory and all its contents. This removes all files and subdirectories.',
why: 'Delete directories LAST. Ensure all processes, services, and scheduled tasks using files in these directories are eliminated first. Deleting a directory while files are in use will fail.'
}
};
function loadEliminationData(data) {
currentReport = data;
const findings = data.findings || data;
// Build tree structure with order of operations
const categories = [
{ key: 'connections', title: 'Outbound Connections', arr: findings.outboundConnections },
{ key: 'processes', title: 'Processes', arr: findings.processes },
{ key: 'services', title: 'Services', arr: findings.services },
{ key: 'tasks', title: 'Scheduled Tasks', arr: findings.scheduledTasks },
{ key: 'autoruns', title: 'AutoRuns', arr: findings.autoRuns || findings.autoruns },
{ key: 'binaries', title: 'Binaries', arr: findings.binaries },
{ key: 'directories', title: 'Directories', arr: findings.directories }
];
elimTree.innerHTML = '';
elimCenter.innerHTML = '<div class="empty-state">Select an item from the tree</div>';
elimWiki.innerHTML = '<div class="empty-state">Item details will appear here</div>';
const hasFindings = categories.some(cat => Array.isArray(cat.arr) && cat.arr.length > 0);
if (!hasFindings) {
elimTree.innerHTML = '<div class="empty-state">No findings to eliminate.<br><br>Run a hunt first or load a previous report.</div>';
return;
}
categories.forEach(cat => {
if (!Array.isArray(cat.arr) || cat.arr.length === 0) return;
const orderInfo = eliminationOrder[cat.key];
const node = document.createElement('div');
node.className = 'tree-node';
node.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center">
<span>▶ ${cat.title}</span>
<span class="tag">${cat.arr.length}</span>
</div>`;
const childContainer = document.createElement('div');
childContainer.style.display = 'none';
cat.arr.forEach((item, idx) => {
const child = document.createElement('div');
child.className = 'tree-child';
child.textContent = getItemLabel(cat.key, item, idx);
child.onclick = (e) => {
e.stopPropagation();
document.querySelectorAll('.tree-child').forEach(c => c.classList.remove('selected'));
child.classList.add('selected');
showItemDetails(cat.key, item, idx);
};
childContainer.appendChild(child);
});
node.onclick = () => {
const isExpanded = childContainer.style.display === 'block';
childContainer.style.display = isExpanded ? 'none' : 'block';
node.classList.toggle('expanded', !isExpanded);
node.querySelector('span').textContent = (isExpanded ? '▶ ' : '▼ ') + cat.title;
};
elimTree.appendChild(node);
elimTree.appendChild(childContainer);
});
}
function getItemLabel(type, item, idx) {
if (type === 'connections') return `${item.process || 'Unknown'}${item.remoteHost || item.remoteAddr}`;
if (type === 'processes') return `${item.name} (PID ${item.pid})`;
if (type === 'services') return item.displayName || item.name;
if (type === 'tasks') return item.name;
if (type === 'autoruns') return item.imageName || item.entry || `Entry ${idx + 1}`;
if (type === 'binaries') return item.path || item;
if (type === 'directories') return item.path || item;
return `Item ${idx + 1}`;
}
function showItemDetails(type, item, idx) {
const orderInfo = eliminationOrder[type];
const wiki = wikiContent[type];
// Center panel - item details
elimCenter.innerHTML = `
<div class="order-badge ${orderInfo.color}">${orderInfo.label}</div>
<h3 style="margin:0 0 16px 0;color:var(--text)">${getItemLabel(type, item, idx)}</h3>
${renderItemFields(type, item, wiki)}
<button class="elim-btn" onclick="eliminateItem('${type}', ${idx})">
<span class="elim-btn-icon">⚠</span>
<span>Eliminate This Item</span>
</button>
`;
// Right panel - wiki
elimWiki.innerHTML = `
<div class="wiki-section">
<div class="wiki-title">${wiki.title}</div>
<div class="wiki-text">${wiki.description}</div>
</div>
<div class="wiki-section">
<div class="wiki-title">What This Means</div>
<div class="wiki-text">${wiki.whatItMeans}</div>
</div>
<div class="wiki-section">
<div class="wiki-title">Field Explanations</div>
${Object.entries(wiki.fields).map(([key, desc]) =>
`<div style="margin:8px 0"><strong style="color:var(--accent);font-size:12px">${key}:</strong> <span class="wiki-text">${desc}</span></div>`
).join('')}
</div>
<div class="wiki-section">
<div class="wiki-title">What Elimination Does</div>
<div class="wiki-text">${wiki.action}</div>
</div>
<div class="wiki-section">
<div class="wiki-title" style="color:${orderInfo.color === 'order-1' ? '#ff5c7a' : orderInfo.color === 'order-2' ? '#ffd166' : '#17e46e'}">Why This Order?</div>
<div class="wiki-text">${wiki.why}</div>
</div>
`;
}
function renderItemFields(type, item, wiki) {
const fields = Object.keys(wiki.fields);
return fields.map(field => {
let value = item[field];
if (value === undefined || value === null || value === '') return '';
if (typeof value === 'boolean') value = value ? 'Yes' : 'No';
return `
<div class="detail-field">
<div class="detail-label">${field}</div>
<div class="detail-value">${escapeHTML(String(value))}</div>
</div>
`;
}).join('');
}
window.eliminateItem = function(type, idx) {
if (!confirm(`Are you sure you want to eliminate this ${type.slice(0, -1)}?\n\nThis action cannot be undone.`)) return;
alert('Elimination functionality requires backend API implementation.\n\nFor now, please use the CLI: rmm-hunter eliminate --cli');
// TODO: Implement API call to backend elimination endpoint
};
// Update renderReport to also load elimination data
const originalRenderReport = renderReport;
renderReport = function(rep) {
originalRenderReport(rep);
loadEliminationData(rep);
};
})();
</script>
</body>
</html>