Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/internal/runner/options.go
2070 views
1
package runner
2
3
import (
4
"bufio"
5
"fmt"
6
"io/fs"
7
"os"
8
"path/filepath"
9
"strconv"
10
"strings"
11
12
"github.com/pkg/errors"
13
14
"github.com/go-playground/validator/v10"
15
16
"github.com/projectdiscovery/goflags"
17
"github.com/projectdiscovery/gologger"
18
"github.com/projectdiscovery/gologger/formatter"
19
"github.com/projectdiscovery/gologger/levels"
20
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
21
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit"
22
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump"
23
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine"
24
"github.com/projectdiscovery/nuclei/v3/pkg/reporting"
25
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
26
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonl"
27
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown"
28
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/sarif"
29
"github.com/projectdiscovery/nuclei/v3/pkg/templates/extensions"
30
"github.com/projectdiscovery/nuclei/v3/pkg/types"
31
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
32
fileutil "github.com/projectdiscovery/utils/file"
33
"github.com/projectdiscovery/utils/generic"
34
stringsutil "github.com/projectdiscovery/utils/strings"
35
)
36
37
const (
38
// Default directory used to save protocols traffic
39
DefaultDumpTrafficOutputFolder = "output"
40
)
41
42
var validateOptions = validator.New()
43
44
func ConfigureOptions() error {
45
// with FileStringSliceOptions, FileNormalizedStringSliceOptions, FileCommaSeparatedStringSliceOptions
46
// if file has the extension `.yaml` or `.json` we consider those as strings and not files to be read
47
isFromFileFunc := func(s string) bool {
48
return !config.IsTemplate(s)
49
}
50
goflags.FileNormalizedStringSliceOptions.IsFromFile = isFromFileFunc
51
goflags.FileStringSliceOptions.IsFromFile = isFromFileFunc
52
goflags.FileCommaSeparatedStringSliceOptions.IsFromFile = isFromFileFunc
53
return nil
54
}
55
56
// ParseOptions parses the command line flags provided by a user
57
func ParseOptions(options *types.Options) {
58
// Check if stdin pipe was given
59
options.Stdin = !options.DisableStdin && fileutil.HasStdin()
60
61
// Read the inputs from env variables that not passed by flag.
62
readEnvInputVars(options)
63
64
// Read the inputs and configure the logging
65
configureOutput(options)
66
67
// Show the user the banner
68
showBanner()
69
70
if options.ShowVarDump {
71
vardump.EnableVarDump = true
72
vardump.Limit = options.VarDumpLimit
73
}
74
if options.ShowActions {
75
options.Logger.Info().Msgf("Showing available headless actions: ")
76
for action := range engine.ActionStringToAction {
77
options.Logger.Print().Msgf("\t%s", action)
78
}
79
os.Exit(0)
80
}
81
82
defaultProfilesPath := filepath.Join(config.DefaultConfig.GetTemplateDir(), "profiles")
83
if options.ListTemplateProfiles {
84
options.Logger.Print().Msgf(
85
"Listing available %v nuclei template profiles for %v",
86
config.DefaultConfig.TemplateVersion,
87
config.DefaultConfig.TemplatesDirectory,
88
)
89
templatesRootDir := config.DefaultConfig.GetTemplateDir()
90
err := filepath.WalkDir(defaultProfilesPath, func(iterItem string, d fs.DirEntry, err error) error {
91
ext := filepath.Ext(iterItem)
92
isYaml := ext == extensions.YAML || ext == extensions.YML
93
if err != nil || d.IsDir() || !isYaml {
94
return nil
95
}
96
if profileRelPath, err := filepath.Rel(templatesRootDir, iterItem); err == nil {
97
options.Logger.Print().Msgf("%s (%s)\n", profileRelPath, strings.TrimSuffix(filepath.Base(iterItem), ext))
98
}
99
return nil
100
})
101
if err != nil {
102
options.Logger.Error().Msgf("%s\n", err)
103
}
104
os.Exit(0)
105
}
106
if options.StoreResponseDir != DefaultDumpTrafficOutputFolder && !options.StoreResponse {
107
options.Logger.Debug().Msgf("Store response directory specified, enabling \"store-resp\" flag automatically\n")
108
options.StoreResponse = true
109
}
110
// Validate the options passed by the user and if any
111
// invalid options have been used, exit.
112
if err := ValidateOptions(options); err != nil {
113
options.Logger.Fatal().Msgf("Program exiting: %s\n", err)
114
}
115
116
// Load the resolvers if user asked for them
117
loadResolvers(options)
118
119
err := protocolinit.Init(options)
120
if err != nil {
121
options.Logger.Fatal().Msgf("Could not initialize protocols: %s\n", err)
122
}
123
124
// Set GitHub token in env variable. runner.getGHClientWithToken() reads token from env
125
if options.GitHubToken != "" && os.Getenv("GITHUB_TOKEN") != options.GitHubToken {
126
_ = os.Setenv("GITHUB_TOKEN", options.GitHubToken)
127
}
128
129
if options.UncoverQuery != nil {
130
options.Uncover = true
131
if len(options.UncoverEngine) == 0 {
132
options.UncoverEngine = append(options.UncoverEngine, "shodan")
133
}
134
}
135
136
if options.OfflineHTTP {
137
options.DisableHTTPProbe = true
138
}
139
}
140
141
// validateOptions validates the configuration options passed
142
func ValidateOptions(options *types.Options) error {
143
if err := validateOptions.Struct(options); err != nil {
144
if _, ok := err.(*validator.InvalidValidationError); ok {
145
return err
146
}
147
errs := []string{}
148
for _, err := range err.(validator.ValidationErrors) {
149
errs = append(errs, err.Namespace()+": "+err.Tag())
150
}
151
return errors.Wrap(errors.New(strings.Join(errs, ", ")), "validation failed for these fields")
152
}
153
if options.Verbose && options.Silent {
154
return errors.New("both verbose and silent mode specified")
155
}
156
157
if (options.HeadlessOptionalArguments != nil || options.ShowBrowser || options.UseInstalledChrome) && !options.Headless {
158
return errors.New("headless mode (-headless) is required if -ho, -sb, -sc or -lha are set")
159
}
160
161
if options.FollowHostRedirects && options.FollowRedirects {
162
return errors.New("both follow host redirects and follow redirects specified")
163
}
164
if options.ShouldFollowHTTPRedirects() && options.DisableRedirects {
165
return errors.New("both follow redirects and disable redirects specified")
166
}
167
// loading the proxy server list from file or cli and test the connectivity
168
if err := loadProxyServers(options); err != nil {
169
return err
170
}
171
if options.Validate {
172
validateTemplatePaths(options.Logger, config.DefaultConfig.TemplatesDirectory, options.Templates, options.Workflows)
173
}
174
if options.DAST {
175
if err := validateDASTOptions(options); err != nil {
176
return err
177
}
178
}
179
180
// Verify if any of the client certificate options were set since it requires all three to work properly
181
if options.HasClientCertificates() {
182
if generic.EqualsAny("", options.ClientCertFile, options.ClientKeyFile, options.ClientCAFile) {
183
return errors.New("if a client certification option is provided, then all three must be provided")
184
}
185
validateCertificatePaths(options.Logger, options.ClientCertFile, options.ClientKeyFile, options.ClientCAFile)
186
}
187
// Verify AWS secrets are passed if a S3 template bucket is passed
188
if options.AwsBucketName != "" && options.UpdateTemplates && !options.AwsTemplateDisableDownload {
189
missing := validateMissingS3Options(options)
190
if missing != nil {
191
return fmt.Errorf("aws s3 bucket details are missing. Please provide %s", strings.Join(missing, ","))
192
}
193
}
194
195
// Verify Azure connection configuration is passed if the Azure template bucket is passed
196
if options.AzureContainerName != "" && options.UpdateTemplates && !options.AzureTemplateDisableDownload {
197
missing := validateMissingAzureOptions(options)
198
if missing != nil {
199
return fmt.Errorf("azure connection details are missing. Please provide %s", strings.Join(missing, ","))
200
}
201
}
202
203
// Verify that all GitLab options are provided if the GitLab server or token is provided
204
if len(options.GitLabTemplateRepositoryIDs) != 0 && options.UpdateTemplates && !options.GitLabTemplateDisableDownload {
205
missing := validateMissingGitLabOptions(options)
206
if missing != nil {
207
return fmt.Errorf("gitlab server details are missing. Please provide %s", strings.Join(missing, ","))
208
}
209
}
210
211
// verify that a valid ip version type was selected (4, 6)
212
if len(options.IPVersion) == 0 {
213
// add ipv4 as default
214
options.IPVersion = append(options.IPVersion, "4")
215
}
216
var useIPV4, useIPV6 bool
217
for _, ipv := range options.IPVersion {
218
switch ipv {
219
case "4":
220
useIPV4 = true
221
case "6":
222
useIPV6 = true
223
default:
224
return fmt.Errorf("unsupported ip version: %s", ipv)
225
}
226
}
227
if !useIPV4 && !useIPV6 {
228
return errors.New("ipv4 and/or ipv6 must be selected")
229
}
230
return nil
231
}
232
233
func validateMissingS3Options(options *types.Options) []string {
234
var missing []string
235
if options.AwsBucketName == "" {
236
missing = append(missing, "AWS_TEMPLATE_BUCKET")
237
}
238
if options.AwsProfile == "" {
239
var missingCreds []string
240
if options.AwsAccessKey == "" {
241
missingCreds = append(missingCreds, "AWS_ACCESS_KEY")
242
}
243
if options.AwsSecretKey == "" {
244
missingCreds = append(missingCreds, "AWS_SECRET_KEY")
245
}
246
if options.AwsRegion == "" {
247
missingCreds = append(missingCreds, "AWS_REGION")
248
}
249
250
missing = append(missing, missingCreds...)
251
252
if len(missingCreds) > 0 {
253
missing = append(missing, "AWS_PROFILE")
254
}
255
}
256
257
return missing
258
}
259
260
func validateMissingAzureOptions(options *types.Options) []string {
261
var missing []string
262
if options.AzureTenantID == "" {
263
missing = append(missing, "AZURE_TENANT_ID")
264
}
265
if options.AzureClientID == "" {
266
missing = append(missing, "AZURE_CLIENT_ID")
267
}
268
if options.AzureClientSecret == "" {
269
missing = append(missing, "AZURE_CLIENT_SECRET")
270
}
271
if options.AzureServiceURL == "" {
272
missing = append(missing, "AZURE_SERVICE_URL")
273
}
274
if options.AzureContainerName == "" {
275
missing = append(missing, "AZURE_CONTAINER_NAME")
276
}
277
return missing
278
}
279
280
func validateMissingGitLabOptions(options *types.Options) []string {
281
var missing []string
282
if options.GitLabToken == "" {
283
missing = append(missing, "GITLAB_TOKEN")
284
}
285
if len(options.GitLabTemplateRepositoryIDs) == 0 {
286
missing = append(missing, "GITLAB_REPOSITORY_IDS")
287
}
288
289
return missing
290
}
291
292
func validateDASTOptions(options *types.Options) error {
293
// Ensure the DAST server token meets minimum length requirement
294
if len(options.DASTServerToken) > 0 && len(options.DASTServerToken) < 16 {
295
return fmt.Errorf("DAST server token must be at least 16 characters long")
296
}
297
return nil
298
}
299
300
func createReportingOptions(options *types.Options) (*reporting.Options, error) {
301
var reportingOptions = &reporting.Options{}
302
if options.ReportingConfig != "" {
303
file, err := os.Open(options.ReportingConfig)
304
if err != nil {
305
return nil, errors.Wrap(err, "could not open reporting config file")
306
}
307
defer func() {
308
_ = file.Close()
309
}()
310
311
if err := yaml.DecodeAndValidate(file, reportingOptions); err != nil {
312
return nil, errors.Wrap(err, "could not parse reporting config file")
313
}
314
Walk(reportingOptions, expandEndVars)
315
}
316
if options.MarkdownExportDirectory != "" {
317
reportingOptions.MarkdownExporter = &markdown.Options{
318
Directory: options.MarkdownExportDirectory,
319
OmitRaw: options.OmitRawRequests,
320
SortMode: options.MarkdownExportSortMode,
321
}
322
}
323
if options.SarifExport != "" {
324
reportingOptions.SarifExporter = &sarif.Options{File: options.SarifExport}
325
}
326
if options.JSONExport != "" {
327
reportingOptions.JSONExporter = &jsonexporter.Options{
328
File: options.JSONExport,
329
OmitRaw: options.OmitRawRequests,
330
}
331
}
332
// Combine options.
333
if options.JSONLExport != "" {
334
// Combine the CLI options with the config file options with the CLI options taking precedence
335
if reportingOptions.JSONLExporter != nil {
336
reportingOptions.JSONLExporter.File = options.JSONLExport
337
reportingOptions.JSONLExporter.OmitRaw = options.OmitRawRequests
338
} else {
339
reportingOptions.JSONLExporter = &jsonl.Options{
340
File: options.JSONLExport,
341
OmitRaw: options.OmitRawRequests,
342
}
343
}
344
}
345
346
reportingOptions.OmitRaw = options.OmitRawRequests
347
reportingOptions.ExecutionId = options.ExecutionId
348
return reportingOptions, nil
349
}
350
351
// configureOutput configures the output logging levels to be displayed on the screen
352
func configureOutput(options *types.Options) {
353
if options.NoColor {
354
options.Logger.SetFormatter(formatter.NewCLI(true))
355
}
356
// If the user desires verbose output, show verbose output
357
if options.Debug || options.DebugRequests || options.DebugResponse {
358
options.Logger.SetMaxLevel(levels.LevelDebug)
359
}
360
// Debug takes precedence before verbose
361
// because debug is a lower logging level.
362
if options.Verbose || options.Validate {
363
options.Logger.SetMaxLevel(levels.LevelVerbose)
364
}
365
if options.NoColor {
366
options.Logger.SetFormatter(formatter.NewCLI(true))
367
}
368
if options.Silent {
369
options.Logger.SetMaxLevel(levels.LevelSilent)
370
}
371
372
// disable standard logger (ref: https://github.com/golang/go/issues/19895)
373
// logutil.DisableDefaultLogger()
374
}
375
376
// loadResolvers loads resolvers from both user-provided flags and file
377
func loadResolvers(options *types.Options) {
378
if options.ResolversFile == "" {
379
return
380
}
381
382
file, err := os.Open(options.ResolversFile)
383
if err != nil {
384
options.Logger.Fatal().Msgf("Could not open resolvers file: %s\n", err)
385
}
386
defer func() {
387
_ = file.Close()
388
}()
389
390
scanner := bufio.NewScanner(file)
391
for scanner.Scan() {
392
part := scanner.Text()
393
if part == "" {
394
continue
395
}
396
if strings.Contains(part, ":") {
397
options.InternalResolversList = append(options.InternalResolversList, part)
398
} else {
399
options.InternalResolversList = append(options.InternalResolversList, part+":53")
400
}
401
}
402
}
403
404
func validateTemplatePaths(logger *gologger.Logger, templatesDirectory string, templatePaths, workflowPaths []string) {
405
allGivenTemplatePaths := append(templatePaths, workflowPaths...)
406
for _, templatePath := range allGivenTemplatePaths {
407
if templatesDirectory != templatePath && filepath.IsAbs(templatePath) {
408
fileInfo, err := os.Stat(templatePath)
409
if err == nil && fileInfo.IsDir() {
410
relativizedPath, err2 := filepath.Rel(templatesDirectory, templatePath)
411
if err2 != nil || (len(relativizedPath) >= 2 && relativizedPath[:2] == "..") {
412
logger.Warning().Msgf("The given path (%s) is outside the default template directory path (%s)! "+
413
"Referenced sub-templates with relative paths in workflows will be resolved against the default template directory.", templatePath, templatesDirectory)
414
break
415
}
416
}
417
}
418
}
419
}
420
421
func validateCertificatePaths(logger *gologger.Logger, certificatePaths ...string) {
422
for _, certificatePath := range certificatePaths {
423
if !fileutil.FileExists(certificatePath) {
424
// The provided path to the PEM certificate does not exist for the client authentication. As this is
425
// required for successful authentication, log and return an error
426
logger.Fatal().Msgf("The given path (%s) to the certificate does not exist!", certificatePath)
427
break
428
}
429
}
430
}
431
432
// Read the input from env and set options
433
func readEnvInputVars(options *types.Options) {
434
options.GitHubToken = os.Getenv("GITHUB_TOKEN")
435
repolist := os.Getenv("GITHUB_TEMPLATE_REPO")
436
if repolist != "" {
437
options.GitHubTemplateRepo = append(options.GitHubTemplateRepo, stringsutil.SplitAny(repolist, ",")...)
438
}
439
440
// GitLab options for downloading templates from a repository
441
options.GitLabServerURL = os.Getenv("GITLAB_SERVER_URL")
442
if options.GitLabServerURL == "" {
443
options.GitLabServerURL = "https://gitlab.com"
444
}
445
options.GitLabToken = os.Getenv("GITLAB_TOKEN")
446
repolist = os.Getenv("GITLAB_REPOSITORY_IDS")
447
// Convert the comma separated list of repository IDs to a list of integers
448
if repolist != "" {
449
for _, repoID := range stringsutil.SplitAny(repolist, ",") {
450
// Attempt to convert the repo ID to an integer
451
repoIDInt, err := strconv.Atoi(repoID)
452
if err != nil {
453
options.Logger.Warning().Msgf("Invalid GitLab template repository ID: %s", repoID)
454
continue
455
}
456
457
// Add the int repository ID to the list
458
options.GitLabTemplateRepositoryIDs = append(options.GitLabTemplateRepositoryIDs, repoIDInt)
459
}
460
}
461
462
// AWS options for downloading templates from an S3 bucket
463
options.AwsAccessKey = os.Getenv("AWS_ACCESS_KEY")
464
options.AwsSecretKey = os.Getenv("AWS_SECRET_KEY")
465
options.AwsBucketName = os.Getenv("AWS_TEMPLATE_BUCKET")
466
options.AwsRegion = os.Getenv("AWS_REGION")
467
options.AwsProfile = os.Getenv("AWS_PROFILE")
468
469
// Azure options for downloading templates from an Azure Blob Storage container
470
options.AzureContainerName = os.Getenv("AZURE_CONTAINER_NAME")
471
options.AzureTenantID = os.Getenv("AZURE_TENANT_ID")
472
options.AzureClientID = os.Getenv("AZURE_CLIENT_ID")
473
options.AzureClientSecret = os.Getenv("AZURE_CLIENT_SECRET")
474
options.AzureServiceURL = os.Getenv("AZURE_SERVICE_URL")
475
476
// Custom public keys for template verification
477
options.CodeTemplateSignaturePublicKey = os.Getenv("NUCLEI_SIGNATURE_PUBLIC_KEY")
478
options.CodeTemplateSignatureAlgorithm = os.Getenv("NUCLEI_SIGNATURE_ALGORITHM")
479
480
// General options to disable the template download locations from being used.
481
// This will override the default behavior of downloading templates from the default locations as well as the
482
// custom locations.
483
// The primary use-case is when the user wants to use custom templates only and does not want to download any
484
// templates from the default locations or is unable to connect to the public internet.
485
options.PublicTemplateDisableDownload = getBoolEnvValue("DISABLE_NUCLEI_TEMPLATES_PUBLIC_DOWNLOAD")
486
options.GitHubTemplateDisableDownload = getBoolEnvValue("DISABLE_NUCLEI_TEMPLATES_GITHUB_DOWNLOAD")
487
options.GitLabTemplateDisableDownload = getBoolEnvValue("DISABLE_NUCLEI_TEMPLATES_GITLAB_DOWNLOAD")
488
options.AwsTemplateDisableDownload = getBoolEnvValue("DISABLE_NUCLEI_TEMPLATES_AWS_DOWNLOAD")
489
options.AzureTemplateDisableDownload = getBoolEnvValue("DISABLE_NUCLEI_TEMPLATES_AZURE_DOWNLOAD")
490
491
// Options to modify the behavior of exporters
492
options.MarkdownExportSortMode = strings.ToLower(os.Getenv("MARKDOWN_EXPORT_SORT_MODE"))
493
// If the user has not specified a valid sort mode, use the default
494
if options.MarkdownExportSortMode != "template" && options.MarkdownExportSortMode != "severity" && options.MarkdownExportSortMode != "host" {
495
options.MarkdownExportSortMode = ""
496
}
497
}
498
499
func getBoolEnvValue(key string) bool {
500
value := os.Getenv(key)
501
return strings.EqualFold(value, "true")
502
}
503
504