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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user