utils.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. package scan
  2. import (
  3. "bufio"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "runtime"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "github.com/zricethezav/gitleaks/v7/options"
  12. "github.com/go-git/go-git/v5"
  13. "github.com/go-git/go-git/v5/plumbing"
  14. "github.com/go-git/go-git/v5/plumbing/object"
  15. "github.com/go-git/go-git/v5/storage/memory"
  16. log "github.com/sirupsen/logrus"
  17. )
  18. const (
  19. diffAddPrefix = "+"
  20. diffDelPrefix = "-"
  21. diffLineSignature = " @@"
  22. defaultLineNumber = 1
  23. )
  24. func obtainCommit(repo *git.Repository, commitSha string) (*object.Commit, error) {
  25. if commitSha == "latest" {
  26. ref, err := repo.Head()
  27. if err != nil {
  28. return nil, err
  29. }
  30. commitSha = ref.Hash().String()
  31. }
  32. return repo.CommitObject(plumbing.NewHash(commitSha))
  33. }
  34. func getRepoName(opts options.Options) string {
  35. if opts.RepoURL != "" {
  36. return filepath.Base(opts.RepoURL)
  37. }
  38. if opts.Path != "" {
  39. return filepath.Base(opts.Path)
  40. }
  41. if opts.CheckUncommitted() {
  42. dir, _ := os.Getwd()
  43. return filepath.Base(dir)
  44. }
  45. return ""
  46. }
  47. func getRepo(opts options.Options) (*git.Repository, error) {
  48. if opts.OpenLocal() {
  49. if opts.Path != "" {
  50. log.Infof("opening %s\n", opts.Path)
  51. } else {
  52. log.Info("opening .")
  53. }
  54. return git.PlainOpen(opts.Path)
  55. }
  56. if opts.CheckUncommitted() {
  57. // open git repo from PWD
  58. dir, err := os.Getwd()
  59. if err != nil {
  60. return nil, err
  61. }
  62. log.Debugf("opening %s as a repo\n", dir)
  63. return git.PlainOpen(dir)
  64. }
  65. return cloneRepo(opts)
  66. }
  67. func cloneRepo(opts options.Options) (*git.Repository, error) {
  68. cloneOpts, err := opts.CloneOptions()
  69. if err != nil {
  70. return nil, err
  71. }
  72. if opts.ClonePath != "" {
  73. log.Infof("cloning... %s to %s", cloneOpts.URL, opts.ClonePath)
  74. return git.PlainClone(opts.ClonePath, false, cloneOpts)
  75. }
  76. log.Infof("cloning... %s", cloneOpts.URL)
  77. return git.Clone(memory.NewStorage(), nil, cloneOpts)
  78. }
  79. // depthReached checks if i meets the depth (--depth=) if set
  80. func depthReached(i int, opts options.Options) bool {
  81. if opts.Depth != 0 && opts.Depth == i {
  82. log.Warnf("Exceeded depth limit (%d)", i)
  83. return true
  84. }
  85. return false
  86. }
  87. // emptyCommit generates an empty commit used for scanning uncommitted changes
  88. func emptyCommit() *object.Commit {
  89. return &object.Commit{
  90. Hash: plumbing.Hash{},
  91. Message: "",
  92. Author: object.Signature{
  93. Name: "",
  94. Email: "",
  95. When: time.Unix(0, 0).UTC(),
  96. },
  97. }
  98. }
  99. // howManyThreads will return a number 1-GOMAXPROCS which is the number
  100. // of goroutines that will spawn during gitleaks execution
  101. func howManyThreads(threads int) int {
  102. maxThreads := runtime.GOMAXPROCS(0)
  103. if threads == 0 {
  104. return 1
  105. } else if threads > maxThreads {
  106. log.Warnf("%d threads set too high, setting to system max, %d", threads, maxThreads)
  107. return maxThreads
  108. }
  109. return threads
  110. }
  111. // getLogOptions determines what log options are used when iterating through commits.
  112. // It is similar to `git log {branch}`. Default behavior is to log ALL branches so
  113. // gitleaks gets the full git history.
  114. func logOptions(repo *git.Repository, opts options.Options) (*git.LogOptions, error) {
  115. var logOpts git.LogOptions
  116. const dateformat string = "2006-01-02"
  117. const timeformat string = "2006-01-02T15:04:05-0700"
  118. if opts.CommitFrom != "" {
  119. logOpts.From = plumbing.NewHash(opts.CommitFrom)
  120. }
  121. if opts.CommitSince != "" {
  122. if t, err := time.Parse(timeformat, opts.CommitSince); err == nil {
  123. logOpts.Since = &t
  124. } else if t, err := time.Parse(dateformat, opts.CommitSince); err == nil {
  125. logOpts.Since = &t
  126. } else {
  127. return nil, err
  128. }
  129. logOpts.All = true
  130. }
  131. if opts.CommitUntil != "" {
  132. if t, err := time.Parse(timeformat, opts.CommitUntil); err == nil {
  133. logOpts.Until = &t
  134. } else if t, err := time.Parse(dateformat, opts.CommitUntil); err == nil {
  135. logOpts.Until = &t
  136. } else {
  137. return nil, err
  138. }
  139. logOpts.All = true
  140. }
  141. if opts.Branch != "" {
  142. ref, err := repo.Storer.Reference(plumbing.NewBranchReferenceName(opts.Branch))
  143. if err != nil {
  144. return nil, fmt.Errorf("could not find branch %s", opts.Branch)
  145. }
  146. logOpts = git.LogOptions{
  147. From: ref.Hash(),
  148. }
  149. if logOpts.From.IsZero() {
  150. return nil, fmt.Errorf("could not find branch %s", opts.Branch)
  151. }
  152. return &logOpts, nil
  153. }
  154. if !logOpts.From.IsZero() || logOpts.Since != nil || logOpts.Until != nil {
  155. return &logOpts, nil
  156. }
  157. return &git.LogOptions{All: true}, nil
  158. }
  159. func optsToCommits(opts options.Options) ([]string, error) {
  160. if opts.Commits != "" {
  161. return strings.Split(opts.Commits, ","), nil
  162. }
  163. file, err := os.Open(opts.CommitsFile)
  164. if err != nil {
  165. return []string{}, err
  166. }
  167. defer rable(file.Close)
  168. scanner := bufio.NewScanner(file)
  169. var commits []string
  170. for scanner.Scan() {
  171. commits = append(commits, scanner.Text())
  172. }
  173. return commits, nil
  174. }
  175. func extractLine(patchContent string, leak Leak, lineLookup map[string]bool) int {
  176. i := strings.Index(patchContent, fmt.Sprintf("\n+++ b/%s", leak.File))
  177. filePatchContent := patchContent[i+1:]
  178. i = strings.Index(filePatchContent, "diff --git")
  179. if i != -1 {
  180. filePatchContent = filePatchContent[:i]
  181. }
  182. chunkStartLine := 0
  183. currLine := 0
  184. for _, patchLine := range strings.Split(filePatchContent, "\n") {
  185. if strings.HasPrefix(patchLine, "@@") {
  186. i := strings.Index(patchLine, diffAddPrefix)
  187. pairs := strings.Split(strings.Split(patchLine[i+1:], diffLineSignature)[0], ",")
  188. chunkStartLine, _ = strconv.Atoi(pairs[0])
  189. currLine = -1
  190. }
  191. if strings.HasPrefix(patchLine, diffDelPrefix) {
  192. currLine--
  193. }
  194. if strings.HasPrefix(patchLine, diffAddPrefix) && strings.Contains(patchLine, leak.Line) {
  195. lineNumber := chunkStartLine + currLine
  196. if _, ok := lineLookup[fmt.Sprintf("%s%s%d%s", leak.Offender, leak.Line, lineNumber, leak.File)]; !ok {
  197. lineLookup[fmt.Sprintf("%s%s%d%s", leak.Offender, leak.Line, lineNumber, leak.File)] = true
  198. return lineNumber
  199. }
  200. }
  201. currLine++
  202. }
  203. return defaultLineNumber
  204. }
  205. // rable is the second half of deferrable... mainly used for defer file.Close()
  206. func rable(f func() error) {
  207. if err := f(); err != nil {
  208. log.Error(err)
  209. }
  210. }