diff --git a/cmd/root.go b/cmd/root.go index c8a4b4f..de4e490 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -30,12 +30,15 @@ var huntCmd = &cobra.Command{ Use: "hunt", Short: "Hunt for RMM software on the system", Long: `Hunt mode scans the system for signs of RMM software including: -- Suspicious processes +- Suspicious Processes +- Suspicious Autoruns - Services -- Binaries and executables -- Network connections -- Scheduled tasks -- Registry entries`, +- Binaries and Executables +- Directories +- Processes +- Outbound Network Connections +- Scheduled Tasks +- Registry Entries`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("Starting RMM Hunt...") runHunt() @@ -54,7 +57,7 @@ Requires a JSON input file containing hunt results to determine what to remove.` os.Exit(1) } - fmt.Printf("Starting Sus Elimination using input file: %s\n", inputFile) + fmt.Printf("Starting RMM Elimination using input file: %s\n", inputFile) // TODO: Call eliminate.Eliminate() function runEliminate() }, @@ -90,7 +93,6 @@ func init() { } func runHunt() { - fmt.Println("Starting Sus Hunt...") if len(excludeRMMs) > 0 { fmt.Printf("Excluding RMMs: %v\n", excludeRMMs) } diff --git a/internal/pkg/hunt/detect/common/functions.go b/internal/pkg/hunt/detect/common/functions.go index ad03854..9487b3b 100644 --- a/internal/pkg/hunt/detect/common/functions.go +++ b/internal/pkg/hunt/detect/common/functions.go @@ -22,17 +22,10 @@ func AnalyzeExecutablePath(command string) (bool, string) { } } - // Check for suspicious installation paths - suspiciousPaths := []string{ - "\\temp\\", "\\tmp\\", "\\appdata\\local\\temp\\", - "\\users\\public\\", "\\programdata\\", - "\\windows\\temp\\", "\\%temp%\\", - } - execPathLower := strings.ToLower(execPath) // Check for suspicious installation paths - suspiciousPaths = []string{ + suspiciousPaths := []string{ "\\temp\\", "\\tmp\\", "\\appdata\\local\\temp\\", "\\users\\public\\", "\\programdata\\", "\\windows\\temp\\", "\\%temp%\\", @@ -50,7 +43,6 @@ func AnalyzeExecutablePath(command string) (bool, string) { "\\oracle\\", "\\citrix\\", "\\vmware\\", - // Add more trusted publishers as needed } isTrusted := false diff --git a/internal/pkg/hunt/detect/connections/outbound.go b/internal/pkg/hunt/detect/connections/outbound.go index 250d1c3..a9eb3e5 100644 --- a/internal/pkg/hunt/detect/connections/outbound.go +++ b/internal/pkg/hunt/detect/connections/outbound.go @@ -45,7 +45,7 @@ func compareConnections(connections []NetworkConnection) []NetworkConnection { for _, dns := range common.CommonDNS { if matchesDNSPattern(remote, dns) { - fmt.Printf(" [?] Found %s\n", conn.RemoteHost) + fmt.Printf(" [?] Found %s\n", conn.RemoteHost) suspiciousConnections = append(suspiciousConnections, conn) break } diff --git a/internal/pkg/hunter/hunter.go b/internal/pkg/hunter/hunter.go index 90a0124..8016cdb 100644 --- a/internal/pkg/hunter/hunter.go +++ b/internal/pkg/hunter/hunter.go @@ -1,6 +1,7 @@ package hunter import ( + "fmt" "rmm-hunter/internal/pkg" "rmm-hunter/internal/pkg/hunt/detect/autorun" "rmm-hunter/internal/pkg/hunt/detect/binaries" @@ -9,47 +10,61 @@ import ( "rmm-hunter/internal/pkg/hunt/detect/processes" "rmm-hunter/internal/pkg/hunt/detect/scheduledTasks" "rmm-hunter/internal/pkg/hunt/detect/services" + "rmm-hunter/internal/pkg/writer" . "rmm-hunter/internal/suspicious" ) type Hunter struct { Options pkg.RunOptions - Sus Suspicious + Sus *Suspicious } func Start(options pkg.RunOptions) { hunter := Hunter{ Options: options, + Sus: &Suspicious{}, } hunter.run() } func (h *Hunter) run() { - // Find suspicious processes - processes := processes.Detect() - h.Sus.Processes = processes + // Find suspicious suspiciousProcesses + suspiciousProcesses := processes.Detect() + h.Sus.Processes = suspiciousProcesses - // Find suspicious services - services := services.Detect() - h.Sus.Services = services + // Find suspicious suspiciousServices + suspiciousServices := services.Detect() + h.Sus.Services = suspiciousServices // Find suspicious autoruns autoruns := autorun.Detect() h.Sus.AutoRuns = autoruns - // Find suspicious outbound connections - connections := connections.DetectOutboundConnections() - h.Sus.OutboundConnections = connections + // Find suspicious outbound outboundConnections + outboundConnections := connections.DetectOutboundConnections() + h.Sus.OutboundConnections = outboundConnections // Find suspicious scheduled tasks tasks := scheduledTasks.Detect() h.Sus.ScheduledTasks = tasks - // Find suspicious binaries - binaries := binaries.Detect() - h.Sus.Binaries = binaries + // Find suspicious suspiciousBinaries + suspiciousBinaries := binaries.Detect() + h.Sus.Binaries = suspiciousBinaries // Find suspicious directories directories := directory.Detect() h.Sus.Directories = directories + + // Write to json + err := writer.WriteJSONReport(h.Sus, &h.Options) + if err != nil { + fmt.Printf("[-] Error writing JSON report: %s\n", err.Error()) + } + + // Write to html + err = writer.WriteHTMLReport(h.Sus, &h.Options) + if err != nil { + fmt.Printf("[-] Error writing HTML report: %s\n", err.Error()) + } } diff --git a/internal/pkg/options.go b/internal/pkg/options.go index 291ff16..89c5b7b 100644 --- a/internal/pkg/options.go +++ b/internal/pkg/options.go @@ -2,4 +2,5 @@ package pkg type RunOptions struct { ExcludeRMMs []string + Name string } diff --git a/internal/pkg/writer/disposition/disposition.go b/internal/pkg/writer/disposition/disposition.go new file mode 100644 index 0000000..8772d3a --- /dev/null +++ b/internal/pkg/writer/disposition/disposition.go @@ -0,0 +1,93 @@ +package disposition + +import ( + "fmt" + "rmm-hunter/internal/suspicious" + "strings" +) + +type Disposition struct { + Score float64 `json:"score"` + Rating string `json:"rating"` + Summary string `json:"summary"` +} + +// CalculateDisposition analyzes the Hunter's findings and returns a risk assessment +func CalculateDisposition(sus *suspicious.Suspicious) *Disposition { + if sus == nil { + return &Disposition{ + Score: 0.0, + Rating: "Low", + Summary: "No suspicious activity detected", + } + } + + var score float64 + var findings []string + + // Score based on different categories + if len(sus.Processes) > 0 { + score += float64(len(sus.Processes)) * 1.5 + findings = append(findings, fmt.Sprintf("%d suspicious processes", len(sus.Processes))) + } + + if len(sus.Services) > 0 { + score += float64(len(sus.Services)) * 2.0 + findings = append(findings, fmt.Sprintf("%d suspicious services", len(sus.Services))) + } + + if len(sus.OutboundConnections) > 0 { + score += float64(len(sus.OutboundConnections)) * 1.8 + findings = append(findings, fmt.Sprintf("%d suspicious outbound connections", len(sus.OutboundConnections))) + } + + if len(sus.ScheduledTasks) > 0 { + score += float64(len(sus.ScheduledTasks)) * 1.2 + findings = append(findings, fmt.Sprintf("%d suspicious scheduled tasks", len(sus.ScheduledTasks))) + } + + if len(sus.AutoRuns) > 0 { + score += float64(len(sus.AutoRuns)) * 1.3 + findings = append(findings, fmt.Sprintf("%d suspicious autoruns", len(sus.AutoRuns))) + } + + if len(sus.Binaries) > 0 { + score += float64(len(sus.Binaries)) * 0.8 + findings = append(findings, fmt.Sprintf("%d suspicious binaries", len(sus.Binaries))) + } + + if len(sus.Directories) > 0 { + score += float64(len(sus.Directories)) * 0.5 + findings = append(findings, fmt.Sprintf("%d suspicious directories", len(sus.Directories))) + } + + // Normalize score to 0-10 scale + if score > 10 { + score = 10.0 + } + + // Determine rating + var rating string + switch { + case score <= 3.0: + rating = "Low" + case score <= 6.0: + rating = "Medium" + default: + rating = "High" + } + + // Generate summary + var summary string + if len(findings) == 0 { + summary = "No suspicious activity detected" + } else { + summary = fmt.Sprintf("Detected: %s", strings.Join(findings, ", ")) + } + + return &Disposition{ + Score: score, + Rating: rating, + Summary: summary, + } +} diff --git a/internal/pkg/writer/html.go b/internal/pkg/writer/html.go index 41a4261..28f514e 100644 --- a/internal/pkg/writer/html.go +++ b/internal/pkg/writer/html.go @@ -1 +1,120 @@ package writer + +import ( + "fmt" + "html/template" + "os" + "path/filepath" + "rmm-hunter/internal/pkg" + "rmm-hunter/internal/pkg/writer/disposition" + "rmm-hunter/internal/suspicious" + "strings" + "time" +) + +type HTMLReportData struct { + ReportName string + GeneratedAt string + RiskRating *disposition.Disposition + Findings interface{} +} + +// WriteHTMLReport generates an HTML report from Hunter findings +func WriteHTMLReport(sus *suspicious.Suspicious, opts *pkg.RunOptions) error { + if opts == nil { + opts = &pkg.RunOptions{Name: "rmm-hunter-report"} + } + + if opts.Name == "" { + opts.Name = "rmm-hunter-report" + } + + if sus == nil { + return fmt.Errorf("suspicious instance is nil") + } + + // Calculate risk disposition + riskRating := disposition.CalculateDisposition(sus) + + // Prepare template data + data := HTMLReportData{ + ReportName: opts.Name, + GeneratedAt: time.Now().UTC().Format("2006-01-02 15:04:05 UTC"), + RiskRating: riskRating, + Findings: sus, + } + + // Parse template + tmpl, err := template.New("report").Funcs(templateFuncs()).Parse(htmlTemplate) + if err != nil { + return fmt.Errorf("failed to parse HTML template: %w", err) + } + + // Ensure output directory exists + filename := fmt.Sprintf("%s.html", opts.Name) + if err := ensureDir(filepath.Dir(filename)); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + // Create output file + file, err := os.Create(filename) + if err != nil { + return fmt.Errorf("failed to create HTML file: %w", err) + } + defer file.Close() + + // Execute template + if err := tmpl.Execute(file, data); err != nil { + return fmt.Errorf("failed to execute HTML template: %w", err) + } + + fmt.Printf("[+] HTML report written to: %s\n", filename) + return nil +} + +// templateFuncs provides helper functions for the HTML template +func templateFuncs() template.FuncMap { + return template.FuncMap{ + "safeHTML": func(s string) template.HTML { + return template.HTML(s) + }, + "riskColor": func(rating string) string { + switch strings.ToLower(rating) { + case "low": + return "#28a745" + case "medium": + return "#ffc107" + case "high": + return "#dc3545" + default: + return "#6c757d" + } + }, + "len": func(v interface{}) int { + if v == nil { + return 0 + } + switch val := v.(type) { + case []interface{}: + return len(val) + case []string: + return len(val) + case []*suspicious.Service: + return len(val) + case []suspicious.Process: + return len(val) + case []suspicious.NetworkConnection: + return len(val) + case []*suspicious.ScheduledTask: + return len(val) + case []suspicious.AutoRun: + return len(val) + default: + return 0 + } + }, + "mul": func(a, b float64) float64 { + return a * b + }, + } +} diff --git a/internal/pkg/writer/htmlTemplate.go b/internal/pkg/writer/htmlTemplate.go new file mode 100644 index 0000000..17bd89f --- /dev/null +++ b/internal/pkg/writer/htmlTemplate.go @@ -0,0 +1,562 @@ +package writer + +const htmlTemplate = ` + +
+ + +