From 3f50f208928fc066b23ab9a4cdc932732fbbcd27 Mon Sep 17 00:00:00 2001 From: Evan Hosinski Date: Sun, 12 Oct 2025 21:58:11 -0400 Subject: [PATCH] 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. --- .../pkg/hunt/detect/directory/directories.go | 107 +++-- internal/pkg/hunt/eliminate/autorun.go | 28 +- internal/web/templates/index.html | 450 +++++++++++++++--- internal/web/webserver.go | 88 ++-- 4 files changed, 536 insertions(+), 137 deletions(-) diff --git a/internal/pkg/hunt/detect/directory/directories.go b/internal/pkg/hunt/detect/directory/directories.go index a0ed870..16afaeb 100644 --- a/internal/pkg/hunt/detect/directory/directories.go +++ b/internal/pkg/hunt/detect/directory/directories.go @@ -7,55 +7,102 @@ import ( "rmm-hunter/internal/pkg/hunt/detect/common" . "rmm-hunter/internal/suspicious" "strings" + "sync" ) var appData = os.Getenv("APPDATA") var userProfile = os.Getenv("USERPROFILE") -func Detect() []Directory { - var suspiciousDirectories []Directory - seen := make(map[string]bool) // Prevent duplicates +const numWorkers = 5 +type searchJob struct { + basePath string + rmmDir string +} + +func Detect() []Directory { 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 _, basePath := range common.SearchBasePaths { - // Replace environment variables - basePath = replaceEnvVars(basePath) - - // 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 - } - } + jobs <- searchJob{ + basePath: basePath, + rmmDir: rmmDir, } } } + // 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)) 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 func replaceEnvVars(path string) string { path = strings.ReplaceAll(path, "{{APPDATA}}", appData) diff --git a/internal/pkg/hunt/eliminate/autorun.go b/internal/pkg/hunt/eliminate/autorun.go index e151d83..1a09858 100644 --- a/internal/pkg/hunt/eliminate/autorun.go +++ b/internal/pkg/hunt/eliminate/autorun.go @@ -10,11 +10,33 @@ import ( // EliminateAutoRun removes an autorun entry from the system func EliminateAutoRun(ar AutoRun) error { all := scurvy.ListAutoruns() + + // Try to find by MD5 first for _, a := range all { - if a.MD5 == ar.MD5 { - // Found it, delete it + if a.MD5 == ar.MD5 && a.MD5 != "" { 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) } diff --git a/internal/web/templates/index.html b/internal/web/templates/index.html index a4eee25..79b2133 100644 --- a/internal/web/templates/index.html +++ b/internal/web/templates/index.html @@ -30,6 +30,10 @@ .nav{max-width:1400px;margin:0 auto;display:flex;align-items:center;gap:8px;padding:12px 24px} .brand{display:flex;align-items:center;gap:12px;font-weight:700;letter-spacing:.5px;font-size:16px;color:var(--text)} .brand img{width:40px;height:40px;object-fit:contain;filter:drop-shadow(0 0 8px rgba(23,228,110,.3))} + .active-report-indicator{display:flex;align-items:center;gap:8px;background:rgba(23,228,110,.08);border:1px solid rgba(23,228,110,.2);border-radius:8px;padding:8px 14px;margin-left:16px;transition:all 0.3s ease} + .active-report-indicator .report-icon{font-size:16px} + .active-report-indicator .report-name{font-size:13px;color:var(--accent);font-weight:500;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .active-report-indicator:hover{background:rgba(23,228,110,.12);border-color:rgba(23,228,110,.3)} .spacer{flex:1} .nav a.btn{display:inline-flex;align-items:center;padding:10px 18px;border:none;border-radius:6px;color:var(--text);text-decoration:none;transition:all .2s;font-weight:500;font-size:14px;position:relative;overflow:hidden} .nav a.btn::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(135deg,rgba(23,228,110,.1),rgba(23,228,110,.05));opacity:0;transition:opacity .2s} @@ -42,6 +46,9 @@ main{max-width:1100px;margin:20px auto;padding:0 16px;flex:1;width:100%} main.full-width{max-width:none;padding:0 20px} .card{background:var(--panel);border:1px solid #133422;border-radius:12px;padding:16px;margin-bottom:16px;box-shadow:0 8px 24px rgba(0,0,0,.25);overflow-wrap:break-word;word-wrap:break-word;word-break:break-word;overflow:hidden} + .expandable-item{transition:all 0.2s ease;position:relative;padding-right:40px} + .expandable-item:hover{border-color:var(--accent);background:#0e1a13} + .expandable-item::after{content:'▼';position:absolute;right:20px;top:20px;color:var(--muted);font-size:12px;transition:transform 0.2s} h1,h2{margin:10px 0} .muted{color:var(--muted)} .grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));min-height:400px} @@ -74,6 +81,9 @@ .tree-child:hover::before,.tree-child.selected::before{opacity:1} .tree-child.eliminating{animation:slideOutRight 0.6s ease-in-out forwards} .tree-child.eliminating::after{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(90deg,transparent,rgba(23,228,110,.3),transparent);animation:shimmer 0.6s ease-in-out} + .tree-child.eliminated{display:none;opacity:0.4;filter:grayscale(1);text-decoration:line-through;pointer-events:none} + .tree-child.eliminated::before{content:'✓ ';color:#17e46e;opacity:1} + .show-eliminated .tree-child.eliminated{display:flex} @keyframes slideOutRight{0%{transform:translateX(0);opacity:1}50%{opacity:0.5}100%{transform:translateX(100%);opacity:0;height:0;margin:0;padding:0}} @keyframes shimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}} .detail-field{margin:12px 0;padding:10px;background:#050805;border:1px solid #0c2819;border-radius:8px} @@ -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.active{display:flex} .modal-content{background:var(--panel);border:1px solid #133422;border-radius:12px;padding:30px;max-width:500px;width:90%;box-shadow:0 20px 60px rgba(0,0,0,.5)} - .modal-icon{font-size: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.error{color:#ff5c7a} @keyframes checkPop{0%{transform:scale(0)}50%{transform:scale(1.2)}100%{transform:scale(1)}} - .modal-title{font-size:24px;font-weight:700;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.error{color:#ff5c7a} - .modal-message{text-align:center;color:var(--muted);margin-bottom:24px;line-height:1.6} - .modal-btn{width:100%;padding:14px;border:none;border-radius:8px;font-weight:600;font-size:15px;cursor:pointer;transition:all 0.2s} - .modal-btn.success{background:var(--accent);color:#000} - .modal-btn.success:hover{filter:brightness(1.2)} - .modal-btn.error{background:rgba(255,92,122,.2);color:#ff5c7a;border:1px solid #ff5c7a} - .modal-btn.error:hover{background:rgba(255,92,122,.3)} + .modal-title.confirm{color:#ffd166} + .modal-message{color:var(--text);margin-bottom:24px;line-height:1.6;white-space:pre-wrap;font-size:15px;text-align:left} + .modal-buttons{display:flex;gap:12px;margin-top:24px} + .modal-btn{flex:1;padding:12px 24px;border:1px solid #1d4a2f;background:var(--panel);color:var(--text);font-weight:500;font-size:14px;cursor:pointer;transition:all 0.2s;border-radius:0} + .modal-btn:hover{background:#0e1a13;border-color:var(--accent)} + .modal-btn.primary{background:var(--accent);color:#000;border-color:var(--accent)} + .modal-btn.primary:hover{filter:brightness(1.2)} + .modal-btn.danger{background:rgba(255,92,122,.15);color:#ff5c7a;border-color:#ff5c7a} + .modal-btn.danger:hover{background:rgba(255,92,122,.25)} + .modal-icon.confirm{color:#ffd166;text-align:left} @@ -161,7 +175,7 @@ - + @@ -169,9 +183,14 @@ @@ -204,7 +223,12 @@