Enhance API error responses with JSON format, improve suspicious directory detection with worker pool implementation, and refine elimination logic with better index validation and data flow updates. Update UI for active report indicators, item expansion, and eliminated state tracking.

This commit is contained in:
Evan Hosinski
2025-10-12 21:58:11 -04:00
parent 25d99c265d
commit 3f50f20892
4 changed files with 536 additions and 137 deletions
@@ -7,55 +7,102 @@ import (
"rmm-hunter/internal/pkg/hunt/detect/common" "rmm-hunter/internal/pkg/hunt/detect/common"
. "rmm-hunter/internal/suspicious" . "rmm-hunter/internal/suspicious"
"strings" "strings"
"sync"
) )
var appData = os.Getenv("APPDATA") var appData = os.Getenv("APPDATA")
var userProfile = os.Getenv("USERPROFILE") var userProfile = os.Getenv("USERPROFILE")
func Detect() []Directory { const numWorkers = 5
var suspiciousDirectories []Directory
seen := make(map[string]bool) // Prevent duplicates
type searchJob struct {
basePath string
rmmDir string
}
func Detect() []Directory {
fmt.Printf("[*] Enumerating Suspicious Directories \n") fmt.Printf("[*] Enumerating Suspicious Directories \n")
// For each known RMM directory, check in all base paths // Create channels
jobs := make(chan searchJob, 100)
results := make(chan Directory, 100)
// WaitGroup to track workers
var wg sync.WaitGroup
// Start worker pool
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go worker(jobs, results, &wg)
}
// Start result collector goroutine
var suspiciousDirectories []Directory
seen := make(map[string]bool)
var resultWg sync.WaitGroup
resultWg.Add(1)
go func() {
defer resultWg.Done()
for dir := range results {
if !seen[dir.Path] {
fmt.Printf(" [?] Found %s\n", dir.Path)
suspiciousDirectories = append(suspiciousDirectories, dir)
seen[dir.Path] = true
}
}
}()
// Send jobs to workers
for _, rmmDir := range common.KnownRMMDirectories { for _, rmmDir := range common.KnownRMMDirectories {
for _, basePath := range common.SearchBasePaths { for _, basePath := range common.SearchBasePaths {
// Replace environment variables jobs <- searchJob{
basePath = replaceEnvVars(basePath) basePath: basePath,
rmmDir: rmmDir,
// Construct full path
fullPath := filepath.Join(basePath, rmmDir)
// Check if this is a prefix pattern (ends with incomplete path like "ScreenConnect Client (")
if isPrefix(rmmDir) {
// Find all directories matching this prefix
matches := findPrefixMatches(fullPath)
for _, match := range matches {
if !seen[match] {
fmt.Printf(" [?] Found %s\n", match)
suspiciousDirectories = append(suspiciousDirectories, Directory{Path: match})
seen[match] = true
}
}
} else {
// Exact match
if _, err := os.Stat(fullPath); err == nil {
if !seen[fullPath] {
fmt.Printf(" [?] Found %s\n", fullPath)
suspiciousDirectories = append(suspiciousDirectories, Directory{Path: fullPath})
seen[fullPath] = true
}
}
} }
} }
} }
// Close jobs channel and wait for workers to finish
close(jobs)
wg.Wait()
// Close results channel and wait for collector to finish
close(results)
resultWg.Wait()
fmt.Printf("[+] Found %d Suspicious Directories\n", len(suspiciousDirectories)) fmt.Printf("[+] Found %d Suspicious Directories\n", len(suspiciousDirectories))
return suspiciousDirectories return suspiciousDirectories
} }
// worker processes search jobs from the jobs channel
func worker(jobs <-chan searchJob, results chan<- Directory, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
// Replace environment variables
basePath := replaceEnvVars(job.basePath)
// Construct full path
fullPath := filepath.Join(basePath, job.rmmDir)
// Check if this is a prefix pattern (ends with incomplete path like "ScreenConnect Client (")
if isPrefix(job.rmmDir) {
// Find all directories matching this prefix
matches := findPrefixMatches(fullPath)
for _, match := range matches {
results <- Directory{Path: match}
}
} else {
// Exact match
if _, err := os.Stat(fullPath); err == nil {
results <- Directory{Path: fullPath}
}
}
}
}
// replaceEnvVars replaces environment variable placeholders with actual paths // replaceEnvVars replaces environment variable placeholders with actual paths
func replaceEnvVars(path string) string { func replaceEnvVars(path string) string {
path = strings.ReplaceAll(path, "{{APPDATA}}", appData) path = strings.ReplaceAll(path, "{{APPDATA}}", appData)
+25 -3
View File
@@ -10,11 +10,33 @@ import (
// EliminateAutoRun removes an autorun entry from the system // EliminateAutoRun removes an autorun entry from the system
func EliminateAutoRun(ar AutoRun) error { func EliminateAutoRun(ar AutoRun) error {
all := scurvy.ListAutoruns() all := scurvy.ListAutoruns()
// Try to find by MD5 first
for _, a := range all { for _, a := range all {
if a.MD5 == ar.MD5 { if a.MD5 == ar.MD5 && a.MD5 != "" {
// Found it, delete it
return scurvy.DeleteAutorun(a) return scurvy.DeleteAutorun(a)
} }
} }
return fmt.Errorf("%s | %s not found", ar.Location, ar.Entry)
// If not found by MD5, try to find by location (for registry entries)
for _, a := range all {
if a.Location == ar.Location && ar.Location != "" {
return scurvy.DeleteAutorun(a)
}
}
// Build a descriptive error message
location := ar.Location
if location == "" {
location = "unknown location"
}
entry := ar.Entry
if entry == "" {
entry = ar.ImageName
}
if entry == "" {
entry = "unknown entry"
}
return fmt.Errorf("autorun entry not found at %s (%s) - it may have already been removed", location, entry)
} }
+375 -75
View File
@@ -30,6 +30,10 @@
.nav{max-width:1400px;margin:0 auto;display:flex;align-items:center;gap:8px;padding:12px 24px} .nav{max-width:1400px;margin:0 auto;display:flex;align-items:center;gap:8px;padding:12px 24px}
.brand{display:flex;align-items:center;gap:12px;font-weight:700;letter-spacing:.5px;font-size:16px;color:var(--text)} .brand{display:flex;align-items:center;gap:12px;font-weight:700;letter-spacing:.5px;font-size:16px;color:var(--text)}
.brand img{width:40px;height:40px;object-fit:contain;filter:drop-shadow(0 0 8px rgba(23,228,110,.3))} .brand img{width:40px;height:40px;object-fit:contain;filter:drop-shadow(0 0 8px rgba(23,228,110,.3))}
.active-report-indicator{display:flex;align-items:center;gap:8px;background:rgba(23,228,110,.08);border:1px solid rgba(23,228,110,.2);border-radius:8px;padding:8px 14px;margin-left:16px;transition:all 0.3s ease}
.active-report-indicator .report-icon{font-size:16px}
.active-report-indicator .report-name{font-size:13px;color:var(--accent);font-weight:500;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.active-report-indicator:hover{background:rgba(23,228,110,.12);border-color:rgba(23,228,110,.3)}
.spacer{flex:1} .spacer{flex:1}
.nav a.btn{display:inline-flex;align-items:center;padding:10px 18px;border:none;border-radius:6px;color:var(--text);text-decoration:none;transition:all .2s;font-weight:500;font-size:14px;position:relative;overflow:hidden} .nav a.btn{display:inline-flex;align-items:center;padding:10px 18px;border:none;border-radius:6px;color:var(--text);text-decoration:none;transition:all .2s;font-weight:500;font-size:14px;position:relative;overflow:hidden}
.nav a.btn::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(135deg,rgba(23,228,110,.1),rgba(23,228,110,.05));opacity:0;transition:opacity .2s} .nav a.btn::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(135deg,rgba(23,228,110,.1),rgba(23,228,110,.05));opacity:0;transition:opacity .2s}
@@ -42,6 +46,9 @@
main{max-width:1100px;margin:20px auto;padding:0 16px;flex:1;width:100%} main{max-width:1100px;margin:20px auto;padding:0 16px;flex:1;width:100%}
main.full-width{max-width:none;padding:0 20px} main.full-width{max-width:none;padding:0 20px}
.card{background:var(--panel);border:1px solid #133422;border-radius:12px;padding:16px;margin-bottom:16px;box-shadow:0 8px 24px rgba(0,0,0,.25);overflow-wrap:break-word;word-wrap:break-word;word-break:break-word;overflow:hidden} .card{background:var(--panel);border:1px solid #133422;border-radius:12px;padding:16px;margin-bottom:16px;box-shadow:0 8px 24px rgba(0,0,0,.25);overflow-wrap:break-word;word-wrap:break-word;word-break:break-word;overflow:hidden}
.expandable-item{transition:all 0.2s ease;position:relative;padding-right:40px}
.expandable-item:hover{border-color:var(--accent);background:#0e1a13}
.expandable-item::after{content:'▼';position:absolute;right:20px;top:20px;color:var(--muted);font-size:12px;transition:transform 0.2s}
h1,h2{margin:10px 0} h1,h2{margin:10px 0}
.muted{color:var(--muted)} .muted{color:var(--muted)}
.grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));min-height:400px} .grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));min-height:400px}
@@ -74,6 +81,9 @@
.tree-child:hover::before,.tree-child.selected::before{opacity:1} .tree-child:hover::before,.tree-child.selected::before{opacity:1}
.tree-child.eliminating{animation:slideOutRight 0.6s ease-in-out forwards} .tree-child.eliminating{animation:slideOutRight 0.6s ease-in-out forwards}
.tree-child.eliminating::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(90deg,transparent,rgba(23,228,110,.3),transparent);animation:shimmer 0.6s ease-in-out} .tree-child.eliminating::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(90deg,transparent,rgba(23,228,110,.3),transparent);animation:shimmer 0.6s ease-in-out}
.tree-child.eliminated{display:none;opacity:0.4;filter:grayscale(1);text-decoration:line-through;pointer-events:none}
.tree-child.eliminated::before{content:'✓ ';color:#17e46e;opacity:1}
.show-eliminated .tree-child.eliminated{display:flex}
@keyframes slideOutRight{0%{transform:translateX(0);opacity:1}50%{opacity:0.5}100%{transform:translateX(100%);opacity:0;height:0;margin:0;padding:0}} @keyframes 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%)}} @keyframes shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
.detail-field{margin:12px 0;padding:10px;background:#050805;border:1px solid #0c2819;border-radius:8px} .detail-field{margin:12px 0;padding:10px;background:#050805;border:1px solid #0c2819;border-radius:8px}
@@ -123,19 +133,23 @@
.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{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.active{display:flex}
.modal-content{background:var(--panel);border:1px solid #133422;border-radius:12px;padding:30px;max-width:500px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,.5)} .modal-content{background:var(--panel);border:1px solid #133422;border-radius:12px;padding:30px;max-width:500px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,.5)}
.modal-icon{font-size:60px;text-align:center;margin-bottom:20px} .modal-icon{font-size:48px;text-align:left;margin-bottom:16px;display:block}
.modal-icon.success{color:var(--accent);animation:checkPop 0.5s ease-out} .modal-icon.success{color:var(--accent);animation:checkPop 0.5s ease-out}
.modal-icon.error{color:#ff5c7a} .modal-icon.error{color:#ff5c7a}
@keyframes checkPop{0%{transform:scale(0)}50%{transform:scale(1.2)}100%{transform:scale(1)}} @keyframes checkPop{0%{transform:scale(0)}50%{transform:scale(1.2)}100%{transform:scale(1)}}
.modal-title{font-size:24px;font-weight:700;text-align:center;margin-bottom:12px} .modal-title{font-size:24px;font-weight:700;margin-bottom:16px;text-align:left}
.modal-title.success{color:var(--accent)} .modal-title.success{color:var(--accent)}
.modal-title.error{color:#ff5c7a} .modal-title.error{color:#ff5c7a}
.modal-message{text-align:center;color:var(--muted);margin-bottom:24px;line-height:1.6} .modal-title.confirm{color:#ffd166}
.modal-btn{width:100%;padding:14px;border:none;border-radius:8px;font-weight:600;font-size:15px;cursor:pointer;transition:all 0.2s} .modal-message{color:var(--text);margin-bottom:24px;line-height:1.6;white-space:pre-wrap;font-size:15px;text-align:left}
.modal-btn.success{background:var(--accent);color:#000} .modal-buttons{display:flex;gap:12px;margin-top:24px}
.modal-btn.success:hover{filter:brightness(1.2)} .modal-btn{flex:1;padding:12px 24px;border:1px solid #1d4a2f;background:var(--panel);color:var(--text);font-weight:500;font-size:14px;cursor:pointer;transition:all 0.2s;border-radius:0}
.modal-btn.error{background:rgba(255,92,122,.2);color:#ff5c7a;border:1px solid #ff5c7a} .modal-btn:hover{background:#0e1a13;border-color:var(--accent)}
.modal-btn.error:hover{background:rgba(255,92,122,.3)} .modal-btn.primary{background:var(--accent);color:#000;border-color:var(--accent)}
.modal-btn.primary:hover{filter:brightness(1.2)}
.modal-btn.danger{background:rgba(255,92,122,.15);color:#ff5c7a;border-color:#ff5c7a}
.modal-btn.danger:hover{background:rgba(255,92,122,.25)}
.modal-icon.confirm{color:#ffd166;text-align:left}
</style> </style>
</head> </head>
<body> <body>
@@ -161,7 +175,7 @@
<div id="modalIcon" class="modal-icon"></div> <div id="modalIcon" class="modal-icon"></div>
<div id="modalTitle" class="modal-title"></div> <div id="modalTitle" class="modal-title"></div>
<div id="modalMessage" class="modal-message"></div> <div id="modalMessage" class="modal-message"></div>
<button id="modalBtn" class="modal-btn"></button> <div id="modalButtons" class="modal-buttons"></div>
</div> </div>
</div> </div>
@@ -169,9 +183,14 @@
<div class="nav"> <div class="nav">
<div class="brand"><img src="/logo" alt="RMM Hunter"><span>RMM Hunter</span></div> <div class="brand"><img src="/logo" alt="RMM Hunter"><span>RMM Hunter</span></div>
<span class="pill">KrakenTech LLC</span> <span class="pill">KrakenTech LLC</span>
<div id="activeReportIndicator" class="active-report-indicator hidden">
<span class="report-icon">📄</span>
<span id="activeReportName" class="report-name">No report loaded</span>
</div>
<div class="spacer"></div> <div class="spacer"></div>
<a href="#hunt" class="btn primary">Hunt</a> <a href="#hunt" class="btn primary">Hunt</a>
<a href="#previous" class="btn">Previous Hunts</a> <a href="#previous" class="btn">Previous Hunts</a>
<a id="reportNavBtn" href="#report" class="btn hidden">Report</a>
<a href="#eliminate" class="btn">Eliminate</a> <a href="#eliminate" class="btn">Eliminate</a>
<a id="quitBtn" class="btn danger">Quit</a> <a id="quitBtn" class="btn danger">Quit</a>
</div> </div>
@@ -204,7 +223,12 @@
</section> </section>
<section id="eliminateSection" class="hidden"> <section id="eliminateSection" class="hidden">
<h2 style="margin:0 0 16px 0">Eliminate Detected Items</h2> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h2 style="margin:0">Eliminate Detected Items</h2>
<button id="toggleEliminatedBtn" class="btn" style="font-size:13px;padding:8px 16px">
<span id="toggleEliminatedText">Show Eliminated Items</span>
</button>
</div>
<div id="elimContent" class="elim-layout"> <div id="elimContent" class="elim-layout">
<div class="elim-tree" id="elimTree"> <div class="elim-tree" id="elimTree">
<div class="empty-state">Load a report first</div> <div class="empty-state">Load a report first</div>
@@ -260,6 +284,9 @@
const huntsEl = document.getElementById('hunts'); const huntsEl = document.getElementById('hunts');
const huntTag = document.getElementById('huntTag'); const huntTag = document.getElementById('huntTag');
const viewReportBtn = document.getElementById('viewReportBtn'); const viewReportBtn = document.getElementById('viewReportBtn');
const reportNavBtn = document.getElementById('reportNavBtn');
const activeReportIndicator = document.getElementById('activeReportIndicator');
const activeReportName = document.getElementById('activeReportName');
const reportSection = document.getElementById('reportSection'); const reportSection = document.getElementById('reportSection');
const reportHeader = document.getElementById('reportHeader'); const reportHeader = document.getElementById('reportHeader');
const reportBody = document.getElementById('reportBody'); const reportBody = document.getElementById('reportBody');
@@ -314,12 +341,29 @@
const r = await fetch('/api/report?file='+encodeURIComponent(file)); const r = await fetch('/api/report?file='+encodeURIComponent(file));
const data = await r.json(); const data = await r.json();
renderReport(data); renderReport(data);
// Set active report indicator
setActiveReport(file);
location.hash = '#report'; location.hash = '#report';
} }
function setActiveReport(reportFileName) {
if (reportFileName) {
activeReportIndicator.classList.remove('hidden');
activeReportName.textContent = reportFileName;
reportNavBtn.classList.remove('hidden');
} else {
activeReportIndicator.classList.add('hidden');
activeReportName.textContent = 'No report loaded';
reportNavBtn.classList.add('hidden');
}
}
function renderReport(rep){ function renderReport(rep){
reportHeader.innerHTML = ''; reportHeader.innerHTML = '';
reportBody.innerHTML = ''; reportBody.innerHTML = '';
const risk = rep.riskRating||{}; 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> 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>`; <div class="muted">${escapeHTML(rep.riskRating?.summary||'')}</div>`;
@@ -342,7 +386,12 @@
const wrap = document.createElement('div'); const wrap = document.createElement('div');
wrap.className='card'; wrap.className='card';
const count = Array.isArray(b.arr)?b.arr.length:0; 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>`; wrap.innerHTML = `
<h3 style="display:flex;align-items:center;gap:12px">
${b.title}
<span style="color:var(--muted);font-size:14px;font-weight:400">(${count})</span>
<span style="cursor:help;color:var(--accent);font-size:14px" title="${b.info}">?</span>
</h3>`;
if(count===0){ if(count===0){
wrap.innerHTML += '<div class="muted">No items found.</div>'; wrap.innerHTML += '<div class="muted">No items found.</div>';
} else { } else {
@@ -358,25 +407,141 @@
} }
function renderItem(kind, item){ function renderItem(kind, item){
const d = document.createElement('div'); d.className='card'; d.style.margin='0'; const d = document.createElement('div');
d.className='card expandable-item';
d.style.margin='0';
d.style.cursor='pointer';
const esc = escapeHTML; const esc = escapeHTML;
let summary = '';
let details = '';
if(kind==='processes'){ if(kind==='processes'){
d.innerHTML = `<strong>${esc(item.name||'')}</strong><div class="muted">PID: ${item.pid} Path: ${esc(item.path||'')}</div>`; summary = `<strong>${esc(item.name||'')}</strong><div class="muted">PID: ${item.pid}${esc(item.path||'')}</div>`;
details = `
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
<div style="display:grid;gap:6px;font-size:13px">
<div><span style="color:var(--muted)">Name:</span> ${esc(item.name||'N/A')}</div>
<div><span style="color:var(--muted)">PID:</span> ${item.pid||'N/A'}</div>
<div><span style="color:var(--muted)">PPID:</span> ${item.ppid||'N/A'}</div>
<div><span style="color:var(--muted)">Parent:</span> ${esc(item.parent||'N/A')}</div>
<div><span style="color:var(--muted)">Path:</span> ${esc(item.path||'N/A')}</div>
<div><span style="color:var(--muted)">Arguments:</span> ${esc(item.args||'N/A')}</div>
<div><span style="color:var(--muted)">Created:</span> ${esc(item.created||'N/A')}</div>
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated</div>' : ''}
</div>
</div>`;
} else if(kind==='services'){ } 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>`; summary = `<strong>${esc(item.displayName||item.name||'')}</strong><div class="muted">${esc(item.startType||'')}${esc(item.binaryPathName||'')}</div>`;
details = `
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
<div style="display:grid;gap:6px;font-size:13px">
<div><span style="color:var(--muted)">Display Name:</span> ${esc(item.displayName||'N/A')}</div>
<div><span style="color:var(--muted)">Service Name:</span> ${esc(item.name||'N/A')}</div>
<div><span style="color:var(--muted)">Service Type:</span> ${esc(item.serviceType||'N/A')}</div>
<div><span style="color:var(--muted)">Start Type:</span> ${esc(item.startType||'N/A')}</div>
<div><span style="color:var(--muted)">Binary Path:</span> ${esc(item.binaryPathName||'N/A')}</div>
<div><span style="color:var(--muted)">Description:</span> ${esc(item.description||'N/A')}</div>
<div><span style="color:var(--muted)">Error Control:</span> ${esc(item.errorControl||'N/A')}</div>
<div><span style="color:var(--muted)">Load Order Group:</span> ${esc(item.loadOrderGroup||'N/A')}</div>
<div><span style="color:var(--muted)">Service Start Name:</span> ${esc(item.serviceStartName||'N/A')}</div>
<div><span style="color:var(--muted)">Delayed Auto Start:</span> ${item.delayedAutoStart ? 'Yes' : 'No'}</div>
<div><span style="color:var(--muted)">Dependencies:</span> ${item.dependencies && item.dependencies.length > 0 ? esc(item.dependencies.join(', ')) : 'None'}</div>
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated</div>' : ''}
</div>
</div>`;
} else if(kind==='connections'){ } else if(kind==='connections'){
d.innerHTML = `<strong>${esc(item.process||'')}</strong><div class="muted">${esc(item.localAddr||'')}${esc(item.remoteAddr||'')} (${esc(item.remoteHost||'')})</div>`; summary = `<strong>${esc(item.process||'')}</strong><div class="muted">${esc(item.localAddr||'')}${esc(item.remoteAddr||'')} (${esc(item.remoteHost||'')})</div>`;
details = `
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
<div style="display:grid;gap:6px;font-size:13px">
<div><span style="color:var(--muted)">Process:</span> ${esc(item.process||'N/A')}</div>
<div><span style="color:var(--muted)">PID:</span> ${item.pid||'N/A'}</div>
<div><span style="color:var(--muted)">Local Address:</span> ${esc(item.localAddr||'N/A')}</div>
<div><span style="color:var(--muted)">Remote Address:</span> ${esc(item.remoteAddr||'N/A')}</div>
<div><span style="color:var(--muted)">Remote Host:</span> ${esc(item.remoteHost||'N/A')}</div>
<div><span style="color:var(--muted)">State:</span> ${esc(item.state||'N/A')}</div>
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated (Firewall Rule)</div>' : ''}
</div>
</div>`;
} else if(kind==='autoruns'){ } else if(kind==='autoruns'){
d.innerHTML = `<strong>${esc(item.name||item.imageName||item.entry||'')}</strong><div class="muted">${esc(item.location||item.type||'')}${esc(item.command||item.imagePath||item.launchString||item.launch_string||'')}</div>`; const displayName = item.image_name || item.imageName || item.entry || 'Unknown AutoRun';
const launchStr = item.launch_string || item.launchString || item.imagePath || item.image_path || '';
summary = `<strong>${esc(displayName)}</strong><div class="muted">${esc(item.type||'')}${esc(item.location||'')}</div>`;
details = `
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
<div style="display:grid;gap:6px;font-size:13px">
<div><span style="color:var(--muted)">Image Name:</span> ${esc(item.image_name||item.imageName||'N/A')}</div>
<div><span style="color:var(--muted)">Entry:</span> ${esc(item.entry||'N/A')}</div>
<div><span style="color:var(--muted)">Type:</span> ${esc(item.type||'N/A')}</div>
<div><span style="color:var(--muted)">Location:</span> ${esc(item.location||'N/A')}</div>
<div><span style="color:var(--muted)">Image Path:</span> ${esc(item.image_path||item.imagePath||'N/A')}</div>
<div><span style="color:var(--muted)">Launch String:</span> ${esc(launchStr||'N/A')}</div>
<div><span style="color:var(--muted)">Arguments:</span> ${esc(item.arguments||'N/A')}</div>
<div><span style="color:var(--muted)">MD5:</span> ${esc(item.md5||'N/A')}</div>
<div><span style="color:var(--muted)">SHA1:</span> ${esc(item.sha1||'N/A')}</div>
<div><span style="color:var(--muted)">SHA256:</span> ${esc(item.sha256||'N/A')}</div>
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated</div>' : ''}
</div>
</div>`;
} else if(kind==='tasks'){ } else if(kind==='tasks'){
d.innerHTML = `<strong>${esc(item.name||'')}</strong><div class="muted">State: ${esc(item.state||'')} Next: ${esc(item.nextRun||'')}</div>`; summary = `<strong>${esc(item.name||'')}</strong><div class="muted">${esc(item.state||'')}${esc(item.path||'')}</div>`;
details = `
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
<div style="display:grid;gap:6px;font-size:13px">
<div><span style="color:var(--muted)">Name:</span> ${esc(item.name||'N/A')}</div>
<div><span style="color:var(--muted)">Path:</span> ${esc(item.path||'N/A')}</div>
<div><span style="color:var(--muted)">State:</span> ${esc(item.state||'N/A')}</div>
<div><span style="color:var(--muted)">Enabled:</span> ${item.enabled ? 'Yes' : 'No'}</div>
<div><span style="color:var(--muted)">Author:</span> ${esc(item.author||'N/A')}</div>
<div><span style="color:var(--muted)">Description:</span> ${esc(item.description||'N/A')}</div>
<div><span style="color:var(--muted)">Next Run:</span> ${esc(item.nextRun||'N/A')}</div>
<div><span style="color:var(--muted)">Last Run:</span> ${esc(item.lastRun||'N/A')}</div>
<div><span style="color:var(--muted)">Last Result:</span> ${esc(item.lastResult||'N/A')}</div>
<div><span style="color:var(--muted)">Created:</span> ${esc(item.createdDate||'N/A')}</div>
<div><span style="color:var(--muted)">Modified:</span> ${esc(item.modifiedDate||'N/A')}</div>
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated</div>' : ''}
</div>
</div>`;
} else if(kind==='binaries'){ } else if(kind==='binaries'){
const path = item.path || item.Path || (typeof item === 'string' ? item : ''); 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>`; summary = `<div class="muted" style="font-size:13px">📄 Binary</div><strong>${esc(path)}</strong>`;
details = `
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
<div style="display:grid;gap:6px;font-size:13px">
<div><span style="color:var(--muted)">Path:</span> ${esc(path)}</div>
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated</div>' : ''}
</div>
</div>`;
} else if(kind==='directories'){ } else if(kind==='directories'){
const path = item.path || item.Path || (typeof item === 'string' ? item : ''); 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>`; summary = `<div class="muted" style="font-size:13px">📁 Directory</div><strong>${esc(path)}</strong>`;
details = `
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #1d4a2f">
<div style="display:grid;gap:6px;font-size:13px">
<div><span style="color:var(--muted)">Path:</span> ${esc(path)}</div>
${item.eliminated ? '<div style="color:var(--accent)">✓ Eliminated</div>' : ''}
</div>
</div>`;
} }
d.innerHTML = `
<div class="item-summary">${summary}</div>
<div class="item-details" style="display:none">${details}</div>
`;
// Toggle expand/collapse on click
d.addEventListener('click', () => {
const detailsEl = d.querySelector('.item-details');
const isExpanded = detailsEl.style.display !== 'none';
if (isExpanded) {
detailsEl.style.display = 'none';
} else {
detailsEl.style.display = 'block';
}
});
return d; return d;
} }
@@ -454,10 +619,14 @@
await typeText(huntTag, 'Report: '+data.reportName+'.json', 30); await typeText(huntTag, 'Report: '+data.reportName+'.json', 30);
// Load report // Load report
const rep = await (await fetch('/api/report?file='+encodeURIComponent(data.reportName+'.json'))).json(); const reportFileName = data.reportName+'.json';
const rep = await (await fetch('/api/report?file='+encodeURIComponent(reportFileName))).json();
lastReportData = rep; lastReportData = rep;
renderReport(rep); renderReport(rep);
// Set active report indicator
setActiveReport(reportFileName);
// Show View Report button AFTER everything is loaded // Show View Report button AFTER everything is loaded
viewReportBtn.classList.remove('hidden'); viewReportBtn.classList.remove('hidden');
} }
@@ -497,9 +666,19 @@
// Elimination interface // Elimination interface
let currentReport = null; let currentReport = null;
let showEliminatedItems = false;
const elimTree = document.getElementById('elimTree'); const elimTree = document.getElementById('elimTree');
const elimCenter = document.getElementById('elimCenter'); const elimCenter = document.getElementById('elimCenter');
const elimWiki = document.getElementById('elimWiki'); const elimWiki = document.getElementById('elimWiki');
const toggleEliminatedBtn = document.getElementById('toggleEliminatedBtn');
const toggleEliminatedText = document.getElementById('toggleEliminatedText');
// Toggle eliminated items visibility
toggleEliminatedBtn.addEventListener('click', () => {
showEliminatedItems = !showEliminatedItems;
elimTree.classList.toggle('show-eliminated', showEliminatedItems);
toggleEliminatedText.textContent = showEliminatedItems ? 'Hide Eliminated Items' : 'Show Eliminated Items';
});
// Order of operations for safe elimination // Order of operations for safe elimination
const eliminationOrder = { const eliminationOrder = {
@@ -621,6 +800,7 @@
function loadEliminationData(data) { function loadEliminationData(data) {
currentReport = data; currentReport = data;
const findings = data.findings || data; const findings = data.findings || data;
console.log('Loading elimination data, findings:', findings);
// Build tree structure using order of operations // Build tree structure using order of operations
const categories = [ const categories = [
@@ -632,6 +812,7 @@
{ key: 'binaries', title: 'Binaries', arr: findings.binaries }, { key: 'binaries', title: 'Binaries', arr: findings.binaries },
{ key: 'directories', title: 'Directories', arr: findings.directories } { key: 'directories', title: 'Directories', arr: findings.directories }
]; ];
console.log('Categories:', categories);
elimTree.innerHTML = ''; elimTree.innerHTML = '';
elimCenter.innerHTML = '<div class="empty-state">Select an item from the tree</div>'; elimCenter.innerHTML = '<div class="empty-state">Select an item from the tree</div>';
@@ -648,11 +829,14 @@
if (!Array.isArray(cat.arr) || cat.arr.length === 0) return; if (!Array.isArray(cat.arr) || cat.arr.length === 0) return;
const orderInfo = eliminationOrder[cat.key]; const orderInfo = eliminationOrder[cat.key];
const activeCount = cat.arr.filter(item => !item.eliminated).length;
const totalCount = cat.arr.length;
const node = document.createElement('div'); const node = document.createElement('div');
node.className = 'tree-node'; node.className = 'tree-node';
node.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center"> node.innerHTML = `<div style="display:flex;justify-content:space-between;align-items:center">
<span>▶ ${cat.title}</span> <span>▶ ${cat.title}</span>
<span class="tag">${cat.arr.length}</span> <span class="tag">${activeCount}${activeCount !== totalCount ? ` / ${totalCount}` : ''}</span>
</div>`; </div>`;
const childContainer = document.createElement('div'); const childContainer = document.createElement('div');
@@ -661,6 +845,14 @@
cat.arr.forEach((item, idx) => { cat.arr.forEach((item, idx) => {
const child = document.createElement('div'); const child = document.createElement('div');
child.className = 'tree-child'; child.className = 'tree-child';
child.dataset.type = cat.key;
child.dataset.index = idx;
// Mark as eliminated if the item has eliminated flag
if (item.eliminated) {
child.classList.add('eliminated');
}
child.textContent = getItemLabel(cat.key, item, idx); child.textContent = getItemLabel(cat.key, item, idx);
child.onclick = (e) => { child.onclick = (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -688,12 +880,44 @@
if (type === 'processes') return `${item.name} (PID ${item.pid})`; if (type === 'processes') return `${item.name} (PID ${item.pid})`;
if (type === 'services') return item.displayName || item.name; if (type === 'services') return item.displayName || item.name;
if (type === 'tasks') return item.name; if (type === 'tasks') return item.name;
if (type === 'autoruns') return item.imageName || item.entry || `Entry ${idx + 1}`; if (type === 'autoruns') return item.image_name || item.imageName || item.entry || 'Unknown AutoRun';
if (type === 'binaries') return item.path || item; if (type === 'binaries') return item.path || item;
if (type === 'directories') return item.path || item; if (type === 'directories') return item.path || item;
return `Item ${idx + 1}`; return `Item ${idx + 1}`;
} }
function updateCategoryCount(type) {
// Find the category node and update its count
const categoryTitles = {
'connections': 'Outbound Connections',
'processes': 'Processes',
'services': 'Services',
'tasks': 'Scheduled Tasks',
'autoruns': 'AutoRuns',
'binaries': 'Binaries',
'directories': 'Directories'
};
const categoryTitle = categoryTitles[type];
const treeNodes = elimTree.querySelectorAll('.tree-node');
treeNodes.forEach(node => {
if (node.textContent.includes(categoryTitle)) {
const childContainer = node.nextElementSibling;
if (childContainer) {
const allChildren = Array.from(childContainer.querySelectorAll('.tree-child'));
const activeCount = allChildren.filter(child => !child.classList.contains('eliminated')).length;
const totalCount = allChildren.length;
const tagElement = node.querySelector('.tag');
if (tagElement) {
tagElement.textContent = `${activeCount}${activeCount !== totalCount ? ` / ${totalCount}` : ''}`;
}
}
}
});
}
function showItemDetails(type, item, idx) { function showItemDetails(type, item, idx) {
const orderInfo = eliminationOrder[type]; const orderInfo = eliminationOrder[type];
const wiki = wikiContent[type]; const wiki = wikiContent[type];
@@ -737,18 +961,53 @@
} }
function renderItemFields(type, item, wiki) { function renderItemFields(type, item, wiki) {
const fields = Object.keys(wiki.fields); // Get all fields from the item, not just wiki fields
return fields.map(field => { const allFields = new Set();
// Add wiki fields first (for ordering)
Object.keys(wiki.fields).forEach(f => allFields.add(f));
// Add all actual item fields (handle snake_case variants)
Object.keys(item).forEach(key => {
if (key !== 'eliminated') { // Skip eliminated flag
allFields.add(key);
// Also add camelCase variant if this is snake_case
if (key.includes('_')) {
const camelCase = key.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
allFields.add(camelCase);
}
}
});
return Array.from(allFields).map(field => {
// Try both snake_case and camelCase
let value = item[field]; let value = item[field];
if (value === undefined || value === null) {
const snakeCase = field.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
value = item[snakeCase];
}
if (value === undefined || value === null || value === '') return ''; if (value === undefined || value === null || value === '') return '';
// Handle arrays
if (Array.isArray(value)) {
value = value.length > 0 ? value.join(', ') : 'None';
}
// Handle booleans
if (typeof value === 'boolean') value = value ? 'Yes' : 'No'; if (typeof value === 'boolean') value = value ? 'Yes' : 'No';
// Format field name for display
const displayName = field.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').trim()
.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return ` return `
<div class="detail-field"> <div class="detail-field">
<div class="detail-label">${field}</div> <div class="detail-label">${displayName}</div>
<div class="detail-value">${escapeHTML(String(value))}</div> <div class="detail-value">${escapeHTML(String(value))}</div>
</div> </div>
`; `;
}).join(''); }).filter(x => x).join('');
} }
// Modal functions // Modal functions
@@ -756,63 +1015,90 @@
const modalIcon = document.getElementById('modalIcon'); const modalIcon = document.getElementById('modalIcon');
const modalTitle = document.getElementById('modalTitle'); const modalTitle = document.getElementById('modalTitle');
const modalMessage = document.getElementById('modalMessage'); const modalMessage = document.getElementById('modalMessage');
const modalBtn = document.getElementById('modalBtn'); const modalButtons = document.getElementById('modalButtons');
function showModal(type, title, message) { function showModal(type, title, message, onConfirm = null) {
modal.classList.add('active'); modal.classList.add('active');
modalIcon.className = `modal-icon ${type}`; modalIcon.className = `modal-icon ${type}`;
modalTitle.className = `modal-title ${type}`; modalTitle.className = `modal-title ${type}`;
modalTitle.textContent = title; modalTitle.textContent = title;
modalMessage.textContent = message; modalMessage.textContent = message;
modalBtn.className = `modal-btn ${type}`;
modalBtn.textContent = 'OK';
if (type === 'success') { if (type === 'success') {
modalIcon.textContent = '✓'; modalIcon.textContent = '✓';
} else { } else if (type === 'error') {
modalIcon.textContent = '✕'; modalIcon.textContent = '✕';
} else if (type === 'confirm') {
modalIcon.textContent = '⚠';
} }
modalBtn.onclick = () => { // Clear existing buttons
modal.classList.remove('active'); modalButtons.innerHTML = '';
};
if (type === 'confirm' && onConfirm) {
// Confirmation modal with Cancel and Eliminate buttons
const cancelBtn = document.createElement('button');
cancelBtn.className = 'modal-btn';
cancelBtn.textContent = 'Cancel';
cancelBtn.onclick = () => {
modal.classList.remove('active');
};
const confirmBtn = document.createElement('button');
confirmBtn.className = 'modal-btn danger';
confirmBtn.textContent = 'Yes, Eliminate This Item';
confirmBtn.onclick = () => {
modal.classList.remove('active');
onConfirm();
};
modalButtons.appendChild(cancelBtn);
modalButtons.appendChild(confirmBtn);
} else {
// Regular modal with just OK button
const okBtn = document.createElement('button');
okBtn.className = `modal-btn primary`;
okBtn.textContent = 'OK';
okBtn.onclick = () => {
modal.classList.remove('active');
};
modalButtons.appendChild(okBtn);
}
} }
window.eliminateItem = async function(type, idx) { window.eliminateItem = async function(type, idx) {
if (!confirm(`Are you sure you want to eliminate this ${type.slice(0, -1)}?\n\nThis action cannot be undone.`)) return;
if (!currentReport) { if (!currentReport) {
showModal('error', 'Error', 'No report loaded'); showModal('error', 'Error', 'No report loaded');
return; return;
} }
// Get the report filename from the current report // Show custom confirmation modal
const reportFile = currentReport.reportName || currentReport.name || 'rmm-hunter-report.json'; const typeName = type.slice(0, -1);
showModal('confirm', 'Confirm Elimination',
`Are you sure you want to eliminate this ${typeName}?\n\nThis action cannot be undone and will permanently remove it from your system.`,
async () => {
// User confirmed, proceed with elimination
await performElimination(type, idx);
}
);
};
// Find the tree child element for this item async function performElimination(type, idx) {
// Get the report filename from the current report
let reportFile = currentReport.reportName || currentReport.name || 'rmm-hunter-report.json';
// Ensure .json extension
if (!reportFile.endsWith('.json')) {
reportFile += '.json';
}
// Find the tree child element for this item using data attributes
const treeChildren = document.querySelectorAll('.tree-child'); const treeChildren = document.querySelectorAll('.tree-child');
let targetElement = null; let targetElement = null;
// Find the element by matching the index and type // Find the element by matching the data attributes
treeChildren.forEach((child, childIdx) => { treeChildren.forEach((child) => {
const parentNode = child.previousElementSibling; if (child.dataset.type === type && parseInt(child.dataset.index) === idx) {
if (parentNode && parentNode.classList.contains('tree-node')) { targetElement = child;
// Check if this is the right category
const categoryText = parentNode.textContent.toLowerCase();
if ((type === 'connections' && categoryText.includes('outbound')) ||
(type === 'processes' && categoryText.includes('processes')) ||
(type === 'services' && categoryText.includes('services')) ||
(type === 'tasks' && categoryText.includes('scheduled')) ||
(type === 'autoruns' && categoryText.includes('autorun')) ||
(type === 'binaries' && categoryText.includes('binaries')) ||
(type === 'directories' && categoryText.includes('directories'))) {
// This is the right category, now check index
const siblings = Array.from(child.parentElement.children).filter(c => c.classList.contains('tree-child'));
const itemIdx = siblings.indexOf(child);
if (itemIdx === idx) {
targetElement = child;
}
}
} }
}); });
@@ -834,33 +1120,47 @@
return; return;
} }
// Success! Trigger slide-out animation // Success! Trigger slide-out animation and mark as eliminated
if (targetElement) { if (targetElement) {
targetElement.classList.add('eliminating'); targetElement.classList.add('eliminating');
// Wait for animation to complete before updating tree // Wait for animation to complete before hiding
setTimeout(async () => { setTimeout(() => {
// Mark as eliminated (hidden by default, shown in greyscale if toggle is on)
targetElement.classList.remove('eliminating');
targetElement.classList.add('eliminated');
// Update the item in currentReport to mark it as eliminated
if (currentReport && currentReport.findings) {
const findings = currentReport.findings;
const typeMap = {
'connections': 'outboundConnections',
'processes': 'processes',
'services': 'services',
'tasks': 'scheduledTasks',
'autoruns': 'autoRuns',
'binaries': 'binaries',
'directories': 'directories'
};
const arrayKey = typeMap[type];
if (findings[arrayKey] && findings[arrayKey][idx]) {
findings[arrayKey][idx].eliminated = true;
}
}
// Update the category count badge
updateCategoryCount(type);
// Show success modal // Show success modal
showModal('success', 'Successfully Eliminated', `The ${type.slice(0, -1)} has been removed from your system.`); showModal('success', 'Successfully Eliminated', `The ${type.slice(0, -1)} has been removed from your system.`);
// Reload the report to get updated data
const updatedReport = await (await fetch('/api/report?file=' + encodeURIComponent(reportFile))).json();
currentReport = updatedReport;
// Reload elimination tree with updated data
loadEliminationData(updatedReport);
// Clear the center and wiki panels // Clear the center and wiki panels
elimCenter.innerHTML = '<div class="empty-state">Select an item from the tree</div>'; 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>'; elimWiki.innerHTML = '<div class="empty-state">Item details will appear here</div>';
}, 600); // Match animation duration }, 600); // Match animation duration
} else { } else {
// Fallback if element not found // Fallback if element not found - just show success
showModal('success', 'Successfully Eliminated', `The ${type.slice(0, -1)} has been removed from your system.`); showModal('success', 'Successfully Eliminated', `The ${type.slice(0, -1)} has been removed from your system.`);
const updatedReport = await (await fetch('/api/report?file=' + encodeURIComponent(reportFile))).json();
currentReport = updatedReport;
loadEliminationData(updatedReport);
elimCenter.innerHTML = '<div class="empty-state">Select an item from the tree</div>'; 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>'; elimWiki.innerHTML = '<div class="empty-state">Item details will appear here</div>';
} }
@@ -868,7 +1168,7 @@
} catch (error) { } catch (error) {
showModal('error', 'Error', `Failed to eliminate: ${error.message}`); showModal('error', 'Error', `Failed to eliminate: ${error.message}`);
} }
}; }
// Update renderReport to also load elimination data // Update renderReport to also load elimination data
const originalRenderReport = renderReport; const originalRenderReport = renderReport;
+59 -29
View File
@@ -216,12 +216,16 @@ func (s *server) handleListHunts(w http.ResponseWriter, r *http.Request) {
func (s *server) handleGetReport(w http.ResponseWriter, r *http.Request) { func (s *server) handleGetReport(w http.ResponseWriter, r *http.Request) {
f := r.URL.Query().Get("file") f := r.URL.Query().Get("file")
if f == "" || strings.Contains(f, "..") { if f == "" || strings.Contains(f, "..") {
http.Error(w, "bad file", 400) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "bad file"})
return return
} }
b, err := os.ReadFile(f) b, err := os.ReadFile(f)
if err != nil { if err != nil {
http.Error(w, "not found", 404) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"error": "not found"})
return return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -230,7 +234,9 @@ func (s *server) handleGetReport(w http.ResponseWriter, r *http.Request) {
func (s *server) handleStartHunt(w http.ResponseWriter, r *http.Request) { func (s *server) handleStartHunt(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "use POST", 405) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(map[string]string{"error": "use POST"})
return return
} }
name := fmt.Sprintf("hunt-%s", time.Now().Format("20060102-150405")) name := fmt.Sprintf("hunt-%s", time.Now().Format("20060102-150405"))
@@ -296,7 +302,9 @@ func (s *server) handleWS(w http.ResponseWriter, r *http.Request) {
func (s *server) handleEliminate(w http.ResponseWriter, r *http.Request) { func (s *server) handleEliminate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(map[string]string{"error": "method not allowed"})
return return
} }
@@ -307,26 +315,42 @@ func (s *server) handleEliminate(w http.ResponseWriter, r *http.Request) {
} }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "invalid request"})
return return
} }
// Load the report file // Load the report file
reportPath := filepath.Join(".", req.ReportFile) reportFile := req.ReportFile
if !strings.HasSuffix(reportFile, ".json") {
reportFile += ".json"
}
reportPath := filepath.Join(".", reportFile)
data, err := os.ReadFile(reportPath) data, err := os.ReadFile(reportPath)
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("failed to read report: %v", err), http.StatusInternalServerError) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("failed to read report: %v", err)})
return return
} }
var report suspicious.Suspicious // Parse the full report structure with findings wrapper
if err := json.Unmarshal(data, &report); err != nil { var fullReport struct {
http.Error(w, fmt.Sprintf("failed to parse report: %v", err), http.StatusInternalServerError) ReportName string `json:"reportName"`
GeneratedAt string `json:"generatedAt"`
RiskRating interface{} `json:"riskRating"`
Findings suspicious.Suspicious `json:"findings"`
}
if err := json.Unmarshal(data, &fullReport); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("failed to parse report: %v", err)})
return return
} }
// Perform elimination based on type // Perform elimination based on type
if err := performElimination(&report, req.Type, req.Index); err != nil { if err := performElimination(&fullReport.Findings, req.Type, req.Index); err != nil {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
@@ -334,14 +358,18 @@ func (s *server) handleEliminate(w http.ResponseWriter, r *http.Request) {
} }
// Save updated report // Save updated report
updatedData, err := json.MarshalIndent(report, "", " ") updatedData, err := json.MarshalIndent(fullReport, "", " ")
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("failed to marshal report: %v", err), http.StatusInternalServerError) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("failed to marshal report: %v", err)})
return return
} }
if err := os.WriteFile(reportPath, updatedData, 0644); err != nil { if err := os.WriteFile(reportPath, updatedData, 0644); err != nil {
http.Error(w, fmt.Sprintf("failed to save report: %v", err), http.StatusInternalServerError) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("failed to save report: %v", err)})
return return
} }
@@ -351,7 +379,9 @@ func (s *server) handleEliminate(w http.ResponseWriter, r *http.Request) {
func (s *server) handleQuit(w http.ResponseWriter, r *http.Request) { func (s *server) handleQuit(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "use POST", 405) w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
json.NewEncoder(w).Encode(map[string]string{"error": "use POST"})
return return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@@ -363,8 +393,8 @@ func (s *server) handleQuit(w http.ResponseWriter, r *http.Request) {
func performElimination(report *suspicious.Suspicious, typeKey string, idx int) error { func performElimination(report *suspicious.Suspicious, typeKey string, idx int) error {
switch typeKey { switch typeKey {
case "connections": case "connections":
if idx >= len(report.OutboundConnections) { if idx < 0 || idx >= len(report.OutboundConnections) {
return fmt.Errorf("invalid index") return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.OutboundConnections))
} }
conn := report.OutboundConnections[idx] conn := report.OutboundConnections[idx]
if err := eliminate.EliminateConnection(conn.RemoteHost); err != nil { if err := eliminate.EliminateConnection(conn.RemoteHost); err != nil {
@@ -373,8 +403,8 @@ func performElimination(report *suspicious.Suspicious, typeKey string, idx int)
report.OutboundConnections[idx].Eliminated = true report.OutboundConnections[idx].Eliminated = true
case "processes": case "processes":
if idx >= len(report.Processes) { if idx < 0 || idx >= len(report.Processes) {
return fmt.Errorf("invalid index") return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.Processes))
} }
proc := report.Processes[idx] proc := report.Processes[idx]
if err := eliminate.EliminateProcess(proc); err != nil { if err := eliminate.EliminateProcess(proc); err != nil {
@@ -383,8 +413,8 @@ func performElimination(report *suspicious.Suspicious, typeKey string, idx int)
report.Processes[idx].Eliminated = true report.Processes[idx].Eliminated = true
case "services": case "services":
if idx >= len(report.Services) { if idx < 0 || idx >= len(report.Services) {
return fmt.Errorf("invalid index") return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.Services))
} }
svc := report.Services[idx] svc := report.Services[idx]
if svc == nil { if svc == nil {
@@ -396,8 +426,8 @@ func performElimination(report *suspicious.Suspicious, typeKey string, idx int)
report.Services[idx].Eliminated = true report.Services[idx].Eliminated = true
case "tasks": case "tasks":
if idx >= len(report.ScheduledTasks) { if idx < 0 || idx >= len(report.ScheduledTasks) {
return fmt.Errorf("invalid index") return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.ScheduledTasks))
} }
task := report.ScheduledTasks[idx] task := report.ScheduledTasks[idx]
if task == nil { if task == nil {
@@ -409,8 +439,8 @@ func performElimination(report *suspicious.Suspicious, typeKey string, idx int)
report.ScheduledTasks[idx].Eliminated = true report.ScheduledTasks[idx].Eliminated = true
case "autoruns": case "autoruns":
if idx >= len(report.AutoRuns) { if idx < 0 || idx >= len(report.AutoRuns) {
return fmt.Errorf("invalid index") return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.AutoRuns))
} }
ar := report.AutoRuns[idx] ar := report.AutoRuns[idx]
if err := eliminate.EliminateAutoRun(ar); err != nil { if err := eliminate.EliminateAutoRun(ar); err != nil {
@@ -419,8 +449,8 @@ func performElimination(report *suspicious.Suspicious, typeKey string, idx int)
report.AutoRuns[idx].Eliminated = true report.AutoRuns[idx].Eliminated = true
case "binaries": case "binaries":
if idx >= len(report.Binaries) { if idx < 0 || idx >= len(report.Binaries) {
return fmt.Errorf("invalid index") return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.Binaries))
} }
bin := report.Binaries[idx] bin := report.Binaries[idx]
// Check if binary is blocked by active processes/services // Check if binary is blocked by active processes/services
@@ -433,8 +463,8 @@ func performElimination(report *suspicious.Suspicious, typeKey string, idx int)
report.Binaries[idx].Eliminated = true report.Binaries[idx].Eliminated = true
case "directories": case "directories":
if idx >= len(report.Directories) { if idx < 0 || idx >= len(report.Directories) {
return fmt.Errorf("invalid index") return fmt.Errorf("invalid index: %d (array length: %d)", idx, len(report.Directories))
} }
dir := report.Directories[idx] dir := report.Directories[idx]
// Check if directory is blocked by active processes/services // Check if directory is blocked by active processes/services