detect.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. package cmd
  2. import (
  3. "os"
  4. "path/filepath"
  5. "time"
  6. "github.com/rs/zerolog/log"
  7. "github.com/spf13/cobra"
  8. "github.com/spf13/viper"
  9. "github.com/zricethezav/gitleaks/v8/config"
  10. "github.com/zricethezav/gitleaks/v8/detect"
  11. "github.com/zricethezav/gitleaks/v8/report"
  12. )
  13. func init() {
  14. rootCmd.AddCommand(detectCmd)
  15. detectCmd.Flags().String("log-opts", "", "git log options")
  16. detectCmd.Flags().Bool("no-git", false, "treat git repo as a regular directory and scan those files, --log-opts has no effect on the scan when --no-git is set")
  17. detectCmd.Flags().Bool("pipe", false, "scan input from stdin, ex: `cat some_file | gitleaks detect --pipe`")
  18. detectCmd.Flags().Bool("follow-symlinks", false, "scan files that are symlinks to other files")
  19. }
  20. var detectCmd = &cobra.Command{
  21. Use: "detect",
  22. Short: "detect secrets in code",
  23. Run: runDetect,
  24. }
  25. func runDetect(cmd *cobra.Command, args []string) {
  26. initConfig()
  27. var (
  28. vc config.ViperConfig
  29. findings []report.Finding
  30. err error
  31. )
  32. // Load config
  33. if err = viper.Unmarshal(&vc); err != nil {
  34. log.Fatal().Err(err).Msg("Failed to load config")
  35. }
  36. cfg, err := vc.Translate()
  37. if err != nil {
  38. log.Fatal().Err(err).Msg("Failed to load config")
  39. }
  40. cfg.Path, _ = cmd.Flags().GetString("config")
  41. // start timer
  42. start := time.Now()
  43. // Setup detector
  44. detector := detect.NewDetector(cfg)
  45. detector.Config.Path, err = cmd.Flags().GetString("config")
  46. if err != nil {
  47. log.Fatal().Err(err).Msg("")
  48. }
  49. source, err := cmd.Flags().GetString("source")
  50. if err != nil {
  51. log.Fatal().Err(err).Msg("")
  52. }
  53. // if config path is not set, then use the {source}/.gitleaks.toml path.
  54. // note that there may not be a `{source}/.gitleaks.toml` file, this is ok.
  55. if detector.Config.Path == "" {
  56. detector.Config.Path = filepath.Join(source, ".gitleaks.toml")
  57. }
  58. // set verbose flag
  59. if detector.Verbose, err = cmd.Flags().GetBool("verbose"); err != nil {
  60. log.Fatal().Err(err).Msg("")
  61. }
  62. // set redact flag
  63. if detector.Redact, err = cmd.Flags().GetBool("redact"); err != nil {
  64. log.Fatal().Err(err).Msg("")
  65. }
  66. if detector.MaxTargetMegaBytes, err = cmd.Flags().GetInt("max-target-megabytes"); err != nil {
  67. log.Fatal().Err(err).Msg("")
  68. }
  69. if fileExists(filepath.Join(source, ".gitleaksignore")) {
  70. if err = detector.AddGitleaksIgnore(filepath.Join(source, ".gitleaksignore")); err != nil {
  71. log.Fatal().Err(err).Msg("could not call AddGitleaksIgnore")
  72. }
  73. }
  74. // ignore findings from the baseline (an existing report in json format generated earlier)
  75. baselinePath, _ := cmd.Flags().GetString("baseline-path")
  76. if baselinePath != "" {
  77. err = detector.AddBaseline(baselinePath)
  78. if err != nil {
  79. log.Error().Msgf("Could not load baseline. The path must point of a gitleaks report generated using the default format: %s", err)
  80. }
  81. }
  82. // set follow symlinks flag
  83. if detector.FollowSymlinks, err = cmd.Flags().GetBool("follow-symlinks"); err != nil {
  84. log.Fatal().Err(err).Msg("")
  85. }
  86. // set exit code
  87. exitCode, err := cmd.Flags().GetInt("exit-code")
  88. if err != nil {
  89. log.Fatal().Err(err).Msg("could not get exit code")
  90. }
  91. // determine what type of scan:
  92. // - git: scan the history of the repo
  93. // - no-git: scan files by treating the repo as a plain directory
  94. noGit, err := cmd.Flags().GetBool("no-git")
  95. if err != nil {
  96. log.Fatal().Err(err).Msg("could not call GetBool() for no-git")
  97. }
  98. fromPipe, err := cmd.Flags().GetBool("pipe")
  99. if err != nil {
  100. log.Fatal().Err(err)
  101. }
  102. // start the detector scan
  103. if noGit {
  104. findings, err = detector.DetectFiles(source)
  105. if err != nil {
  106. // don't exit on error, just log it
  107. log.Error().Err(err).Msg("")
  108. }
  109. } else if fromPipe {
  110. findings, err = detector.DetectReader(os.Stdin, 10)
  111. if err != nil {
  112. // log fatal to exit, no need to continue since a report
  113. // will not be generated when scanning from a pipe...for now
  114. log.Fatal().Err(err).Msg("")
  115. }
  116. } else {
  117. var logOpts string
  118. logOpts, err = cmd.Flags().GetString("log-opts")
  119. if err != nil {
  120. log.Fatal().Err(err).Msg("")
  121. }
  122. findings, err = detector.DetectGit(source, logOpts, detect.DetectType)
  123. if err != nil {
  124. // don't exit on error, just log it
  125. log.Error().Err(err).Msg("")
  126. }
  127. }
  128. // log info about the scan
  129. if err == nil {
  130. log.Info().Msgf("scan completed in %s", FormatDuration(time.Since(start)))
  131. if len(findings) != 0 {
  132. log.Warn().Msgf("leaks found: %d", len(findings))
  133. } else {
  134. log.Info().Msg("no leaks found")
  135. }
  136. } else {
  137. log.Warn().Msgf("partial scan completed in %s", FormatDuration(time.Since(start)))
  138. if len(findings) != 0 {
  139. log.Warn().Msgf("%d leaks found in partial scan", len(findings))
  140. } else {
  141. log.Warn().Msg("no leaks found in partial scan")
  142. }
  143. }
  144. // write report if desired
  145. reportPath, _ := cmd.Flags().GetString("report-path")
  146. ext, _ := cmd.Flags().GetString("report-format")
  147. if reportPath != "" {
  148. if err := report.Write(findings, cfg, ext, reportPath); err != nil {
  149. log.Fatal().Err(err).Msg("could not write")
  150. }
  151. }
  152. if err != nil {
  153. os.Exit(1)
  154. }
  155. if len(findings) != 0 {
  156. os.Exit(exitCode)
  157. }
  158. }
  159. func fileExists(fileName string) bool {
  160. // check for a .gitleaksignore file
  161. info, err := os.Stat(fileName)
  162. if err != nil && !os.IsNotExist(err) {
  163. return false
  164. }
  165. if info != nil && err == nil {
  166. if !info.IsDir() {
  167. return true
  168. }
  169. }
  170. return false
  171. }
  172. func FormatDuration(d time.Duration) string {
  173. scale := 100 * time.Second
  174. // look for the max scale that is smaller than d
  175. for scale > d {
  176. scale = scale / 10
  177. }
  178. return d.Round(scale / 100).String()
  179. }