options.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "regexp"
  8. "strconv"
  9. "strings"
  10. )
  11. const usage = `
  12. usage: gitleaks [options] <URL>/<path_to_repo>
  13. Options:
  14. -u --user Git user mode
  15. -r --repo Git repo mode
  16. -o --org Git organization mode
  17. -l --local Local mode, gitleaks will look for local repo in <path>
  18. -v --verbose Verbose mode, will output leaks as gitleaks finds them
  19. --report-path=<STR> Report output, default $GITLEAKS_HOME/report
  20. --clone-path=<STR> Gitleaks will clone repos here, default $GITLEAKS_HOME/clones
  21. -t --temp Clone to temporary directory
  22. --concurrency=<INT> Upper bound on concurrent "git diff"
  23. --since=<STR> Commit to stop at
  24. --b64Entropy=<INT> Base64 entropy cutoff (default is 70)
  25. --hexEntropy=<INT> Hex entropy cutoff (default is 40)
  26. -e --entropy Enable entropy
  27. -h --help Display this message
  28. --token=<STR> Github API token
  29. --stopwords Enables stopwords
  30. `
  31. // Options for gitleaks
  32. type Options struct {
  33. URL string
  34. RepoPath string
  35. ReportPath string
  36. ClonePath string
  37. Concurrency int
  38. B64EntropyCutoff int
  39. HexEntropyCutoff int
  40. UserMode bool
  41. OrgMode bool
  42. RepoMode bool
  43. LocalMode bool
  44. Strict bool
  45. Entropy bool
  46. SinceCommit string
  47. Tmp bool
  48. Token string
  49. Verbose bool
  50. RegexFile string
  51. }
  52. // help prints the usage string and exits
  53. func help() {
  54. os.Stderr.WriteString(usage)
  55. }
  56. // optionsNextInt is a parseOptions helper that returns the value (int) of an option if valid
  57. func (opts *Options) nextInt(args []string, i *int) int {
  58. if len(args) > *i+1 {
  59. *i++
  60. } else {
  61. help()
  62. }
  63. argInt, err := strconv.Atoi(args[*i])
  64. if err != nil {
  65. opts.failF("Invalid %s option: %s\n", args[*i-1], args[*i])
  66. }
  67. return argInt
  68. }
  69. // optionsNextString is a parseOptions helper that returns the value (string) of an option if valid
  70. func (opts *Options) nextString(args []string, i *int) string {
  71. if len(args) > *i+1 {
  72. *i++
  73. } else {
  74. opts.failF("Invalid %s option: %s\n", args[*i-1], args[*i])
  75. }
  76. return args[*i]
  77. }
  78. // optInt grabs the string ...
  79. func (opts *Options) optString(arg string, prefixes ...string) (bool, string) {
  80. for _, prefix := range prefixes {
  81. if strings.HasPrefix(arg, prefix) {
  82. return true, arg[len(prefix):]
  83. }
  84. }
  85. return false, ""
  86. }
  87. // optInt grabs the int ...
  88. func (opts *Options) optInt(arg string, prefixes ...string) (bool, int) {
  89. for _, prefix := range prefixes {
  90. if strings.HasPrefix(arg, prefix) {
  91. i, err := strconv.Atoi(arg[len(prefix):])
  92. if err != nil {
  93. opts.failF("Invalid %s int option\n", prefix)
  94. }
  95. return true, i
  96. }
  97. }
  98. return false, 0
  99. }
  100. // newOpts generates opts and parses arguments
  101. func newOpts(args []string) *Options {
  102. opts, err := defaultOptions()
  103. if err != nil {
  104. opts.failF("%v", err)
  105. }
  106. err = opts.parseOptions(args)
  107. if err != nil {
  108. opts.failF("%v", err)
  109. }
  110. return opts
  111. }
  112. // deafultOptions provides the default options used by newOpts
  113. func defaultOptions() (*Options, error) {
  114. return &Options{
  115. Concurrency: 10,
  116. B64EntropyCutoff: 70,
  117. HexEntropyCutoff: 40,
  118. }, nil
  119. }
  120. // parseOptions will parse options supplied by the user.
  121. func (opts *Options) parseOptions(args []string) error {
  122. if len(args) == 0 {
  123. opts.LocalMode = true
  124. opts.RepoPath, _ = os.Getwd()
  125. }
  126. for i := 0; i < len(args); i++ {
  127. arg := args[i]
  128. switch arg {
  129. case "--stopwords":
  130. opts.Strict = true
  131. case "-e", "--entropy":
  132. opts.Entropy = true
  133. case "-o", "--org":
  134. opts.OrgMode = true
  135. case "-u", "--user":
  136. opts.UserMode = true
  137. case "-r", "--repo":
  138. opts.RepoMode = true
  139. case "-l", "--local":
  140. opts.LocalMode = true
  141. case "-v", "--verbose":
  142. opts.Verbose = true
  143. case "-t", "--temp":
  144. opts.Tmp = true
  145. case "-h", "--help":
  146. help()
  147. os.Exit(ExitClean)
  148. default:
  149. if match, value := opts.optString(arg, "--token="); match {
  150. opts.Token = value
  151. } else if match, value := opts.optString(arg, "--since="); match {
  152. opts.SinceCommit = value
  153. } else if match, value := opts.optString(arg, "--report-path="); match {
  154. opts.ReportPath = value
  155. } else if match, value := opts.optString(arg, "--clone-path="); match {
  156. opts.ClonePath = value
  157. } else if match, value := opts.optInt(arg, "--b64Entropy="); match {
  158. opts.B64EntropyCutoff = value
  159. } else if match, value := opts.optInt(arg, "--hexEntropy="); match {
  160. opts.HexEntropyCutoff = value
  161. } else if match, value := opts.optInt(arg, "--concurrency="); match {
  162. opts.Concurrency = value
  163. } else if match, value := opts.optString(arg, "--regex-file="); match {
  164. opts.RegexFile = value
  165. } else if i == len(args)-1 {
  166. if opts.LocalMode {
  167. opts.RepoPath = filepath.Clean(args[i])
  168. } else {
  169. if isGithubTarget(args[i]) {
  170. opts.URL = args[i]
  171. } else {
  172. help()
  173. return fmt.Errorf("Unknown option %s\n", arg)
  174. }
  175. }
  176. } else {
  177. help()
  178. return fmt.Errorf("Unknown option %s\n", arg)
  179. }
  180. }
  181. }
  182. if opts.RegexFile != "" {
  183. err := opts.loadExternalRegex()
  184. if err != nil {
  185. return fmt.Errorf("unable to load regex from file %s: %v",
  186. opts.RegexFile, err)
  187. }
  188. }
  189. if !opts.RepoMode && !opts.UserMode && !opts.OrgMode && !opts.LocalMode {
  190. if opts.URL != "" {
  191. opts.RepoMode = true
  192. err := opts.guards()
  193. if err != nil {
  194. return err
  195. }
  196. return nil
  197. }
  198. pwd, _ = os.Getwd()
  199. // check if pwd contains a .git, if it does, run local mode
  200. dotGitPath := filepath.Join(pwd, ".git")
  201. if _, err := os.Stat(dotGitPath); os.IsNotExist(err) {
  202. return fmt.Errorf("gitleaks has no target: %v", err)
  203. } else {
  204. opts.LocalMode = true
  205. opts.RepoPath = pwd
  206. opts.RepoMode = false
  207. }
  208. }
  209. err := opts.guards()
  210. if err != nil {
  211. return err
  212. }
  213. return err
  214. }
  215. // loadExternalRegex loads regexes from a text file if available.
  216. func (opts *Options) loadExternalRegex() error {
  217. file, err := os.Open(opts.RegexFile)
  218. if err != nil {
  219. return err
  220. }
  221. defer file.Close()
  222. scanner := bufio.NewScanner(file)
  223. for scanner.Scan() {
  224. externalRegex = append(externalRegex, regexp.MustCompile(scanner.Text()))
  225. }
  226. return nil
  227. }
  228. // failF prints a failure message out to stderr, displays help
  229. // and exits with a exit code 2
  230. func (opts *Options) failF(format string, args ...interface{}) {
  231. fmt.Fprintf(os.Stderr, format, args...)
  232. help()
  233. os.Exit(ExitFailure)
  234. }
  235. // guards will prevent gitleaks from continuing if any invalid options
  236. // are found.
  237. func (opts *Options) guards() error {
  238. if (opts.RepoMode || opts.OrgMode || opts.UserMode) && opts.LocalMode {
  239. return fmt.Errorf("Cannot run Gitleaks on repo/user/org mode and local mode\n")
  240. } else if (opts.RepoMode || opts.OrgMode || opts.UserMode) && !isGithubTarget(opts.URL) {
  241. return fmt.Errorf("Not valid github target %s\n", opts.URL)
  242. } else if (opts.RepoMode || opts.UserMode) && opts.OrgMode {
  243. return fmt.Errorf("Cannot run Gitleaks on more than one mode\n")
  244. } else if (opts.OrgMode || opts.UserMode) && opts.RepoMode {
  245. return fmt.Errorf("Cannot run Gitleaks on more than one mode\n")
  246. } else if (opts.OrgMode || opts.RepoMode) && opts.UserMode {
  247. return fmt.Errorf("Cannot run Gitleaks on more than one mode\n")
  248. } else if opts.LocalMode && opts.Tmp {
  249. return fmt.Errorf("Cannot run Gitleaks with temp settings and local mode\n")
  250. } else if opts.SinceCommit != "" && (opts.OrgMode || opts.UserMode) {
  251. return fmt.Errorf("Cannot run Gitleaks with since commit flag and a owner mode\n")
  252. } else if opts.ClonePath != "" && opts.Tmp {
  253. return fmt.Errorf("Cannot run Gitleaks with --clone-path set and temporary repo\n")
  254. }
  255. return nil
  256. }
  257. // isGithubTarget checks if url is a valid github target
  258. func isGithubTarget(url string) bool {
  259. re := regexp.MustCompile("github.com")
  260. return re.MatchString(url)
  261. }