From 4c14e2294a76aa59d6078302572c71a1f2126c99 Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Sat, 13 Dec 2025 07:56:28 +0100 Subject: [PATCH] fix: get templates before running scan --- internal/scanner/runner.go | 5 +- internal/scanner/scanner.go | 133 +++++++++++++++++++++++++++++++++--- 2 files changed, 129 insertions(+), 9 deletions(-) diff --git a/internal/scanner/runner.go b/internal/scanner/runner.go index aeef9d0..8106c62 100644 --- a/internal/scanner/runner.go +++ b/internal/scanner/runner.go @@ -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() diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index 2bcce21..c44286f 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -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