diff --git a/internal/scan/builtin/nuclei_module.go b/internal/scan/builtin/nuclei_module.go new file mode 100644 index 0000000..39eaaca --- /dev/null +++ b/internal/scan/builtin/nuclei_module.go @@ -0,0 +1,141 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2025 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package builtin + +import ( + "context" + "fmt" + "github.com/dropalldatabases/sif/internal/modules" + "github.com/dropalldatabases/sif/internal/scan" +) + +type NucleiModule struct{} + +func (m *NucleiModule) Info() modules.Info { + return modules.Info{ + ID: "nuclei-scan", + Name: "Nuclei Vulnerability Scanner", + Author: "sif", + Severity: "high", + Description: "Runs Nuclei vulnerability scanning templates against target", + Tags: []string{"vuln", "nuclei", "cve"}, + } +} + +func (m *NucleiModule) Type() modules.ModuleType { + return modules.TypeScript +} + +func (m *NucleiModule) Execute(ctx context.Context, target string, opts modules.Options) (*modules.Result, error) { + // Call existing legacy scan.Nuclei function + nucleiResults, err := scan.Nuclei(target, opts.Timeout, opts.Threads, opts.LogDir) + + if err != nil { + return nil, err + } + + result := &modules.Result{ + ModuleID: m.Info().ID, + Target: target, + Findings: make([]modules.Finding, 0, len(nucleiResults)), + } + + // Process nuclei results into module findings + for _, event := range nucleiResults { + severity := "info" + + switch event.Info.SeverityHolder.Severity.String() { + case "critical": + severity = "critical" + case "high": + severity = "high" + case "medium": + severity = "medium" + case "low": + severity = "low" + } + + evidence := fmt.Sprintf("[%s] %s", event.TemplateID, event.Info.Name) + if event.Matched != "" { + evidence = fmt.Sprintf("[%s] %s - matched: %s", event.TemplateID, event.Info.Name, event.Matched) + } + + finding := modules.Finding{ + URL: event.Host, + Severity: severity, + Evidence: evidence, + Extracted: map[string]string{ + "template_id": event.TemplateID, + "template_name": event.Info.Name, + "severity": event.Info.SeverityHolder.Severity.String(), + }, + } + + // Template info + if event.Type != "" { + finding.Extracted["type"] = event.Type + } + + // Matcher name + if event.MatcherName != "" { + finding.Extracted["matcher_name"] = event.MatcherName + } + + // Extractor name + if event.ExtractorName != "" { + finding.Extracted["extractor_name"] = event.ExtractorName + } + + // Matched line/data + if event.Matched != "" { + finding.Extracted["matched"] = event.Matched + } + + // Metadata + if len(event.Info.Metadata) > 0 { + for key, value := range event.Info.Metadata { + finding.Extracted[fmt.Sprintf("metadata_%s", key)] = fmt.Sprintf("%v", value) + } + } + + // Tags + if !event.Info.Tags.IsEmpty() { + tagStr := "" + for _, tag := range event.Info.Tags.ToSlice() { + if tagStr != "" { + tagStr += ", " + } + tagStr += tag + } + + finding.Extracted["tags"] = tagStr + } + + // Reference + if event.Info.Reference != nil && !event.Info.Reference.IsEmpty() { + refStr := "" + for _, ref := range event.Info.Reference.ToSlice() { + if refStr != "" { + refStr += "; " + } + refStr += ref + } + + finding.Extracted["references"] = refStr + } + + result.Findings = append(result.Findings, finding) + } + + return result, nil +} diff --git a/internal/scan/builtin/register.go b/internal/scan/builtin/register.go index 330298a..619dffa 100644 --- a/internal/scan/builtin/register.go +++ b/internal/scan/builtin/register.go @@ -18,4 +18,5 @@ import "github.com/dropalldatabases/sif/internal/modules" // Allows complex Go scans to participate in the module system func Register() { modules.Register(&ShodanModule{}) + modules.Register(&NucleiModule{}) } diff --git a/internal/scan/nuclei.go b/internal/scan/nuclei.go index 6a35c06..91e4393 100644 --- a/internal/scan/nuclei.go +++ b/internal/scan/nuclei.go @@ -23,6 +23,7 @@ import ( "github.com/dropalldatabases/sif/internal/nuclei/format" "github.com/dropalldatabases/sif/internal/nuclei/templates" sifoutput "github.com/dropalldatabases/sif/internal/output" + "github.com/logrusorgru/aurora" "github.com/projectdiscovery/nuclei/v2/pkg/catalog/config" "github.com/projectdiscovery/nuclei/v2/pkg/catalog/disk" "github.com/projectdiscovery/nuclei/v2/pkg/catalog/loader" @@ -59,6 +60,12 @@ func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]ou options.TemplateThreads = threads options.Timeout = int(timeout.Seconds()) + if logdir != "" { + options.ProjectPath = logdir + } + + options.Headless = false + // Get templates templates.Install(nucleilog) pwd, err := os.Getwd() @@ -101,14 +108,16 @@ func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]ou protocolinit.Init(options) executorOpts := protocols.ExecutorOptions{ - Output: outputWriter, - Progress: progressClient, - Catalog: catalog, - Options: options, - IssuesClient: reportingClient, - RateLimiter: ratelimit.New(context.Background(), 150, time.Second), - Interactsh: interactClient, - ResumeCfg: types.NewResumeCfg(), + Colorizer: aurora.NewAurora(false), + Output: outputWriter, + Progress: progressClient, + Catalog: catalog, + Options: options, + IssuesClient: reportingClient, + RateLimiter: ratelimit.New(context.Background(), 150, time.Second), + Interactsh: interactClient, + HostErrorsCache: cache, + ResumeCfg: types.NewResumeCfg(), } engine := core.New(options) engine.SetExecuterOptions(executorOpts)