2025-10-10 15:35:17 -04:00
|
|
|
package writer
|
2025-10-10 16:06:48 -04:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|