git.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. package sources
  2. import (
  3. "bufio"
  4. "errors"
  5. "io"
  6. "os/exec"
  7. "path/filepath"
  8. "regexp"
  9. "strings"
  10. "github.com/gitleaks/go-gitdiff/gitdiff"
  11. "github.com/rs/zerolog/log"
  12. )
  13. var quotedOptPattern = regexp.MustCompile(`^(?:"[^"]+"|'[^']+')$`)
  14. // GitCmd helps to work with Git's output.
  15. type GitCmd struct {
  16. cmd *exec.Cmd
  17. diffFilesCh <-chan *gitdiff.File
  18. errCh <-chan error
  19. }
  20. // NewGitLogCmd returns `*DiffFilesCmd` with two channels: `<-chan *gitdiff.File` and `<-chan error`.
  21. // Caller should read everything from channels until receiving a signal about their closure and call
  22. // the `func (*DiffFilesCmd) Wait()` error in order to release resources.
  23. func NewGitLogCmd(source string, logOpts string) (*GitCmd, error) {
  24. sourceClean := filepath.Clean(source)
  25. var cmd *exec.Cmd
  26. if logOpts != "" {
  27. args := []string{"-C", sourceClean, "log", "-p", "-U0"}
  28. // Ensure that the user-provided |logOpts| aren't wrapped in quotes.
  29. // https://github.com/gitleaks/gitleaks/issues/1153
  30. userArgs := strings.Split(logOpts, " ")
  31. var quotedOpts []string
  32. for _, element := range userArgs {
  33. if quotedOptPattern.MatchString(element) {
  34. quotedOpts = append(quotedOpts, element)
  35. }
  36. }
  37. if len(quotedOpts) > 0 {
  38. log.Warn().Msgf("the following `--log-opts` values may not work as expected: %v\n\tsee https://github.com/gitleaks/gitleaks/issues/1153 for more information", quotedOpts)
  39. }
  40. args = append(args, userArgs...)
  41. cmd = exec.Command("git", args...)
  42. } else {
  43. cmd = exec.Command("git", "-C", sourceClean, "log", "-p", "-U0",
  44. "--full-history", "--all")
  45. }
  46. log.Debug().Msgf("executing: %s", cmd.String())
  47. stdout, err := cmd.StdoutPipe()
  48. if err != nil {
  49. return nil, err
  50. }
  51. stderr, err := cmd.StderrPipe()
  52. if err != nil {
  53. return nil, err
  54. }
  55. if err := cmd.Start(); err != nil {
  56. return nil, err
  57. }
  58. errCh := make(chan error)
  59. go listenForStdErr(stderr, errCh)
  60. gitdiffFiles, err := gitdiff.Parse(stdout)
  61. if err != nil {
  62. return nil, err
  63. }
  64. return &GitCmd{
  65. cmd: cmd,
  66. diffFilesCh: gitdiffFiles,
  67. errCh: errCh,
  68. }, nil
  69. }
  70. // NewGitDiffCmd returns `*DiffFilesCmd` with two channels: `<-chan *gitdiff.File` and `<-chan error`.
  71. // Caller should read everything from channels until receiving a signal about their closure and call
  72. // the `func (*DiffFilesCmd) Wait()` error in order to release resources.
  73. func NewGitDiffCmd(source string, staged bool) (*GitCmd, error) {
  74. sourceClean := filepath.Clean(source)
  75. var cmd *exec.Cmd
  76. cmd = exec.Command("git", "-C", sourceClean, "diff", "-U0", "--no-ext-diff", ".")
  77. if staged {
  78. cmd = exec.Command("git", "-C", sourceClean, "diff", "-U0", "--no-ext-diff",
  79. "--staged", ".")
  80. }
  81. log.Debug().Msgf("executing: %s", cmd.String())
  82. stdout, err := cmd.StdoutPipe()
  83. if err != nil {
  84. return nil, err
  85. }
  86. stderr, err := cmd.StderrPipe()
  87. if err != nil {
  88. return nil, err
  89. }
  90. if err := cmd.Start(); err != nil {
  91. return nil, err
  92. }
  93. errCh := make(chan error)
  94. go listenForStdErr(stderr, errCh)
  95. gitdiffFiles, err := gitdiff.Parse(stdout)
  96. if err != nil {
  97. return nil, err
  98. }
  99. return &GitCmd{
  100. cmd: cmd,
  101. diffFilesCh: gitdiffFiles,
  102. errCh: errCh,
  103. }, nil
  104. }
  105. // DiffFilesCh returns a channel with *gitdiff.File.
  106. func (c *GitCmd) DiffFilesCh() <-chan *gitdiff.File {
  107. return c.diffFilesCh
  108. }
  109. // ErrCh returns a channel that could produce an error if there is something in stderr.
  110. func (c *GitCmd) ErrCh() <-chan error {
  111. return c.errCh
  112. }
  113. // Wait waits for the command to exit and waits for any copying to
  114. // stdin or copying from stdout or stderr to complete.
  115. //
  116. // Wait also closes underlying stdout and stderr.
  117. func (c *GitCmd) Wait() (err error) {
  118. return c.cmd.Wait()
  119. }
  120. // listenForStdErr listens for stderr output from git, prints it to stdout,
  121. // sends to errCh and closes it.
  122. func listenForStdErr(stderr io.ReadCloser, errCh chan<- error) {
  123. defer close(errCh)
  124. var errEncountered bool
  125. scanner := bufio.NewScanner(stderr)
  126. for scanner.Scan() {
  127. // if git throws one of the following errors:
  128. //
  129. // exhaustive rename detection was skipped due to too many files.
  130. // you may want to set your diff.renameLimit variable to at least
  131. // (some large number) and retry the command.
  132. //
  133. // inexact rename detection was skipped due to too many files.
  134. // you may want to set your diff.renameLimit variable to at least
  135. // (some large number) and retry the command.
  136. //
  137. // we skip exiting the program as git log -p/git diff will continue
  138. // to send data to stdout and finish executing. This next bit of
  139. // code prevents gitleaks from stopping mid scan if this error is
  140. // encountered
  141. if strings.Contains(scanner.Text(),
  142. "exhaustive rename detection was skipped") ||
  143. strings.Contains(scanner.Text(),
  144. "inexact rename detection was skipped") ||
  145. strings.Contains(scanner.Text(),
  146. "you may want to set your diff.renameLimit") {
  147. log.Warn().Msg(scanner.Text())
  148. } else {
  149. log.Error().Msgf("[git] %s", scanner.Text())
  150. errEncountered = true
  151. }
  152. }
  153. if errEncountered {
  154. errCh <- errors.New("stderr is not empty")
  155. return
  156. }
  157. }