|
|
@@ -3,53 +3,64 @@ package main
|
|
|
import (
|
|
|
"fmt"
|
|
|
"os"
|
|
|
+ "path/filepath"
|
|
|
+ "regexp"
|
|
|
"strconv"
|
|
|
+ "strings"
|
|
|
)
|
|
|
|
|
|
-const usage = `usage: gitleaks [options] <url>
|
|
|
+const usage = `
|
|
|
+usage: gitleaks [options] <URL>/<path_to_repo>
|
|
|
|
|
|
Options:
|
|
|
- -c --concurrency Upper bound on concurrent diffs
|
|
|
- -u --user Git user url
|
|
|
- -r --repo Git repo url
|
|
|
- -o --org Git organization url
|
|
|
- -s --since Commit to stop at
|
|
|
- -b --b64Entropy Base64 entropy cutoff (default is 70)
|
|
|
- -x --hexEntropy Hex entropy cutoff (default is 40)
|
|
|
+ -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 <path>
|
|
|
+ -v --verbose Verbose mode, will output leaks as gitleaks finds them
|
|
|
+ --report-path=<STR> Report output, default $GITLEAKS_HOME/report
|
|
|
+ --clone-path=<STR> Gitleaks will clone repos here, default $GITLEAKS_HOME/clones
|
|
|
+ -t --temp Clone to temporary directory
|
|
|
+ --concurrency=<INT> Upper bound on concurrent diffs
|
|
|
+ --since=<STR> Commit to stop at
|
|
|
+ --b64Entropy=<INT> Base64 entropy cutoff (default is 70)
|
|
|
+ --hexEntropy=<INT> Hex entropy cutoff (default is 40)
|
|
|
-e --entropy Enable entropy
|
|
|
- -j --json Output gitleaks report
|
|
|
-h --help Display this message
|
|
|
- --token Github API token
|
|
|
- --strict Enables stopwords
|
|
|
+ --token=<STR> Github API token
|
|
|
+ --stopwords Enables stopwords
|
|
|
+
|
|
|
`
|
|
|
|
|
|
-// Options for gitleaks
|
|
|
+// 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
|
|
|
- UserURL string
|
|
|
- OrgURL string
|
|
|
- RepoURL string
|
|
|
- Strict bool
|
|
|
- Entropy bool
|
|
|
- SinceCommit string
|
|
|
- Persist bool
|
|
|
- IncludeForks bool
|
|
|
- Tmp bool
|
|
|
- EnableJSON bool
|
|
|
- Token string
|
|
|
- Verbose bool
|
|
|
+ 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)
|
|
|
- os.Exit(1)
|
|
|
}
|
|
|
|
|
|
// optionsNextInt is a parseOptions helper that returns the value (int) of an option if valid
|
|
|
-func optionsNextInt(args []string, i *int) int {
|
|
|
+func (opts *Options) nextInt(args []string, i *int) int {
|
|
|
if len(args) > *i+1 {
|
|
|
*i++
|
|
|
} else {
|
|
|
@@ -57,80 +68,196 @@ func optionsNextInt(args []string, i *int) int {
|
|
|
}
|
|
|
argInt, err := strconv.Atoi(args[*i])
|
|
|
if err != nil {
|
|
|
- fmt.Printf("Invalid %s option: %s\n", args[*i-1], args[*i])
|
|
|
- help()
|
|
|
+ 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 optionsNextString(args []string, i *int) string {
|
|
|
+func (opts *Options) nextString(args []string, i *int) string {
|
|
|
if len(args) > *i+1 {
|
|
|
*i++
|
|
|
} else {
|
|
|
- fmt.Printf("Invalid %s option: %s\n", args[*i-1], args[*i])
|
|
|
- help()
|
|
|
+ opts.failF("Invalid %s option: %s\n", args[*i-1], args[*i])
|
|
|
}
|
|
|
return args[*i]
|
|
|
}
|
|
|
|
|
|
-// parseOptions
|
|
|
-func parseOptions(args []string) *Options {
|
|
|
- opts := &Options{
|
|
|
+// 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 {
|
|
|
- help()
|
|
|
+ opts.LocalMode = true
|
|
|
+ opts.RepoPath, _ = os.Getwd()
|
|
|
}
|
|
|
|
|
|
for i := 0; i < len(args); i++ {
|
|
|
arg := args[i]
|
|
|
switch arg {
|
|
|
- case "-s", "--since":
|
|
|
- opts.SinceCommit = optionsNextString(args, &i)
|
|
|
- case "--strict":
|
|
|
+ case "--stopwords":
|
|
|
opts.Strict = true
|
|
|
- case "-b", "--b64Entropy":
|
|
|
- opts.B64EntropyCutoff = optionsNextInt(args, &i)
|
|
|
- case "-x", "--hexEntropy":
|
|
|
- opts.HexEntropyCutoff = optionsNextInt(args, &i)
|
|
|
case "-e", "--entropy":
|
|
|
opts.Entropy = true
|
|
|
- case "-c", "--concurrency":
|
|
|
- opts.Concurrency = optionsNextInt(args, &i)
|
|
|
case "-o", "--org":
|
|
|
- opts.OrgURL = optionsNextString(args, &i)
|
|
|
+ opts.OrgMode = true
|
|
|
case "-u", "--user":
|
|
|
- opts.UserURL = optionsNextString(args, &i)
|
|
|
+ opts.UserMode = true
|
|
|
case "-r", "--repo":
|
|
|
- opts.RepoURL = optionsNextString(args, &i)
|
|
|
- case "-t", "--temporary":
|
|
|
+ opts.RepoMode = true
|
|
|
+ case "-l", "--local":
|
|
|
+ opts.LocalMode = true
|
|
|
+ case "-v", "--verbose":
|
|
|
+ opts.Verbose = true
|
|
|
+ case "-t", "--temp":
|
|
|
opts.Tmp = true
|
|
|
- case "--token":
|
|
|
- opts.Token = optionsNextString(args, &i)
|
|
|
- case "-j", "--json":
|
|
|
- opts.EnableJSON = true
|
|
|
case "-h", "--help":
|
|
|
help()
|
|
|
- return nil
|
|
|
+ os.Exit(ExitClean)
|
|
|
default:
|
|
|
- if i == len(args)-1 && opts.OrgURL == "" && opts.RepoURL == "" &&
|
|
|
- opts.UserURL == "" {
|
|
|
- opts.RepoURL = arg
|
|
|
+ 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 {
|
|
|
+ fmt.Println(args[i])
|
|
|
+ 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 {
|
|
|
- fmt.Printf("Unknown option %s\n\n", arg)
|
|
|
help()
|
|
|
+ return fmt.Errorf("Unknown option %s\n", arg)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // "guards"
|
|
|
- if opts.Tmp && opts.EnableJSON {
|
|
|
- fmt.Println("Report generation with temporary clones not supported")
|
|
|
+ // 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
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- return opts
|
|
|
+ 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)
|
|
|
}
|