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:58:53 -04:00
html { background : var ( - - bg ) }
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 21:58:11 -04:00
. active-report-indicator { display : flex ; align-items : center ; gap : 8 px ; background : rgba ( 23 , 228 , 110 , .08 ) ; border : 1 px solid rgba ( 23 , 228 , 110 , .2 ) ; border-radius : 8 px ; padding : 8 px 14 px ; margin-left : 16 px ; transition : all 0.3 s ease }
. active-report-indicator . report-icon { font-size : 16 px }
. active-report-indicator . report-name { font-size : 13 px ; color : var ( - - accent ) ; font-weight : 500 ; max-width : 300 px ; 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 ) }
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 21:58:11 -04:00
. expandable-item { transition : all 0.2 s ease ; position : relative ; padding-right : 40 px }
. expandable-item : hover { border-color : var ( - - accent ) ; background : #0e1a13 }
. expandable-item :: after { content : '▼' ; position : absolute ; right : 20 px ; top : 20 px ; color : var ( - - muted ) ; font-size : 12 px ; transition : transform 0.2 s }
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 }
2025-10-12 20:58:53 -04:00
. 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 ; position : relative ; overflow : hidden }
2025-10-12 20:02:49 -04:00
. 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 }
2025-10-12 20:58:53 -04:00
. tree-child . eliminating { animation : slideOutRight 0.6 s ease-in-out forwards }
. tree-child . eliminating :: after { content : '' ; position : absolute ; top : 0 ; left : 0 ; right : 0 ; bottom : 0 ; background : linear-gradient ( 90 deg , transparent , rgba ( 23 , 228 , 110 , .3 ) , transparent ) ; animation : shimmer 0.6 s ease-in-out }
2025-10-12 21:58:11 -04:00
. 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 }
2025-10-12 20:58:53 -04:00
@ 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 % ) } }
2025-10-12 20:02:49 -04:00
. 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 20:58:53 -04:00
/* 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 : 80 px ; height : 80 px ; border : 4 px solid rgba ( 23 , 228 , 110 , .1 ) ; border-top : 4 px solid var ( - - accent ) ; border-radius : 50 % ; animation : spin 1 s linear infinite ; margin-bottom : 30 px }
. spinner . hide { display : none }
@ keyframes spin { 0 % { transform : rotate ( 0 deg ) } 100 % { transform : rotate ( 360 deg ) } }
. shutdown-title { font-size : 28 px ; font-weight : 700 ; color : var ( - - accent ) ; margin-bottom : 12 px ; letter-spacing : 1.5 px }
. shutdown-message { font-size : 16 px ; color : var ( - - muted ) ; margin-bottom : 20 px }
. shutdown-success { font-size : 18 px ; 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 : 1 px solid #133422 ; border-radius : 12 px ; padding : 30 px ; max-width : 500 px ; width : 90 % ; box-shadow : 0 20 px 60 px rgba ( 0 , 0 , 0 , .5 ) }
2025-10-12 21:58:11 -04:00
. modal-icon { font-size : 48 px ; text-align : left ; margin-bottom : 16 px ; display : block }
2025-10-12 20:58:53 -04:00
. modal-icon . success { color : var ( - - accent ) ; animation : checkPop 0.5 s ease-out }
. modal-icon . error { color : #ff5c7a }
@ keyframes checkPop { 0 % { transform : scale ( 0 ) } 50 % { transform : scale ( 1.2 ) } 100 % { transform : scale ( 1 ) } }
2025-10-12 21:58:11 -04:00
. modal-title { font-size : 24 px ; font-weight : 700 ; margin-bottom : 16 px ; text-align : left }
2025-10-12 20:58:53 -04:00
. modal-title . success { color : var ( - - accent ) }
. modal-title . error { color : #ff5c7a }
2025-10-12 21:58:11 -04:00
. modal-title . confirm { color : #ffd166 }
. modal-message { color : var ( - - text ) ; margin-bottom : 24 px ; line-height : 1.6 ; white-space : pre-wrap ; font-size : 15 px ; text-align : left }
. modal-buttons { display : flex ; gap : 12 px ; margin-top : 24 px }
. modal-btn { flex : 1 ; padding : 12 px 24 px ; border : 1 px solid #1d4a2f ; background : var ( - - panel ) ; color : var ( - - text ) ; font-weight : 500 ; font-size : 14 px ; cursor : pointer ; transition : all 0.2 s ; 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 }
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 20:58:53 -04:00
<!-- 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 >
2025-10-12 21:58:11 -04:00
< div id = "modalButtons" class = "modal-buttons" > < / div >
2025-10-12 20:58:53 -04:00
< / 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 21:58:11 -04:00
< div id = "activeReportIndicator" class = "active-report-indicator hidden" >
< span class = "report-icon" > 📄< / span >
< span id = "activeReportName" class = "report-name" > No report loaded< / span >
< / div >
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 21:58:11 -04:00
< a id = "reportNavBtn" href = "#report" class = "btn hidden" > Report< / 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 >
2025-10-12 20:58:53 -04:00
< button id = "viewReportBtn" class = "btn hidden" style = "margin-left:12px" > View Report< / button >
2025-10-12 18:53:07 -04:00
< / 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" >
2025-10-12 21:58:11 -04:00
< 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 >
2025-10-12 20:02:49 -04:00
< 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' ) ;
2025-10-12 20:58:53 -04:00
const viewReportBtn = document . getElementById ( 'viewReportBtn' ) ;
2025-10-12 21:58:11 -04:00
const reportNavBtn = document . getElementById ( 'reportNavBtn' ) ;
const activeReportIndicator = document . getElementById ( 'activeReportIndicator' ) ;
const activeReportName = document . getElementById ( 'activeReportName' ) ;
2025-10-12 18:53:07 -04:00
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 ) ;
2025-10-12 21:58:11 -04:00
// Set active report indicator
setActiveReport ( file ) ;
2025-10-12 18:53:07 -04:00
location . hash = '#report' ;
}
2025-10-12 21:58:11 -04:00
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' ) ;
}
}
2025-10-12 18:53:07 -04:00
function renderReport ( rep ) {
reportHeader . innerHTML = '' ;
reportBody . innerHTML = '' ;
2025-10-12 21:58:11 -04:00
2025-10-12 18:53:07 -04:00
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 ;
2025-10-12 21:58:11 -04:00
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> ` ;
2025-10-12 18:53:07 -04:00
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 ) {
2025-10-12 21:58:11 -04:00
const d = document . createElement ( 'div' ) ;
d . className = 'card expandable-item' ;
d . style . margin = '0' ;
d . style . cursor = 'pointer' ;
2025-10-12 18:53:07 -04:00
const esc = escapeHTML ;
2025-10-12 21:58:11 -04:00
let summary = '' ;
let details = '' ;
2025-10-12 18:53:07 -04:00
if ( kind === 'processes' ) {
2025-10-12 21:58:11 -04:00
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> ` ;
2025-10-12 18:53:07 -04:00
} else if ( kind === 'services' ) {
2025-10-12 21:58:11 -04:00
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> ` ;
2025-10-12 18:53:07 -04:00
} else if ( kind === 'connections' ) {
2025-10-12 21:58:11 -04:00
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> ` ;
2025-10-12 18:53:07 -04:00
} else if ( kind === 'autoruns' ) {
2025-10-12 21:58:11 -04:00
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> ` ;
2025-10-12 18:53:07 -04:00
} else if ( kind === 'tasks' ) {
2025-10-12 21:58:11 -04:00
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> ` ;
2025-10-12 18:53:07 -04:00
} else if ( kind === 'binaries' ) {
2025-10-12 20:02:49 -04:00
const path = item . path || item . Path || ( typeof item === 'string' ? item : '' ) ;
2025-10-12 21:58:11 -04:00
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> ` ;
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 : '' ) ;
2025-10-12 21:58:11 -04:00
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> ` ;
2025-10-12 18:53:07 -04:00
}
2025-10-12 21:58:11 -04:00
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' ;
}
} ) ;
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
2025-10-12 20:58:53 -04:00
let lastReportData = null ;
let currentReportName = null ;
2025-10-12 18:53:07 -04:00
document . getElementById ( 'startHunt' ) . addEventListener ( 'click' , async ( ) => {
2025-10-12 20:58:53 -04:00
logEl . textContent = '' ;
huntTag . classList . add ( 'hidden' ) ;
viewReportBtn . classList . add ( 'hidden' ) ;
currentReportName = null ;
2025-10-12 18:53:07 -04:00
let ws ;
2025-10-12 20:58:53 -04:00
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 ;
}
} ;
2025-10-12 18:53:07 -04:00
} catch ( e ) { console . error ( e ) ; }
2025-10-12 20:58:53 -04:00
// 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 ) ) ;
2025-10-12 18:53:07 -04:00
const r = await fetch ( '/api/hunt/start' , { method : 'POST' } ) ;
const data = await r . json ( ) ;
2025-10-12 20:58:53 -04:00
2025-10-12 18:53:07 -04:00
if ( data && data . reportName ) {
2025-10-12 20:58:53 -04:00
currentReportName = data . reportName ;
// Wait for hunt to actually complete
while ( ! huntComplete ) {
await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
}
2025-10-12 18:53:07 -04:00
huntTag . classList . remove ( 'hidden' ) ;
2025-10-12 20:02:49 -04:00
await typeText ( huntTag , 'Report: ' + data . reportName + '.json' , 30 ) ;
2025-10-12 20:58:53 -04:00
2025-10-12 18:53:07 -04:00
// Load report
2025-10-12 21:58:11 -04:00
const reportFileName = data . reportName + '.json' ;
const rep = await ( await fetch ( '/api/report?file=' + encodeURIComponent ( reportFileName ) ) ) . json ( ) ;
2025-10-12 20:58:53 -04:00
lastReportData = rep ;
renderReport ( rep ) ;
2025-10-12 21:58:11 -04:00
// Set active report indicator
setActiveReport ( reportFileName ) ;
2025-10-12 20:58:53 -04:00
// Show View Report button AFTER everything is loaded
viewReportBtn . classList . remove ( 'hidden' ) ;
2025-10-12 18:53:07 -04:00
}
if ( ws ) ws . close ( ) ;
listHunts ( ) ;
} ) ;
2025-10-12 20:58:53 -04:00
// View Report button handler
viewReportBtn . addEventListener ( 'click' , ( ) => {
if ( lastReportData ) {
location . hash = '#report' ;
}
} ) ;
2025-10-12 18:53:07 -04:00
document . getElementById ( 'quitBtn' ) . addEventListener ( 'click' , async ( ) => {
2025-10-12 20:58:53 -04:00
// 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 ) ;
2025-10-12 18:53:07 -04:00
} ) ;
listHunts ( ) ;
2025-10-12 20:02:49 -04:00
// Elimination interface
let currentReport = null ;
2025-10-12 21:58:11 -04:00
let showEliminatedItems = false ;
2025-10-12 20:02:49 -04:00
const elimTree = document . getElementById ( 'elimTree' ) ;
const elimCenter = document . getElementById ( 'elimCenter' ) ;
const elimWiki = document . getElementById ( 'elimWiki' ) ;
2025-10-12 21:58:11 -04:00
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' ;
} ) ;
2025-10-12 20:02:49 -04:00
// 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 21:58:11 -04:00
console . log ( 'Loading elimination data, findings:' , findings ) ;
2025-10-12 20:02:49 -04:00
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 }
] ;
2025-10-12 21:58:11 -04:00
console . log ( 'Categories:' , categories ) ;
2025-10-12 20:02:49 -04:00
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 ] ;
2025-10-12 21:58:11 -04:00
const activeCount = cat . arr . filter ( item => ! item . eliminated ) . length ;
const totalCount = cat . arr . length ;
2025-10-12 20:02:49 -04:00
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>
2025-10-12 21:58:11 -04:00
<span class="tag"> ${ activeCount } ${ activeCount !== totalCount ? ` / ${ totalCount } ` : '' } </span>
2025-10-12 20:02:49 -04:00
</div> ` ;
const childContainer = document . createElement ( 'div' ) ;
childContainer . style . display = 'none' ;
cat . arr . forEach ( ( item , idx ) => {
const child = document . createElement ( 'div' ) ;
child . className = 'tree-child' ;
2025-10-12 21:58:11 -04:00
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' ) ;
}
2025-10-12 20:02:49 -04:00
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 ;
2025-10-12 21:58:11 -04:00
if ( type === 'autoruns' ) return item . image _name || item . imageName || item . entry || 'Unknown AutoRun' ;
2025-10-12 20:02:49 -04:00
if ( type === 'binaries' ) return item . path || item ;
if ( type === 'directories' ) return item . path || item ;
return ` Item ${ idx + 1 } ` ;
}
2025-10-12 21:58:11 -04:00
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 } ` : '' } ` ;
}
}
}
} ) ;
}
2025-10-12 20:02:49 -04:00
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 ) {
2025-10-12 21:58:11 -04:00
// 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
2025-10-12 20:02:49 -04:00
let value = item [ field ] ;
2025-10-12 21:58:11 -04:00
if ( value === undefined || value === null ) {
const snakeCase = field . replace ( /[A-Z]/g , letter => ` _ ${ letter . toLowerCase ( ) } ` ) ;
value = item [ snakeCase ] ;
}
2025-10-12 20:02:49 -04:00
if ( value === undefined || value === null || value === '' ) return '' ;
2025-10-12 21:58:11 -04:00
// Handle arrays
if ( Array . isArray ( value ) ) {
value = value . length > 0 ? value . join ( ', ' ) : 'None' ;
}
// Handle booleans
2025-10-12 20:02:49 -04:00
if ( typeof value === 'boolean' ) value = value ? 'Yes' : 'No' ;
2025-10-12 21:58:11 -04:00
// 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 ( ' ' ) ;
2025-10-12 20:02:49 -04:00
return `
<div class="detail-field">
2025-10-12 21:58:11 -04:00
<div class="detail-label"> ${ displayName } </div>
2025-10-12 20:02:49 -04:00
<div class="detail-value"> ${ escapeHTML ( String ( value ) ) } </div>
</div>
` ;
2025-10-12 21:58:11 -04:00
} ) . filter ( x => x ) . join ( '' ) ;
2025-10-12 20:02:49 -04:00
}
2025-10-12 20:58:53 -04:00
// Modal functions
const modal = document . getElementById ( 'modal' ) ;
const modalIcon = document . getElementById ( 'modalIcon' ) ;
const modalTitle = document . getElementById ( 'modalTitle' ) ;
const modalMessage = document . getElementById ( 'modalMessage' ) ;
2025-10-12 21:58:11 -04:00
const modalButtons = document . getElementById ( 'modalButtons' ) ;
2025-10-12 20:58:53 -04:00
2025-10-12 21:58:11 -04:00
function showModal ( type , title , message , onConfirm = null ) {
2025-10-12 20:58:53 -04:00
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 = '✓' ;
2025-10-12 21:58:11 -04:00
} else if ( type === 'error' ) {
2025-10-12 20:58:53 -04:00
modalIcon . textContent = '✕' ;
2025-10-12 21:58:11 -04:00
} else if ( type === 'confirm' ) {
modalIcon . textContent = '⚠' ;
2025-10-12 20:58:53 -04:00
}
2025-10-12 21:58:11 -04:00
// 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 ) ;
}
2025-10-12 20:58:53 -04:00
}
window . eliminateItem = async function ( type , idx ) {
if ( ! currentReport ) {
showModal ( 'error' , 'Error' , 'No report loaded' ) ;
return ;
}
2025-10-12 21:58:11 -04:00
// Show custom confirmation modal
const typeName = type . slice ( 0 , - 1 ) ;
showModal ( 'confirm' , 'Confirm Elimination' ,
` Are you sure you want to eliminate this ${ typeName } ? \n \n This 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 ) {
2025-10-12 20:58:53 -04:00
// Get the report filename from the current report
2025-10-12 21:58:11 -04:00
let reportFile = currentReport . reportName || currentReport . name || 'rmm-hunter-report.json' ;
// Ensure .json extension
if ( ! reportFile . endsWith ( '.json' ) ) {
reportFile += '.json' ;
}
2025-10-12 20:58:53 -04:00
2025-10-12 21:58:11 -04:00
// Find the tree child element for this item using data attributes
2025-10-12 20:58:53 -04:00
const treeChildren = document . querySelectorAll ( '.tree-child' ) ;
let targetElement = null ;
2025-10-12 21:58:11 -04:00
// Find the element by matching the data attributes
treeChildren . forEach ( ( child ) => {
if ( child . dataset . type === type && parseInt ( child . dataset . index ) === idx ) {
targetElement = child ;
2025-10-12 20:58:53 -04:00
}
} ) ;
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 ;
}
2025-10-12 21:58:11 -04:00
// Success! Trigger slide-out animation and mark as eliminated
2025-10-12 20:58:53 -04:00
if ( targetElement ) {
targetElement . classList . add ( 'eliminating' ) ;
2025-10-12 21:58:11 -04:00
// 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 ;
}
}
2025-10-12 20:58:53 -04:00
2025-10-12 21:58:11 -04:00
// Update the category count badge
updateCategoryCount ( type ) ;
2025-10-12 20:58:53 -04:00
2025-10-12 21:58:11 -04:00
// Show success modal
showModal ( 'success' , 'Successfully Eliminated' , ` The ${ type . slice ( 0 , - 1 ) } has been removed from your system. ` ) ;
2025-10-12 20:58:53 -04:00
// 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 {
2025-10-12 21:58:11 -04:00
// Fallback if element not found - just show success
2025-10-12 20:58:53 -04:00
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 } ` ) ;
}
2025-10-12 21:58:11 -04:00
}
2025-10-12 20:02:49 -04:00
// 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 >