detect.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  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. // set color flag
  70. if detector.NoColor, err = cmd.Flags().GetBool("no-color"); err != nil {
  71. log.Fatal().Err(err).Msg("")
  72. }
  73. if fileExists(filepath.Join(source, ".gitleaksignore")) {
  74. if err = detector.AddGitleaksIgnore(filepath.Join(source, ".gitleaksignore")); err != nil {
  75. log.Fatal().Err(err).Msg("could not call AddGitleaksIgnore")
  76. }
  77. }
  78. // ignore findings from the baseline (an existing report in json format generated earlier)
  79. baselinePath, _ := cmd.Flags().GetString("baseline-path")
  80. if baselinePath != "" {
  81. err = detector.AddBaseline(baselinePath, source)
  82. if err != nil {
  83. log.Error().Msgf("Could not load baseline. The path must point of a gitleaks report generated using the default format: %s", err)
  84. }
  85. }
  86. // set follow symlinks flag
  87. if detector.FollowSymlinks, err = cmd.Flags().GetBool("follow-symlinks"); err != nil {
  88. log.Fatal().Err(err).Msg("")
  89. }
  90. // set exit code
  91. exitCode, err := cmd.Flags().GetInt("exit-code")
  92. if err != nil {
  93. log.Fatal().Err(err).Msg("could not get exit code")
  94. }
  95. // determine what type of scan:
  96. // - git: scan the history of the repo
  97. // - no-git: scan files by treating the repo as a plain directory
  98. noGit, err := cmd.Flags().GetBool("no-git")
  99. if err != nil {
  100. log.Fatal().Err(err).Msg("could not call GetBool() for no-git")
  101. }
  102. fromPipe, err := cmd.Flags().GetBool("pipe")
  103. if err != nil {
  104. log.Fatal().Err(err)
  105. }
  106. // start the detector scan
  107. if noGit {
  108. findings, err = detector.DetectFiles(source)
  109. if err != nil {
  110. // don't exit on error, just log it
  111. log.Error().Err(err).Msg("")
  112. }
  113. } else if fromPipe {
  114. findings, err = detector.DetectReader(os.Stdin, 10)
  115. if err != nil {
  116. // log fatal to exit, no need to continue since a report
  117. // will not be generated when scanning from a pipe...for now
  118. log.Fatal().Err(err).Msg("")
  119. }
  120. } else {
  121. var logOpts string
  122. logOpts, err = cmd.Flags().GetString("log-opts")
  123. if err != nil {
  124. log.Fatal().Err(err).Msg("")
  125. }
  126. findings, err = detector.DetectGit(source, logOpts, detect.DetectType)
  127. if err != nil {
  128. // don't exit on error, just log it
  129. log.Error().Err(err).Msg("")
  130. }
  131. }
  132. // log info about the scan
  133. if err == nil {
  134. log.Info().Msgf("scan completed in %s", FormatDuration(time.Since(start)))
  135. if len(findings) != 0 {
  136. log.Warn().Msgf("leaks found: %d", len(findings))
  137. } else {
  138. log.Info().Msg("no leaks found")
  139. }
  140. } else {
  141. log.Warn().Msgf("partial scan completed in %s", FormatDuration(time.Since(start)))
  142. if len(findings) != 0 {
  143. log.Warn().Msgf("%d leaks found in partial scan", len(findings))
  144. } else {
  145. log.Warn().Msg("no leaks found in partial scan")
  146. }
  147. }
  148. // write report if desired
  149. reportPath, _ := cmd.Flags().GetString("report-path")
  150. ext, _ := cmd.Flags().GetString("report-format")
  151. if reportPath != "" {
  152. if err := report.Write(findings, cfg, ext, reportPath); err != nil {
  153. log.Fatal().Err(err).Msg("could not write")
  154. }
  155. }
  156. if err != nil {
  157. os.Exit(1)
  158. }
  159. if len(findings) != 0 {
  160. os.Exit(exitCode)
  161. }
  162. }
  163. func fileExists(fileName string) bool {
  164. // check for a .gitleaksignore file
  165. info, err := os.Stat(fileName)
  166. if err != nil && !os.IsNotExist(err) {
  167. return false
  168. }
  169. if info != nil && err == nil {
  170. if !info.IsDir() {
  171. return true
  172. }
  173. }
  174. return false
  175. }
  176. func FormatDuration(d time.Duration) string {
  177. scale := 100 * time.Second
  178. // look for the max scale that is smaller than d
  179. for scale > d {
  180. scale = scale / 10
  181. }
  182. return d.Round(scale / 100).String()
  183. }