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 @@