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 }, } }