repo.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. package gitleaks
  2. import (
  3. "crypto/md5"
  4. "fmt"
  5. "github.com/hako/durafmt"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "sync"
  10. "time"
  11. log "github.com/sirupsen/logrus"
  12. "gopkg.in/src-d/go-git.v4"
  13. "gopkg.in/src-d/go-git.v4/plumbing"
  14. diffType "gopkg.in/src-d/go-git.v4/plumbing/format/diff"
  15. "gopkg.in/src-d/go-git.v4/plumbing/object"
  16. "gopkg.in/src-d/go-git.v4/plumbing/storer"
  17. gitHttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
  18. "gopkg.in/src-d/go-git.v4/storage/memory"
  19. "gopkg.in/src-d/go-git.v4/utils/merkletrie"
  20. )
  21. // Commit represents a git commit
  22. type Commit struct {
  23. content string
  24. commit *object.Commit
  25. filePath string
  26. repoName string
  27. sha string
  28. message string
  29. author string
  30. email string
  31. date time.Time
  32. }
  33. // Leak represents a leaked secret or regex match.
  34. type Leak struct {
  35. Line string `json:"line"`
  36. Commit string `json:"commit"`
  37. Offender string `json:"offender"`
  38. Rule string `json:"rule"`
  39. Info string `json:"info"`
  40. Message string `json:"commitMsg"`
  41. Author string `json:"author"`
  42. Email string `json:"email"`
  43. File string `json:"file"`
  44. Repo string `json:"repo"`
  45. Date time.Time `json:"date"`
  46. Tags string `json:"tags"`
  47. Severity string `json:"severity"`
  48. }
  49. // Repo contains a src-d git repository and other data about the repo
  50. type Repo struct {
  51. leaks []Leak
  52. path string
  53. url string
  54. name string
  55. repository *git.Repository
  56. err error
  57. auditDuration string
  58. numCommits int64
  59. }
  60. func newRepo() (*Repo, error) {
  61. for _, re := range config.WhiteList.repos {
  62. if re.FindString(opts.Repo) != "" {
  63. return nil, fmt.Errorf("skipping %s, whitelisted", opts.Repo)
  64. }
  65. }
  66. return &Repo{
  67. path: opts.RepoPath,
  68. url: opts.Repo,
  69. name: filepath.Base(opts.Repo),
  70. }, nil
  71. }
  72. // clone will clone a repo
  73. func (repo *Repo) clone() error {
  74. var (
  75. err error
  76. repository *git.Repository
  77. )
  78. // check if cloning to disk
  79. if opts.Disk {
  80. log.Infof("cloning %s to disk", opts.Repo)
  81. cloneTarget := fmt.Sprintf("%s/%x", dir, md5.Sum([]byte(fmt.Sprintf("%s%s", opts.GithubUser, opts.Repo))))
  82. if strings.HasPrefix(opts.Repo, "git") {
  83. // private
  84. repository, err = git.PlainClone(cloneTarget, false, &git.CloneOptions{
  85. URL: opts.Repo,
  86. Progress: os.Stdout,
  87. Auth: config.sshAuth,
  88. })
  89. } else {
  90. // public
  91. options := &git.CloneOptions{
  92. URL: opts.Repo,
  93. Progress: os.Stdout,
  94. }
  95. if os.Getenv("GITHUB_TOKEN") != "" {
  96. options.Auth = &gitHttp.BasicAuth{
  97. Username: "fakeUsername", // yes, this can be anything except an empty string
  98. Password: os.Getenv("GITHUB_TOKEN"),
  99. }
  100. }
  101. repository, err = git.PlainClone(cloneTarget, false, options)
  102. }
  103. } else if repo.path != "" {
  104. log.Infof("opening %s", repo.path)
  105. repository, err = git.PlainOpen(repo.path)
  106. if err != nil {
  107. log.Errorf("unable to open %s", repo.path)
  108. }
  109. } else {
  110. // cloning to memory
  111. log.Infof("cloning %s", opts.Repo)
  112. if strings.HasPrefix(opts.Repo, "git") {
  113. repository, err = git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
  114. URL: opts.Repo,
  115. Progress: os.Stdout,
  116. Auth: config.sshAuth,
  117. })
  118. } else {
  119. options := &git.CloneOptions{
  120. URL: opts.Repo,
  121. Progress: os.Stdout,
  122. }
  123. if os.Getenv("GITHUB_TOKEN") != "" {
  124. options.Auth = &gitHttp.BasicAuth{
  125. Username: "fakeUsername", // yes, this can be anything except an empty string
  126. Password: os.Getenv("GITHUB_TOKEN"),
  127. }
  128. }
  129. repository, err = git.Clone(memory.NewStorage(), nil, options)
  130. }
  131. }
  132. repo.repository = repository
  133. repo.err = err
  134. return err
  135. }
  136. // audit performs an audit
  137. func (repo *Repo) audit() error {
  138. var (
  139. err error
  140. commitCount int64
  141. commitWg sync.WaitGroup
  142. semaphore chan bool
  143. logOpts git.LogOptions
  144. )
  145. for _, re := range config.WhiteList.repos {
  146. if re.FindString(repo.name) != "" {
  147. return fmt.Errorf("skipping %s, whitelisted", repo.name)
  148. }
  149. }
  150. start := time.Now()
  151. // check if target contains an external gitleaks toml
  152. if opts.RepoConfig {
  153. err := config.updateFromRepo(repo)
  154. if err != nil {
  155. log.Warn(err)
  156. }
  157. }
  158. if opts.Commit != "" {
  159. h := plumbing.NewHash(opts.Commit)
  160. c, err := repo.repository.CommitObject(h)
  161. if err != nil {
  162. return err
  163. }
  164. totalCommits = totalCommits + 1
  165. repo.numCommits = 1
  166. return repo.auditSingleCommit(c)
  167. } else if opts.Branch != "" {
  168. refs, err := repo.repository.Storer.IterReferences()
  169. if err != nil {
  170. return err
  171. }
  172. err = refs.ForEach(func(ref *plumbing.Reference) error {
  173. if ref.Name().IsTag() {
  174. return nil
  175. }
  176. // check heads first
  177. if ref.Name().String() == "refs/heads/"+opts.Branch {
  178. logOpts = git.LogOptions{
  179. From: ref.Hash(),
  180. }
  181. return nil
  182. } else if ref.Name().String() == "refs/remotes/origin/"+opts.Branch {
  183. logOpts = git.LogOptions{
  184. From: ref.Hash(),
  185. }
  186. return nil
  187. }
  188. return nil
  189. })
  190. } else {
  191. logOpts = git.LogOptions{
  192. All: true,
  193. }
  194. }
  195. // iterate all through commits
  196. cIter, err := repo.repository.Log(&logOpts)
  197. if err != nil {
  198. return err
  199. }
  200. if opts.Threads != 0 {
  201. threads = opts.Threads
  202. }
  203. if opts.RepoPath != "" {
  204. threads = 1
  205. }
  206. semaphore = make(chan bool, threads)
  207. err = cIter.ForEach(func(c *object.Commit) error {
  208. if c == nil || (opts.Depth != 0 && commitCount == opts.Depth) {
  209. return storer.ErrStop
  210. }
  211. if config.WhiteList.commits[c.Hash.String()] {
  212. log.Infof("skipping commit: %s\n", c.Hash.String())
  213. return nil
  214. }
  215. // commits w/o parent (root of git the git ref)
  216. if len(c.ParentHashes) == 0 {
  217. commitCount = commitCount + 1
  218. totalCommits = totalCommits + 1
  219. err := repo.auditSingleCommit(c)
  220. if err != nil {
  221. return err
  222. }
  223. return nil
  224. }
  225. commitCount = commitCount + 1
  226. totalCommits = totalCommits + 1
  227. // regular commit audit
  228. err = c.Parents().ForEach(func(parent *object.Commit) error {
  229. commitWg.Add(1)
  230. semaphore <- true
  231. go func(c *object.Commit, parent *object.Commit) {
  232. var (
  233. filePath string
  234. skipFile bool
  235. )
  236. defer func() {
  237. commitWg.Done()
  238. <-semaphore
  239. if r := recover(); r != nil {
  240. log.Warnf("recovering from panic on commit %s, likely large diff causing panic", c.Hash.String())
  241. }
  242. }()
  243. patch, err := c.Patch(parent)
  244. if err != nil {
  245. log.Warnf("problem generating patch for commit: %s\n", c.Hash.String())
  246. return
  247. }
  248. for _, f := range patch.FilePatches() {
  249. if f.IsBinary() {
  250. continue
  251. }
  252. skipFile = false
  253. from, to := f.Files()
  254. filePath = "???"
  255. if from != nil {
  256. filePath = from.Path()
  257. } else if to != nil {
  258. filePath = to.Path()
  259. }
  260. for _, fr := range config.FileRules {
  261. for _, r := range fr.fileTypes {
  262. if r.FindString(filePath) != "" {
  263. commitInfo := &Commit{
  264. repoName: repo.name,
  265. filePath: filePath,
  266. sha: c.Hash.String(),
  267. author: c.Author.Name,
  268. email: c.Author.Email,
  269. message: strings.Replace(c.Message, "\n", " ", -1),
  270. date: c.Author.When,
  271. }
  272. leak := *newLeak("N/A", fmt.Sprintf("filetype %s found", r.String()), r.String(), fr, commitInfo)
  273. mutex.Lock()
  274. repo.leaks = append(repo.leaks, leak)
  275. mutex.Unlock()
  276. }
  277. }
  278. }
  279. for _, re := range config.WhiteList.files {
  280. if re.FindString(filePath) != "" {
  281. log.Debugf("skipping whitelisted file (matched regex '%s'): %s", re.String(), filePath)
  282. skipFile = true
  283. break
  284. }
  285. }
  286. if skipFile {
  287. continue
  288. }
  289. chunks := f.Chunks()
  290. for _, chunk := range chunks {
  291. if chunk.Type() == diffType.Add || chunk.Type() == diffType.Delete {
  292. diff := &Commit{
  293. repoName: repo.name,
  294. filePath: filePath,
  295. content: chunk.Content(),
  296. sha: c.Hash.String(),
  297. author: c.Author.Name,
  298. email: c.Author.Email,
  299. message: strings.Replace(c.Message, "\n", " ", -1),
  300. date: c.Author.When,
  301. }
  302. chunkLeaks := inspect(diff)
  303. for _, leak := range chunkLeaks {
  304. mutex.Lock()
  305. repo.leaks = append(repo.leaks, leak)
  306. mutex.Unlock()
  307. }
  308. }
  309. }
  310. }
  311. }(c, parent)
  312. return nil
  313. })
  314. return nil
  315. })
  316. commitWg.Wait()
  317. repo.numCommits = commitCount
  318. repo.auditDuration = durafmt.Parse(time.Now().Sub(start)).String()
  319. return nil
  320. }
  321. func (repo *Repo) auditSingleCommit(c *object.Commit) error {
  322. fIter, err := c.Files()
  323. if err != nil {
  324. return err
  325. }
  326. // If current commit has parents then search for leaks in tree change,
  327. // that means scan in changed/modified files from one commit to another.
  328. if len(c.ParentHashes) > 0 {
  329. prevCommitObject, err := c.Parents().Next()
  330. if err != nil {
  331. return err
  332. }
  333. return repo.auditTreeChange(prevCommitObject, c)
  334. }
  335. // Scan for leaks in files related to current commit
  336. err = fIter.ForEach(func(f *object.File) error {
  337. bin, err := f.IsBinary()
  338. if bin || err != nil {
  339. return nil
  340. }
  341. for _, re := range config.WhiteList.files {
  342. if re.FindString(f.Name) != "" {
  343. log.Debugf("skipping whitelisted file (matched regex '%s'): %s", re.String(), f.Name)
  344. return nil
  345. }
  346. }
  347. content, err := f.Contents()
  348. if err != nil {
  349. return nil
  350. }
  351. diff := &Commit{
  352. repoName: repo.name,
  353. filePath: f.Name,
  354. content: content,
  355. sha: c.Hash.String(),
  356. author: c.Author.Name,
  357. email: c.Author.Email,
  358. message: strings.Replace(c.Message, "\n", " ", -1),
  359. date: c.Author.When,
  360. }
  361. fileLeaks := inspect(diff)
  362. mutex.Lock()
  363. repo.leaks = append(repo.leaks, fileLeaks...)
  364. mutex.Unlock()
  365. return nil
  366. })
  367. return err
  368. }
  369. func (repo *Repo) report() {
  370. if len(repo.leaks) != 0 {
  371. log.Warnf("%d leaks detected. %d commits inspected in %s", len(repo.leaks), repo.numCommits, repo.auditDuration)
  372. } else {
  373. log.Infof("No leaks detected. %d commits inspected in %s", repo.numCommits, repo.auditDuration)
  374. }
  375. }
  376. // auditTreeChange will search for leaks in changed/modified files from one
  377. // commit to another
  378. func (repo *Repo) auditTreeChange(src, dst *object.Commit) error {
  379. var (
  380. skip bool
  381. )
  382. // Get state of src commit
  383. srcState, err := src.Tree()
  384. if err != nil {
  385. return err
  386. }
  387. // Get state of destination commit
  388. dstState, err := dst.Tree()
  389. if err != nil {
  390. return err
  391. }
  392. changes, err := srcState.Diff(dstState)
  393. // Run through each change
  394. for _, change := range changes {
  395. // Ignore deleted files
  396. action, err := change.Action()
  397. if err != nil {
  398. return err
  399. }
  400. if action == merkletrie.Delete {
  401. continue
  402. }
  403. // Get list of involved files
  404. _, to, err := change.Files()
  405. bin, err := to.IsBinary()
  406. if bin || err != nil {
  407. continue
  408. }
  409. for _, re := range config.WhiteList.files {
  410. if re.FindString(to.Name) != "" {
  411. log.Debugf("skipping whitelisted file (matched regex '%s'): %s", re.String(), to.Name)
  412. skip = true
  413. }
  414. }
  415. if skip {
  416. skip = false
  417. continue
  418. }
  419. content, err := to.Contents()
  420. if err != nil {
  421. return err
  422. }
  423. diff := &Commit{
  424. repoName: repo.name,
  425. filePath: to.Name,
  426. content: content,
  427. sha: dst.Hash.String(),
  428. author: dst.Author.Name,
  429. email: dst.Author.Email,
  430. message: strings.Replace(dst.Message, "\n", " ", -1),
  431. date: dst.Author.When,
  432. }
  433. fileLeaks := inspect(diff)
  434. mutex.Lock()
  435. repo.leaks = append(repo.leaks, fileLeaks...)
  436. mutex.Unlock()
  437. }
  438. return nil
  439. }