1184 lines
61 KiB
HTML
1184 lines
61 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}
|
|
html{background:var(--bg)}
|
|
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}
|
|
|
|
/* 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))}
|
|
.active-report-indicator{display:flex;align-items:center;gap:8px;background:rgba(23,228,110,.08);border:1px solid rgba(23,228,110,.2);border-radius:8px;padding:8px 14px;margin-left:16px;transition:all 0.3s ease}
|
|
.active-report-indicator .report-icon{font-size:16px}
|
|
.active-report-indicator .report-name{font-size:13px;color:var(--accent);font-weight:500;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.active-report-indicator:hover{background:rgba(23,228,110,.12);border-color: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}
|
|
.expandable-item{transition:all 0.2s ease;position:relative;padding-right:40px}
|
|
.expandable-item:hover{border-color:var(--accent);background:#0e1a13}
|
|
.expandable-item::after{content:'▼';position:absolute;right:20px;top:20px;color:var(--muted);font-size:12px;transition:transform 0.2s}
|
|
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;position:relative;overflow:hidden}
|
|
.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}
|
|
.tree-child.eliminating{animation:slideOutRight 0.6s ease-in-out forwards}
|
|
.tree-child.eliminating::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(90deg,transparent,rgba(23,228,110,.3),transparent);animation:shimmer 0.6s ease-in-out}
|
|
.tree-child.eliminated{display:none;opacity:0.4;filter:grayscale(1);text-decoration:line-through;pointer-events:none}
|
|
.tree-child.eliminated::before{content:'✓ ';color:#17e46e;opacity:1}
|
|
.show-eliminated .tree-child.eliminated{display:flex}
|
|
@keyframes slideOutRight{0%{transform:translateX(0);opacity:1}50%{opacity:0.5}100%{transform:translateX(100%);opacity:0;height:0;margin:0;padding:0}}
|
|
@keyframes shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
|
|
.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%}}
|
|
|
|
/* Shutdown Screen */
|
|
#shutdownScreen{position:fixed;top:0;left:0;right:0;bottom:0;background:radial-gradient(circle at center,#0f1511,#0b0f0c,#000);z-index:10000;display:none;flex-direction:column;align-items:center;justify-content:center}
|
|
#shutdownScreen.active{display:flex}
|
|
.spinner{width:80px;height:80px;border:4px solid rgba(23,228,110,.1);border-top:4px solid var(--accent);border-radius:50%;animation:spin 1s linear infinite;margin-bottom:30px}
|
|
.spinner.hide{display:none}
|
|
@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}
|
|
.shutdown-title{font-size:28px;font-weight:700;color:var(--accent);margin-bottom:12px;letter-spacing:1.5px}
|
|
.shutdown-message{font-size:16px;color:var(--muted);margin-bottom:20px}
|
|
.shutdown-success{font-size:18px;color:var(--accent);font-weight:600;display:none}
|
|
.shutdown-success.show{display:block}
|
|
|
|
/* Modal */
|
|
.modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.8);z-index:10001;display:none;align-items:center;justify-content:center}
|
|
.modal.active{display:flex}
|
|
.modal-content{background:var(--panel);border:1px solid #133422;border-radius:12px;padding:30px;max-width:500px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,.5)}
|
|
.modal-icon{font-size:48px;text-align:left;margin-bottom:16px;display:block}
|
|
.modal-icon.success{color:var(--accent);animation:checkPop 0.5s ease-out}
|
|
.modal-icon.error{color:#ff5c7a}
|
|
@keyframes checkPop{0%{transform:scale(0)}50%{transform:scale(1.2)}100%{transform:scale(1)}}
|
|
.modal-title{font-size:24px;font-weight:700;margin-bottom:16px;text-align:left}
|
|
.modal-title.success{color:var(--accent)}
|
|
.modal-title.error{color:#ff5c7a}
|
|
.modal-title.confirm{color:#ffd166}
|
|
.modal-message{color:var(--text);margin-bottom:24px;line-height:1.6;white-space:pre-wrap;font-size:15px;text-align:left}
|
|
.modal-buttons{display:flex;gap:12px;margin-top:24px}
|
|
.modal-btn{flex:1;padding:12px 24px;border:1px solid #1d4a2f;background:var(--panel);color:var(--text);font-weight:500;font-size:14px;cursor:pointer;transition:all 0.2s;border-radius:0}
|
|
.modal-btn:hover{background:#0e1a13;border-color:var(--accent)}
|
|
.modal-btn.primary{background:var(--accent);color:#000;border-color:var(--accent)}
|
|
.modal-btn.primary:hover{filter:brightness(1.2)}
|
|
.modal-btn.danger{background:rgba(255,92,122,.15);color:#ff5c7a;border-color:#ff5c7a}
|
|
.modal-btn.danger:hover{background:rgba(255,92,122,.25)}
|
|
.modal-icon.confirm{color:#ffd166;text-align:left}
|
|
</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>
|
|
|
|
<!-- Shutdown Screen -->
|
|
<div id="shutdownScreen">
|
|
<div class="spinner"></div>
|
|
<div class="shutdown-title">Shutting Down</div>
|
|
<div class="shutdown-message">RMM Hunter is closing...</div>
|
|
<div class="shutdown-success">✓ You can now close this browser tab</div>
|
|
</div>
|
|
|
|
<!-- Modal -->
|
|
<div id="modal" class="modal">
|
|
<div class="modal-content">
|
|
<div id="modalIcon" class="modal-icon"></div>
|
|
<div id="modalTitle" class="modal-title"></div>
|
|
<div id="modalMessage" class="modal-message"></div>
|
|
<div id="modalButtons" class="modal-buttons"></div>
|
|
</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 id="activeReportIndicator" class="active-report-indicator hidden">
|
|
<span class="report-icon">📄</span>
|
|
<span id="activeReportName" class="report-name">No report loaded</span>
|
|
</div>
|
|
<div class="spacer"></div>
|
|
<a href="#hunt" class="btn primary">Hunt</a>
|
|
<a href="#previous" class="btn">Previous Hunts</a>
|
|
<a id="reportNavBtn" href="#report" class="btn hidden">Report</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>
|
|
<button id="viewReportBtn" class="btn hidden" style="margin-left:12px">View Report</button>
|
|
</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">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
|
<h2 style="margin:0">Eliminate Detected Items</h2>
|
|
<button id="toggleEliminatedBtn" class="btn" style="font-size:13px;padding:8px 16px">
|
|
<span id="toggleEliminatedText">Show Eliminated Items</span>
|
|
</button>
|
|
</div>
|
|
<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); // Display splash for 1.5 seconds, give the// server time to start
|
|
});
|
|
|
|
const logEl = document.getElementById('log');
|
|
const huntsEl = document.getElementById('hunts');
|
|
const huntTag = document.getElementById('huntTag');
|
|
const viewReportBtn = document.getElementById('viewReportBtn');
|
|
const reportNavBtn = document.getElementById('reportNavBtn');
|
|
const activeReportIndicator = document.getElementById('activeReportIndicator');
|
|
const activeReportName = document.getElementById('activeReportName');
|
|
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 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);
|
|
|
|
// Set active report indicator
|
|
setActiveReport(file);
|
|
|
|
location.hash = '#report';
|
|
}
|
|
|
|
function setActiveReport(reportFileName) {
|
|
if (reportFileName) {
|
|
activeReportIndicator.classList.remove('hidden');
|
|
activeReportName.textContent = reportFileName;
|
|
reportNavBtn.classList.remove('hidden');
|
|
} else {
|
|
activeReportIndicator.classList.add('hidden');
|
|
activeReportName.textContent = 'No report loaded';
|
|
reportNavBtn.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
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 style="display:flex;align-items:center;gap:12px">
|
|
${b.title}
|
|
<span style="color:var(--muted);font-size:14px;font-weight:400">(${count})</span>
|
|
<span style="cursor:help;color:var(--accent);font-size:14px" 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 expandable-item';
|
|
d.style.margin='0';
|
|
d.style.cursor='pointer';
|
|
|
|
const esc = escapeHTML;
|
|
let summary = '';
|
|
let details = '';
|
|
|
|
if(kind==='processes'){
|
|
summary = `<strong>${esc(item.name||'')}</strong><div class="muted">PID: ${item.pid} • ${esc(item.path||'')}</div>`;
|
|
details = `
|
|
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
|
|
<div style="display:grid;gap:6px;font-size:13px">
|
|
<div><span style="color:var(--muted)">Name:</span> ${esc(item.name||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">PID:</span> ${item.pid||'N/A'}</div>
|
|
<div><span style="color:var(--muted)">PPID:</span> ${item.ppid||'N/A'}</div>
|
|
<div><span style="color:var(--muted)">Parent:</span> ${esc(item.parent||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Path:</span> ${esc(item.path||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Arguments:</span> ${esc(item.args||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Created:</span> ${esc(item.created||'N/A')}</div>
|
|
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated</div>' : ''}
|
|
</div>
|
|
</div>`;
|
|
} else if(kind==='services'){
|
|
summary = `<strong>${esc(item.displayName||item.name||'')}</strong><div class="muted">${esc(item.startType||'')} • ${esc(item.binaryPathName||'')}</div>`;
|
|
details = `
|
|
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
|
|
<div style="display:grid;gap:6px;font-size:13px">
|
|
<div><span style="color:var(--muted)">Display Name:</span> ${esc(item.displayName||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Service Name:</span> ${esc(item.name||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Service Type:</span> ${esc(item.serviceType||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Start Type:</span> ${esc(item.startType||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Binary Path:</span> ${esc(item.binaryPathName||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Description:</span> ${esc(item.description||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Error Control:</span> ${esc(item.errorControl||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Load Order Group:</span> ${esc(item.loadOrderGroup||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Service Start Name:</span> ${esc(item.serviceStartName||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Delayed Auto Start:</span> ${item.delayedAutoStart ? 'Yes' : 'No'}</div>
|
|
<div><span style="color:var(--muted)">Dependencies:</span> ${item.dependencies && item.dependencies.length > 0 ? esc(item.dependencies.join(', ')) : 'None'}</div>
|
|
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated</div>' : ''}
|
|
</div>
|
|
</div>`;
|
|
} else if(kind==='connections'){
|
|
summary = `<strong>${esc(item.process||'')}</strong><div class="muted">${esc(item.localAddr||'')} → ${esc(item.remoteAddr||'')} (${esc(item.remoteHost||'')})</div>`;
|
|
details = `
|
|
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
|
|
<div style="display:grid;gap:6px;font-size:13px">
|
|
<div><span style="color:var(--muted)">Process:</span> ${esc(item.process||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">PID:</span> ${item.pid||'N/A'}</div>
|
|
<div><span style="color:var(--muted)">Local Address:</span> ${esc(item.localAddr||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Remote Address:</span> ${esc(item.remoteAddr||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Remote Host:</span> ${esc(item.remoteHost||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">State:</span> ${esc(item.state||'N/A')}</div>
|
|
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated (Firewall Rule)</div>' : ''}
|
|
</div>
|
|
</div>`;
|
|
} else if(kind==='autoruns'){
|
|
const displayName = item.image_name || item.imageName || item.entry || 'Unknown AutoRun';
|
|
const launchStr = item.launch_string || item.launchString || item.imagePath || item.image_path || '';
|
|
summary = `<strong>${esc(displayName)}</strong><div class="muted">${esc(item.type||'')} • ${esc(item.location||'')}</div>`;
|
|
details = `
|
|
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
|
|
<div style="display:grid;gap:6px;font-size:13px">
|
|
<div><span style="color:var(--muted)">Image Name:</span> ${esc(item.image_name||item.imageName||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Entry:</span> ${esc(item.entry||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Type:</span> ${esc(item.type||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Location:</span> ${esc(item.location||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Image Path:</span> ${esc(item.image_path||item.imagePath||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Launch String:</span> ${esc(launchStr||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Arguments:</span> ${esc(item.arguments||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">MD5:</span> ${esc(item.md5||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">SHA1:</span> ${esc(item.sha1||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">SHA256:</span> ${esc(item.sha256||'N/A')}</div>
|
|
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated</div>' : ''}
|
|
</div>
|
|
</div>`;
|
|
} else if(kind==='tasks'){
|
|
summary = `<strong>${esc(item.name||'')}</strong><div class="muted">${esc(item.state||'')} • ${esc(item.path||'')}</div>`;
|
|
details = `
|
|
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
|
|
<div style="display:grid;gap:6px;font-size:13px">
|
|
<div><span style="color:var(--muted)">Name:</span> ${esc(item.name||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Path:</span> ${esc(item.path||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">State:</span> ${esc(item.state||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Enabled:</span> ${item.enabled ? 'Yes' : 'No'}</div>
|
|
<div><span style="color:var(--muted)">Author:</span> ${esc(item.author||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Description:</span> ${esc(item.description||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Next Run:</span> ${esc(item.nextRun||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Last Run:</span> ${esc(item.lastRun||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Last Result:</span> ${esc(item.lastResult||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Created:</span> ${esc(item.createdDate||'N/A')}</div>
|
|
<div><span style="color:var(--muted)">Modified:</span> ${esc(item.modifiedDate||'N/A')}</div>
|
|
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated</div>' : ''}
|
|
</div>
|
|
</div>`;
|
|
} else if(kind==='binaries'){
|
|
const path = item.path || item.Path || (typeof item === 'string' ? item : '');
|
|
summary = `<div class="muted" style="font-size:13px">📄 Binary</div><strong>${esc(path)}</strong>`;
|
|
details = `
|
|
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
|
|
<div style="display:grid;gap:6px;font-size:13px">
|
|
<div><span style="color:var(--muted)">Path:</span> ${esc(path)}</div>
|
|
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated</div>' : ''}
|
|
</div>
|
|
</div>`;
|
|
} else if(kind==='directories'){
|
|
const path = item.path || item.Path || (typeof item === 'string' ? item : '');
|
|
summary = `<div class="muted" style="font-size:13px">📁 Directory</div><strong>${esc(path)}</strong>`;
|
|
details = `
|
|
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
|
|
<div style="display:grid;gap:6px;font-size:13px">
|
|
<div><span style="color:var(--muted)">Path:</span> ${esc(path)}</div>
|
|
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated</div>' : ''}
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
d.innerHTML = `
|
|
<div class="item-summary">${summary}</div>
|
|
<div class="item-details" style="display:none">${details}</div>
|
|
`;
|
|
|
|
// Toggle expand/collapse on click
|
|
d.addEventListener('click', () => {
|
|
const detailsEl = d.querySelector('.item-details');
|
|
const isExpanded = detailsEl.style.display !== 'none';
|
|
|
|
if (isExpanded) {
|
|
detailsEl.style.display = 'none';
|
|
} else {
|
|
detailsEl.style.display = 'block';
|
|
}
|
|
});
|
|
|
|
return d;
|
|
}
|
|
|
|
function escapeHTML(s){ return (s||'').toString().replace(/[&<>"']/g, c=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[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'; }
|
|
|
|
// Type 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
|
|
let lastReportData = null;
|
|
let currentReportName = null;
|
|
|
|
document.getElementById('startHunt').addEventListener('click', async ()=>{
|
|
logEl.textContent='';
|
|
huntTag.classList.add('hidden');
|
|
viewReportBtn.classList.add('hidden');
|
|
currentReportName = null;
|
|
|
|
let ws;
|
|
let wsReady = false;
|
|
let huntComplete = false;
|
|
|
|
try{
|
|
ws = new WebSocket((location.protocol==='https:'?'wss':'ws')+'://'+location.host+'/ws/hunt');
|
|
ws.onopen = () => { wsReady = true; };
|
|
ws.onmessage = ev => {
|
|
logEl.textContent += ev.data + '\n';
|
|
logEl.scrollTop = logEl.scrollHeight;
|
|
|
|
// Check if hunt is complete
|
|
if (ev.data.includes('[+] Hunt complete')) {
|
|
huntComplete = true;
|
|
}
|
|
};
|
|
}catch(e){ console.error(e); }
|
|
|
|
// Wait for WebSocket to be ready before starting hunt
|
|
while(!wsReady && ws && ws.readyState !== WebSocket.OPEN) {
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
}
|
|
|
|
// Give it a tiny bit more time to ensure connection is stable
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
const r = await fetch('/api/hunt/start',{method:'POST'});
|
|
const data = await r.json();
|
|
|
|
if(data && data.reportName){
|
|
currentReportName = data.reportName;
|
|
|
|
// Wait for hunt to actually complete
|
|
while(!huntComplete) {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
}
|
|
|
|
huntTag.classList.remove('hidden');
|
|
await typeText(huntTag, 'Report: '+data.reportName+'.json', 30);
|
|
|
|
// Load report
|
|
const reportFileName = data.reportName+'.json';
|
|
const rep = await (await fetch('/api/report?file='+encodeURIComponent(reportFileName))).json();
|
|
lastReportData = rep;
|
|
renderReport(rep);
|
|
|
|
// Set active report indicator
|
|
setActiveReport(reportFileName);
|
|
|
|
// Show View Report button AFTER everything is loaded
|
|
viewReportBtn.classList.remove('hidden');
|
|
}
|
|
if(ws) ws.close();
|
|
listHunts();
|
|
});
|
|
|
|
// View Report button handler
|
|
viewReportBtn.addEventListener('click', () => {
|
|
if (lastReportData) {
|
|
location.hash = '#report';
|
|
}
|
|
});
|
|
|
|
document.getElementById('quitBtn').addEventListener('click', async ()=>{
|
|
// Show shutdown screen
|
|
const shutdownScreen = document.getElementById('shutdownScreen');
|
|
const spinner = shutdownScreen.querySelector('.spinner');
|
|
const shutdownSuccess = shutdownScreen.querySelector('.shutdown-success');
|
|
shutdownScreen.classList.add('active');
|
|
|
|
// Send quit request
|
|
try {
|
|
await fetch('/api/quit',{method:'POST'});
|
|
} catch(e) {
|
|
// Server is shutting down, connection will be lost
|
|
}
|
|
|
|
// After 2 seconds, hide spinner and show success message
|
|
setTimeout(() => {
|
|
spinner.classList.add('hide');
|
|
shutdownSuccess.classList.add('show');
|
|
}, 2000);
|
|
});
|
|
|
|
listHunts();
|
|
|
|
// Elimination interface
|
|
let currentReport = null;
|
|
let showEliminatedItems = false;
|
|
const elimTree = document.getElementById('elimTree');
|
|
const elimCenter = document.getElementById('elimCenter');
|
|
const elimWiki = document.getElementById('elimWiki');
|
|
const toggleEliminatedBtn = document.getElementById('toggleEliminatedBtn');
|
|
const toggleEliminatedText = document.getElementById('toggleEliminatedText');
|
|
|
|
// Toggle eliminated items visibility
|
|
toggleEliminatedBtn.addEventListener('click', () => {
|
|
showEliminatedItems = !showEliminatedItems;
|
|
elimTree.classList.toggle('show-eliminated', showEliminatedItems);
|
|
toggleEliminatedText.textContent = showEliminatedItems ? 'Hide Eliminated Items' : 'Show Eliminated Items';
|
|
});
|
|
|
|
// 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;
|
|
console.log('Loading elimination data, findings:', findings);
|
|
|
|
// Build tree structure using 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 }
|
|
];
|
|
console.log('Categories:', categories);
|
|
|
|
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 activeCount = cat.arr.filter(item => !item.eliminated).length;
|
|
const totalCount = cat.arr.length;
|
|
|
|
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">${activeCount}${activeCount !== totalCount ? ` / ${totalCount}` : ''}</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.dataset.type = cat.key;
|
|
child.dataset.index = idx;
|
|
|
|
// Mark as eliminated if the item has eliminated flag
|
|
if (item.eliminated) {
|
|
child.classList.add('eliminated');
|
|
}
|
|
|
|
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.image_name || item.imageName || item.entry || 'Unknown AutoRun';
|
|
if (type === 'binaries') return item.path || item;
|
|
if (type === 'directories') return item.path || item;
|
|
return `Item ${idx + 1}`;
|
|
}
|
|
|
|
function updateCategoryCount(type) {
|
|
// Find the category node and update its count
|
|
const categoryTitles = {
|
|
'connections': 'Outbound Connections',
|
|
'processes': 'Processes',
|
|
'services': 'Services',
|
|
'tasks': 'Scheduled Tasks',
|
|
'autoruns': 'AutoRuns',
|
|
'binaries': 'Binaries',
|
|
'directories': 'Directories'
|
|
};
|
|
|
|
const categoryTitle = categoryTitles[type];
|
|
const treeNodes = elimTree.querySelectorAll('.tree-node');
|
|
|
|
treeNodes.forEach(node => {
|
|
if (node.textContent.includes(categoryTitle)) {
|
|
const childContainer = node.nextElementSibling;
|
|
if (childContainer) {
|
|
const allChildren = Array.from(childContainer.querySelectorAll('.tree-child'));
|
|
const activeCount = allChildren.filter(child => !child.classList.contains('eliminated')).length;
|
|
const totalCount = allChildren.length;
|
|
|
|
const tagElement = node.querySelector('.tag');
|
|
if (tagElement) {
|
|
tagElement.textContent = `${activeCount}${activeCount !== totalCount ? ` / ${totalCount}` : ''}`;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function showItemDetails(type, item, idx) {
|
|
const orderInfo = eliminationOrder[type];
|
|
const wiki = wikiContent[type];
|
|
|
|
// | |x| | 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>
|
|
`;
|
|
|
|
// | | |x| - 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) {
|
|
// Get all fields from the item, not just wiki fields
|
|
const allFields = new Set();
|
|
|
|
// Add wiki fields first (for ordering)
|
|
Object.keys(wiki.fields).forEach(f => allFields.add(f));
|
|
|
|
// Add all actual item fields (handle snake_case variants)
|
|
Object.keys(item).forEach(key => {
|
|
if (key !== 'eliminated') { // Skip eliminated flag
|
|
allFields.add(key);
|
|
// Also add camelCase variant if this is snake_case
|
|
if (key.includes('_')) {
|
|
const camelCase = key.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
|
|
allFields.add(camelCase);
|
|
}
|
|
}
|
|
});
|
|
|
|
return Array.from(allFields).map(field => {
|
|
// Try both snake_case and camelCase
|
|
let value = item[field];
|
|
if (value === undefined || value === null) {
|
|
const snakeCase = field.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
value = item[snakeCase];
|
|
}
|
|
|
|
if (value === undefined || value === null || value === '') return '';
|
|
|
|
// Handle arrays
|
|
if (Array.isArray(value)) {
|
|
value = value.length > 0 ? value.join(', ') : 'None';
|
|
}
|
|
|
|
// Handle booleans
|
|
if (typeof value === 'boolean') value = value ? 'Yes' : 'No';
|
|
|
|
// Format field name for display
|
|
const displayName = field.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim()
|
|
.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
|
|
return `
|
|
<div class="detail-field">
|
|
<div class="detail-label">${displayName}</div>
|
|
<div class="detail-value">${escapeHTML(String(value))}</div>
|
|
</div>
|
|
`;
|
|
}).filter(x => x).join('');
|
|
}
|
|
|
|
// Modal functions
|
|
const modal = document.getElementById('modal');
|
|
const modalIcon = document.getElementById('modalIcon');
|
|
const modalTitle = document.getElementById('modalTitle');
|
|
const modalMessage = document.getElementById('modalMessage');
|
|
const modalButtons = document.getElementById('modalButtons');
|
|
|
|
function showModal(type, title, message, onConfirm = null) {
|
|
modal.classList.add('active');
|
|
modalIcon.className = `modal-icon ${type}`;
|
|
modalTitle.className = `modal-title ${type}`;
|
|
modalTitle.textContent = title;
|
|
modalMessage.textContent = message;
|
|
|
|
if (type === 'success') {
|
|
modalIcon.textContent = '✓';
|
|
} else if (type === 'error') {
|
|
modalIcon.textContent = '✕';
|
|
} else if (type === 'confirm') {
|
|
modalIcon.textContent = '⚠';
|
|
}
|
|
|
|
// Clear existing buttons
|
|
modalButtons.innerHTML = '';
|
|
|
|
if (type === 'confirm' && onConfirm) {
|
|
// Confirmation modal with Cancel and Eliminate buttons
|
|
const cancelBtn = document.createElement('button');
|
|
cancelBtn.className = 'modal-btn';
|
|
cancelBtn.textContent = 'Cancel';
|
|
cancelBtn.onclick = () => {
|
|
modal.classList.remove('active');
|
|
};
|
|
|
|
const confirmBtn = document.createElement('button');
|
|
confirmBtn.className = 'modal-btn danger';
|
|
confirmBtn.textContent = 'Yes, Eliminate This Item';
|
|
confirmBtn.onclick = () => {
|
|
modal.classList.remove('active');
|
|
onConfirm();
|
|
};
|
|
|
|
modalButtons.appendChild(cancelBtn);
|
|
modalButtons.appendChild(confirmBtn);
|
|
} else {
|
|
// Regular modal with just OK button
|
|
const okBtn = document.createElement('button');
|
|
okBtn.className = `modal-btn primary`;
|
|
okBtn.textContent = 'OK';
|
|
okBtn.onclick = () => {
|
|
modal.classList.remove('active');
|
|
};
|
|
modalButtons.appendChild(okBtn);
|
|
}
|
|
}
|
|
|
|
window.eliminateItem = async function(type, idx) {
|
|
if (!currentReport) {
|
|
showModal('error', 'Error', 'No report loaded');
|
|
return;
|
|
}
|
|
|
|
// Show custom confirmation modal
|
|
const typeName = type.slice(0, -1);
|
|
showModal('confirm', 'Confirm Elimination',
|
|
`Are you sure you want to eliminate this ${typeName}?\n\nThis action cannot be undone and will permanently remove it from your system.`,
|
|
async () => {
|
|
// User confirmed, proceed with elimination
|
|
await performElimination(type, idx);
|
|
}
|
|
);
|
|
};
|
|
|
|
async function performElimination(type, idx) {
|
|
// Get the report filename from the current report
|
|
let reportFile = currentReport.reportName || currentReport.name || 'rmm-hunter-report.json';
|
|
// Ensure .json extension
|
|
if (!reportFile.endsWith('.json')) {
|
|
reportFile += '.json';
|
|
}
|
|
|
|
// Find the tree child element for this item using data attributes
|
|
const treeChildren = document.querySelectorAll('.tree-child');
|
|
let targetElement = null;
|
|
|
|
// Find the element by matching the data attributes
|
|
treeChildren.forEach((child) => {
|
|
if (child.dataset.type === type && parseInt(child.dataset.index) === idx) {
|
|
targetElement = child;
|
|
}
|
|
});
|
|
|
|
try {
|
|
const response = await fetch('/api/eliminate', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
reportFile: reportFile,
|
|
type: type,
|
|
index: idx
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok) {
|
|
showModal('error', 'Elimination Failed', result.error || 'Unknown error occurred');
|
|
return;
|
|
}
|
|
|
|
// Success! Trigger slide-out animation and mark as eliminated
|
|
if (targetElement) {
|
|
targetElement.classList.add('eliminating');
|
|
|
|
// Wait for animation to complete before hiding
|
|
setTimeout(() => {
|
|
// Mark as eliminated (hidden by default, shown in greyscale if toggle is on)
|
|
targetElement.classList.remove('eliminating');
|
|
targetElement.classList.add('eliminated');
|
|
|
|
// Update the item in currentReport to mark it as eliminated
|
|
if (currentReport && currentReport.findings) {
|
|
const findings = currentReport.findings;
|
|
const typeMap = {
|
|
'connections': 'outboundConnections',
|
|
'processes': 'processes',
|
|
'services': 'services',
|
|
'tasks': 'scheduledTasks',
|
|
'autoruns': 'autoRuns',
|
|
'binaries': 'binaries',
|
|
'directories': 'directories'
|
|
};
|
|
const arrayKey = typeMap[type];
|
|
if (findings[arrayKey] && findings[arrayKey][idx]) {
|
|
findings[arrayKey][idx].eliminated = true;
|
|
}
|
|
}
|
|
|
|
// Update the category count badge
|
|
updateCategoryCount(type);
|
|
|
|
// Show success modal
|
|
showModal('success', 'Successfully Eliminated', `The ${type.slice(0, -1)} has been removed from your system.`);
|
|
|
|
// Clear the center and wiki panels
|
|
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>';
|
|
}, 600); // Match animation duration
|
|
} else {
|
|
// Fallback if element not found - just show success
|
|
showModal('success', 'Successfully Eliminated', `The ${type.slice(0, -1)} has been removed from your system.`);
|
|
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>';
|
|
}
|
|
|
|
} catch (error) {
|
|
showModal('error', 'Error', `Failed to eliminate: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Update renderReport to also load elimination data
|
|
const originalRenderReport = renderReport;
|
|
renderReport = function(rep) {
|
|
originalRenderReport(rep);
|
|
loadEliminationData(rep);
|
|
};
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|