manager.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. package manager
  2. import (
  3. "crypto/sha1"
  4. "encoding/csv"
  5. "encoding/hex"
  6. "encoding/json"
  7. "fmt"
  8. "os"
  9. "os/signal"
  10. "runtime"
  11. "sync"
  12. "text/tabwriter"
  13. "time"
  14. "github.com/zricethezav/gitleaks/config"
  15. "github.com/zricethezav/gitleaks/options"
  16. "github.com/hako/durafmt"
  17. "github.com/mattn/go-colorable"
  18. log "github.com/sirupsen/logrus"
  19. "gopkg.in/src-d/go-git.v4"
  20. )
  21. // Manager is a struct containing options and configs as well CloneOptions and CloneDir.
  22. // This struct is passed into each NewRepo so we are not passing around the manager in func params.
  23. type Manager struct {
  24. Opts options.Options
  25. Config config.Config
  26. CloneOptions *git.CloneOptions
  27. CloneDir string
  28. leaks []Leak
  29. leakChan chan Leak
  30. leakWG *sync.WaitGroup
  31. leakCache map[string]bool
  32. stopChan chan os.Signal
  33. metadata Metadata
  34. metaWG *sync.WaitGroup
  35. }
  36. // Leak is a struct that contains information about some line of code that contains
  37. // sensitive information as determined by the rules set in a gitleaks config
  38. type Leak struct {
  39. Line string `json:"line"`
  40. Offender string `json:"offender"`
  41. Commit string `json:"commit"`
  42. Repo string `json:"repo"`
  43. Rule string `json:"rule"`
  44. Message string `json:"commitMessage"`
  45. Author string `json:"author"`
  46. Email string `json:"email"`
  47. File string `json:"file"`
  48. Date time.Time `json:"date"`
  49. Tags string `json:"tags"`
  50. lookupHash string
  51. }
  52. // AuditTime is a type used to determine total audit time
  53. type AuditTime int64
  54. // PatchTime is a type used to determine total patch time during an audit
  55. type PatchTime int64
  56. // CloneTime is a type used to determine total clone time
  57. type CloneTime int64
  58. // RegexTime is a type used to determine the time each rules' regex takes. This is especially useful
  59. // if you notice that gitleaks is taking a long time. You can use --debug to see the output of the regexTime
  60. // so you can determine which regex is not performing well.
  61. type RegexTime struct {
  62. Time int64
  63. Regex string
  64. }
  65. // Metadata is a struct used to communicate metadata about an audit like timings and total commit counts.
  66. type Metadata struct {
  67. mux sync.Mutex
  68. data map[string]interface{}
  69. timings chan interface{}
  70. RegexTime map[string]int64
  71. Commits int
  72. AuditTime int64
  73. patchTime int64
  74. cloneTime int64
  75. }
  76. func init() {
  77. log.SetOutput(os.Stdout)
  78. log.SetFormatter(&log.TextFormatter{
  79. ForceColors: true,
  80. FullTimestamp: true,
  81. })
  82. // Fix colors on Windows
  83. if runtime.GOOS == "windows" {
  84. log.SetOutput(colorable.NewColorableStdout())
  85. }
  86. }
  87. // GetLeaks returns all available leaks
  88. func (manager *Manager) GetLeaks() []Leak {
  89. // need to wait for any straggling leaks
  90. manager.leakWG.Wait()
  91. return manager.leaks
  92. }
  93. // SendLeaks accepts a leak and is used by the audit pkg. This is the public function
  94. // that allows other packages to send leaks to the manager.
  95. func (manager *Manager) SendLeaks(l Leak) {
  96. h := sha1.New()
  97. h.Write([]byte(l.Commit + l.Offender + l.File))
  98. l.lookupHash = hex.EncodeToString(h.Sum(nil))
  99. manager.leakWG.Add(1)
  100. manager.leakChan <- l
  101. }
  102. func (manager *Manager) alreadySeen(leak Leak) bool {
  103. if _, ok := manager.leakCache[leak.lookupHash]; ok {
  104. return true
  105. }
  106. manager.leakCache[leak.lookupHash] = true
  107. return false
  108. }
  109. // receiveLeaks listens to leakChan for incoming leaks. If any are received, they are appended to the
  110. // manager's leaks for future reporting. If the -v/--verbose option is set the leaks will marshaled into
  111. // json and printed out.
  112. func (manager *Manager) receiveLeaks() {
  113. for leak := range manager.leakChan {
  114. if manager.alreadySeen(leak) {
  115. manager.leakWG.Done()
  116. continue
  117. }
  118. manager.leaks = append(manager.leaks, leak)
  119. if manager.Opts.Verbose {
  120. var b []byte
  121. if manager.Opts.PrettyPrint {
  122. b, _ = json.MarshalIndent(leak, "", " ")
  123. } else {
  124. b, _ = json.Marshal(leak)
  125. }
  126. fmt.Println(string(b))
  127. }
  128. manager.leakWG.Done()
  129. }
  130. }
  131. // GetMetadata returns the metadata. TODO this may not need to be private
  132. func (manager *Manager) GetMetadata() Metadata {
  133. manager.metaWG.Wait()
  134. return manager.metadata
  135. }
  136. // receiveMetadata is where the messages sent to the metadata channel get consumed. You can view metadata
  137. // by running gitleaks with the --debug option set. This is extremely useful when trying to optimize regular
  138. // expressions as that what gitleaks spends most of its cycles on.
  139. func (manager *Manager) receiveMetadata() {
  140. for t := range manager.metadata.timings {
  141. switch ti := t.(type) {
  142. case CloneTime:
  143. manager.metadata.cloneTime += int64(ti)
  144. case AuditTime:
  145. manager.metadata.AuditTime += int64(ti)
  146. case PatchTime:
  147. manager.metadata.patchTime += int64(ti)
  148. case RegexTime:
  149. manager.metadata.RegexTime[ti.Regex] = manager.metadata.RegexTime[ti.Regex] + ti.Time
  150. }
  151. manager.metaWG.Done()
  152. }
  153. }
  154. // IncrementCommits increments total commits during an audit by i.
  155. func (manager *Manager) IncrementCommits(i int) {
  156. manager.metadata.mux.Lock()
  157. manager.metadata.Commits += i
  158. manager.metadata.mux.Unlock()
  159. }
  160. // RecordTime accepts an interface and sends it to the manager's time channel
  161. func (manager *Manager) RecordTime(t interface{}) {
  162. manager.metaWG.Add(1)
  163. manager.metadata.timings <- t
  164. }
  165. // NewManager accepts options and returns a manager struct. The manager is a container for gitleaks configurations,
  166. // options and channel receivers.
  167. func NewManager(opts options.Options, cfg config.Config) (*Manager, error) {
  168. cloneOpts, err := opts.CloneOptions()
  169. if err != nil {
  170. return nil, err
  171. }
  172. m := &Manager{
  173. Opts: opts,
  174. Config: cfg,
  175. CloneOptions: cloneOpts,
  176. stopChan: make(chan os.Signal, 1),
  177. leakChan: make(chan Leak),
  178. leakWG: &sync.WaitGroup{},
  179. leakCache: make(map[string]bool),
  180. metaWG: &sync.WaitGroup{},
  181. metadata: Metadata{
  182. RegexTime: make(map[string]int64),
  183. timings: make(chan interface{}),
  184. data: make(map[string]interface{}),
  185. },
  186. }
  187. signal.Notify(m.stopChan, os.Interrupt)
  188. // start receiving leaks and metadata
  189. go m.receiveLeaks()
  190. go m.receiveMetadata()
  191. go m.receiveInterrupt()
  192. return m, nil
  193. }
  194. // DebugOutput logs metadata and other messages that occurred during a gitleaks audit
  195. func (manager *Manager) DebugOutput() {
  196. log.Debugf("-------------------------\n")
  197. log.Debugf("| Times and Commit Counts|\n")
  198. log.Debugf("-------------------------\n")
  199. fmt.Println("totalAuditTime: ", durafmt.Parse(time.Duration(manager.metadata.AuditTime)*time.Nanosecond))
  200. fmt.Println("totalPatchTime: ", durafmt.Parse(time.Duration(manager.metadata.patchTime)*time.Nanosecond))
  201. fmt.Println("totalCloneTime: ", durafmt.Parse(time.Duration(manager.metadata.cloneTime)*time.Nanosecond))
  202. fmt.Println("totalCommits: ", manager.metadata.Commits)
  203. const padding = 6
  204. w := tabwriter.NewWriter(os.Stdout, 0, 0, padding, '.', 0)
  205. log.Debugf("--------------------------\n")
  206. log.Debugf("| Individual Regex Times |\n")
  207. log.Debugf("--------------------------\n")
  208. for k, v := range manager.metadata.RegexTime {
  209. fmt.Fprintf(w, "%s\t%s\n", k, durafmt.Parse(time.Duration(v)*time.Nanosecond))
  210. }
  211. w.Flush()
  212. }
  213. // Report saves gitleaks leaks to a json specified by --report={report.json}
  214. func (manager *Manager) Report() error {
  215. close(manager.leakChan)
  216. close(manager.metadata.timings)
  217. if log.IsLevelEnabled(log.DebugLevel) {
  218. manager.DebugOutput()
  219. }
  220. if manager.Opts.Report != "" {
  221. if len(manager.GetLeaks()) == 0 {
  222. log.Infof("no leaks found, skipping writing report")
  223. return nil
  224. }
  225. file, err := os.Create(manager.Opts.Report)
  226. if err != nil {
  227. return err
  228. }
  229. if manager.Opts.ReportFormat == "json" {
  230. encoder := json.NewEncoder(file)
  231. encoder.SetIndent("", " ")
  232. err = encoder.Encode(manager.leaks)
  233. if err != nil {
  234. return err
  235. }
  236. } else {
  237. w := csv.NewWriter(file)
  238. w.Write([]string{"repo", "line", "commit", "offender", "rule", "tags", "commitMsg", "author", "email", "file", "date"})
  239. for _, leak := range manager.GetLeaks() {
  240. w.Write([]string{leak.Repo, leak.Line, leak.Commit, leak.Offender, leak.Rule, leak.Tags, leak.Message, leak.Author, leak.Email, leak.File, leak.Date.Format(time.RFC3339)})
  241. }
  242. w.Flush()
  243. }
  244. file.Close()
  245. log.Infof("report written to %s", manager.Opts.Report)
  246. }
  247. return nil
  248. }
  249. func (manager *Manager) receiveInterrupt() {
  250. <-manager.stopChan
  251. if manager.Opts.Report != "" {
  252. err := manager.Report()
  253. if err != nil {
  254. log.Error(err)
  255. }
  256. }
  257. log.Info("gitleaks received interrupt, stopping audit")
  258. os.Exit(options.ErrorEncountered)
  259. }