mirror of
https://github.com/morten-olsen/homelab-nuclei-operator.git
synced 2026-02-08 02:16:23 +01:00
fix: get templates before running scan
This commit is contained in:
@@ -118,8 +118,11 @@ func (r *Runner) Run(ctx context.Context) error {
|
||||
|
||||
logger.Info("Starting scan",
|
||||
"targets", len(scan.Spec.Targets),
|
||||
"targetList", scan.Spec.Targets,
|
||||
"templates", scan.Spec.Templates,
|
||||
"severity", scan.Spec.Severity)
|
||||
"templatesCount", len(scan.Spec.Templates),
|
||||
"severity", scan.Spec.Severity,
|
||||
"severityCount", len(scan.Spec.Severity))
|
||||
|
||||
// Update status to indicate scan has started
|
||||
startTime := metav1.Now()
|
||||
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
nucleiv1alpha1 "github.com/mortenolsen/nuclei-operator/api/v1alpha1"
|
||||
)
|
||||
|
||||
@@ -95,6 +97,8 @@ func NewNucleiScannerWithDefaults() *NucleiScanner {
|
||||
|
||||
// Scan executes a Nuclei scan against the given targets
|
||||
func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options ScanOptions) (*ScanResult, error) {
|
||||
logger := log.FromContext(ctx).WithName("nuclei-scanner")
|
||||
|
||||
if len(targets) == 0 {
|
||||
return nil, fmt.Errorf("no targets provided for scan")
|
||||
}
|
||||
@@ -110,10 +114,56 @@ func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options Scan
|
||||
|
||||
// Write targets to a file
|
||||
targetsFile := filepath.Join(tmpDir, "targets.txt")
|
||||
if err := os.WriteFile(targetsFile, []byte(strings.Join(targets, "\n")), 0600); err != nil {
|
||||
targetsContent := strings.Join(targets, "\n")
|
||||
if err := os.WriteFile(targetsFile, []byte(targetsContent), 0600); err != nil {
|
||||
return nil, fmt.Errorf("failed to write targets file: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Targets file created", "targetsFile", targetsFile, "targetCount", len(targets))
|
||||
|
||||
// Check if nuclei binary exists and is executable
|
||||
if _, err := os.Stat(s.nucleiBinaryPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("nuclei binary not found at %s", s.nucleiBinaryPath)
|
||||
}
|
||||
|
||||
// Verify nuclei is executable
|
||||
if err := exec.Command(s.nucleiBinaryPath, "-version").Run(); err != nil {
|
||||
logger.Error(err, "Failed to execute nuclei -version, nuclei may not be properly installed")
|
||||
}
|
||||
|
||||
// Check templates availability if templates path is set
|
||||
templatesAvailable := false
|
||||
if s.templatesPath != "" {
|
||||
if info, err := os.Stat(s.templatesPath); err != nil || !info.IsDir() {
|
||||
logger.Info("Templates path does not exist or is not a directory, nuclei will use default templates",
|
||||
"templatesPath", s.templatesPath,
|
||||
"error", err)
|
||||
} else {
|
||||
// Count template files
|
||||
entries, err := os.ReadDir(s.templatesPath)
|
||||
if err == nil {
|
||||
templateCount := 0
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && (strings.HasSuffix(entry.Name(), ".yaml") || strings.HasSuffix(entry.Name(), ".yml")) {
|
||||
templateCount++
|
||||
}
|
||||
}
|
||||
templatesAvailable = templateCount > 0
|
||||
logger.Info("Templates directory found", "templatesPath", s.templatesPath, "templateCount", templateCount)
|
||||
if templateCount == 0 {
|
||||
logger.Info("Templates directory is empty, nuclei will download templates on first run or use default location")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.Info("No templates path configured, nuclei will use default template location (~/.nuclei/templates)")
|
||||
}
|
||||
|
||||
// If no specific templates are provided and templates path is empty, warn
|
||||
if len(options.Templates) == 0 && !templatesAvailable && s.templatesPath != "" {
|
||||
logger.Info("Warning: No templates specified and templates directory appears empty. Nuclei may not run any scans.")
|
||||
}
|
||||
|
||||
// Build the nuclei command arguments
|
||||
args := s.buildArgs(targetsFile, options)
|
||||
|
||||
@@ -123,6 +173,16 @@ func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options Scan
|
||||
timeout = 30 * time.Minute
|
||||
}
|
||||
|
||||
// Log the command being executed
|
||||
fullCommand := fmt.Sprintf("%s %s", s.nucleiBinaryPath, strings.Join(args, " "))
|
||||
logger.Info("Executing nuclei scan",
|
||||
"command", fullCommand,
|
||||
"timeout", timeout,
|
||||
"templates", len(options.Templates),
|
||||
"templatesList", options.Templates,
|
||||
"severity", options.Severity,
|
||||
"templatesPath", s.templatesPath)
|
||||
|
||||
// Create context with timeout
|
||||
scanCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
@@ -134,14 +194,31 @@ func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options Scan
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
logger.Info("Starting nuclei execution")
|
||||
err = cmd.Run()
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// Log stderr output (nuclei often outputs warnings/info to stderr)
|
||||
stderrStr := stderr.String()
|
||||
if stderrStr != "" {
|
||||
logger.Info("Nuclei stderr output", "stderr", stderrStr)
|
||||
}
|
||||
|
||||
// Log stdout size for debugging
|
||||
stdoutSize := len(stdout.Bytes())
|
||||
logger.Info("Nuclei execution completed",
|
||||
"duration", duration,
|
||||
"exitCode", cmd.ProcessState.ExitCode(),
|
||||
"stdoutSize", stdoutSize,
|
||||
"stderrSize", len(stderrStr))
|
||||
|
||||
// Check for context cancellation
|
||||
if scanCtx.Err() == context.DeadlineExceeded {
|
||||
logger.Error(nil, "Scan timed out", "timeout", timeout, "stderr", stderrStr)
|
||||
return nil, fmt.Errorf("scan timed out after %v", timeout)
|
||||
}
|
||||
if scanCtx.Err() == context.Canceled {
|
||||
logger.Error(nil, "Scan was cancelled", "stderr", stderrStr)
|
||||
return nil, fmt.Errorf("scan was cancelled")
|
||||
}
|
||||
|
||||
@@ -151,22 +228,40 @@ func (s *NucleiScanner) Scan(ctx context.Context, targets []string, options Scan
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
// Exit code 1 can mean "no results found" which is not an error
|
||||
if exitErr.ExitCode() != 1 {
|
||||
return nil, fmt.Errorf("nuclei execution failed: %w, stderr: %s", err, stderr.String())
|
||||
logger.Error(err, "Nuclei execution failed",
|
||||
"exitCode", exitErr.ExitCode(),
|
||||
"stderr", stderrStr,
|
||||
"stdout", stdout.String())
|
||||
return nil, fmt.Errorf("nuclei execution failed: %w, stderr: %s", err, stderrStr)
|
||||
}
|
||||
logger.Info("Nuclei exited with code 1 (no results found)", "stderr", stderrStr)
|
||||
} else {
|
||||
logger.Error(err, "Failed to execute nuclei", "stderr", stderrStr)
|
||||
return nil, fmt.Errorf("failed to execute nuclei: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the JSONL output
|
||||
findings, err := ParseJSONLOutput(stdout.Bytes())
|
||||
stdoutBytes := stdout.Bytes()
|
||||
logger.Info("Parsing nuclei output", "outputSize", len(stdoutBytes))
|
||||
findings, err := ParseJSONLOutput(stdoutBytes)
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to parse nuclei output",
|
||||
"stdout", string(stdoutBytes),
|
||||
"stderr", stderrStr)
|
||||
return nil, fmt.Errorf("failed to parse nuclei output: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Parsed findings", "count", len(findings))
|
||||
|
||||
// Calculate summary
|
||||
summary := calculateSummary(findings, len(targets), duration)
|
||||
|
||||
logger.Info("Scan completed",
|
||||
"findings", len(findings),
|
||||
"duration", duration,
|
||||
"targetsScanned", len(targets))
|
||||
|
||||
return &ScanResult{
|
||||
Findings: findings,
|
||||
Summary: summary,
|
||||
@@ -181,11 +276,8 @@ func (s *NucleiScanner) buildArgs(targetsFile string, options ScanOptions) []str
|
||||
"-jsonl",
|
||||
"-silent",
|
||||
"-no-color",
|
||||
}
|
||||
|
||||
// Add templates path if configured
|
||||
if s.templatesPath != "" {
|
||||
args = append(args, "-t", s.templatesPath)
|
||||
"-rate-limit", "150", // Limit rate to avoid overwhelming targets
|
||||
"-bulk-size", "25", // Process targets in bulk
|
||||
}
|
||||
|
||||
// Add specific templates if provided
|
||||
@@ -193,6 +285,31 @@ func (s *NucleiScanner) buildArgs(targetsFile string, options ScanOptions) []str
|
||||
for _, t := range options.Templates {
|
||||
args = append(args, "-t", t)
|
||||
}
|
||||
} else {
|
||||
// When no templates are specified, nuclei should use all available templates
|
||||
// Only add templates path if it's configured AND contains templates
|
||||
// Otherwise, let nuclei use its default template location (~/.nuclei/templates)
|
||||
if s.templatesPath != "" {
|
||||
// Check if templates directory exists and has content
|
||||
if info, err := os.Stat(s.templatesPath); err == nil && info.IsDir() {
|
||||
entries, err := os.ReadDir(s.templatesPath)
|
||||
if err == nil {
|
||||
hasTemplates := false
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && (strings.HasSuffix(entry.Name(), ".yaml") || strings.HasSuffix(entry.Name(), ".yml")) {
|
||||
hasTemplates = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasTemplates {
|
||||
args = append(args, "-t", s.templatesPath)
|
||||
}
|
||||
// If no templates found, don't add -t flag, let nuclei use default location
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no templates path or it's empty, nuclei will use default location
|
||||
// which it will download templates to on first run if needed
|
||||
}
|
||||
|
||||
// Add severity filter if provided
|
||||
|
||||
Reference in New Issue
Block a user