git.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. package sources
  2. import (
  3. "bufio"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "regexp"
  11. "strings"
  12. "github.com/gitleaks/go-gitdiff/gitdiff"
  13. "github.com/zricethezav/gitleaks/v8/logging"
  14. )
  15. var quotedOptPattern = regexp.MustCompile(`^(?:"[^"]+"|'[^']+')$`)
  16. // GitCmd helps to work with Git's output.
  17. type GitCmd struct {
  18. repoPath string
  19. cmd *exec.Cmd
  20. diffFilesCh <-chan *gitdiff.File
  21. errCh <-chan error
  22. }
  23. type GitInfo struct {
  24. Source string
  25. Commit string
  26. Link string
  27. Author string
  28. Email string
  29. Date string
  30. Message string
  31. }
  32. // NewGitLogCmd returns `*DiffFilesCmd` with two channels: `<-chan *gitdiff.File` and `<-chan error`.
  33. // Caller should read everything from channels until receiving a signal about their closure and call
  34. // the `func (*DiffFilesCmd) Wait()` error in order to release resources.
  35. func NewGitLogCmd(source string, logOpts string) (*GitCmd, error) {
  36. sourceClean := filepath.Clean(source)
  37. var cmd *exec.Cmd
  38. if logOpts != "" {
  39. args := []string{"-C", sourceClean, "log", "-p", "-U0"}
  40. // Ensure that the user-provided |logOpts| aren't wrapped in quotes.
  41. // https://github.com/gitleaks/gitleaks/issues/1153
  42. userArgs := strings.Split(logOpts, " ")
  43. var quotedOpts []string
  44. for _, element := range userArgs {
  45. if quotedOptPattern.MatchString(element) {
  46. quotedOpts = append(quotedOpts, element)
  47. }
  48. }
  49. if len(quotedOpts) > 0 {
  50. logging.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)
  51. }
  52. args = append(args, userArgs...)
  53. cmd = exec.Command("git", args...)
  54. } else {
  55. cmd = exec.Command("git", "-C", sourceClean, "log", "-p", "-U0",
  56. "--full-history", "--all")
  57. }
  58. logging.Debug().Msgf("executing: %s", cmd.String())
  59. stdout, err := cmd.StdoutPipe()
  60. if err != nil {
  61. return nil, err
  62. }
  63. stderr, err := cmd.StderrPipe()
  64. if err != nil {
  65. return nil, err
  66. }
  67. if err := cmd.Start(); err != nil {
  68. return nil, err
  69. }
  70. errCh := make(chan error)
  71. go listenForStdErr(stderr, errCh)
  72. gitdiffFiles, err := gitdiff.Parse(stdout)
  73. if err != nil {
  74. return nil, err
  75. }
  76. return &GitCmd{
  77. repoPath: sourceClean,
  78. cmd: cmd,
  79. diffFilesCh: gitdiffFiles,
  80. errCh: errCh,
  81. }, nil
  82. }
  83. // NewGitDiffCmd returns `*DiffFilesCmd` with two channels: `<-chan *gitdiff.File` and `<-chan error`.
  84. // Caller should read everything from channels until receiving a signal about their closure and call
  85. // the `func (*DiffFilesCmd) Wait()` error in order to release resources.
  86. func NewGitDiffCmd(source string, staged bool) (*GitCmd, error) {
  87. sourceClean := filepath.Clean(source)
  88. var cmd *exec.Cmd
  89. cmd = exec.Command("git", "-C", sourceClean, "diff", "-U0", "--no-ext-diff", ".")
  90. if staged {
  91. cmd = exec.Command("git", "-C", sourceClean, "diff", "-U0", "--no-ext-diff",
  92. "--staged", ".")
  93. }
  94. logging.Debug().Msgf("executing: %s", cmd.String())
  95. stdout, err := cmd.StdoutPipe()
  96. if err != nil {
  97. return nil, err
  98. }
  99. stderr, err := cmd.StderrPipe()
  100. if err != nil {
  101. return nil, err
  102. }
  103. if err := cmd.Start(); err != nil {
  104. return nil, err
  105. }
  106. errCh := make(chan error)
  107. go listenForStdErr(stderr, errCh)
  108. gitdiffFiles, err := gitdiff.Parse(stdout)
  109. if err != nil {
  110. return nil, err
  111. }
  112. return &GitCmd{
  113. repoPath: sourceClean,
  114. cmd: cmd,
  115. diffFilesCh: gitdiffFiles,
  116. errCh: errCh,
  117. }, nil
  118. }
  119. // CheckoutBlob writes the contents of the blob at commit:filepath into a temp file
  120. // and returns its path.
  121. func (g *GitCmd) CheckoutBlob(commit, filepathInRepo string) (string, error) {
  122. // Create a temp file with the same extension as the blob, if possible
  123. ext := filepath.Ext(filepathInRepo)
  124. tmpFile, err := os.CreateTemp("", "gitleaks-blob-*"+ext)
  125. if err != nil {
  126. return "", fmt.Errorf("creating temp file for blob: %w", err)
  127. }
  128. tmpFilePath := tmpFile.Name()
  129. tmpFile.Close()
  130. // git show <commit>:<path>
  131. gitArgs := []string{"-C", g.repoPath, "show", fmt.Sprintf("%s:%s", commit, filepathInRepo)}
  132. cmd := exec.Command("git", gitArgs...)
  133. cmd.Stdout, err = os.OpenFile(tmpFilePath, os.O_WRONLY, 0o644)
  134. if err != nil {
  135. os.Remove(tmpFilePath)
  136. return "", fmt.Errorf("opening temp file for write: %w", err)
  137. }
  138. if err := cmd.Run(); err != nil {
  139. os.Remove(tmpFilePath)
  140. return "", fmt.Errorf("git show failed: %w", err)
  141. }
  142. return tmpFilePath, nil
  143. }
  144. // DiffFilesCh returns a channel with *gitdiff.File.
  145. func (c *GitCmd) DiffFilesCh() <-chan *gitdiff.File {
  146. return c.diffFilesCh
  147. }
  148. // ErrCh returns a channel that could produce an error if there is something in stderr.
  149. func (c *GitCmd) ErrCh() <-chan error {
  150. return c.errCh
  151. }
  152. // Wait waits for the command to exit and waits for any copying to
  153. // stdin or copying from stdout or stderr to complete.
  154. //
  155. // Wait also closes underlying stdout and stderr.
  156. func (c *GitCmd) Wait() (err error) {
  157. return c.cmd.Wait()
  158. }
  159. // listenForStdErr listens for stderr output from git, prints it to stdout,
  160. // sends to errCh and closes it.
  161. func listenForStdErr(stderr io.ReadCloser, errCh chan<- error) {
  162. defer close(errCh)
  163. var errEncountered bool
  164. scanner := bufio.NewScanner(stderr)
  165. for scanner.Scan() {
  166. // if git throws one of the following errors:
  167. //
  168. // exhaustive rename detection was skipped due to too many files.
  169. // you may want to set your diff.renameLimit variable to at least
  170. // (some large number) and retry the command.
  171. //
  172. // inexact rename detection was skipped due to too many files.
  173. // you may want to set your diff.renameLimit variable to at least
  174. // (some large number) and retry the command.
  175. //
  176. // Auto packing the repository in background for optimum performance.
  177. // See "git help gc" for manual housekeeping.
  178. //
  179. // we skip exiting the program as git log -p/git diff will continue
  180. // to send data to stdout and finish executing. This next bit of
  181. // code prevents gitleaks from stopping mid scan if this error is
  182. // encountered
  183. if strings.Contains(scanner.Text(),
  184. "exhaustive rename detection was skipped") ||
  185. strings.Contains(scanner.Text(),
  186. "inexact rename detection was skipped") ||
  187. strings.Contains(scanner.Text(),
  188. "you may want to set your diff.renameLimit") ||
  189. strings.Contains(scanner.Text(),
  190. "See \"git help gc\" for manual housekeeping") ||
  191. strings.Contains(scanner.Text(),
  192. "Auto packing the repository in background for optimum performance") {
  193. logging.Warn().Msg(scanner.Text())
  194. } else {
  195. logging.Error().Msgf("[git] %s", scanner.Text())
  196. errEncountered = true
  197. }
  198. }
  199. if errEncountered {
  200. errCh <- errors.New("stderr is not empty")
  201. return
  202. }
  203. }