Add JSON and HTML writers for reporting Hunter findings
This commit is contained in:
+9
-7
@@ -30,12 +30,15 @@ var huntCmd = &cobra.Command{
|
|||||||
Use: "hunt",
|
Use: "hunt",
|
||||||
Short: "Hunt for RMM software on the system",
|
Short: "Hunt for RMM software on the system",
|
||||||
Long: `Hunt mode scans the system for signs of RMM software including:
|
Long: `Hunt mode scans the system for signs of RMM software including:
|
||||||
- Suspicious processes
|
- Suspicious Processes
|
||||||
|
- Suspicious Autoruns
|
||||||
- Services
|
- Services
|
||||||
- Binaries and executables
|
- Binaries and Executables
|
||||||
- Network connections
|
- Directories
|
||||||
- Scheduled tasks
|
- Processes
|
||||||
- Registry entries`,
|
- Outbound Network Connections
|
||||||
|
- Scheduled Tasks
|
||||||
|
- Registry Entries`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
fmt.Println("Starting RMM Hunt...")
|
fmt.Println("Starting RMM Hunt...")
|
||||||
runHunt()
|
runHunt()
|
||||||
@@ -54,7 +57,7 @@ Requires a JSON input file containing hunt results to determine what to remove.`
|
|||||||
os.Exit(1)
|
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
|
// TODO: Call eliminate.Eliminate() function
|
||||||
runEliminate()
|
runEliminate()
|
||||||
},
|
},
|
||||||
@@ -90,7 +93,6 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runHunt() {
|
func runHunt() {
|
||||||
fmt.Println("Starting Sus Hunt...")
|
|
||||||
if len(excludeRMMs) > 0 {
|
if len(excludeRMMs) > 0 {
|
||||||
fmt.Printf("Excluding RMMs: %v\n", excludeRMMs)
|
fmt.Printf("Excluding RMMs: %v\n", excludeRMMs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
execPathLower := strings.ToLower(execPath)
|
||||||
|
|
||||||
// Check for suspicious installation paths
|
// Check for suspicious installation paths
|
||||||
suspiciousPaths = []string{
|
suspiciousPaths := []string{
|
||||||
"\\temp\\", "\\tmp\\", "\\appdata\\local\\temp\\",
|
"\\temp\\", "\\tmp\\", "\\appdata\\local\\temp\\",
|
||||||
"\\users\\public\\", "\\programdata\\",
|
"\\users\\public\\", "\\programdata\\",
|
||||||
"\\windows\\temp\\", "\\%temp%\\",
|
"\\windows\\temp\\", "\\%temp%\\",
|
||||||
@@ -50,7 +43,6 @@ func AnalyzeExecutablePath(command string) (bool, string) {
|
|||||||
"\\oracle\\",
|
"\\oracle\\",
|
||||||
"\\citrix\\",
|
"\\citrix\\",
|
||||||
"\\vmware\\",
|
"\\vmware\\",
|
||||||
// Add more trusted publishers as needed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isTrusted := false
|
isTrusted := false
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package hunter
|
package hunter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"rmm-hunter/internal/pkg"
|
"rmm-hunter/internal/pkg"
|
||||||
"rmm-hunter/internal/pkg/hunt/detect/autorun"
|
"rmm-hunter/internal/pkg/hunt/detect/autorun"
|
||||||
"rmm-hunter/internal/pkg/hunt/detect/binaries"
|
"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/processes"
|
||||||
"rmm-hunter/internal/pkg/hunt/detect/scheduledTasks"
|
"rmm-hunter/internal/pkg/hunt/detect/scheduledTasks"
|
||||||
"rmm-hunter/internal/pkg/hunt/detect/services"
|
"rmm-hunter/internal/pkg/hunt/detect/services"
|
||||||
|
"rmm-hunter/internal/pkg/writer"
|
||||||
. "rmm-hunter/internal/suspicious"
|
. "rmm-hunter/internal/suspicious"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Hunter struct {
|
type Hunter struct {
|
||||||
Options pkg.RunOptions
|
Options pkg.RunOptions
|
||||||
Sus Suspicious
|
Sus *Suspicious
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(options pkg.RunOptions) {
|
func Start(options pkg.RunOptions) {
|
||||||
hunter := Hunter{
|
hunter := Hunter{
|
||||||
Options: options,
|
Options: options,
|
||||||
|
Sus: &Suspicious{},
|
||||||
}
|
}
|
||||||
hunter.run()
|
hunter.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hunter) run() {
|
func (h *Hunter) run() {
|
||||||
// Find suspicious processes
|
// Find suspicious suspiciousProcesses
|
||||||
processes := processes.Detect()
|
suspiciousProcesses := processes.Detect()
|
||||||
h.Sus.Processes = processes
|
h.Sus.Processes = suspiciousProcesses
|
||||||
|
|
||||||
// Find suspicious services
|
// Find suspicious suspiciousServices
|
||||||
services := services.Detect()
|
suspiciousServices := services.Detect()
|
||||||
h.Sus.Services = services
|
h.Sus.Services = suspiciousServices
|
||||||
|
|
||||||
// Find suspicious autoruns
|
// Find suspicious autoruns
|
||||||
autoruns := autorun.Detect()
|
autoruns := autorun.Detect()
|
||||||
h.Sus.AutoRuns = autoruns
|
h.Sus.AutoRuns = autoruns
|
||||||
|
|
||||||
// Find suspicious outbound connections
|
// Find suspicious outbound outboundConnections
|
||||||
connections := connections.DetectOutboundConnections()
|
outboundConnections := connections.DetectOutboundConnections()
|
||||||
h.Sus.OutboundConnections = connections
|
h.Sus.OutboundConnections = outboundConnections
|
||||||
|
|
||||||
// Find suspicious scheduled tasks
|
// Find suspicious scheduled tasks
|
||||||
tasks := scheduledTasks.Detect()
|
tasks := scheduledTasks.Detect()
|
||||||
h.Sus.ScheduledTasks = tasks
|
h.Sus.ScheduledTasks = tasks
|
||||||
|
|
||||||
// Find suspicious binaries
|
// Find suspicious suspiciousBinaries
|
||||||
binaries := binaries.Detect()
|
suspiciousBinaries := binaries.Detect()
|
||||||
h.Sus.Binaries = binaries
|
h.Sus.Binaries = suspiciousBinaries
|
||||||
|
|
||||||
// Find suspicious directories
|
// Find suspicious directories
|
||||||
directories := directory.Detect()
|
directories := directory.Detect()
|
||||||
h.Sus.Directories = directories
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ package pkg
|
|||||||
|
|
||||||
type RunOptions struct {
|
type RunOptions struct {
|
||||||
ExcludeRMMs []string
|
ExcludeRMMs []string
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,120 @@
|
|||||||
package writer
|
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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,562 @@
|
|||||||
|
package writer
|
||||||
|
|
||||||
|
const htmlTemplate = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.ReportName}} - RMM Hunter Report</title>
|
||||||
|
|
||||||
|
<!-- Modern font -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background: radial-gradient(circle at top left, #101820, #0a0a0a);
|
||||||
|
color: #e5e5e5;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1250px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 50px 20px 40px;
|
||||||
|
background: linear-gradient(135deg, #1f2933, #273543);
|
||||||
|
border-radius: 16px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
box-shadow: 0 8px 20px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-name {
|
||||||
|
font-size: 2.8em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #00aaff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-link {
|
||||||
|
color: #00aaff;
|
||||||
|
font-size: 1.05em;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-link:hover {
|
||||||
|
color: #66c2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-title {
|
||||||
|
font-size: 2.2em;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-section {
|
||||||
|
margin-top: 25px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-banner {
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 1.3em;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 14px 30px;
|
||||||
|
border-radius: 25px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-gauge {
|
||||||
|
width: 130px;
|
||||||
|
height: 130px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: conic-gradient({{riskColor .RiskRating.Rating}} {{mul .RiskRating.Score 10}}%, #333 0%);
|
||||||
|
box-shadow: inset 0 0 12px rgba(0,0,0,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-gauge-inner {
|
||||||
|
width: 85px;
|
||||||
|
height: 85px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #1a1a1a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
background-color: rgba(40, 40, 40, 0.9);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #00aaff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-detail strong {
|
||||||
|
color: #00aaff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background-color: rgba(25,25,25,0.95);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 6px 20px rgba(0,0,0,0.5);
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar h3 {
|
||||||
|
color: #00aaff;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar li { margin-bottom: 6px; }
|
||||||
|
|
||||||
|
.nav-sidebar a {
|
||||||
|
color: #ddd;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar a:hover {
|
||||||
|
background-color: #00aaff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
background-color: #222;
|
||||||
|
color: #ddd;
|
||||||
|
font-size: 0.95em;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box::placeholder { color: #777; }
|
||||||
|
|
||||||
|
.content { margin-left: 260px; }
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background-color: rgba(44,44,44,0.85);
|
||||||
|
margin-bottom: 35px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:hover { transform: translateY(-2px); }
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
background-color: #263445;
|
||||||
|
padding: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header:hover { background-color: #2f435b; }
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-count {
|
||||||
|
background-color: #00aaff;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
padding: 20px;
|
||||||
|
display: none;
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content.active { display: block; }
|
||||||
|
|
||||||
|
.item {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #00aaff;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 3px 8px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #00aaff;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-detail {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-sidebar { display: none; }
|
||||||
|
.content { margin-left: 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="nav-sidebar">
|
||||||
|
<input type="text" id="search" placeholder="Search findings..." class="search-box">
|
||||||
|
<h3>Navigation</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#processes">Processes</a></li>
|
||||||
|
<li><a href="#services">Services</a></li>
|
||||||
|
<li><a href="#connections">Connections</a></li>
|
||||||
|
<li><a href="#tasks">Scheduled Tasks</a></li>
|
||||||
|
<li><a href="#autoruns">AutoRuns</a></li>
|
||||||
|
<li><a href="#binaries">Binaries</a></li>
|
||||||
|
<li><a href="#directories">Directories</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<div class="company-name">KrakenTech LLC</div>
|
||||||
|
<a href="https://krakensec.tech" class="company-link" target="_blank">https://krakensec.tech</a>
|
||||||
|
<div class="report-title">{{.ReportName}}</div>
|
||||||
|
|
||||||
|
<div class="risk-section">
|
||||||
|
<div class="risk-banner" style="background-color: {{riskColor .RiskRating.Rating}};">
|
||||||
|
Risk Level: {{.RiskRating.Rating}} ({{printf "%.1f" .RiskRating.Score}}/10)
|
||||||
|
</div>
|
||||||
|
<div class="risk-gauge">
|
||||||
|
<div class="risk-gauge-inner">{{printf "%.1f" .RiskRating.Score}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metadata">
|
||||||
|
<h3>Report Metadata</h3>
|
||||||
|
<div class="item-detail"><strong>Generated:</strong> {{.GeneratedAt}}</div>
|
||||||
|
<div class="item-detail"><strong>Risk Summary:</strong> {{.RiskRating.Summary}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{/* === Sections === */}}
|
||||||
|
|
||||||
|
{{define "section"}}
|
||||||
|
<div class="section" id="{{.ID}}">
|
||||||
|
<div class="section-header" onclick="toggleSection('{{.ID}}')">
|
||||||
|
<span class="section-title">{{.Title}}</span>
|
||||||
|
<span class="section-count">{{.Count}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
{{if .HasItems}}
|
||||||
|
{{range .Items}}
|
||||||
|
<div class="item">{{.}}</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-state">No {{.Title}} found</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- Processes -->
|
||||||
|
<div class="section" id="processes">
|
||||||
|
<div class="section-header" onclick="toggleSection('processes')">
|
||||||
|
<span class="section-title">Suspicious Processes</span>
|
||||||
|
<span class="section-count">{{len .Findings.Processes}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
{{if .Findings.Processes}}
|
||||||
|
{{range .Findings.Processes}}
|
||||||
|
<div class="item">
|
||||||
|
<div class="item-title">{{.Name}}</div>
|
||||||
|
<div class="item-detail"><strong>PID:</strong> {{.PID}}</div>
|
||||||
|
<div class="item-detail"><strong>PPID:</strong> {{.PPID}}</div>
|
||||||
|
{{if .Path}}<div class="item-detail"><strong>Path:</strong> {{.Path}}</div>{{end}}
|
||||||
|
{{if .Parent}}<div class="item-detail"><strong>Parent:</strong> {{.Parent}}</div>{{end}}
|
||||||
|
{{if .Args}}<div class="item-detail"><strong>Args:</strong> {{.Args}}</div>{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-state">No suspicious processes found</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Services -->
|
||||||
|
<div class="section" id="services">
|
||||||
|
<div class="section-header" onclick="toggleSection('services')">
|
||||||
|
<span class="section-title">Suspicious Services</span>
|
||||||
|
<span class="section-count">{{len .Findings.Services}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
{{if .Findings.Services}}
|
||||||
|
{{range .Findings.Services}}
|
||||||
|
<div class="item">
|
||||||
|
<div class="item-title">{{.DisplayName}}</div>
|
||||||
|
<div class="item-detail"><strong>Name:</strong> {{.Name}}</div>
|
||||||
|
<div class="item-detail"><strong>Binary Path:</strong> {{.BinaryPathName}}</div>
|
||||||
|
<div class="item-detail"><strong>Start Type:</strong> {{.StartType}}</div>
|
||||||
|
{{if .Description}}<div class="item-detail"><strong>Description:</strong> {{.Description}}</div>{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-state">No suspicious services found</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connections -->
|
||||||
|
<div class="section" id="connections">
|
||||||
|
<div class="section-header" onclick="toggleSection('connections')">
|
||||||
|
<span class="section-title">Suspicious Outbound Connections</span>
|
||||||
|
<span class="section-count">{{len .Findings.OutboundConnections}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
{{if .Findings.OutboundConnections}}
|
||||||
|
{{range .Findings.OutboundConnections}}
|
||||||
|
<div class="item">
|
||||||
|
<div class="item-title">{{.Process}}</div>
|
||||||
|
<div class="item-detail"><strong>Local:</strong> {{.LocalAddr}}</div>
|
||||||
|
<div class="item-detail"><strong>Remote:</strong> {{.RemoteAddr}}</div>
|
||||||
|
{{if .RemoteHost}}<div class="item-detail"><strong>Host:</strong> {{.RemoteHost}}</div>{{end}}
|
||||||
|
<div class="item-detail"><strong>State:</strong> {{.State}}</div>
|
||||||
|
<div class="item-detail"><strong>PID:</strong> {{.PID}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-state">No suspicious outbound connections found</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scheduled Tasks -->
|
||||||
|
<div class="section" id="tasks">
|
||||||
|
<div class="section-header" onclick="toggleSection('tasks')">
|
||||||
|
<span class="section-title">Suspicious Scheduled Tasks</span>
|
||||||
|
<span class="section-count">{{len .Findings.ScheduledTasks}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
{{if .Findings.ScheduledTasks}}
|
||||||
|
{{range .Findings.ScheduledTasks}}
|
||||||
|
<div class="item">
|
||||||
|
<div class="item-title">{{.Name}}</div>
|
||||||
|
{{if .Author}}<div class="item-detail"><strong>Author:</strong> {{.Author}}</div>{{end}}
|
||||||
|
{{if .Path}}<div class="item-detail"><strong>Path:</strong> {{.Path}}</div>{{end}}
|
||||||
|
<div class="item-detail"><strong>State:</strong> {{.State}}</div>
|
||||||
|
<div class="item-detail"><strong>Enabled:</strong> {{.Enabled}}</div>
|
||||||
|
{{if .Description}}<div class="item-detail"><strong>Description:</strong> {{.Description}}</div>{{end}}
|
||||||
|
{{if .LastRun}}<div class="item-detail"><strong>Last Run:</strong> {{.LastRun}}</div>{{end}}
|
||||||
|
{{if .NextRun}}<div class="item-detail"><strong>Next Run:</strong> {{.NextRun}}</div>{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-state">No suspicious scheduled tasks found</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AutoRuns -->
|
||||||
|
<div class="section" id="autoruns">
|
||||||
|
<div class="section-header" onclick="toggleSection('autoruns')">
|
||||||
|
<span class="section-title">Suspicious AutoRuns</span>
|
||||||
|
<span class="section-count">{{len .Findings.AutoRuns}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
{{if .Findings.AutoRuns}}
|
||||||
|
{{range .Findings.AutoRuns}}
|
||||||
|
<div class="item">
|
||||||
|
<div class="item-title">{{.Name}}</div>
|
||||||
|
<div class="item-detail"><strong>Command:</strong> {{.Command}}</div>
|
||||||
|
<div class="item-detail"><strong>Location:</strong> {{.Location}}</div>
|
||||||
|
<div class="item-detail"><strong>Enabled:</strong> {{.Enabled}}</div>
|
||||||
|
{{if .Description}}<div class="item-detail"><strong>Description:</strong> {{.Description}}</div>{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-state">No suspicious autoruns found</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Binaries -->
|
||||||
|
<div class="section" id="binaries">
|
||||||
|
<div class="section-header" onclick="toggleSection('binaries')">
|
||||||
|
<span class="section-title">Suspicious Binaries</span>
|
||||||
|
<span class="section-count">{{len .Findings.Binaries}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
{{if .Findings.Binaries}}
|
||||||
|
{{range .Findings.Binaries}}
|
||||||
|
<div class="item">
|
||||||
|
<div class="item-detail">{{.}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-state">No suspicious binaries found</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Directories -->
|
||||||
|
<div class="section" id="directories">
|
||||||
|
<div class="section-header" onclick="toggleSection('directories')">
|
||||||
|
<span class="section-title">Suspicious Directories</span>
|
||||||
|
<span class="section-count">{{len .Findings.Directories}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="section-content">
|
||||||
|
{{if .Findings.Directories}}
|
||||||
|
{{range .Findings.Directories}}
|
||||||
|
<div class="item">
|
||||||
|
<div class="item-detail">{{.}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-state">No suspicious directories found</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleSection(sectionId) {
|
||||||
|
const content = document.querySelector('#' + sectionId + ' .section-content');
|
||||||
|
if (content) {
|
||||||
|
content.classList.toggle('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Auto-expand first section that has items
|
||||||
|
const sections = document.querySelectorAll('.section');
|
||||||
|
for (let section of sections) {
|
||||||
|
const count = parseInt(section.querySelector('.section-count').textContent);
|
||||||
|
if (count > 0) {
|
||||||
|
const content = section.querySelector('.section-content');
|
||||||
|
if (content) {
|
||||||
|
content.classList.add('active');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth scrolling for navigation links
|
||||||
|
document.querySelectorAll('.nav-sidebar a').forEach(link => {
|
||||||
|
link.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const targetId = this.getAttribute('href').substring(1);
|
||||||
|
const targetElement = document.getElementById(targetId);
|
||||||
|
if (targetElement) {
|
||||||
|
targetElement.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
const content = targetElement.querySelector('.section-content');
|
||||||
|
if (content && !content.classList.contains('active')) {
|
||||||
|
content.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Live search filter
|
||||||
|
const searchBox = document.getElementById('search');
|
||||||
|
if (searchBox) {
|
||||||
|
searchBox.addEventListener('input', function(e) {
|
||||||
|
const q = e.target.value.toLowerCase();
|
||||||
|
const sections = document.querySelectorAll('.section');
|
||||||
|
|
||||||
|
sections.forEach(section => {
|
||||||
|
const items = section.querySelectorAll('.item');
|
||||||
|
let hasVisibleItems = false;
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
if (item.textContent.toLowerCase().includes(q)) {
|
||||||
|
item.style.display = '';
|
||||||
|
hasVisibleItems = true;
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-expand sections with matching items
|
||||||
|
if (q && hasVisibleItems) {
|
||||||
|
const content = section.querySelector('.section-content');
|
||||||
|
if (content && !content.classList.contains('active')) {
|
||||||
|
content.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
@@ -1 +1,81 @@
|
|||||||
package writer
|
package writer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"rmm-hunter/internal/pkg"
|
||||||
|
"rmm-hunter/internal/pkg/writer/disposition"
|
||||||
|
"rmm-hunter/internal/suspicious"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JSONReport struct {
|
||||||
|
ReportName string `json:"reportName"`
|
||||||
|
GeneratedAt string `json:"generatedAt"`
|
||||||
|
RiskRating *disposition.Disposition `json:"riskRating"`
|
||||||
|
Findings interface{} `json:"findings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteJSONReport generates a JSON report from Hunter findings
|
||||||
|
func WriteJSONReport(sus *suspicious.Suspicious, opts *pkg.RunOptions) error {
|
||||||
|
if sus == nil {
|
||||||
|
return fmt.Errorf("suspicious instance is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts == nil {
|
||||||
|
opts = &pkg.RunOptions{Name: "rmm-hunter-report"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Name == "" {
|
||||||
|
opts.Name = "rmm-hunter-report"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate risk disposition
|
||||||
|
riskRating := disposition.CalculateDisposition(sus)
|
||||||
|
|
||||||
|
// Create report structure
|
||||||
|
report := JSONReport{
|
||||||
|
ReportName: opts.Name,
|
||||||
|
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
|
||||||
|
RiskRating: riskRating,
|
||||||
|
Findings: safeFindings(sus),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal to JSON
|
||||||
|
jsonData, err := json.MarshalIndent(report, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure output directory exists
|
||||||
|
filename := fmt.Sprintf("%s.json", opts.Name)
|
||||||
|
if err := ensureDir(filepath.Dir(filename)); err != nil {
|
||||||
|
return fmt.Errorf("failed to create output directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
if err := os.WriteFile(filename, jsonData, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write JSON file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[+] JSON report written to: %s\n", filename)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeFindings ensures all findings are safe for JSON serialization
|
||||||
|
func safeFindings(sus interface{}) interface{} {
|
||||||
|
if sus == nil {
|
||||||
|
return map[string]interface{}{}
|
||||||
|
}
|
||||||
|
return sus
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureDir creates directory if it doesn't exist
|
||||||
|
func ensureDir(dir string) error {
|
||||||
|
if dir == "" || dir == "." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.MkdirAll(dir, 0755)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user