options.go 7.5 KB

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