diff --git a/v2/cmd/nuclei/main.go b/v2/cmd/nuclei/main.go index 66333e82..197e4dd9 100644 --- a/v2/cmd/nuclei/main.go +++ b/v2/cmd/nuclei/main.go @@ -1,19 +1,109 @@ package main import ( + "os" + "path" + "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/nuclei/v2/internal/runner" + "github.com/projectdiscovery/nuclei/v2/pkg/types" + "github.com/spf13/cast" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var ( + cfgFile string + + options = &types.Options{} + rootCmd = &cobra.Command{ + Use: "nuclei", + Short: "Nuclei is a fast and extensible security scanner", + Long: `Nuclei is a fast tool for configurable targeted scanning +based on templates offering massive extensibility and ease of use.`, + Run: func(cmd *cobra.Command, args []string) { + mergeViperConfiguration(cmd) + + runner.ParseOptions(options) + + nucleiRunner, err := runner.New(options) + if err != nil { + gologger.Fatal().Msgf("Could not create runner: %s\n", err) + } + + nucleiRunner.RunEnumeration() + nucleiRunner.Close() + }, + } ) func main() { - // Parse the command line flags and read config files - options := runner.ParseOptions() - - nucleiRunner, err := runner.New(options) - if err != nil { - gologger.Fatal().Msgf("Could not create runner: %s\n", err) - } - - nucleiRunner.RunEnumeration() - nucleiRunner.Close() + rootCmd.Execute() +} + +// mergeViperConfiguration merges the flag configuration with viper file. +func mergeViperConfiguration(cmd *cobra.Command) { + cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) { + if !f.Changed && viper.IsSet(f.Name) { + switch p := viper.Get(f.Name).(type) { + case []interface{}: + for _, item := range p { + cmd.PersistentFlags().Set(f.Name, cast.ToString(item)) + } + default: + cmd.PersistentFlags().Set(f.Name, viper.GetString(f.Name)) + } + } + }) +} + +func init() { + home, _ := os.UserHomeDir() + templatesDirectory := path.Join(home, "nuclei-templates") + + cobra.OnInitialize(func() { + if cfgFile != "" { + viper.SetConfigFile(cfgFile) + if err := viper.ReadInConfig(); err != nil { + gologger.Fatal().Msgf("Could not read config: %s\n", err) + } + } + }) + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "Nuclei config file (default is $HOME/.nuclei.yaml)") + rootCmd.PersistentFlags().BoolVar(&options.Metrics, "metrics", false, "Expose nuclei metrics on a port") + rootCmd.PersistentFlags().IntVar(&options.MetricsPort, "metrics-port", 9092, "Port to expose nuclei metrics on") + rootCmd.PersistentFlags().StringVar(&options.Target, "target", "", "Target is a single target to scan using template") + rootCmd.PersistentFlags().StringSliceVarP(&options.Templates, "templates", "t", []string{}, "Template input dir/file/files to run on host. Can be used multiple times. Supports globbing.") + rootCmd.PersistentFlags().StringSliceVar(&options.ExcludedTemplates, "exclude", []string{}, "Template input dir/file/files to exclude. Can be used multiple times. Supports globbing.") + rootCmd.PersistentFlags().StringVar(&options.Severity, "severity", "", "Filter templates based on their severity and only run the matching ones. Comma-separated values can be used to specify multiple severities.") + rootCmd.PersistentFlags().StringVarP(&options.Targets, "list", "l", "", "List of URLs to run templates on") + rootCmd.PersistentFlags().StringVarP(&options.Output, "output", "o", "", "File to write output to (optional)") + rootCmd.PersistentFlags().StringVar(&options.ProxyURL, "proxy-url", "", "URL of the proxy server") + rootCmd.PersistentFlags().StringVar(&options.ProxySocksURL, "proxy-socks-url", "", "URL of the proxy socks server") + rootCmd.PersistentFlags().BoolVar(&options.Silent, "silent", false, "Show only results in output") + rootCmd.PersistentFlags().BoolVar(&options.Version, "version", false, "Show version of nuclei") + rootCmd.PersistentFlags().BoolVarP(&options.Verbose, "verbose", "v", false, "Show Verbose output") + rootCmd.PersistentFlags().BoolVar(&options.NoColor, "no-color", false, "Disable colors in output") + rootCmd.PersistentFlags().IntVar(&options.Timeout, "timeout", 5, "Time to wait in seconds before timeout") + rootCmd.PersistentFlags().IntVar(&options.Retries, "retries", 1, "Number of times to retry a failed request") + rootCmd.PersistentFlags().BoolVar(&options.RandomAgent, "random-agent", false, "Use randomly selected HTTP User-Agent header value") + rootCmd.PersistentFlags().StringSliceVarP(&options.CustomHeaders, "header", "H", []string{}, "Custom Header.") + rootCmd.PersistentFlags().BoolVar(&options.Debug, "debug", false, "Allow debugging of request/responses") + rootCmd.PersistentFlags().BoolVar(&options.UpdateTemplates, "update-templates", false, "Update Templates updates the installed templates (optional)") + rootCmd.PersistentFlags().StringVar(&options.TraceLogFile, "trace-log", "", "File to write sent requests trace log") + rootCmd.PersistentFlags().StringVar(&options.TemplatesDirectory, "update-directory", templatesDirectory, "Directory to use for storing nuclei-templates") + rootCmd.PersistentFlags().BoolVar(&options.JSON, "json", false, "Write json output to files") + rootCmd.PersistentFlags().BoolVar(&options.JSONRequests, "include-rr", false, "Write requests/responses for matches in JSON output") + rootCmd.PersistentFlags().BoolVar(&options.EnableProgressBar, "stats", false, "Display stats of the running scan") + rootCmd.PersistentFlags().BoolVar(&options.TemplateList, "tl", false, "List available templates") + rootCmd.PersistentFlags().IntVar(&options.RateLimit, "rate-limit", 150, "Rate-Limit (maximum requests/second") + rootCmd.PersistentFlags().BoolVar(&options.StopAtFirstMatch, "stop-at-first-match", false, "Stop processing http requests at first match (this may break template/workflow logic)") + rootCmd.PersistentFlags().IntVar(&options.BulkSize, "bulk-size", 25, "Maximum Number of hosts analyzed in parallel per template") + rootCmd.PersistentFlags().IntVarP(&options.TemplateThreads, "concurrency", "c", 10, "Maximum Number of templates executed in parallel") + rootCmd.PersistentFlags().BoolVar(&options.Project, "project", false, "Use a project folder to avoid sending same request multiple times") + rootCmd.PersistentFlags().StringVar(&options.ProjectPath, "project-path", "", "Use a user defined project folder, temporary folder is used if not specified but enabled") + rootCmd.PersistentFlags().BoolVar(&options.NoMeta, "no-meta", false, "Don't display metadata for the matches") + rootCmd.PersistentFlags().BoolVar(&options.TemplatesVersion, "templates-version", false, "Shows the installed nuclei-templates version") + rootCmd.PersistentFlags().StringVar(&options.BurpCollaboratorBiid, "burp-collaborator-biid", "", "Burp Collaborator BIID") } diff --git a/v2/internal/runner/options.go b/v2/internal/runner/options.go index 4134cb6b..bf50a5ac 100644 --- a/v2/internal/runner/options.go +++ b/v2/internal/runner/options.go @@ -2,10 +2,8 @@ package runner import ( "errors" - "flag" "net/url" "os" - "path" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/gologger/formatter" @@ -14,51 +12,7 @@ import ( ) // ParseOptions parses the command line flags provided by a user -func ParseOptions() *types.Options { - options := &types.Options{} - - home, _ := os.UserHomeDir() - templatesDirectory := path.Join(home, "nuclei-templates") - - flag.BoolVar(&options.Sandbox, "sandbox", false, "Run workflows in isolated sandbox mode") - flag.BoolVar(&options.Metrics, "metrics", false, "Expose nuclei metrics on a port") - flag.IntVar(&options.MetricsPort, "metrics-port", 9092, "Port to expose nuclei metrics on") - flag.IntVar(&options.MaxWorkflowDuration, "workflow-duration", 10, "Max time for workflow run on single URL in minutes") - flag.StringVar(&options.Target, "target", "", "Target is a single target to scan using template") - flag.Var(&options.Templates, "t", "Template input dir/file/files to run on host. Can be used multiple times. Supports globbing.") - flag.Var(&options.ExcludedTemplates, "exclude", "Template input dir/file/files to exclude. Can be used multiple times. Supports globbing.") - flag.StringVar(&options.Severity, "severity", "", "Filter templates based on their severity and only run the matching ones. Comma-separated values can be used to specify multiple severities.") - flag.StringVar(&options.Targets, "l", "", "List of URLs to run templates on") - flag.StringVar(&options.Output, "o", "", "File to write output to (optional)") - flag.StringVar(&options.ProxyURL, "proxy-url", "", "URL of the proxy server") - flag.StringVar(&options.ProxySocksURL, "proxy-socks-url", "", "URL of the proxy socks server") - flag.BoolVar(&options.Silent, "silent", false, "Show only results in output") - flag.BoolVar(&options.Version, "version", false, "Show version of nuclei") - flag.BoolVar(&options.Verbose, "v", false, "Show Verbose output") - flag.BoolVar(&options.NoColor, "no-color", false, "Disable colors in output") - flag.IntVar(&options.Timeout, "timeout", 5, "Time to wait in seconds before timeout") - flag.IntVar(&options.Retries, "retries", 1, "Number of times to retry a failed request") - flag.BoolVar(&options.RandomAgent, "random-agent", false, "Use randomly selected HTTP User-Agent header value") - flag.Var(&options.CustomHeaders, "H", "Custom Header.") - flag.BoolVar(&options.Debug, "debug", false, "Allow debugging of request/responses") - flag.BoolVar(&options.UpdateTemplates, "update-templates", false, "Update Templates updates the installed templates (optional)") - flag.StringVar(&options.TraceLogFile, "trace-log", "", "File to write sent requests trace log") - flag.StringVar(&options.TemplatesDirectory, "update-directory", templatesDirectory, "Directory to use for storing nuclei-templates") - flag.BoolVar(&options.JSON, "json", false, "Write json output to files") - flag.BoolVar(&options.JSONRequests, "include-rr", false, "Write requests/responses for matches in JSON output") - flag.BoolVar(&options.EnableProgressBar, "stats", false, "Display stats of the running scan") - flag.BoolVar(&options.TemplateList, "tl", false, "List available templates") - flag.IntVar(&options.RateLimit, "rate-limit", 150, "Rate-Limit (maximum requests/second") - flag.BoolVar(&options.StopAtFirstMatch, "stop-at-first-match", false, "Stop processing http requests at first match (this may break template/workflow logic)") - flag.IntVar(&options.BulkSize, "bulk-size", 25, "Maximum Number of hosts analyzed in parallel per template") - flag.IntVar(&options.TemplateThreads, "c", 10, "Maximum Number of templates executed in parallel") - flag.BoolVar(&options.Project, "project", false, "Use a project folder to avoid sending same request multiple times") - flag.StringVar(&options.ProjectPath, "project-path", "", "Use a user defined project folder, temporary folder is used if not specified but enabled") - flag.BoolVar(&options.NoMeta, "no-meta", false, "Don't display metadata for the matches") - flag.BoolVar(&options.TemplatesVersion, "templates-version", false, "Shows the installed nuclei-templates version") - flag.StringVar(&options.BurpCollaboratorBiid, "burp-collaborator-biid", "", "Burp Collaborator BIID") - flag.Parse() - +func ParseOptions(options *types.Options) { // Check if stdin pipe was given options.Stdin = hasStdin() @@ -87,7 +41,6 @@ func ParseOptions() *types.Options { if err != nil { gologger.Fatal().Msgf("Program exiting: %s\n", err) } - return options } // hasStdin returns true if we have stdin input diff --git a/v2/internal/runner/templates.go b/v2/internal/runner/templates.go index c37506a6..50e39cf3 100644 --- a/v2/internal/runner/templates.go +++ b/v2/internal/runner/templates.go @@ -13,10 +13,8 @@ import ( // getParsedTemplatesFor parse the specified templates and returns a slice of the parsable ones, optionally filtered // by severity, along with a flag indicating if workflows are present. -func (r *Runner) getParsedTemplatesFor(templatePaths []string, severities string) (parsedTemplates []*templates.Template, workflowCount int) { +func (r *Runner) getParsedTemplatesFor(templatePaths []string, severities []string) (parsedTemplates []*templates.Template, workflowCount int) { workflowCount = 0 - severities = strings.ToLower(severities) - allSeverities := strings.Split(severities, ",") filterBySeverity := len(severities) > 0 gologger.Info().Msgf("Loading templates...") @@ -31,7 +29,7 @@ func (r *Runner) getParsedTemplatesFor(templatePaths []string, severities string workflowCount++ } sev := strings.ToLower(t.Info["severity"]) - if !filterBySeverity || hasMatchingSeverity(sev, allSeverities) { + if !filterBySeverity || hasMatchingSeverity(sev, severities) { parsedTemplates = append(parsedTemplates, t) gologger.Info().Msgf("%s\n", r.templateLogMsg(t.ID, t.Info["name"], t.Info["author"], t.Info["severity"])) } else { @@ -113,11 +111,11 @@ func (r *Runner) listAvailableTemplates() { func hasMatchingSeverity(templateSeverity string, allowedSeverities []string) bool { for _, s := range allowedSeverities { + s = strings.ToLower(s) if s != "" && strings.HasPrefix(templateSeverity, s) { return true } } - return false } diff --git a/v2/pkg/protocols/common/generators/load.go b/v2/pkg/protocols/common/generators/load.go index 8c449e81..0c44b613 100644 --- a/v2/pkg/protocols/common/generators/load.go +++ b/v2/pkg/protocols/common/generators/load.go @@ -2,7 +2,6 @@ package generators import ( "bufio" - "fmt" "io" "os" "strings" @@ -30,7 +29,6 @@ func loadPayloads(payloads map[string]interface{}) (map[string][]string, error) loadedPayloads[name] = payloads } case interface{}: - fmt.Printf("%v elements\n", pt) loadedPayloads[name] = cast.ToStringSlice(pt) } } diff --git a/v2/pkg/protocols/http/http.go b/v2/pkg/protocols/http/http.go index 84018c73..3953cb75 100644 --- a/v2/pkg/protocols/http/http.go +++ b/v2/pkg/protocols/http/http.go @@ -8,7 +8,6 @@ import ( "github.com/projectdiscovery/nuclei/v2/pkg/protocols" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/httpclientpool" - "github.com/projectdiscovery/nuclei/v2/pkg/types" "github.com/projectdiscovery/rawhttp" "github.com/projectdiscovery/retryablehttp-go" ) @@ -66,7 +65,7 @@ type Request struct { options *protocols.ExecuterOptions attackType generators.Type totalRequests int - customHeaders types.StringSlice + customHeaders []string generator *generators.Generator // optional, only enabled when using payloads httpClient *retryablehttp.Client rawhttpClient *rawhttp.Client diff --git a/v2/pkg/protocols/http/request.go b/v2/pkg/protocols/http/request.go index c4fba94c..19955de4 100644 --- a/v2/pkg/protocols/http/request.go +++ b/v2/pkg/protocols/http/request.go @@ -213,7 +213,7 @@ func (r *Request) executeRequest(reqURL string, request *generatedRequest, dynam builder := &strings.Builder{} builder.WriteString("User-Agent: ") builder.WriteString(uarand.GetRandom()) - r.customHeaders.Set(builder.String()) + r.customHeaders = append(r.customHeaders, builder.String()) } r.setCustomHeaders(request) diff --git a/v2/pkg/types/types.go b/v2/pkg/types/types.go index 2744cbf5..57e122d5 100644 --- a/v2/pkg/types/types.go +++ b/v2/pkg/types/types.go @@ -1,17 +1,11 @@ package types -import ( - "strings" -) - // Options contains the configuration options for nuclei scanner. type Options struct { // RandomAgent generates random User-Agent RandomAgent bool // Metrics enables display of metrics via an http endpoint Metrics bool - // Sandbox mode allows users to run isolated workflows with system commands disabled - Sandbox bool // Debug mode allows debugging request/responses for the engine Debug bool // Silent suppresses any extra text and only writes found URLs on screen. @@ -44,8 +38,6 @@ type Options struct { Project bool // MetricsPort is the port to show metrics on MetricsPort int - // MaxWorkflowDuration is the maximum time a workflow can run for a URL - MaxWorkflowDuration int // BulkSize is the of targets analyzed in parallel for each template BulkSize int // TemplateThreads is the number of templates executed in parallel @@ -56,14 +48,12 @@ type Options struct { Retries int // Rate-Limit is the maximum number of requests per specified target RateLimit int - // Thread controls the number of concurrent requests to make. - Threads int // BurpCollaboratorBiid is the Burp Collaborator BIID for polling interactions. BurpCollaboratorBiid string // ProjectPath allows nuclei to use a user defined project folder ProjectPath string // Severity filters templates based on their severity and only run the matching ones. - Severity string + Severity []string // Target is a single URL/Domain to scan using a template Target string // Targets specifies the targets to scan using templates. @@ -79,23 +69,9 @@ type Options struct { // TraceLogFile specifies a file to write with the trace of all requests TraceLogFile string // Templates specifies the template/templates to use - Templates StringSlice + Templates []string // ExcludedTemplates specifies the template/templates to exclude - ExcludedTemplates StringSlice + ExcludedTemplates []string // CustomHeaders is the list of custom global headers to send with each request. - CustomHeaders StringSlice -} - -// StringSlice is a slice of strings as input -type StringSlice []string - -// String returns the stringified version of string slice -func (s *StringSlice) String() string { - return strings.Join(*s, ",") -} - -// Set appends a value to the string slice -func (s *StringSlice) Set(value string) error { - *s = append(*s, value) - return nil + CustomHeaders []string }