github.com/projectdiscovery/nuclei/v2@v2.9.15/internal/runner/runner.go (about)

     1  package runner
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"net/http"
     7  	_ "net/http/pprof"
     8  	"os"
     9  	"reflect"
    10  	"strconv"
    11  	"strings"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	"github.com/projectdiscovery/nuclei/v2/internal/installer"
    16  	"github.com/projectdiscovery/nuclei/v2/internal/runner/nucleicloud"
    17  	uncoverlib "github.com/projectdiscovery/uncover"
    18  	permissionutil "github.com/projectdiscovery/utils/permission"
    19  	updateutils "github.com/projectdiscovery/utils/update"
    20  
    21  	"github.com/logrusorgru/aurora"
    22  	"github.com/pkg/errors"
    23  	"github.com/projectdiscovery/ratelimit"
    24  
    25  	"github.com/projectdiscovery/gologger"
    26  	"github.com/projectdiscovery/nuclei/v2/internal/colorizer"
    27  	"github.com/projectdiscovery/nuclei/v2/pkg/catalog"
    28  	"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
    29  	"github.com/projectdiscovery/nuclei/v2/pkg/catalog/disk"
    30  	"github.com/projectdiscovery/nuclei/v2/pkg/catalog/loader"
    31  	"github.com/projectdiscovery/nuclei/v2/pkg/core"
    32  	"github.com/projectdiscovery/nuclei/v2/pkg/core/inputs/hybrid"
    33  	"github.com/projectdiscovery/nuclei/v2/pkg/external/customtemplates"
    34  	"github.com/projectdiscovery/nuclei/v2/pkg/input"
    35  	"github.com/projectdiscovery/nuclei/v2/pkg/output"
    36  	"github.com/projectdiscovery/nuclei/v2/pkg/parsers"
    37  	"github.com/projectdiscovery/nuclei/v2/pkg/progress"
    38  	"github.com/projectdiscovery/nuclei/v2/pkg/projectfile"
    39  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols"
    40  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/automaticscan"
    41  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
    42  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/hosterrorscache"
    43  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/interactsh"
    44  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolinit"
    45  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/uncover"
    46  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/utils/excludematchers"
    47  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/headless/engine"
    48  	"github.com/projectdiscovery/nuclei/v2/pkg/protocols/http/httpclientpool"
    49  	"github.com/projectdiscovery/nuclei/v2/pkg/reporting"
    50  	"github.com/projectdiscovery/nuclei/v2/pkg/reporting/exporters/jsonexporter"
    51  	"github.com/projectdiscovery/nuclei/v2/pkg/reporting/exporters/jsonl"
    52  	"github.com/projectdiscovery/nuclei/v2/pkg/reporting/exporters/markdown"
    53  	"github.com/projectdiscovery/nuclei/v2/pkg/reporting/exporters/sarif"
    54  	"github.com/projectdiscovery/nuclei/v2/pkg/templates"
    55  	"github.com/projectdiscovery/nuclei/v2/pkg/types"
    56  	"github.com/projectdiscovery/nuclei/v2/pkg/utils"
    57  	"github.com/projectdiscovery/nuclei/v2/pkg/utils/stats"
    58  	"github.com/projectdiscovery/nuclei/v2/pkg/utils/yaml"
    59  	"github.com/projectdiscovery/retryablehttp-go"
    60  	ptrutil "github.com/projectdiscovery/utils/ptr"
    61  )
    62  
    63  // Runner is a client for running the enumeration process.
    64  type Runner struct {
    65  	output            output.Writer
    66  	interactsh        *interactsh.Client
    67  	options           *types.Options
    68  	projectFile       *projectfile.ProjectFile
    69  	catalog           catalog.Catalog
    70  	progress          progress.Progress
    71  	colorizer         aurora.Aurora
    72  	issuesClient      reporting.Client
    73  	hmapInputProvider *hybrid.Input
    74  	browser           *engine.Browser
    75  	rateLimiter       *ratelimit.Limiter
    76  	hostErrors        hosterrorscache.CacheInterface
    77  	resumeCfg         *types.ResumeCfg
    78  	pprofServer       *http.Server
    79  	cloudClient       *nucleicloud.Client
    80  	cloudTargets      []string
    81  }
    82  
    83  const pprofServerAddress = "127.0.0.1:8086"
    84  
    85  // New creates a new client for running the enumeration process.
    86  func New(options *types.Options) (*Runner, error) {
    87  	runner := &Runner{
    88  		options: options,
    89  	}
    90  
    91  	if options.HealthCheck {
    92  		gologger.Print().Msgf("%s\n", DoHealthCheck(options))
    93  		os.Exit(0)
    94  	}
    95  
    96  	if options.Cloud {
    97  		runner.cloudClient = nucleicloud.New(options.CloudURL, options.CloudAPIKey)
    98  	}
    99  
   100  	//  Version check by default
   101  	if config.DefaultConfig.CanCheckForUpdates() {
   102  		if err := installer.NucleiVersionCheck(); err != nil {
   103  			if options.Verbose || options.Debug {
   104  				gologger.Error().Msgf("nuclei version check failed got: %s\n", err)
   105  			}
   106  		}
   107  
   108  		// check for custom template updates and update if available
   109  		ctm, err := customtemplates.NewCustomTemplatesManager(options)
   110  		if err != nil {
   111  			gologger.Error().Label("custom-templates").Msgf("Failed to create custom templates manager: %s\n", err)
   112  		}
   113  
   114  		// Check for template updates and update if available.
   115  		// If the custom templates manager is not nil, we will install custom templates if there is a fresh installation
   116  		tm := &installer.TemplateManager{
   117  			CustomTemplates:        ctm,
   118  			DisablePublicTemplates: options.PublicTemplateDisableDownload,
   119  		}
   120  		if err := tm.FreshInstallIfNotExists(); err != nil {
   121  			gologger.Warning().Msgf("failed to install nuclei templates: %s\n", err)
   122  		}
   123  		if err := tm.UpdateIfOutdated(); err != nil {
   124  			gologger.Warning().Msgf("failed to update nuclei templates: %s\n", err)
   125  		}
   126  
   127  		if config.DefaultConfig.NeedsIgnoreFileUpdate() {
   128  			if err := installer.UpdateIgnoreFile(); err != nil {
   129  				gologger.Warning().Msgf("failed to update nuclei ignore file: %s\n", err)
   130  			}
   131  		}
   132  
   133  		if options.UpdateTemplates {
   134  			// we automatically check for updates unless explicitly disabled
   135  			// this print statement is only to inform the user that there are no updates
   136  			if !config.DefaultConfig.NeedsTemplateUpdate() {
   137  				gologger.Info().Msgf("No new updates found for nuclei templates")
   138  			}
   139  			// manually trigger update of custom templates
   140  			if ctm != nil {
   141  				ctm.Update(context.TODO())
   142  			}
   143  		}
   144  	}
   145  
   146  	if options.Validate {
   147  		parsers.ShouldValidate = true
   148  	}
   149  
   150  	// TODO: refactor to pass options reference globally without cycles
   151  	parsers.NoStrictSyntax = options.NoStrictSyntax
   152  	yaml.StrictSyntax = !options.NoStrictSyntax
   153  
   154  	if options.Headless {
   155  		if engine.MustDisableSandbox() {
   156  			gologger.Warning().Msgf("The current platform and privileged user will run the browser without sandbox\n")
   157  		}
   158  		browser, err := engine.New(options)
   159  		if err != nil {
   160  			return nil, err
   161  		}
   162  		runner.browser = browser
   163  	}
   164  
   165  	runner.catalog = disk.NewCatalog(config.DefaultConfig.TemplatesDirectory)
   166  
   167  	var httpclient *retryablehttp.Client
   168  	if options.ProxyInternal && types.ProxyURL != "" || types.ProxySocksURL != "" {
   169  		var err error
   170  		httpclient, err = httpclientpool.Get(options, &httpclientpool.Configuration{})
   171  		if err != nil {
   172  			return nil, err
   173  		}
   174  	}
   175  
   176  	if err := reporting.CreateConfigIfNotExists(); err != nil {
   177  		return nil, err
   178  	}
   179  	reportingOptions, err := createReportingOptions(options)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	if reportingOptions != nil && httpclient != nil {
   184  		reportingOptions.HttpClient = httpclient
   185  	}
   186  
   187  	if reportingOptions != nil {
   188  		client, err := reporting.New(reportingOptions, options.ReportingDB)
   189  		if err != nil {
   190  			return nil, errors.Wrap(err, "could not create issue reporting client")
   191  		}
   192  		runner.issuesClient = client
   193  	}
   194  
   195  	// output coloring
   196  	useColor := !options.NoColor
   197  	runner.colorizer = aurora.NewAurora(useColor)
   198  	templates.Colorizer = runner.colorizer
   199  	templates.SeverityColorizer = colorizer.New(runner.colorizer)
   200  
   201  	if options.EnablePprof {
   202  		server := &http.Server{
   203  			Addr:    pprofServerAddress,
   204  			Handler: http.DefaultServeMux,
   205  		}
   206  		gologger.Info().Msgf("Listening pprof debug server on: %s", pprofServerAddress)
   207  		runner.pprofServer = server
   208  		go func() {
   209  			_ = server.ListenAndServe()
   210  		}()
   211  	}
   212  
   213  	if (len(options.Templates) == 0 || !options.NewTemplates || (options.TargetsFilePath == "" && !options.Stdin && len(options.Targets) == 0)) && (options.UpdateTemplates && !options.Cloud) {
   214  		os.Exit(0)
   215  	}
   216  
   217  	// Initialize the input source
   218  	hmapInput, err := hybrid.New(&hybrid.Options{
   219  		Options: options,
   220  		NotFoundCallback: func(target string) bool {
   221  			if !options.Cloud {
   222  				return false
   223  			}
   224  			parsed, parseErr := strconv.ParseInt(target, 10, 64)
   225  			if parseErr != nil {
   226  				if err := runner.cloudClient.ExistsDataSourceItem(nucleicloud.ExistsDataSourceItemRequest{Contents: target, Type: "targets"}); err == nil {
   227  					runner.cloudTargets = append(runner.cloudTargets, target)
   228  					return true
   229  				}
   230  				return false
   231  			}
   232  			if exists, err := runner.cloudClient.ExistsTarget(parsed); err == nil {
   233  				runner.cloudTargets = append(runner.cloudTargets, exists.Reference)
   234  				return true
   235  			}
   236  			return false
   237  		},
   238  	})
   239  	if err != nil {
   240  		return nil, errors.Wrap(err, "could not create input provider")
   241  	}
   242  	runner.hmapInputProvider = hmapInput
   243  
   244  	// Create the output file if asked
   245  	outputWriter, err := output.NewStandardWriter(options)
   246  	if err != nil {
   247  		return nil, errors.Wrap(err, "could not create output file")
   248  	}
   249  	runner.output = outputWriter
   250  
   251  	if options.JSONL && options.EnableProgressBar {
   252  		options.StatsJSON = true
   253  	}
   254  	if options.StatsJSON {
   255  		options.EnableProgressBar = true
   256  	}
   257  	// Creates the progress tracking object
   258  	var progressErr error
   259  	statsInterval := options.StatsInterval
   260  	if options.Cloud && !options.EnableProgressBar {
   261  		statsInterval = -1
   262  		options.EnableProgressBar = true
   263  	}
   264  	runner.progress, progressErr = progress.NewStatsTicker(statsInterval, options.EnableProgressBar, options.StatsJSON, options.Metrics, options.Cloud, options.MetricsPort)
   265  	if progressErr != nil {
   266  		return nil, progressErr
   267  	}
   268  
   269  	// create project file if requested or load the existing one
   270  	if options.Project {
   271  		var projectFileErr error
   272  		runner.projectFile, projectFileErr = projectfile.New(&projectfile.Options{Path: options.ProjectPath, Cleanup: utils.IsBlank(options.ProjectPath)})
   273  		if projectFileErr != nil {
   274  			return nil, projectFileErr
   275  		}
   276  	}
   277  
   278  	// create the resume configuration structure
   279  	resumeCfg := types.NewResumeCfg()
   280  	if runner.options.ShouldLoadResume() {
   281  		gologger.Info().Msg("Resuming from save checkpoint")
   282  		file, err := os.ReadFile(runner.options.Resume)
   283  		if err != nil {
   284  			return nil, err
   285  		}
   286  		err = json.Unmarshal(file, &resumeCfg)
   287  		if err != nil {
   288  			return nil, err
   289  		}
   290  		resumeCfg.Compile()
   291  	}
   292  	runner.resumeCfg = resumeCfg
   293  
   294  	opts := interactsh.DefaultOptions(runner.output, runner.issuesClient, runner.progress)
   295  	opts.Debug = runner.options.Debug
   296  	opts.NoColor = runner.options.NoColor
   297  	if options.InteractshURL != "" {
   298  		opts.ServerURL = options.InteractshURL
   299  	}
   300  	opts.Authorization = options.InteractshToken
   301  	opts.CacheSize = options.InteractionsCacheSize
   302  	opts.Eviction = time.Duration(options.InteractionsEviction) * time.Second
   303  	opts.CooldownPeriod = time.Duration(options.InteractionsCoolDownPeriod) * time.Second
   304  	opts.PollDuration = time.Duration(options.InteractionsPollDuration) * time.Second
   305  	opts.NoInteractsh = runner.options.NoInteractsh
   306  	opts.StopAtFirstMatch = runner.options.StopAtFirstMatch
   307  	opts.Debug = runner.options.Debug
   308  	opts.DebugRequest = runner.options.DebugRequests
   309  	opts.DebugResponse = runner.options.DebugResponse
   310  	if httpclient != nil {
   311  		opts.HTTPClient = httpclient
   312  	}
   313  	if opts.HTTPClient == nil {
   314  		httpOpts := retryablehttp.DefaultOptionsSingle
   315  		httpOpts.Timeout = 20 * time.Second // for stability reasons
   316  		if options.Timeout > 20 {
   317  			httpOpts.Timeout = time.Duration(options.Timeout) * time.Second
   318  		}
   319  		// in testing it was found most of times when interactsh failed, it was due to failure in registering /polling requests
   320  		opts.HTTPClient = retryablehttp.NewClient(retryablehttp.DefaultOptionsSingle)
   321  	}
   322  	interactshClient, err := interactsh.New(opts)
   323  	if err != nil {
   324  		gologger.Error().Msgf("Could not create interactsh client: %s", err)
   325  	} else {
   326  		runner.interactsh = interactshClient
   327  	}
   328  
   329  	if options.RateLimitMinute > 0 {
   330  		runner.rateLimiter = ratelimit.New(context.Background(), uint(options.RateLimitMinute), time.Minute)
   331  	} else if options.RateLimit > 0 {
   332  		runner.rateLimiter = ratelimit.New(context.Background(), uint(options.RateLimit), time.Second)
   333  	} else {
   334  		runner.rateLimiter = ratelimit.NewUnlimited(context.Background())
   335  	}
   336  	return runner, nil
   337  }
   338  
   339  func createReportingOptions(options *types.Options) (*reporting.Options, error) {
   340  	var reportingOptions *reporting.Options
   341  	if options.ReportingConfig != "" {
   342  		file, err := os.Open(options.ReportingConfig)
   343  		if err != nil {
   344  			return nil, errors.Wrap(err, "could not open reporting config file")
   345  		}
   346  		defer file.Close()
   347  
   348  		reportingOptions = &reporting.Options{}
   349  		if err := yaml.DecodeAndValidate(file, reportingOptions); err != nil {
   350  			return nil, errors.Wrap(err, "could not parse reporting config file")
   351  		}
   352  		Walk(reportingOptions, expandEndVars)
   353  	}
   354  	if options.MarkdownExportDirectory != "" {
   355  		if reportingOptions != nil {
   356  			reportingOptions.MarkdownExporter = &markdown.Options{
   357  				Directory:         options.MarkdownExportDirectory,
   358  				IncludeRawPayload: !options.OmitRawRequests,
   359  				SortMode:          options.MarkdownExportSortMode,
   360  			}
   361  		} else {
   362  			reportingOptions = &reporting.Options{}
   363  			reportingOptions.MarkdownExporter = &markdown.Options{
   364  				Directory:         options.MarkdownExportDirectory,
   365  				IncludeRawPayload: !options.OmitRawRequests,
   366  				SortMode:          options.MarkdownExportSortMode,
   367  			}
   368  		}
   369  	}
   370  	if options.SarifExport != "" {
   371  		if reportingOptions != nil {
   372  			reportingOptions.SarifExporter = &sarif.Options{File: options.SarifExport}
   373  		} else {
   374  			reportingOptions = &reporting.Options{}
   375  			reportingOptions.SarifExporter = &sarif.Options{File: options.SarifExport}
   376  		}
   377  	}
   378  	if options.JSONExport != "" {
   379  		if reportingOptions != nil {
   380  			reportingOptions.JSONExporter = &jsonexporter.Options{
   381  				File:              options.JSONExport,
   382  				IncludeRawPayload: !options.OmitRawRequests,
   383  			}
   384  		} else {
   385  			reportingOptions = &reporting.Options{}
   386  			reportingOptions.JSONExporter = &jsonexporter.Options{
   387  				File:              options.JSONExport,
   388  				IncludeRawPayload: !options.OmitRawRequests,
   389  			}
   390  		}
   391  	}
   392  	if options.JSONLExport != "" {
   393  		if reportingOptions != nil {
   394  			reportingOptions.JSONLExporter = &jsonl.Options{
   395  				File:              options.JSONLExport,
   396  				IncludeRawPayload: !options.OmitRawRequests,
   397  			}
   398  		} else {
   399  			reportingOptions = &reporting.Options{}
   400  			reportingOptions.JSONLExporter = &jsonl.Options{
   401  				File:              options.JSONLExport,
   402  				IncludeRawPayload: !options.OmitRawRequests,
   403  			}
   404  		}
   405  	}
   406  
   407  	return reportingOptions, nil
   408  }
   409  
   410  // Close releases all the resources and cleans up
   411  func (r *Runner) Close() {
   412  	if r.output != nil {
   413  		r.output.Close()
   414  	}
   415  	if r.projectFile != nil {
   416  		r.projectFile.Close()
   417  	}
   418  	r.hmapInputProvider.Close()
   419  	protocolinit.Close()
   420  	if r.pprofServer != nil {
   421  		_ = r.pprofServer.Shutdown(context.Background())
   422  	}
   423  	if r.rateLimiter != nil {
   424  		r.rateLimiter.Stop()
   425  	}
   426  }
   427  
   428  // RunEnumeration sets up the input layer for giving input nuclei.
   429  // binary and runs the actual enumeration
   430  func (r *Runner) RunEnumeration() error {
   431  	// If user asked for new templates to be executed, collect the list from the templates' directory.
   432  	if r.options.NewTemplates {
   433  		if arr := config.DefaultConfig.GetNewAdditions(); len(arr) > 0 {
   434  			r.options.Templates = append(r.options.Templates, arr...)
   435  		}
   436  	}
   437  	if len(r.options.NewTemplatesWithVersion) > 0 {
   438  		if arr := installer.GetNewTemplatesInVersions(r.options.NewTemplatesWithVersion...); len(arr) > 0 {
   439  			r.options.Templates = append(r.options.Templates, arr...)
   440  		}
   441  	}
   442  	// Exclude ignored file for validation
   443  	if !r.options.Validate {
   444  		ignoreFile := config.ReadIgnoreFile()
   445  		r.options.ExcludeTags = append(r.options.ExcludeTags, ignoreFile.Tags...)
   446  		r.options.ExcludedTemplates = append(r.options.ExcludedTemplates, ignoreFile.Files...)
   447  	}
   448  
   449  	// Create the executor options which will be used throughout the execution
   450  	// stage by the nuclei engine modules.
   451  	executorOpts := protocols.ExecutorOptions{
   452  		Output:          r.output,
   453  		Options:         r.options,
   454  		Progress:        r.progress,
   455  		Catalog:         r.catalog,
   456  		IssuesClient:    r.issuesClient,
   457  		RateLimiter:     r.rateLimiter,
   458  		Interactsh:      r.interactsh,
   459  		ProjectFile:     r.projectFile,
   460  		Browser:         r.browser,
   461  		Colorizer:       r.colorizer,
   462  		ResumeCfg:       r.resumeCfg,
   463  		ExcludeMatchers: excludematchers.New(r.options.ExcludeMatchers),
   464  		InputHelper:     input.NewHelper(),
   465  	}
   466  
   467  	if r.options.ShouldUseHostError() {
   468  		cache := hosterrorscache.New(r.options.MaxHostError, hosterrorscache.DefaultMaxHostsCount, r.options.TrackError)
   469  		cache.SetVerbose(r.options.Verbose)
   470  		r.hostErrors = cache
   471  		executorOpts.HostErrorsCache = cache
   472  	}
   473  
   474  	executorEngine := core.New(r.options)
   475  	executorEngine.SetExecuterOptions(executorOpts)
   476  
   477  	workflowLoader, err := parsers.NewLoader(&executorOpts)
   478  	if err != nil {
   479  		return errors.Wrap(err, "Could not create loader.")
   480  	}
   481  	executorOpts.WorkflowLoader = workflowLoader
   482  
   483  	store, err := loader.New(loader.NewConfig(r.options, r.catalog, executorOpts))
   484  	if err != nil {
   485  		return errors.Wrap(err, "could not load templates from config")
   486  	}
   487  
   488  	var cloudTemplates []string
   489  	if r.options.Cloud {
   490  		// hook template loading
   491  		store.NotFoundCallback = func(template string) bool {
   492  			parsed, parseErr := strconv.ParseInt(template, 10, 64)
   493  			if parseErr != nil {
   494  				if err := r.cloudClient.ExistsDataSourceItem(nucleicloud.ExistsDataSourceItemRequest{Type: "templates", Contents: template}); err == nil {
   495  					cloudTemplates = append(cloudTemplates, template)
   496  					return true
   497  				}
   498  				return false
   499  			}
   500  			if exists, err := r.cloudClient.ExistsTemplate(parsed); err == nil {
   501  				cloudTemplates = append(cloudTemplates, exists.Reference)
   502  				return true
   503  			}
   504  			return false
   505  		}
   506  	}
   507  	if r.options.Validate {
   508  		if err := store.ValidateTemplates(); err != nil {
   509  			return err
   510  		}
   511  		if stats.GetValue(parsers.SyntaxErrorStats) == 0 && stats.GetValue(parsers.SyntaxWarningStats) == 0 && stats.GetValue(parsers.RuntimeWarningsStats) == 0 {
   512  			gologger.Info().Msgf("All templates validated successfully\n")
   513  		} else {
   514  			return errors.New("encountered errors while performing template validation")
   515  		}
   516  		return nil // exit
   517  	}
   518  	store.Load()
   519  	// TODO: remove below functions after v3 or update warning messages
   520  	disk.PrintDeprecatedPathsMsgIfApplicable(r.options.Silent)
   521  	templates.PrintDeprecatedProtocolNameMsgIfApplicable(r.options.Silent, r.options.Verbose)
   522  
   523  	// add the hosts from the metadata queries of loaded templates into input provider
   524  	if r.options.Uncover && len(r.options.UncoverQuery) == 0 {
   525  		uncoverOpts := &uncoverlib.Options{
   526  			Limit:         r.options.UncoverLimit,
   527  			MaxRetry:      r.options.Retries,
   528  			Timeout:       r.options.Timeout,
   529  			RateLimit:     uint(r.options.UncoverRateLimit),
   530  			RateLimitUnit: time.Minute, // default unit is minute
   531  		}
   532  		ret := uncover.GetUncoverTargetsFromMetadata(context.TODO(), store.Templates(), r.options.UncoverField, uncoverOpts)
   533  		for host := range ret {
   534  			r.hmapInputProvider.Set(host)
   535  		}
   536  	}
   537  	// list all templates
   538  	if r.options.TemplateList || r.options.TemplateDisplay {
   539  		r.listAvailableStoreTemplates(store)
   540  		os.Exit(0)
   541  	}
   542  
   543  	// display execution info like version , templates used etc
   544  	r.displayExecutionInfo(store)
   545  
   546  	// If not explicitly disabled, check if http based protocols
   547  	// are used, and if inputs are non-http to pre-perform probing
   548  	// of urls and storing them for execution.
   549  	if !r.options.DisableHTTPProbe && loader.IsHTTPBasedProtocolUsed(store) && r.isInputNonHTTP() {
   550  		inputHelpers, err := r.initializeTemplatesHTTPInput()
   551  		if err != nil {
   552  			return errors.Wrap(err, "could not probe http input")
   553  		}
   554  		executorOpts.InputHelper.InputsHTTP = inputHelpers
   555  	}
   556  
   557  	enumeration := false
   558  	var results *atomic.Bool
   559  	if r.options.Cloud {
   560  		if r.options.ScanList {
   561  			err = r.getScanList(r.options.OutputLimit)
   562  		} else if r.options.DeleteScan != "" {
   563  			err = r.deleteScan(r.options.DeleteScan)
   564  		} else if r.options.ScanOutput != "" {
   565  			err = r.getResults(r.options.ScanOutput, r.options.OutputLimit)
   566  		} else if r.options.ListDatasources {
   567  			err = r.listDatasources()
   568  		} else if r.options.ListTargets {
   569  			err = r.listTargets()
   570  		} else if r.options.ListTemplates {
   571  			err = r.listTemplates()
   572  		} else if r.options.ListReportingSources {
   573  			err = r.listReportingSources()
   574  		} else if r.options.AddDatasource != "" {
   575  			err = r.addCloudDataSource(r.options.AddDatasource)
   576  		} else if r.options.RemoveDatasource != "" {
   577  			err = r.removeDatasource(r.options.RemoveDatasource)
   578  		} else if r.options.DisableReportingSource != "" {
   579  			err = r.toggleReportingSource(r.options.DisableReportingSource, false)
   580  		} else if r.options.EnableReportingSource != "" {
   581  			err = r.toggleReportingSource(r.options.EnableReportingSource, true)
   582  		} else if r.options.AddTarget != "" {
   583  			err = r.addTarget(r.options.AddTarget)
   584  		} else if r.options.AddTemplate != "" {
   585  			err = r.addTemplate(r.options.AddTemplate)
   586  		} else if r.options.GetTarget != "" {
   587  			err = r.getTarget(r.options.GetTarget)
   588  		} else if r.options.GetTemplate != "" {
   589  			err = r.getTemplate(r.options.GetTemplate)
   590  		} else if r.options.RemoveTarget != "" {
   591  			err = r.removeTarget(r.options.RemoveTarget)
   592  		} else if r.options.RemoveTemplate != "" {
   593  			err = r.removeTemplate(r.options.RemoveTemplate)
   594  		} else if r.options.ReportingConfig != "" {
   595  			err = r.addCloudReportingSource()
   596  		} else {
   597  			if len(store.Templates())+len(store.Workflows())+len(cloudTemplates) == 0 {
   598  				return errors.New("no templates provided for scan")
   599  			}
   600  			gologger.Info().Msgf("Running scan on cloud with URL %s", r.options.CloudURL)
   601  			results, err = r.runCloudEnumeration(store, cloudTemplates, r.cloudTargets, r.options.NoStore, r.options.OutputLimit)
   602  			enumeration = true
   603  		}
   604  	} else {
   605  		results, err = r.runStandardEnumeration(executorOpts, store, executorEngine)
   606  		enumeration = true
   607  	}
   608  
   609  	if !enumeration {
   610  		return err
   611  	}
   612  
   613  	if r.interactsh != nil {
   614  		matched := r.interactsh.Close()
   615  		if matched {
   616  			results.CompareAndSwap(false, true)
   617  		}
   618  	}
   619  	r.progress.Stop()
   620  
   621  	if executorOpts.InputHelper != nil {
   622  		_ = executorOpts.InputHelper.Close()
   623  	}
   624  	if r.issuesClient != nil {
   625  		r.issuesClient.Close()
   626  	}
   627  
   628  	// todo: error propagation without canonical straight error check is required by cloud?
   629  	// use safe dereferencing to avoid potential panics in case of previous unchecked errors
   630  	if v := ptrutil.Safe(results); !v.Load() {
   631  		gologger.Info().Msgf("No results found. Better luck next time!")
   632  	}
   633  	if r.browser != nil {
   634  		r.browser.Close()
   635  	}
   636  	// check if a passive scan was requested but no target was provided
   637  	if r.options.OfflineHTTP && len(r.options.Targets) == 0 && r.options.TargetsFilePath == "" {
   638  		return errors.Wrap(err, "missing required input (http response) to run passive templates")
   639  	}
   640  
   641  	return err
   642  }
   643  
   644  func (r *Runner) isInputNonHTTP() bool {
   645  	var nonURLInput bool
   646  	r.hmapInputProvider.Scan(func(value *contextargs.MetaInput) bool {
   647  		if !strings.Contains(value.Input, "://") {
   648  			nonURLInput = true
   649  			return false
   650  		}
   651  		return true
   652  	})
   653  	return nonURLInput
   654  }
   655  
   656  func (r *Runner) executeSmartWorkflowInput(executorOpts protocols.ExecutorOptions, store *loader.Store, engine *core.Engine) (*atomic.Bool, error) {
   657  	r.progress.Init(r.hmapInputProvider.Count(), 0, 0)
   658  
   659  	service, err := automaticscan.New(automaticscan.Options{
   660  		ExecuterOpts: executorOpts,
   661  		Store:        store,
   662  		Engine:       engine,
   663  		Target:       r.hmapInputProvider,
   664  	})
   665  	if err != nil {
   666  		return nil, errors.Wrap(err, "could not create automatic scan service")
   667  	}
   668  	service.Execute()
   669  	result := &atomic.Bool{}
   670  	result.Store(service.Close())
   671  	return result, nil
   672  }
   673  
   674  func (r *Runner) executeTemplatesInput(store *loader.Store, engine *core.Engine) (*atomic.Bool, error) {
   675  	var unclusteredRequests int64
   676  	for _, template := range store.Templates() {
   677  		// workflows will dynamically adjust the totals while running, as
   678  		// it can't be known in advance which requests will be called
   679  		if len(template.Workflows) > 0 {
   680  			continue
   681  		}
   682  		unclusteredRequests += int64(template.TotalRequests) * r.hmapInputProvider.Count()
   683  	}
   684  
   685  	if r.options.VerboseVerbose {
   686  		for _, template := range store.Templates() {
   687  			r.logAvailableTemplate(template.Path)
   688  		}
   689  		for _, template := range store.Workflows() {
   690  			r.logAvailableTemplate(template.Path)
   691  		}
   692  	}
   693  
   694  	// Cluster the templates first because we want info on how many
   695  	// templates did we cluster for showing to user in CLI
   696  	originalTemplatesCount := len(store.Templates())
   697  	finalTemplates, clusterCount := templates.ClusterTemplates(store.Templates(), engine.ExecuterOptions())
   698  	finalTemplates = append(finalTemplates, store.Workflows()...)
   699  
   700  	var totalRequests int64
   701  	for _, t := range finalTemplates {
   702  		if len(t.Workflows) > 0 {
   703  			continue
   704  		}
   705  		totalRequests += int64(t.Executer.Requests()) * r.hmapInputProvider.Count()
   706  	}
   707  	if totalRequests < unclusteredRequests {
   708  		gologger.Info().Msgf("Templates clustered: %d (Reduced %d Requests)", clusterCount, unclusteredRequests-totalRequests)
   709  	}
   710  	workflowCount := len(store.Workflows())
   711  	templateCount := originalTemplatesCount + workflowCount
   712  
   713  	// 0 matches means no templates were found in the directory
   714  	if templateCount == 0 {
   715  		return &atomic.Bool{}, errors.New("no valid templates were found")
   716  	}
   717  
   718  	// tracks global progress and captures stdout/stderr until p.Wait finishes
   719  	r.progress.Init(r.hmapInputProvider.Count(), templateCount, totalRequests)
   720  
   721  	results := engine.ExecuteScanWithOpts(finalTemplates, r.hmapInputProvider, true)
   722  	return results, nil
   723  }
   724  
   725  // displayExecutionInfo displays misc info about the nuclei engine execution
   726  func (r *Runner) displayExecutionInfo(store *loader.Store) {
   727  	// Display stats for any loaded templates' syntax warnings or errors
   728  	stats.Display(parsers.SyntaxWarningStats)
   729  	stats.Display(parsers.SyntaxErrorStats)
   730  	stats.Display(parsers.RuntimeWarningsStats)
   731  
   732  	cfg := config.DefaultConfig
   733  
   734  	gologger.Info().Msgf("Current nuclei version: %v %v", config.Version, updateutils.GetVersionDescription(config.Version, cfg.LatestNucleiVersion))
   735  	gologger.Info().Msgf("Current nuclei-templates version: %v %v", cfg.TemplateVersion, updateutils.GetVersionDescription(cfg.TemplateVersion, cfg.LatestNucleiTemplatesVersion))
   736  
   737  	if len(store.Templates()) > 0 {
   738  		gologger.Info().Msgf("New templates added in latest release: %d", len(config.DefaultConfig.GetNewAdditions()))
   739  		gologger.Info().Msgf("Templates loaded for current scan: %d", len(store.Templates()))
   740  	}
   741  	if len(store.Workflows()) > 0 {
   742  		gologger.Info().Msgf("Workflows loaded for current scan: %d", len(store.Workflows()))
   743  	}
   744  	if r.hmapInputProvider.Count() > 0 {
   745  		gologger.Info().Msgf("Targets loaded for current scan: %d", r.hmapInputProvider.Count())
   746  	}
   747  }
   748  
   749  // SaveResumeConfig to file
   750  func (r *Runner) SaveResumeConfig(path string) error {
   751  	resumeCfgClone := r.resumeCfg.Clone()
   752  	resumeCfgClone.ResumeFrom = resumeCfgClone.Current
   753  	data, _ := json.MarshalIndent(resumeCfgClone, "", "\t")
   754  
   755  	return os.WriteFile(path, data, permissionutil.ConfigFilePermission)
   756  }
   757  
   758  type WalkFunc func(reflect.Value, reflect.StructField)
   759  
   760  // Walk traverses a struct and executes a callback function on each value in the struct.
   761  // The interface{} passed to the function should be a pointer to a struct or a struct.
   762  // WalkFunc is the callback function used for each value in the struct. It is passed the
   763  // reflect.Value and reflect.Type properties of the value in the struct.
   764  func Walk(s interface{}, callback WalkFunc) {
   765  	structValue := reflect.ValueOf(s)
   766  	if structValue.Kind() == reflect.Ptr {
   767  		structValue = structValue.Elem()
   768  	}
   769  	if structValue.Kind() != reflect.Struct {
   770  		return
   771  	}
   772  	for i := 0; i < structValue.NumField(); i++ {
   773  		field := structValue.Field(i)
   774  		fieldType := structValue.Type().Field(i)
   775  		if !fieldType.IsExported() {
   776  			continue
   777  		}
   778  		if field.Kind() == reflect.Struct {
   779  			Walk(field.Addr().Interface(), callback)
   780  		} else if field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct {
   781  			Walk(field.Interface(), callback)
   782  		} else {
   783  			callback(field, fieldType)
   784  		}
   785  	}
   786  }
   787  
   788  // expandEndVars looks for values in a struct tagged with "yaml" and checks if they are prefixed with '$'.
   789  // If they are, it will try to retrieve the value from the environment and if it exists, it will set the
   790  // value of the field to that of the environment variable.
   791  func expandEndVars(f reflect.Value, fieldType reflect.StructField) {
   792  	if _, ok := fieldType.Tag.Lookup("yaml"); !ok {
   793  		return
   794  	}
   795  	if f.Kind() == reflect.String {
   796  		str := f.String()
   797  		if strings.HasPrefix(str, "$") {
   798  			env := strings.TrimPrefix(str, "$")
   799  			retrievedEnv := os.Getenv(env)
   800  			if retrievedEnv != "" {
   801  				f.SetString(os.Getenv(env))
   802  			}
   803  		}
   804  	}
   805  }