fix: get templates before running scan

This commit is contained in:
Morten Olsen
2025-12-13 07:56:28 +01:00
parent 1677d02aa7
commit 4c14e2294a
2 changed files with 129 additions and 9 deletions

View File

@@ -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()

View File

@@ -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