package main import ( "fmt" "os" "path/filepath" "regexp" "strconv" "strings" ) const usage = ` usage: gitleaks [options] / Options: -u --user Git user mode -r --repo Git repo mode -o --org Git organization mode -l --local Local mode, gitleaks will look for local repo in -v --verbose Verbose mode, will output leaks as gitleaks finds them --report-path= Report output, default $GITLEAKS_HOME/report --clone-path= Gitleaks will clone repos here, default $GITLEAKS_HOME/clones -t --temp Clone to temporary directory --concurrency= Upper bound on concurrent diffs --since= Commit to stop at --b64Entropy= Base64 entropy cutoff (default is 70) --hexEntropy= Hex entropy cutoff (default is 40) -e --entropy Enable entropy -h --help Display this message --token= Github API token --stopwords Enables stopwords ` // Options for gitleaks. need to support remote repo/owner // and local repo/owner mode type Options struct { URL string RepoPath string ReportPath string ClonePath string Concurrency int B64EntropyCutoff int HexEntropyCutoff int UserMode bool OrgMode bool RepoMode bool LocalMode bool Strict bool Entropy bool SinceCommit string Tmp bool Token string Verbose bool } // help prints the usage string and exits func help() { os.Stderr.WriteString(usage) } // optionsNextInt is a parseOptions helper that returns the value (int) of an option if valid func (opts *Options) nextInt(args []string, i *int) int { if len(args) > *i+1 { *i++ } else { help() } argInt, err := strconv.Atoi(args[*i]) if err != nil { opts.failF("Invalid %s option: %s\n", args[*i-1], args[*i]) } return argInt } // optionsNextString is a parseOptions helper that returns the value (string) of an option if valid func (opts *Options) nextString(args []string, i *int) string { if len(args) > *i+1 { *i++ } else { opts.failF("Invalid %s option: %s\n", args[*i-1], args[*i]) } return args[*i] } // optInt grabs the string ... func (opts *Options) optString(arg string, prefixes ...string) (bool, string) { for _, prefix := range prefixes { if strings.HasPrefix(arg, prefix) { return true, arg[len(prefix):] } } return false, "" } // optInt grabs the int ... func (opts *Options) optInt(arg string, prefixes ...string) (bool, int) { for _, prefix := range prefixes { if strings.HasPrefix(arg, prefix) { i, err := strconv.Atoi(arg[len(prefix):]) if err != nil { opts.failF("Invalid %s int option\n", prefix) } return true, i } } return false, 0 } // newOpts generates opts and parses arguments func newOpts(args []string) *Options { opts, err := defaultOptions() if err != nil { opts.failF("%v", err) } err = opts.parseOptions(args) if err != nil { opts.failF("%v", err) } return opts } // deafultOptions provides the default options func defaultOptions() (*Options, error) { return &Options{ Concurrency: 10, B64EntropyCutoff: 70, HexEntropyCutoff: 40, }, nil } // parseOptions func (opts *Options) parseOptions(args []string) error { if len(args) == 0 { opts.LocalMode = true opts.RepoPath, _ = os.Getwd() } for i := 0; i < len(args); i++ { arg := args[i] switch arg { case "--stopwords": opts.Strict = true case "-e", "--entropy": opts.Entropy = true case "-o", "--org": opts.OrgMode = true case "-u", "--user": opts.UserMode = true case "-r", "--repo": opts.RepoMode = true case "-l", "--local": opts.LocalMode = true case "-v", "--verbose": opts.Verbose = true case "-t", "--temp": opts.Tmp = true case "-h", "--help": help() os.Exit(ExitClean) default: if match, value := opts.optString(arg, "--token="); match { opts.Token = value } else if match, value := opts.optString(arg, "--since="); match { opts.SinceCommit = value } else if match, value := opts.optString(arg, "--report-path="); match { opts.ReportPath = value } else if match, value := opts.optString(arg, "--clone-path="); match { opts.ClonePath = value } else if match, value := opts.optInt(arg, "--b64Entropy="); match { opts.B64EntropyCutoff = value } else if match, value := opts.optInt(arg, "--hexEntropy="); match { opts.HexEntropyCutoff = value } else if match, value := opts.optInt(arg, "--concurrency="); match { opts.Concurrency = value } else if i == len(args)-1 { if opts.LocalMode { opts.RepoPath = filepath.Clean(args[i]) } else { if isGithubTarget(args[i]) { opts.URL = args[i] } else { help() return fmt.Errorf("Unknown option %s\n", arg) } } } else { help() return fmt.Errorf("Unknown option %s\n", arg) } } } // TODO cleanup this logic if !opts.RepoMode && !opts.UserMode && !opts.OrgMode && !opts.LocalMode { if opts.URL != "" { opts.RepoMode = true err := opts.guards() if err != nil{ return err } return nil } pwd, _ = os.Getwd() // check if pwd contains a .git, if it does, run local mode dotGitPath := filepath.Join(pwd, ".git") if _, err := os.Stat(dotGitPath); os.IsNotExist(err) { return fmt.Errorf("gitleaks has no target: %v", err) } else { opts.LocalMode = true opts.RepoPath = pwd opts.RepoMode = false } } err := opts.guards() if err != nil{ return err } return err } // failF prints a failure message out to stderr, displays help // and exits with a exit code 2 func (opts *Options) failF(format string, args ...interface{}) { fmt.Fprintf(os.Stderr, format, args...) help() os.Exit(ExitFailure) } // guards will prevent gitleaks from continuing if any invalid options // are found. func (opts *Options) guards() error { if (opts.RepoMode || opts.OrgMode || opts.UserMode) && opts.LocalMode { return fmt.Errorf("Cannot run Gitleaks on repo/user/org mode and local mode\n") } else if (opts.RepoMode || opts.OrgMode || opts.UserMode) && !isGithubTarget(opts.URL) { return fmt.Errorf("Not valid github target %s\n", opts.URL) } else if (opts.RepoMode || opts.UserMode) && opts.OrgMode { return fmt.Errorf("Cannot run Gitleaks on more than one mode\n") } else if (opts.OrgMode || opts.UserMode) && opts.RepoMode { return fmt.Errorf("Cannot run Gitleaks on more than one mode\n") } else if (opts.OrgMode || opts.RepoMode) && opts.UserMode { return fmt.Errorf("Cannot run Gitleaks on more than one mode\n") } else if opts.LocalMode && opts.Tmp { return fmt.Errorf("Cannot run Gitleaks with temp settings and local mode\n") } else if opts.SinceCommit != "" && (opts.OrgMode || opts.UserMode) { return fmt.Errorf("Cannot run Gitleaks with since commit flag and a owner mode\n") } else if opts.ClonePath != "" && opts.Tmp { return fmt.Errorf("Cannot run Gitleaks with --clone-path set and temporary repo\n") } return nil } // isGithubTarget checks if url is a valid github target func isGithubTarget(url string) bool { re := regexp.MustCompile("github.com") return re.MatchString(url) }