diff --git a/v2/internal/runner/options.go b/v2/internal/runner/options.go index baab72b9e..5f252ca6b 100644 --- a/v2/internal/runner/options.go +++ b/v2/internal/runner/options.go @@ -12,7 +12,7 @@ import ( // the template requesting process. type Options struct { Debug bool // Debug mode allows debugging request/responses for the engine - Templates string // Signature specifies the template/templates to use + Templates multiStringFlag // Signature specifies the template/templates to use Target string // Target is a single URL/Domain to scan usng a template Targets string // Targets specifies the targets to scan using templates. Threads int // Thread controls the number of concurrent requests to make. @@ -33,12 +33,23 @@ type Options struct { Stdin bool // Stdin specifies whether stdin input was given to the process } +type multiStringFlag []string + +func (m *multiStringFlag) String() string { + return "" +} + +func (m *multiStringFlag) Set(value string) error { + *m = append(*m, value) + return nil +} + // ParseOptions parses the command line flags provided by a user func ParseOptions() *Options { options := &Options{} flag.StringVar(&options.Target, "target", "", "Target is a single target to scan using template") - flag.StringVar(&options.Templates, "t", "", "Template input file/files to run on host") + flag.Var(&options.Templates, "t","Template input file/files to run on host. Can be used multiple times.") 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") diff --git a/v2/internal/runner/runner.go b/v2/internal/runner/runner.go index 0d5e693c9..c35c85aad 100644 --- a/v2/internal/runner/runner.go +++ b/v2/internal/runner/runner.go @@ -44,7 +44,7 @@ func New(options *Options) (*Runner, error) { if err := runner.updateTemplates(); err != nil { gologger.Warningf("Could not update templates: %s\n", err) } - if (options.Templates == "" || (options.Targets == "" && !options.Stdin && options.Target == "")) && options.UpdateTemplates { + if (len(options.Templates) == 0 || (options.Targets == "" && !options.Stdin && options.Target == "")) && options.UpdateTemplates { os.Exit(0) } @@ -88,84 +88,112 @@ func (r *Runner) Close() { os.Remove(r.tempFile) } +func isFilePath(path string) (bool, error) { + info, err := os.Stat(path) + if err != nil { + return false, err + } + return info.Mode().IsRegular(), nil +} + +func (r *Runner) resolvePathIfRelative(path string) (string, error) { + if r.isRelative(path) { + newPath, err := r.resolvePath(path) + if err != nil { + return "", err + } + return newPath, nil + } + return path, nil +} + +func isNewPath(path string, pathMap map[string]bool) bool { + if _, already := pathMap[path]; already { + gologger.Warningf("Skipping already specified path '%s'", path) + return false + } + return true +} + // RunEnumeration sets up the input layer for giving input nuclei. // binary and runs the actual enumeration func (r *Runner) RunEnumeration() { - var err error + // keeps track of processed dirs and files + processed := make(map[string]bool) + allTemplates := []string{} - // Check if the template is an absolute path or relative path. - // If the path is absolute, use it. Otherwise, - if r.isRelative(r.options.Templates) { - newPath, err := r.resolvePath(r.options.Templates) + // parses user input, handle file/directory cases and produce a list of unique templates + for _, t := range r.options.Templates { + // resolve and convert relative to absolute path + absPath, err := r.resolvePathIfRelative(t) if err != nil { - gologger.Errorf("Could not find template file '%s': %s\n", r.options.Templates, err) - return + gologger.Errorf("Could not find template file '%s': %s\n", t, err) + continue } - r.options.Templates = newPath - } - // Single yaml provided - if strings.HasSuffix(r.options.Templates, ".yaml") { - t, err := r.parse(r.options.Templates) - switch t.(type) { - case *templates.Template: - var results bool - template := t.(*templates.Template) - // process http requests - for _, request := range template.BulkRequestsHTTP { - results = r.processTemplateRequest(template, request) - } - // process dns requests - for _, request := range template.RequestsDNS { - dnsResults := r.processTemplateRequest(template, request) - if !results { - results = dnsResults - } - } - - if !results { - if r.output != nil { - outputFile := r.output.Name() - r.output.Close() - os.Remove(outputFile) - } - gologger.Infof("No results found for the template. Happy hacking!") - } - case *workflows.Workflow: - workflow := t.(*workflows.Workflow) - r.ProcessWorkflowWithList(workflow) - default: - gologger.Errorf("Could not parse file '%s': %s\n", r.options.Templates, err) + // determine file/directory + isFile, err := isFilePath(absPath) + if err != nil { + gologger.Errorf("Could not stat '%s': %s\n", absPath, err) + continue } - return - } - // If the template passed is a directory - matches := []string{} + // test for uniqueness + if !isNewPath(absPath, processed) { + continue + } - // Recursively walk down the Templates directory and run all the template file checks - err = godirwalk.Walk(r.options.Templates, &godirwalk.Options{ - Callback: func(path string, d *godirwalk.Dirent) error { - if !d.IsDir() && strings.HasSuffix(path, ".yaml") { - matches = append(matches, path) + // mark this absolute path as processed + // - if it's a file, we'll never process it again + // - if it's a dir, we'll never walk it again + processed[absPath] = true + + if isFile { + allTemplates = append(allTemplates, absPath) + } else { + matches := []string{} + + // Recursively walk down the Templates directory and run all the template file checks + err = godirwalk.Walk(absPath, &godirwalk.Options{ + Callback: func(path string, d *godirwalk.Dirent) error { + if !d.IsDir() && strings.HasSuffix(path, ".yaml") { + if isNewPath(path, processed) { + matches = append(matches, path) + processed[path] = true + } + } + return nil + }, + ErrorCallback: func(path string, err error) godirwalk.ErrorAction { + return godirwalk.SkipNode + }, + Unsorted: true, + }) + + // directory couldn't be walked + if err != nil { + gologger.Labelf("Could not find templates in directory '%s': %s\n", absPath, err) + continue } - return nil - }, - ErrorCallback: func(path string, err error) godirwalk.ErrorAction { - return godirwalk.SkipNode - }, - Unsorted: true, - }) - if err != nil { - gologger.Fatalf("Could not find templates in directory '%s': %s\n", r.options.Templates, err) + + // couldn't find templates in directory + if len(matches) == 0 { + gologger.Labelf("Error, no templates were found in '%s'.\n", absPath) + continue + } + + allTemplates = append(allTemplates, matches...) + } } + // 0 matches means no templates were found in directory - if len(matches) == 0 { - gologger.Fatalf("Error, no templates found in directory: '%s'\n", r.options.Templates) + if len(allTemplates) == 0 { + gologger.Fatalf("Error, no templates were found.\n") } + // run with the specified templates var results bool - for _, match := range matches { + for _, match := range allTemplates { t, err := r.parse(match) switch t.(type) { case *templates.Template: @@ -195,7 +223,7 @@ func (r *Runner) RunEnumeration() { r.output.Close() os.Remove(outputFile) } - gologger.Infof("No results found for the template. Happy hacking!") + gologger.Infof("No results found. Happy hacking!") } return } diff --git a/v2/internal/runner/validate.go b/v2/internal/runner/validate.go index 67233438d..2fb0e00c0 100644 --- a/v2/internal/runner/validate.go +++ b/v2/internal/runner/validate.go @@ -15,7 +15,7 @@ func (options *Options) validateOptions() error { } // Check if a list of templates was provided and it exists - if options.Templates == "" && !options.UpdateTemplates { + if len(options.Templates) == 0 && !options.UpdateTemplates { return errors.New("no template/templates provided") }