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