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  }