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 }