Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/internal/runner/runner.go
2070 views
1
package runner
2
3
import (
4
"context"
5
"fmt"
6
"os"
7
"path/filepath"
8
"reflect"
9
"strings"
10
"sync/atomic"
11
"time"
12
13
"github.com/projectdiscovery/gologger"
14
"github.com/projectdiscovery/nuclei/v3/internal/pdcp"
15
"github.com/projectdiscovery/nuclei/v3/internal/server"
16
"github.com/projectdiscovery/nuclei/v3/pkg/authprovider"
17
"github.com/projectdiscovery/nuclei/v3/pkg/fuzz/frequency"
18
"github.com/projectdiscovery/nuclei/v3/pkg/input/provider"
19
"github.com/projectdiscovery/nuclei/v3/pkg/installer"
20
"github.com/projectdiscovery/nuclei/v3/pkg/loader/parser"
21
outputstats "github.com/projectdiscovery/nuclei/v3/pkg/output/stats"
22
"github.com/projectdiscovery/nuclei/v3/pkg/scan/events"
23
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
24
uncoverlib "github.com/projectdiscovery/uncover"
25
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
26
"github.com/projectdiscovery/utils/env"
27
fileutil "github.com/projectdiscovery/utils/file"
28
permissionutil "github.com/projectdiscovery/utils/permission"
29
pprofutil "github.com/projectdiscovery/utils/pprof"
30
updateutils "github.com/projectdiscovery/utils/update"
31
32
"github.com/logrusorgru/aurora"
33
"github.com/pkg/errors"
34
"github.com/projectdiscovery/ratelimit"
35
36
"github.com/projectdiscovery/nuclei/v3/internal/colorizer"
37
"github.com/projectdiscovery/nuclei/v3/internal/httpapi"
38
"github.com/projectdiscovery/nuclei/v3/pkg/catalog"
39
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
40
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
41
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/loader"
42
"github.com/projectdiscovery/nuclei/v3/pkg/core"
43
"github.com/projectdiscovery/nuclei/v3/pkg/external/customtemplates"
44
fuzzStats "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/stats"
45
"github.com/projectdiscovery/nuclei/v3/pkg/input"
46
parsers "github.com/projectdiscovery/nuclei/v3/pkg/loader/workflow"
47
"github.com/projectdiscovery/nuclei/v3/pkg/output"
48
"github.com/projectdiscovery/nuclei/v3/pkg/progress"
49
"github.com/projectdiscovery/nuclei/v3/pkg/projectfile"
50
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
51
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/automaticscan"
52
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
53
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
54
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache"
55
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
56
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
57
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/uncover"
58
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/excludematchers"
59
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
60
httpProtocol "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
61
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
62
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
63
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
64
"github.com/projectdiscovery/nuclei/v3/pkg/types"
65
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
66
"github.com/projectdiscovery/nuclei/v3/pkg/utils/stats"
67
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
68
"github.com/projectdiscovery/retryablehttp-go"
69
ptrutil "github.com/projectdiscovery/utils/ptr"
70
)
71
72
var (
73
// HideAutoSaveMsg is a global variable to hide the auto-save message
74
HideAutoSaveMsg = false
75
// EnableCloudUpload is global variable to enable cloud upload
76
EnableCloudUpload = false
77
)
78
79
// Runner is a client for running the enumeration process.
80
type Runner struct {
81
output output.Writer
82
interactsh *interactsh.Client
83
options *types.Options
84
projectFile *projectfile.ProjectFile
85
catalog catalog.Catalog
86
progress progress.Progress
87
colorizer aurora.Aurora
88
issuesClient reporting.Client
89
browser *engine.Browser
90
rateLimiter *ratelimit.Limiter
91
hostErrors hosterrorscache.CacheInterface
92
resumeCfg *types.ResumeCfg
93
pprofServer *pprofutil.PprofServer
94
pdcpUploadErrMsg string
95
inputProvider provider.InputProvider
96
fuzzFrequencyCache *frequency.Tracker
97
httpStats *outputstats.Tracker
98
Logger *gologger.Logger
99
100
//general purpose temporary directory
101
tmpDir string
102
parser parser.Parser
103
httpApiEndpoint *httpapi.Server
104
fuzzStats *fuzzStats.Tracker
105
dastServer *server.DASTServer
106
}
107
108
// New creates a new client for running the enumeration process.
109
func New(options *types.Options) (*Runner, error) {
110
runner := &Runner{
111
options: options,
112
Logger: options.Logger,
113
}
114
115
if options.HealthCheck {
116
runner.Logger.Print().Msgf("%s\n", DoHealthCheck(options))
117
os.Exit(0)
118
}
119
120
// Version check by default
121
if config.DefaultConfig.CanCheckForUpdates() {
122
if err := installer.NucleiVersionCheck(); err != nil {
123
if options.Verbose || options.Debug {
124
runner.Logger.Error().Msgf("nuclei version check failed got: %s\n", err)
125
}
126
}
127
128
// if template list or template display is enabled, enable all templates
129
if options.TemplateList || options.TemplateDisplay {
130
options.EnableCodeTemplates = true
131
options.EnableFileTemplates = true
132
options.EnableSelfContainedTemplates = true
133
options.EnableGlobalMatchersTemplates = true
134
}
135
136
// check for custom template updates and update if available
137
ctm, err := customtemplates.NewCustomTemplatesManager(options)
138
if err != nil {
139
runner.Logger.Error().Label("custom-templates").Msgf("Failed to create custom templates manager: %s\n", err)
140
}
141
142
// Check for template updates and update if available.
143
// If the custom templates manager is not nil, we will install custom templates if there is a fresh installation
144
tm := &installer.TemplateManager{
145
CustomTemplates: ctm,
146
DisablePublicTemplates: options.PublicTemplateDisableDownload,
147
}
148
if err := tm.FreshInstallIfNotExists(); err != nil {
149
runner.Logger.Warning().Msgf("failed to install nuclei templates: %s\n", err)
150
}
151
if err := tm.UpdateIfOutdated(); err != nil {
152
runner.Logger.Warning().Msgf("failed to update nuclei templates: %s\n", err)
153
}
154
155
if config.DefaultConfig.NeedsIgnoreFileUpdate() {
156
if err := installer.UpdateIgnoreFile(); err != nil {
157
runner.Logger.Warning().Msgf("failed to update nuclei ignore file: %s\n", err)
158
}
159
}
160
161
if options.UpdateTemplates {
162
// we automatically check for updates unless explicitly disabled
163
// this print statement is only to inform the user that there are no updates
164
if !config.DefaultConfig.NeedsTemplateUpdate() {
165
runner.Logger.Info().Msgf("No new updates found for nuclei templates")
166
}
167
// manually trigger update of custom templates
168
if ctm != nil {
169
ctm.Update(context.TODO())
170
}
171
}
172
}
173
174
if op, ok := options.Parser.(*templates.Parser); ok {
175
// Enable passing in an existing parser instance
176
// This uses a type assertion to avoid an import loop
177
runner.parser = op
178
} else {
179
parser := templates.NewParser()
180
if options.Validate {
181
parser.ShouldValidate = true
182
}
183
// TODO: refactor to pass options reference globally without cycles
184
parser.NoStrictSyntax = options.NoStrictSyntax
185
runner.parser = parser
186
}
187
188
yaml.StrictSyntax = !options.NoStrictSyntax
189
190
if options.Headless {
191
if engine.MustDisableSandbox() {
192
runner.Logger.Warning().Msgf("The current platform and privileged user will run the browser without sandbox\n")
193
}
194
browser, err := engine.New(options)
195
if err != nil {
196
return nil, err
197
}
198
runner.browser = browser
199
}
200
201
runner.catalog = disk.NewCatalog(config.DefaultConfig.TemplatesDirectory)
202
203
var httpclient *retryablehttp.Client
204
if options.ProxyInternal && options.AliveHttpProxy != "" || options.AliveSocksProxy != "" {
205
var err error
206
httpclient, err = httpclientpool.Get(options, &httpclientpool.Configuration{})
207
if err != nil {
208
return nil, err
209
}
210
}
211
212
if err := reporting.CreateConfigIfNotExists(); err != nil {
213
return nil, err
214
}
215
reportingOptions, err := createReportingOptions(options)
216
if err != nil {
217
return nil, err
218
}
219
if reportingOptions != nil && httpclient != nil {
220
reportingOptions.HttpClient = httpclient
221
}
222
223
if reportingOptions != nil {
224
client, err := reporting.New(reportingOptions, options.ReportingDB, false)
225
if err != nil {
226
return nil, errors.Wrap(err, "could not create issue reporting client")
227
}
228
runner.issuesClient = client
229
}
230
231
// output coloring
232
useColor := !options.NoColor
233
runner.colorizer = aurora.NewAurora(useColor)
234
templates.Colorizer = runner.colorizer
235
templates.SeverityColorizer = colorizer.New(runner.colorizer)
236
237
if options.EnablePprof {
238
runner.pprofServer = pprofutil.NewPprofServer()
239
runner.pprofServer.Start()
240
}
241
242
if options.HttpApiEndpoint != "" {
243
apiServer := httpapi.New(options.HttpApiEndpoint, options)
244
runner.Logger.Info().Msgf("Listening api endpoint on: %s", options.HttpApiEndpoint)
245
runner.httpApiEndpoint = apiServer
246
go func() {
247
if err := apiServer.Start(); err != nil {
248
runner.Logger.Error().Msgf("Failed to start API server: %s", err)
249
}
250
}()
251
}
252
253
if (len(options.Templates) == 0 || !options.NewTemplates || (options.TargetsFilePath == "" && !options.Stdin && len(options.Targets) == 0)) && options.UpdateTemplates {
254
os.Exit(0)
255
}
256
257
// create the input provider and load the inputs
258
inputProvider, err := provider.NewInputProvider(provider.InputOptions{Options: options})
259
if err != nil {
260
return nil, errors.Wrap(err, "could not create input provider")
261
}
262
runner.inputProvider = inputProvider
263
264
// Create the output file if asked
265
outputWriter, err := output.NewStandardWriter(options)
266
if err != nil {
267
return nil, errors.Wrap(err, "could not create output file")
268
}
269
// setup a proxy writer to automatically upload results to PDCP
270
runner.output = runner.setupPDCPUpload(outputWriter)
271
if options.HTTPStats {
272
runner.httpStats = outputstats.NewTracker()
273
runner.output = output.NewMultiWriter(runner.output, output.NewTrackerWriter(runner.httpStats))
274
}
275
276
if options.JSONL && options.EnableProgressBar {
277
options.StatsJSON = true
278
}
279
if options.StatsJSON {
280
options.EnableProgressBar = true
281
}
282
// Creates the progress tracking object
283
var progressErr error
284
statsInterval := options.StatsInterval
285
runner.progress, progressErr = progress.NewStatsTicker(statsInterval, options.EnableProgressBar, options.StatsJSON, false, options.MetricsPort)
286
if progressErr != nil {
287
return nil, progressErr
288
}
289
290
// create project file if requested or load the existing one
291
if options.Project {
292
var projectFileErr error
293
runner.projectFile, projectFileErr = projectfile.New(&projectfile.Options{Path: options.ProjectPath, Cleanup: utils.IsBlank(options.ProjectPath)})
294
if projectFileErr != nil {
295
return nil, projectFileErr
296
}
297
}
298
299
// create the resume configuration structure
300
resumeCfg := types.NewResumeCfg()
301
if runner.options.ShouldLoadResume() {
302
runner.Logger.Info().Msg("Resuming from save checkpoint")
303
file, err := os.ReadFile(runner.options.Resume)
304
if err != nil {
305
return nil, err
306
}
307
err = json.Unmarshal(file, &resumeCfg)
308
if err != nil {
309
return nil, err
310
}
311
resumeCfg.Compile()
312
}
313
runner.resumeCfg = resumeCfg
314
315
if options.DASTReport || options.DASTServer {
316
var err error
317
runner.fuzzStats, err = fuzzStats.NewTracker()
318
if err != nil {
319
return nil, errors.Wrap(err, "could not create fuzz stats db")
320
}
321
if !options.DASTServer {
322
dastServer, err := server.NewStatsServer(runner.fuzzStats)
323
if err != nil {
324
return nil, errors.Wrap(err, "could not create dast server")
325
}
326
runner.dastServer = dastServer
327
}
328
}
329
330
if runner.fuzzStats != nil {
331
outputWriter.JSONLogRequestHook = func(request *output.JSONLogRequest) {
332
if request.Error == "none" || request.Error == "" {
333
return
334
}
335
runner.fuzzStats.RecordErrorEvent(fuzzStats.ErrorEvent{
336
TemplateID: request.Template,
337
URL: request.Input,
338
Error: request.Error,
339
})
340
}
341
}
342
343
opts := interactsh.DefaultOptions(runner.output, runner.issuesClient, runner.progress)
344
opts.Logger = runner.Logger
345
opts.Debug = runner.options.Debug
346
opts.NoColor = runner.options.NoColor
347
if options.InteractshURL != "" {
348
opts.ServerURL = options.InteractshURL
349
}
350
opts.Authorization = options.InteractshToken
351
opts.CacheSize = options.InteractionsCacheSize
352
opts.Eviction = time.Duration(options.InteractionsEviction) * time.Second
353
opts.CooldownPeriod = time.Duration(options.InteractionsCoolDownPeriod) * time.Second
354
opts.PollDuration = time.Duration(options.InteractionsPollDuration) * time.Second
355
opts.NoInteractsh = runner.options.NoInteractsh
356
opts.StopAtFirstMatch = runner.options.StopAtFirstMatch
357
opts.Debug = runner.options.Debug
358
opts.DebugRequest = runner.options.DebugRequests
359
opts.DebugResponse = runner.options.DebugResponse
360
if httpclient != nil {
361
opts.HTTPClient = httpclient
362
}
363
if opts.HTTPClient == nil {
364
httpOpts := retryablehttp.DefaultOptionsSingle
365
httpOpts.Timeout = 20 * time.Second // for stability reasons
366
if options.Timeout > 20 {
367
httpOpts.Timeout = time.Duration(options.Timeout) * time.Second
368
}
369
// in testing it was found most of times when interactsh failed, it was due to failure in registering /polling requests
370
opts.HTTPClient = retryablehttp.NewClient(retryablehttp.DefaultOptionsSingle)
371
}
372
interactshClient, err := interactsh.New(opts)
373
if err != nil {
374
runner.Logger.Error().Msgf("Could not create interactsh client: %s", err)
375
} else {
376
runner.interactsh = interactshClient
377
}
378
379
if options.RateLimitMinute > 0 {
380
runner.Logger.Print().Msgf("[%v] %v", aurora.BrightYellow("WRN"), "rate limit per minute is deprecated - use rate-limit-duration")
381
options.RateLimit = options.RateLimitMinute
382
options.RateLimitDuration = time.Minute
383
}
384
if options.RateLimit > 0 && options.RateLimitDuration == 0 {
385
options.RateLimitDuration = time.Second
386
}
387
if options.RateLimit == 0 && options.RateLimitDuration == 0 {
388
runner.rateLimiter = ratelimit.NewUnlimited(context.Background())
389
} else {
390
runner.rateLimiter = ratelimit.New(context.Background(), uint(options.RateLimit), options.RateLimitDuration)
391
}
392
393
if tmpDir, err := os.MkdirTemp("", "nuclei-tmp-*"); err == nil {
394
runner.tmpDir = tmpDir
395
}
396
397
return runner, nil
398
}
399
400
// runStandardEnumeration runs standard enumeration
401
func (r *Runner) runStandardEnumeration(executerOpts *protocols.ExecutorOptions, store *loader.Store, engine *core.Engine) (*atomic.Bool, error) {
402
if r.options.AutomaticScan {
403
return r.executeSmartWorkflowInput(executerOpts, store, engine)
404
}
405
return r.executeTemplatesInput(store, engine)
406
}
407
408
// Close releases all the resources and cleans up
409
func (r *Runner) Close() {
410
if r.dastServer != nil {
411
r.dastServer.Close()
412
}
413
if r.httpStats != nil {
414
r.httpStats.DisplayTopStats(r.options.NoColor)
415
}
416
// dump hosterrors cache
417
if r.hostErrors != nil {
418
r.hostErrors.Close()
419
}
420
if r.output != nil {
421
r.output.Close()
422
}
423
if r.issuesClient != nil {
424
r.issuesClient.Close()
425
}
426
if r.projectFile != nil {
427
r.projectFile.Close()
428
}
429
if r.inputProvider != nil {
430
r.inputProvider.Close()
431
}
432
protocolinit.Close(r.options.ExecutionId)
433
if r.pprofServer != nil {
434
r.pprofServer.Stop()
435
}
436
if r.rateLimiter != nil {
437
r.rateLimiter.Stop()
438
}
439
r.progress.Stop()
440
if r.browser != nil {
441
r.browser.Close()
442
}
443
if r.tmpDir != "" {
444
_ = os.RemoveAll(r.tmpDir)
445
}
446
447
//this is no-op unless nuclei is built with stats build tag
448
events.Close()
449
}
450
451
// setupPDCPUpload sets up the PDCP upload writer
452
// by creating a new writer and returning it
453
func (r *Runner) setupPDCPUpload(writer output.Writer) output.Writer {
454
// if scanid is given implicitly consider that scan upload is enabled
455
if r.options.ScanID != "" {
456
r.options.EnableCloudUpload = true
457
}
458
if !r.options.EnableCloudUpload && !EnableCloudUpload {
459
r.pdcpUploadErrMsg = "Scan results upload to cloud is disabled."
460
return writer
461
}
462
h := &pdcpauth.PDCPCredHandler{}
463
creds, err := h.GetCreds()
464
if err != nil {
465
if err != pdcpauth.ErrNoCreds && !HideAutoSaveMsg {
466
r.Logger.Verbose().Msgf("Could not get credentials for cloud upload: %s\n", err)
467
}
468
r.pdcpUploadErrMsg = fmt.Sprintf("To view results on Cloud Dashboard, configure API key from %v", pdcpauth.DashBoardURL)
469
return writer
470
}
471
uploadWriter, err := pdcp.NewUploadWriter(context.Background(), r.Logger, creds)
472
if err != nil {
473
r.pdcpUploadErrMsg = fmt.Sprintf("PDCP (%v) Auto-Save Failed: %s\n", pdcpauth.DashBoardURL, err)
474
return writer
475
}
476
if r.options.ScanID != "" {
477
// ignore and use empty scan id if invalid
478
_ = uploadWriter.SetScanID(r.options.ScanID)
479
}
480
if r.options.ScanName != "" {
481
uploadWriter.SetScanName(r.options.ScanName)
482
}
483
if r.options.TeamID != "" {
484
uploadWriter.SetTeamID(r.options.TeamID)
485
}
486
return output.NewMultiWriter(writer, uploadWriter)
487
}
488
489
// RunEnumeration sets up the input layer for giving input nuclei.
490
// binary and runs the actual enumeration
491
func (r *Runner) RunEnumeration() error {
492
// If the user has asked for DAST server mode, run the live
493
// DAST fuzzing server.
494
if r.options.DASTServer {
495
execurOpts := &server.NucleiExecutorOptions{
496
Options: r.options,
497
Output: r.output,
498
Progress: r.progress,
499
Catalog: r.catalog,
500
IssuesClient: r.issuesClient,
501
RateLimiter: r.rateLimiter,
502
Interactsh: r.interactsh,
503
ProjectFile: r.projectFile,
504
Browser: r.browser,
505
Colorizer: r.colorizer,
506
Parser: r.parser,
507
TemporaryDirectory: r.tmpDir,
508
FuzzStatsDB: r.fuzzStats,
509
Logger: r.Logger,
510
}
511
dastServer, err := server.New(&server.Options{
512
Address: r.options.DASTServerAddress,
513
Templates: r.options.Templates,
514
OutputWriter: r.output,
515
Verbose: r.options.Verbose,
516
Token: r.options.DASTServerToken,
517
InScope: r.options.Scope,
518
OutScope: r.options.OutOfScope,
519
NucleiExecutorOptions: execurOpts,
520
})
521
if err != nil {
522
return err
523
}
524
r.dastServer = dastServer
525
return dastServer.Start()
526
}
527
528
// If user asked for new templates to be executed, collect the list from the templates' directory.
529
if r.options.NewTemplates {
530
if arr := config.DefaultConfig.GetNewAdditions(); len(arr) > 0 {
531
r.options.Templates = append(r.options.Templates, arr...)
532
}
533
}
534
if len(r.options.NewTemplatesWithVersion) > 0 {
535
if arr := installer.GetNewTemplatesInVersions(r.options.NewTemplatesWithVersion...); len(arr) > 0 {
536
r.options.Templates = append(r.options.Templates, arr...)
537
}
538
}
539
// Exclude ignored file for validation
540
if !r.options.Validate {
541
ignoreFile := config.ReadIgnoreFile()
542
r.options.ExcludeTags = append(r.options.ExcludeTags, ignoreFile.Tags...)
543
r.options.ExcludedTemplates = append(r.options.ExcludedTemplates, ignoreFile.Files...)
544
}
545
546
fuzzFreqCache := frequency.New(frequency.DefaultMaxTrackCount, r.options.FuzzParamFrequency)
547
r.fuzzFrequencyCache = fuzzFreqCache
548
549
// Create the executor options which will be used throughout the execution
550
// stage by the nuclei engine modules.
551
executorOpts := &protocols.ExecutorOptions{
552
Output: r.output,
553
Options: r.options,
554
Progress: r.progress,
555
Catalog: r.catalog,
556
IssuesClient: r.issuesClient,
557
RateLimiter: r.rateLimiter,
558
Interactsh: r.interactsh,
559
ProjectFile: r.projectFile,
560
Browser: r.browser,
561
Colorizer: r.colorizer,
562
ResumeCfg: r.resumeCfg,
563
ExcludeMatchers: excludematchers.New(r.options.ExcludeMatchers),
564
InputHelper: input.NewHelper(),
565
TemporaryDirectory: r.tmpDir,
566
Parser: r.parser,
567
FuzzParamsFrequency: fuzzFreqCache,
568
GlobalMatchers: globalmatchers.New(),
569
DoNotCache: r.options.DoNotCacheTemplates,
570
Logger: r.Logger,
571
}
572
573
if config.DefaultConfig.IsDebugArgEnabled(config.DebugExportURLPattern) {
574
// Go StdLib style experimental/debug feature switch
575
executorOpts.ExportReqURLPattern = true
576
}
577
578
if len(r.options.SecretsFile) > 0 && !r.options.Validate {
579
authTmplStore, err := GetAuthTmplStore(r.options, r.catalog, executorOpts)
580
if err != nil {
581
return errors.Wrap(err, "failed to load dynamic auth templates")
582
}
583
authOpts := &authprovider.AuthProviderOptions{SecretsFiles: r.options.SecretsFile}
584
authOpts.LazyFetchSecret = GetLazyAuthFetchCallback(&AuthLazyFetchOptions{
585
TemplateStore: authTmplStore,
586
ExecOpts: executorOpts,
587
})
588
// initialize auth provider
589
provider, err := authprovider.NewAuthProvider(authOpts)
590
if err != nil {
591
return errors.Wrap(err, "could not create auth provider")
592
}
593
executorOpts.AuthProvider = provider
594
}
595
596
if r.options.ShouldUseHostError() {
597
maxHostError := r.options.MaxHostError
598
if r.options.TemplateThreads > maxHostError {
599
r.Logger.Print().Msgf("[%v] The concurrency value is higher than max-host-error", r.colorizer.BrightYellow("WRN"))
600
r.Logger.Info().Msgf("Adjusting max-host-error to the concurrency value: %d", r.options.TemplateThreads)
601
602
maxHostError = r.options.TemplateThreads
603
}
604
605
cache := hosterrorscache.New(maxHostError, hosterrorscache.DefaultMaxHostsCount, r.options.TrackError)
606
cache.SetVerbose(r.options.Verbose)
607
608
r.hostErrors = cache
609
executorOpts.HostErrorsCache = cache
610
}
611
612
executorEngine := core.New(r.options)
613
executorEngine.SetExecuterOptions(executorOpts)
614
615
workflowLoader, err := parsers.NewLoader(executorOpts)
616
if err != nil {
617
return errors.Wrap(err, "Could not create loader.")
618
}
619
executorOpts.WorkflowLoader = workflowLoader
620
621
// If using input-file flags, only load http fuzzing based templates.
622
loaderConfig := loader.NewConfig(r.options, r.catalog, executorOpts)
623
if !strings.EqualFold(r.options.InputFileMode, "list") || r.options.DAST {
624
// if input type is not list (implicitly enable fuzzing)
625
r.options.DAST = true
626
}
627
store, err := loader.New(loaderConfig)
628
if err != nil {
629
return errors.Wrap(err, "Could not create loader.")
630
}
631
632
// list all templates or tags as specified by user.
633
// This uses a separate parser to reduce time taken as
634
// normally nuclei does a lot of compilation and stuff
635
// for templates, which we don't want for these simp
636
if r.options.TemplateList || r.options.TemplateDisplay || r.options.TagList {
637
if err := store.LoadTemplatesOnlyMetadata(); err != nil {
638
return err
639
}
640
641
if r.options.TagList {
642
r.listAvailableStoreTags(store)
643
} else {
644
r.listAvailableStoreTemplates(store)
645
}
646
os.Exit(0)
647
}
648
649
if r.options.Validate {
650
if err := store.ValidateTemplates(); err != nil {
651
return err
652
}
653
if stats.GetValue(templates.SyntaxErrorStats) == 0 && stats.GetValue(templates.SyntaxWarningStats) == 0 && stats.GetValue(templates.RuntimeWarningsStats) == 0 {
654
r.Logger.Info().Msgf("All templates validated successfully")
655
} else {
656
return errors.New("encountered errors while performing template validation")
657
}
658
return nil // exit
659
}
660
store.Load()
661
// TODO: remove below functions after v3 or update warning messages
662
disk.PrintDeprecatedPathsMsgIfApplicable(r.options.Silent)
663
templates.PrintDeprecatedProtocolNameMsgIfApplicable(r.options.Silent, r.options.Verbose)
664
665
// add the hosts from the metadata queries of loaded templates into input provider
666
if r.options.Uncover && len(r.options.UncoverQuery) == 0 {
667
uncoverOpts := &uncoverlib.Options{
668
Limit: r.options.UncoverLimit,
669
MaxRetry: r.options.Retries,
670
Timeout: r.options.Timeout,
671
RateLimit: uint(r.options.UncoverRateLimit),
672
RateLimitUnit: time.Minute, // default unit is minute
673
}
674
ret := uncover.GetUncoverTargetsFromMetadata(context.TODO(), store.Templates(), r.options.UncoverField, uncoverOpts)
675
for host := range ret {
676
_ = r.inputProvider.SetWithExclusions(r.options.ExecutionId, host)
677
}
678
}
679
// display execution info like version , templates used etc
680
r.displayExecutionInfo(store)
681
682
// prefetch secrets if enabled
683
if executorOpts.AuthProvider != nil && r.options.PreFetchSecrets {
684
r.Logger.Info().Msgf("Pre-fetching secrets from authprovider[s]")
685
if err := executorOpts.AuthProvider.PreFetchSecrets(); err != nil {
686
return errors.Wrap(err, "could not pre-fetch secrets")
687
}
688
}
689
690
// If not explicitly disabled, check if http based protocols
691
// are used, and if inputs are non-http to pre-perform probing
692
// of urls and storing them for execution.
693
if !r.options.DisableHTTPProbe && loader.IsHTTPBasedProtocolUsed(store) && r.isInputNonHTTP() {
694
inputHelpers, err := r.initializeTemplatesHTTPInput()
695
if err != nil {
696
return errors.Wrap(err, "could not probe http input")
697
}
698
executorOpts.InputHelper.InputsHTTP = inputHelpers
699
}
700
701
// initialize stats worker ( this is no-op unless nuclei is built with stats build tag)
702
// during execution a directory with 2 files will be created in the current directory
703
// config.json - containing below info
704
// events.jsonl - containing all start and end times of all templates
705
events.InitWithConfig(&events.ScanConfig{
706
Name: "nuclei-stats", // make this configurable
707
TargetCount: int(r.inputProvider.Count()),
708
TemplatesCount: len(store.Templates()) + len(store.Workflows()),
709
TemplateConcurrency: r.options.TemplateThreads,
710
PayloadConcurrency: r.options.PayloadConcurrency,
711
JsConcurrency: r.options.JsConcurrency,
712
Retries: r.options.Retries,
713
}, "")
714
715
if r.dastServer != nil {
716
go func() {
717
if err := r.dastServer.Start(); err != nil {
718
r.Logger.Error().Msgf("could not start dast server: %v", err)
719
}
720
}()
721
}
722
723
now := time.Now()
724
enumeration := false
725
var results *atomic.Bool
726
results, err = r.runStandardEnumeration(executorOpts, store, executorEngine)
727
enumeration = true
728
729
if !enumeration {
730
return err
731
}
732
733
if executorOpts.FuzzStatsDB != nil {
734
executorOpts.FuzzStatsDB.Close()
735
}
736
if r.interactsh != nil {
737
matched := r.interactsh.Close()
738
if matched {
739
results.CompareAndSwap(false, true)
740
}
741
}
742
if executorOpts.InputHelper != nil {
743
_ = executorOpts.InputHelper.Close()
744
}
745
r.fuzzFrequencyCache.Close()
746
747
r.progress.Stop()
748
timeTaken := time.Since(now)
749
// todo: error propagation without canonical straight error check is required by cloud?
750
// use safe dereferencing to avoid potential panics in case of previous unchecked errors
751
if v := ptrutil.Safe(results); !v.Load() {
752
r.Logger.Info().Msgf("Scan completed in %s. No results found.", shortDur(timeTaken))
753
} else {
754
matchCount := r.output.ResultCount()
755
r.Logger.Info().Msgf("Scan completed in %s. %d matches found.", shortDur(timeTaken), matchCount)
756
}
757
758
// check if a passive scan was requested but no target was provided
759
if r.options.OfflineHTTP && len(r.options.Targets) == 0 && r.options.TargetsFilePath == "" {
760
return errors.Wrap(err, "missing required input (http response) to run passive templates")
761
}
762
763
return err
764
}
765
766
func shortDur(d time.Duration) string {
767
if d < time.Minute {
768
return d.String()
769
}
770
771
// Truncate to the nearest minute
772
d = d.Truncate(time.Minute)
773
s := d.String()
774
775
if strings.HasSuffix(s, "m0s") {
776
s = s[:len(s)-2]
777
}
778
if strings.HasSuffix(s, "h0m") {
779
s = s[:len(s)-2]
780
}
781
return s
782
}
783
784
func (r *Runner) isInputNonHTTP() bool {
785
var nonURLInput bool
786
r.inputProvider.Iterate(func(value *contextargs.MetaInput) bool {
787
if !strings.Contains(value.Input, "://") {
788
nonURLInput = true
789
return false
790
}
791
return true
792
})
793
return nonURLInput
794
}
795
796
func (r *Runner) executeSmartWorkflowInput(executorOpts *protocols.ExecutorOptions, store *loader.Store, engine *core.Engine) (*atomic.Bool, error) {
797
r.progress.Init(r.inputProvider.Count(), 0, 0)
798
799
service, err := automaticscan.New(automaticscan.Options{
800
ExecuterOpts: executorOpts,
801
Store: store,
802
Engine: engine,
803
Target: r.inputProvider,
804
})
805
if err != nil {
806
return nil, errors.Wrap(err, "could not create automatic scan service")
807
}
808
if err := service.Execute(); err != nil {
809
return nil, errors.Wrap(err, "could not execute automatic scan")
810
}
811
result := &atomic.Bool{}
812
result.Store(service.Close())
813
return result, nil
814
}
815
816
func (r *Runner) executeTemplatesInput(store *loader.Store, engine *core.Engine) (*atomic.Bool, error) {
817
if r.options.VerboseVerbose {
818
for _, template := range store.Templates() {
819
r.logAvailableTemplate(template.Path)
820
}
821
for _, template := range store.Workflows() {
822
r.logAvailableTemplate(template.Path)
823
}
824
}
825
826
finalTemplates := []*templates.Template{}
827
finalTemplates = append(finalTemplates, store.Templates()...)
828
finalTemplates = append(finalTemplates, store.Workflows()...)
829
830
if len(finalTemplates) == 0 {
831
return nil, errors.New("no templates provided for scan")
832
}
833
834
// pass input provider to engine
835
// TODO: this should be not necessary after r.hmapInputProvider is removed + refactored
836
if r.inputProvider == nil {
837
return nil, errors.New("no input provider found")
838
}
839
results := engine.ExecuteScanWithOpts(context.Background(), finalTemplates, r.inputProvider, r.options.DisableClustering)
840
return results, nil
841
}
842
843
// displayExecutionInfo displays misc info about the nuclei engine execution
844
func (r *Runner) displayExecutionInfo(store *loader.Store) {
845
// Display stats for any loaded templates' syntax warnings or errors
846
stats.Display(templates.SyntaxWarningStats)
847
stats.Display(templates.SyntaxErrorStats)
848
stats.Display(templates.RuntimeWarningsStats)
849
tmplCount := len(store.Templates())
850
workflowCount := len(store.Workflows())
851
if r.options.Verbose || (tmplCount == 0 && workflowCount == 0) {
852
// only print these stats in verbose mode
853
stats.ForceDisplayWarning(templates.ExcludedHeadlessTmplStats)
854
stats.ForceDisplayWarning(templates.ExcludedCodeTmplStats)
855
stats.ForceDisplayWarning(templates.ExludedDastTmplStats)
856
stats.ForceDisplayWarning(templates.TemplatesExcludedStats)
857
stats.ForceDisplayWarning(templates.ExcludedFileStats)
858
stats.ForceDisplayWarning(templates.ExcludedSelfContainedStats)
859
}
860
861
if tmplCount == 0 && workflowCount == 0 {
862
// if dast flag is used print explicit warning
863
if r.options.DAST {
864
r.Logger.Print().Msgf("[%v] No DAST templates found", aurora.BrightYellow("WRN"))
865
}
866
stats.ForceDisplayWarning(templates.SkippedCodeTmplTamperedStats)
867
} else {
868
stats.DisplayAsWarning(templates.SkippedCodeTmplTamperedStats)
869
}
870
stats.DisplayAsWarning(httpProtocol.SetThreadToCountZero)
871
stats.ForceDisplayWarning(templates.SkippedUnsignedStats)
872
stats.ForceDisplayWarning(templates.SkippedRequestSignatureStats)
873
874
cfg := config.DefaultConfig
875
876
updateutils.Aurora = r.colorizer
877
versionInfo := func(version, latestVersion, versionType string) string {
878
if !cfg.CanCheckForUpdates() {
879
return fmt.Sprintf("Current %s version: %v (%s) - remove '-duc' flag to enable update checks", versionType, version, r.colorizer.BrightYellow("unknown"))
880
}
881
return fmt.Sprintf("Current %s version: %v %v", versionType, version, updateutils.GetVersionDescription(version, latestVersion))
882
}
883
884
gologger.Info().Msg(versionInfo(config.Version, cfg.LatestNucleiVersion, "nuclei"))
885
gologger.Info().Msg(versionInfo(cfg.TemplateVersion, cfg.LatestNucleiTemplatesVersion, "nuclei-templates"))
886
if !HideAutoSaveMsg {
887
if r.pdcpUploadErrMsg != "" {
888
r.Logger.Warning().Msgf("%s", r.pdcpUploadErrMsg)
889
} else {
890
r.Logger.Info().Msgf("To view results on cloud dashboard, visit %v/scans upon scan completion.", pdcpauth.DashBoardURL)
891
}
892
}
893
894
if tmplCount > 0 || workflowCount > 0 {
895
if len(store.Templates()) > 0 {
896
r.Logger.Info().Msgf("New templates added in latest release: %d", len(config.DefaultConfig.GetNewAdditions()))
897
r.Logger.Info().Msgf("Templates loaded for current scan: %d", len(store.Templates()))
898
}
899
if len(store.Workflows()) > 0 {
900
r.Logger.Info().Msgf("Workflows loaded for current scan: %d", len(store.Workflows()))
901
}
902
for k, v := range templates.SignatureStats {
903
value := v.Load()
904
if value > 0 {
905
if k == templates.Unsigned && !r.options.Silent && !config.DefaultConfig.HideTemplateSigWarning {
906
r.Logger.Print().Msgf("[%v] Loading %d unsigned templates for scan. Use with caution.", r.colorizer.BrightYellow("WRN"), value)
907
} else {
908
r.Logger.Info().Msgf("Executing %d signed templates from %s", value, k)
909
}
910
}
911
}
912
}
913
914
if r.inputProvider.Count() > 0 {
915
r.Logger.Info().Msgf("Targets loaded for current scan: %d", r.inputProvider.Count())
916
}
917
}
918
919
// SaveResumeConfig to file
920
func (r *Runner) SaveResumeConfig(path string) error {
921
dir := filepath.Dir(path)
922
if !fileutil.FolderExists(dir) {
923
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
924
return err
925
}
926
}
927
resumeCfgClone := r.resumeCfg.Clone()
928
resumeCfgClone.ResumeFrom = resumeCfgClone.Current
929
data, _ := json.MarshalIndent(resumeCfgClone, "", "\t")
930
931
return os.WriteFile(path, data, permissionutil.ConfigFilePermission)
932
}
933
934
// upload existing scan results to cloud with progress
935
func UploadResultsToCloud(options *types.Options) error {
936
h := &pdcpauth.PDCPCredHandler{}
937
creds, err := h.GetCreds()
938
if err != nil {
939
return errors.Wrap(err, "could not get credentials for cloud upload")
940
}
941
ctx := context.TODO()
942
uploadWriter, err := pdcp.NewUploadWriter(ctx, options.Logger, creds)
943
if err != nil {
944
return errors.Wrap(err, "could not create upload writer")
945
}
946
if options.ScanID != "" {
947
_ = uploadWriter.SetScanID(options.ScanID)
948
}
949
if options.ScanName != "" {
950
uploadWriter.SetScanName(options.ScanName)
951
}
952
if options.TeamID != "" {
953
uploadWriter.SetTeamID(options.TeamID)
954
}
955
956
// Open file to count the number of results first
957
file, err := os.Open(options.ScanUploadFile)
958
if err != nil {
959
return errors.Wrap(err, "could not open scan upload file")
960
}
961
defer func() {
962
_ = file.Close()
963
}()
964
965
options.Logger.Info().Msgf("Uploading scan results to cloud dashboard from %s", options.ScanUploadFile)
966
dec := json.NewDecoder(file)
967
for dec.More() {
968
var r output.ResultEvent
969
err := dec.Decode(&r)
970
if err != nil {
971
options.Logger.Warning().Msgf("Could not decode jsonl: %s\n", err)
972
continue
973
}
974
if err = uploadWriter.Write(&r); err != nil {
975
options.Logger.Warning().Msgf("[%s] failed to upload: %s\n", r.TemplateID, err)
976
}
977
}
978
uploadWriter.Close()
979
return nil
980
}
981
982
type WalkFunc func(reflect.Value, reflect.StructField)
983
984
// Walk traverses a struct and executes a callback function on each value in the struct.
985
// The interface{} passed to the function should be a pointer to a struct or a struct.
986
// WalkFunc is the callback function used for each value in the struct. It is passed the
987
// reflect.Value and reflect.Type properties of the value in the struct.
988
func Walk(s interface{}, callback WalkFunc) {
989
structValue := reflect.ValueOf(s)
990
if structValue.Kind() == reflect.Ptr {
991
structValue = structValue.Elem()
992
}
993
if structValue.Kind() != reflect.Struct {
994
return
995
}
996
for i := 0; i < structValue.NumField(); i++ {
997
field := structValue.Field(i)
998
fieldType := structValue.Type().Field(i)
999
if !fieldType.IsExported() {
1000
continue
1001
}
1002
if field.Kind() == reflect.Struct {
1003
Walk(field.Addr().Interface(), callback)
1004
} else if field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct {
1005
Walk(field.Interface(), callback)
1006
} else {
1007
callback(field, fieldType)
1008
}
1009
}
1010
}
1011
1012
// expandEndVars looks for values in a struct tagged with "yaml" and checks if they are prefixed with '$'.
1013
// If they are, it will try to retrieve the value from the environment and if it exists, it will set the
1014
// value of the field to that of the environment variable.
1015
func expandEndVars(f reflect.Value, fieldType reflect.StructField) {
1016
if _, ok := fieldType.Tag.Lookup("yaml"); !ok {
1017
return
1018
}
1019
if f.Kind() == reflect.String {
1020
str := f.String()
1021
if strings.HasPrefix(str, "$") {
1022
env := strings.TrimPrefix(str, "$")
1023
retrievedEnv := os.Getenv(env)
1024
if retrievedEnv != "" {
1025
f.SetString(os.Getenv(env))
1026
}
1027
}
1028
}
1029
}
1030
1031
func init() {
1032
HideAutoSaveMsg = env.GetEnvOrDefault("DISABLE_CLOUD_UPLOAD_WRN", false)
1033
EnableCloudUpload = env.GetEnvOrDefault("ENABLE_CLOUD_UPLOAD", false)
1034
}
1035
1036