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