2025-10-12 18:53:07 -04:00
<!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 >
2025-10-12 20:02:49 -04:00
< 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" >
2025-10-12 18:53:07 -04:00
< style >
: root {
--bg : #0b0f0c ; --bg2 : #111612 ; --panel : #0f1511 ; --accent : #17e46e ; --accent2 : #0eea5a ; --muted : #a7b5a9 ; --text : #e6f4ea ; --danger : #ff5c7a ; --warn : #ffd166 ;
}
* { box-sizing : border-box }
2025-10-12 20:02:49 -04:00
body { margin : 0 ; font-family : Inter , system-ui , Segoe UI , Roboto , Helvetica , Arial , sans-serif ; background : linear-gradient ( 180 deg , var ( - - bg ) , #070a08 ) ; color : var ( - - text ) ; min-height : 100 vh ; display : flex ; flex-direction : column }
2025-10-12 20:07:16 -04:00
/* Scrollbar styling */
2025-10-12 20:02:49 -04:00
* :: -webkit-scrollbar { width : 12 px ; height : 12 px }
* :: -webkit-scrollbar-track { background : #050805 ; border-radius : 10 px }
* :: -webkit-scrollbar-thumb { background : #124b2b ; border-radius : 10 px ; border : 2 px solid #050805 }
* :: -webkit-scrollbar-thumb : hover { background : #17e46e }
* :: -webkit-scrollbar-corner { background : #050805 }
/* Firefox scrollbar */
* { scrollbar-width : thin ; scrollbar-color : #124b2b #050805 }
2025-10-12 18:53:07 -04:00
a { color : var ( - - accent ) }
2025-10-12 20:02:49 -04:00
header { position : sticky ; top : 0 ; z-index : 10 ; backdrop-filter : blur ( 12 px ) ; background : rgba ( 11 , 15 , 12 , .95 ) ; border-bottom : 1 px solid rgba ( 23 , 228 , 110 , .15 ) ; box-shadow : 0 4 px 20 px rgba ( 0 , 0 , 0 , .3 ) }
. nav { max-width : 1400 px ; margin : 0 auto ; display : flex ; align-items : center ; gap : 8 px ; padding : 12 px 24 px }
. brand { display : flex ; align-items : center ; gap : 12 px ; font-weight : 700 ; letter-spacing : .5 px ; font-size : 16 px ; color : var ( - - text ) }
. brand img { width : 40 px ; height : 40 px ; object-fit : contain ; filter : drop-shadow ( 0 0 8 px rgba ( 23 , 228 , 110 , .3 ) ) }
2025-10-12 18:53:07 -04:00
. spacer { flex : 1 }
2025-10-12 20:02:49 -04:00
. nav a . btn { display : inline-flex ; align-items : center ; padding : 10 px 18 px ; border : none ; border-radius : 6 px ; color : var ( - - text ) ; text-decoration : none ; transition : all .2 s ; font-weight : 500 ; font-size : 14 px ; position : relative ; overflow : hidden }
. nav a . btn :: before { content : '' ; position : absolute ; top : 0 ; left : 0 ; right : 0 ; bottom : 0 ; background : linear-gradient ( 135 deg , rgba ( 23 , 228 , 110 , .1 ) , rgba ( 23 , 228 , 110 , .05 ) ) ; opacity : 0 ; transition : opacity .2 s }
. nav a . btn : hover :: before { opacity : 1 }
. nav a . btn : hover { background : rgba ( 23 , 228 , 110 , .08 ) ; transform : translateY ( -1 px ) }
. nav a . primary { background : linear-gradient ( 135 deg , #17e46e , #0eea5a ) ; color : #000 ; font-weight : 600 ; box-shadow : 0 4 px 12 px rgba ( 23 , 228 , 110 , .25 ) }
. nav a . primary : hover { box-shadow : 0 6 px 20 px rgba ( 23 , 228 , 110 , .4 ) ; transform : translateY ( -2 px ) }
. nav a . danger { background : rgba ( 255 , 92 , 122 , .1 ) ; color : #ff5c7a }
. nav a . danger : hover { background : rgba ( 255 , 92 , 122 , .2 ) }
main { max-width : 1100 px ; margin : 20 px auto ; padding : 0 16 px ; flex : 1 ; width : 100 % }
main . full-width { max-width : none ; padding : 0 20 px }
. card { background : var ( - - panel ) ; border : 1 px solid #133422 ; border-radius : 12 px ; padding : 16 px ; margin-bottom : 16 px ; box-shadow : 0 8 px 24 px rgba ( 0 , 0 , 0 , .25 ) ; overflow-wrap : break-word ; word-wrap : break-word ; word-break : break-word ; overflow : hidden }
2025-10-12 18:53:07 -04:00
h1 , h2 { margin : 10 px 0 }
. muted { color : var ( - - muted ) }
2025-10-12 20:02:49 -04:00
. grid { display : grid ; gap : 12 px ; grid-template-columns : repeat ( auto - fill , minmax ( 260 px , 1 fr ) ) ; min-height : 400 px }
. pill { display : inline-flex ; align-items : center ; gap : 6 px ; background : rgba ( 23 , 228 , 110 , .08 ) ; border : 1 px solid rgba ( 23 , 228 , 110 , .2 ) ; border-radius : 6 px ; padding : 6 px 12 px ; font-size : 12 px ; color : var ( - - accent ) ; font-weight : 500 }
. log { background : #050805 ; border : 1 px solid #0c2819 ; border-radius : 10 px ; padding : 10 px ; height : calc ( 100 vh - 400 px ) ; min-height : 400 px ; overflow : auto ; font-family : ui-monospace , Consolas , monospace ; font-size : 12 px ; color : #b7f6c8 ; white-space : pre-wrap ; word-wrap : break-word }
2025-10-12 18:53:07 -04:00
. actions { display : flex ; gap : 10 px ; flex-wrap : wrap }
. btn { cursor : pointer ; user-select : none ; display : inline-flex ; align-items : center ; gap : 8 px ; padding : 10 px 14 px ; border-radius : 10 px ; border : 1 px solid #124b2b ; background : #0e1a13 ; color : var ( - - text ) }
. btn : hover { filter : brightness ( 1.08 ) }
. btn . primary { background : linear-gradient ( 180 deg , #0e351e , #0a2a18 ) ; border-color : #1d7e4a }
. danger { color : #fff ; border-color : #4d121b ; background : #1a0e10 }
2025-10-12 20:02:49 -04:00
. tag { display : inline-block ; padding : 6 px 12 px ; border-radius : 6 px ; border : 1 px solid rgba ( 23 , 228 , 110 , .3 ) ; background : rgba ( 23 , 228 , 110 , .08 ) ; color : var ( - - accent ) ; font-weight : 500 ; font-size : 13 px }
. typing { overflow : hidden ; white-space : nowrap ; display : inline-block ; border-right : 2 px solid var ( - - accent ) ; animation : blink 0.7 s step-end infinite }
@ keyframes blink { 0 % , 100 % { border-color : var ( - - accent ) } 50 % { border-color : transparent } }
2025-10-12 18:53:07 -04:00
. hidden { display : none }
2025-10-12 20:02:49 -04:00
footer { padding : 20 px 16 px ; color : var ( - - muted ) ; text-align : center ; font-size : 13 px ; border-top : 1 px solid #0d2015 ; margin-top : auto }
. footer-icon { display : inline-flex ; align-items : center ; justify-content : center ; width : 40 px ; height : 40 px ; border-radius : 8 px ; background : #0e1a13 ; border : 1 px solid #124b2b ; color : var ( - - accent ) ; transition : all 0.2 s ease ; text-decoration : none }
. footer-icon : hover { background : #103e24 ; border-color : #1d7e4a ; color : var ( - - accent2 ) ; transform : translateY ( -2 px ) }
2025-10-12 18:53:07 -04:00
. tooltip { border-bottom : 1 px dotted var ( - - muted ) ; cursor : help }
2025-10-12 20:02:49 -04:00
. elim-layout { display : grid ; grid-template-columns : 280 px 1 fr 320 px ; gap : 16 px ; height : calc ( 100 vh - 200 px ) ; min-height : 600 px }
. elim-tree { background : var ( - - panel ) ; border : 1 px solid #133422 ; border-radius : 12 px ; padding : 12 px ; overflow-y : auto }
. elim-center { background : var ( - - panel ) ; border : 1 px solid #133422 ; border-radius : 12 px ; padding : 20 px ; overflow-y : auto ; display : flex ; flex-direction : column }
. elim-wiki { background : var ( - - panel ) ; border : 1 px solid #133422 ; border-radius : 12 px ; padding : 16 px ; overflow-y : auto }
. tree-node { cursor : pointer ; padding : 8 px 10 px ; border-radius : 6 px ; margin : 4 px 0 ; user-select : none ; font-weight : 600 ; color : var ( - - text ) ; border : 1 px solid transparent }
. tree-node : hover { background : #0e1a13 ; border-color : #124b2b }
. tree-node . expanded { background : #0e1a13 ; border-color : #124b2b }
. tree-child { cursor : pointer ; padding : 6 px 10 px 6 px 12 px ; border-radius : 6 px ; margin : 2 px 0 ; font-size : 13 px ; color : var ( - - muted ) ; border : 1 px solid transparent ; display : flex ; align-items : center ; gap : 8 px ; transition : all 0.15 s }
. 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.15 s }
. tree-child : hover :: before , . tree-child . selected :: before { opacity : 1 }
. detail-field { margin : 12 px 0 ; padding : 10 px ; background : #050805 ; border : 1 px solid #0c2819 ; border-radius : 8 px }
. detail-label { font-size : 11 px ; color : var ( - - muted ) ; text-transform : uppercase ; letter-spacing : 0.5 px ; margin-bottom : 4 px }
. detail-value { color : var ( - - text ) ; font-size : 14 px ; word-break : break-all }
. order-badge { display : inline-block ; padding : 6 px 14 px ; border-radius : 6 px ; font-size : 11 px ; font-weight : 600 ; margin-bottom : 12 px ; width : fit-content }
. order-1 { background : #1a0e10 ; color : #ff5c7a ; border : 1 px solid #4d121b }
. order-2 { background : #1a1510 ; color : #ffd166 ; border : 1 px solid #4d3b12 }
. order-3 { background : #0e1a13 ; color : #17e46e ; border : 1 px solid #124b2b }
. wiki-section { margin-bottom : 20 px }
. wiki-title { font-weight : 600 ; color : var ( - - accent ) ; margin-bottom : 8 px ; font-size : 15 px }
. wiki-text { color : var ( - - muted ) ; font-size : 13 px ; line-height : 1.6 }
. wiki-list { margin : 8 px 0 ; padding-left : 20 px ; color : var ( - - muted ) ; font-size : 13 px }
. wiki-list li { margin : 4 px 0 }
. elim-btn { width : fit-content ; align-self : center ; margin-top : auto ; padding : 16 px 32 px ; background : rgba ( 255 , 92 , 122 , .08 ) ; border : 1 px solid rgba ( 255 , 92 , 122 , .3 ) ; color : #ff5c7a ; border-radius : 8 px ; cursor : pointer ; font-weight : 600 ; font-size : 15 px ; transition : all 0.25 s ; position : relative ; overflow : hidden ; display : flex ; align-items : center ; justify-content : center ; gap : 10 px }
. elim-btn :: before { content : '' ; position : absolute ; top : 0 ; left : 0 ; right : 0 ; bottom : 0 ; background : linear-gradient ( 135 deg , rgba ( 255 , 92 , 122 , .15 ) , rgba ( 255 , 92 , 122 , .05 ) ) ; opacity : 0 ; transition : opacity 0.25 s }
. elim-btn : hover { background : rgba ( 255 , 92 , 122 , .15 ) ; border-color : #ff5c7a ; transform : translateY ( -2 px ) ; box-shadow : 0 6 px 20 px rgba ( 255 , 92 , 122 , .3 ) }
. elim-btn : hover :: before { opacity : 1 }
. elim-btn : active { transform : translateY ( 0 ) ; box-shadow : 0 2 px 8 px rgba ( 255 , 92 , 122 , .2 ) }
. elim-btn : disabled { opacity : 0.4 ; cursor : not-allowed ; transform : none }
. elim-btn-icon { font-size : 18 px }
. empty-state { text-align : center ; padding : 40 px ; 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.5 s ease-out }
# splash . fade-out { opacity : 0 ; pointer-events : none }
. splash-logo { width : 180 px ; height : 180 px ; margin-bottom : 30 px ; animation : logoFloat 3 s ease-in-out infinite ; filter : drop-shadow ( 0 0 40 px rgba ( 23 , 228 , 110 , .6 ) ) }
@ keyframes logoFloat { 0 % , 100 % { transform : translateY ( 0 px ) } 50 % { transform : translateY ( -15 px ) } }
. splash-title { font-size : 32 px ; font-weight : 700 ; color : var ( - - accent ) ; margin-bottom : 12 px ; letter-spacing : 2 px ; text-shadow : 0 0 20 px rgba ( 23 , 228 , 110 , .5 ) }
. splash-subtitle { font-size : 14 px ; color : var ( - - muted ) ; letter-spacing : 1 px ; margin-bottom : 40 px }
. splash-loader { width : 200 px ; height : 3 px ; background : rgba ( 23 , 228 , 110 , .1 ) ; border-radius : 3 px ; overflow : hidden ; position : relative }
. splash-loader :: after { content : '' ; position : absolute ; top : 0 ; left : 0 ; height : 100 % ; width : 40 % ; background : linear-gradient ( 90 deg , transparent , var ( - - accent ) , transparent ) ; animation : loading 1.5 s ease-in-out infinite }
@ keyframes loading { 0 % { left : -40 % } 100 % { left : 100 % } }
2025-10-12 18:53:07 -04:00
< / style >
< / head >
< body >
2025-10-12 20:02:49 -04:00
<!-- 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 >
2025-10-12 18:53:07 -04:00
< header >
< div class = "nav" >
< div class = "brand" > < img src = "/logo" alt = "RMM Hunter" > < span > RMM Hunter< / span > < / div >
2025-10-12 20:02:49 -04:00
< span class = "pill" > KrakenTech LLC< / span >
2025-10-12 18:53:07 -04:00
< div class = "spacer" > < / div >
< a href = "#hunt" class = "btn primary" > Hunt< / a >
2025-10-12 20:02:49 -04:00
< a href = "#previous" class = "btn" > Previous Hunts< / a >
2025-10-12 18:53:07 -04:00
< 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" >
2025-10-12 20:02:49 -04:00
< a href = "#eliminate" class = "btn primary" > Proceed to Eliminate< / a >
2025-10-12 18:53:07 -04:00
< / div >
< / section >
2025-10-12 20:02:49 -04:00
< 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 >
2025-10-12 18:53:07 -04:00
< / section >
< / main >
< footer >
2025-10-12 20:02:49 -04:00
< 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 >
2025-10-12 18:53:07 -04:00
< / footer >
< script >
( function ( ) {
2025-10-12 20:02:49 -04:00
// 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 ) ;
2025-10-12 20:07:16 -04:00
} , 1500 ) ; // Display splash for 1.5 seconds, give the// server time to start
2025-10-12 20:02:49 -04:00
} ) ;
2025-10-12 18:53:07 -04:00
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' ;
2025-10-12 20:02:49 -04:00
const mainEl = document . querySelector ( 'main' ) ;
2025-10-12 18:53:07 -04:00
huntSection . classList . toggle ( 'hidden' , h !== '#hunt' ) ;
previousSection . classList . toggle ( 'hidden' , h !== '#previous' ) ;
2025-10-12 20:02:49 -04:00
reportSection . classList . toggle ( 'hidden' , h !== '#report' ) ;
2025-10-12 18:53:07 -04:00
eliminateSection . classList . toggle ( 'hidden' , h !== '#eliminate' ) ;
2025-10-12 20:02:49 -04:00
// Make eliminate page full-width
mainEl . classList . toggle ( 'full-width' , h === '#eliminate' ) ;
2025-10-12 20:07:16 -04:00
// If navigating to eliminate without a loaded report, show message
2025-10-12 20:02:49 -04:00
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>' ;
}
2025-10-12 18:53:07 -04:00
}
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> ` ;
2025-10-12 20:02:49 -04:00
const f = rep . findings || rep || { } ;
console . log ( 'Report data:' , rep ) ;
console . log ( 'Findings:' , f ) ;
console . log ( 'Binaries:' , f . binaries ) ;
2025-10-12 18:53:07 -04:00
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' ;
2025-10-12 20:02:49 -04:00
for ( const item of b . arr ) {
console . log ( ` Rendering ${ b . id } : ` , item ) ;
list . appendChild ( renderItem ( b . id , item ) ) ;
}
2025-10-12 18:53:07 -04:00
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' ) {
2025-10-12 20:02:49 -04:00
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> ` ;
2025-10-12 18:53:07 -04:00
} 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' ) {
2025-10-12 20:02:49 -04:00
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> ` ;
2025-10-12 18:53:07 -04:00
} else if ( kind === 'directories' ) {
2025-10-12 20:02:49 -04:00
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> ` ;
2025-10-12 18:53:07 -04:00
}
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' ; }
2025-10-12 20:07:16 -04:00
// Type animation function
2025-10-12 20:02:49 -04:00
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 ) ;
} ) ;
}
2025-10-12 18:53:07 -04:00
// 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' ) ;
2025-10-12 20:02:49 -04:00
await typeText ( huntTag , 'Report: ' + data . reportName + '.json' , 30 ) ;
2025-10-12 18:53:07 -04:00
// 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 ( ) ;
2025-10-12 20:02:49 -04:00
// 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 ;
2025-10-12 20:07:16 -04:00
// Build tree structure using order of operations
2025-10-12 20:02:49 -04:00
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 ] ;
2025-10-12 20:07:16 -04:00
// | |x| | item details
2025-10-12 20:02:49 -04:00
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>
` ;
2025-10-12 20:07:16 -04:00
// | | |x| - wiki
2025-10-12 20:02:49 -04:00
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 \n This action cannot be undone. ` ) ) return ;
2025-10-12 20:07:16 -04:00
alert ( 'Elimination functionality not yet implemented in web UI.\n\nPlease use the CLI: rmm-hunter eliminate --cli' ) ;
2025-10-12 20:02:49 -04:00
// 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 ) ;
} ;
2025-10-12 18:53:07 -04:00
} ) ( ) ;
< / script >
< / body >
< / html >