Zachary Rice 6 лет назад
Родитель
Сommit
6d980b4eab
11 измененных файлов с 239 добавлено и 195 удалено
  1. 1 2
      go.mod
  2. 9 0
      go.sum
  3. 2 9
      main.go
  4. 4 6
      src/config.go
  5. 2 0
      src/constants.go
  6. 35 40
      src/core.go
  7. 37 20
      src/github.go
  8. 16 13
      src/gitlab.go
  9. 18 11
      src/gitleaks_test.go
  10. 109 75
      src/repo.go
  11. 6 19
      src/utils.go

+ 1 - 2
go.mod

@@ -14,8 +14,7 @@ require (
 	github.com/onsi/gomega v1.5.0 // indirect
 	github.com/sirupsen/logrus v1.0.6
 	github.com/xanzy/go-gitlab v0.11.3
-	golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b // indirect
-	golang.org/x/net v0.0.0-20180925072008-f04abc6bdfa7 // indirect
+	golang.org/x/lint v0.0.0-20190409202823-959b441ac422 // indirect
 	golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
 	golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect
 	google.golang.org/appengine v1.2.0 // indirect

+ 9 - 0
go.sum

@@ -74,10 +74,16 @@ github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnW
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ=
 golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180925072008-f04abc6bdfa7 h1:zKzVgSQ8WOSHzD7I4k8LQjrHUUCNOlBsgc0PcYLVNnY=
 golang.org/x/net v0.0.0-20180925072008-f04abc6bdfa7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -87,10 +93,13 @@ golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180925112736-b09afc3d579e h1:LSlw/Dbj0MkNvPYAAkGinYmGliq+aqS7eKPYlE4oWC4=
 golang.org/x/sys v0.0.0-20180925112736-b09afc3d579e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd h1:/e+gpKk9r3dJobndpTytxS2gOy6m5uvpg+ISQoEcusQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 google.golang.org/appengine v1.2.0 h1:S0iUepdCWODXRvtE+gcRDd15L+k+k1AiHlMiMjefH24=
 google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=

+ 2 - 9
main.go

@@ -5,11 +5,11 @@ import (
 	"strings"
 
 	log "github.com/sirupsen/logrus"
-	gitleaks "github.com/zricethezav/gitleaks/src"
+	"github.com/zricethezav/gitleaks/src"
 )
 
 func main() {
-	report, err := gitleaks.Run(gitleaks.ParseOpts())
+	_, err := gitleaks.Run(gitleaks.ParseOpts())
 	if err != nil {
 		if strings.Contains(err.Error(), "whitelisted") {
 			log.Info(err.Error())
@@ -18,11 +18,4 @@ func main() {
 		log.Error(err)
 		os.Exit(gitleaks.ErrExit)
 	}
-
-	if len(report.Leaks) != 0 {
-		log.Warnf("%d leaks detected. %d commits inspected in %s", len(report.Leaks), report.Commits, report.Duration)
-		os.Exit(gitleaks.LeakExit)
-	} else {
-		log.Infof("%d leaks detected. %d commits inspected in %s", len(report.Leaks), report.Commits, report.Duration)
-	}
 }

+ 4 - 6
src/config.go

@@ -187,23 +187,21 @@ func getEntropyRanges(entropyLimitStr []string) ([]*entropyRange, error) {
 
 // externalConfig will attempt to load a pinned ".gitleaks.toml" configuration file
 // from a remote or local repo. Use the --repo-config option to trigger this.
-func (config *Config) updateFromRepo(repo *RepoInfo) error {
+func (config *Config) updateFromRepo(repo *Repo) error {
 	var tomlConfig TomlConfig
 	wt, err := repo.repository.Worktree()
 	if err != nil {
 		return err
 	}
 	f, err := wt.Filesystem.Open(".gitleaks.toml")
+	defer f.Close()
 	if err != nil {
-		return err
+		return fmt.Errorf("problem loading config: %v", err)
 	}
 	if _, err := toml.DecodeReader(f, &tomlConfig); err != nil {
 		return fmt.Errorf("problem loading config: %v", err)
 	}
-	f.Close()
-	if err != nil {
-		return err
-	}
+
 	return config.update(tomlConfig)
 }
 

+ 2 - 0
src/constants.go

@@ -2,6 +2,8 @@ package gitleaks
 
 const version = "2.0.0"
 
+const NoLeaks = 0
+
 const defaultGithubURL = "https://api.github.com/"
 const defaultThreadNum = 1
 

+ 35 - 40
src/core.go

@@ -1,13 +1,10 @@
 package gitleaks
 
 import (
+	log "github.com/sirupsen/logrus"
 	"io/ioutil"
 	"os"
 	"sync"
-	"time"
-
-	"github.com/hako/durafmt"
-	log "github.com/sirupsen/logrus"
 )
 
 var (
@@ -16,8 +13,6 @@ var (
 	dir          string
 	threads      int
 	totalCommits int64
-	commitMap    = make(map[string]bool)
-	auditDone    bool
 	mutex        = &sync.Mutex{}
 )
 
@@ -35,17 +30,16 @@ type Report struct {
 }
 
 // Run is the entry point for gitleaks
-func Run(optsL *Options) (*Report, error) {
+func Run(optsL *Options) (int, error) {
 	var (
-		leaks []Leak
 		err   error
+		leaks []Leak
 	)
 
-	now := time.Now()
 	opts = optsL
 	config, err = newConfig()
 	if err != nil {
-		return nil, err
+		return NoLeaks, err
 	}
 
 	if opts.Disk {
@@ -53,60 +47,61 @@ func Run(optsL *Options) (*Report, error) {
 		dir, err = ioutil.TempDir("", "gitleaks")
 		defer os.RemoveAll(dir)
 		if err != nil {
-			goto postAudit
+			return NoLeaks, err
 		}
 	}
 
 	// start audits
 	if opts.Repo != "" || opts.RepoPath != "" {
-		var repoInfo *RepoInfo
-		repoInfo, err = newRepoInfo()
+		var repo *Repo
+		repo, err = newRepo()
+		if err != nil {
+			return NoLeaks, err
+		}
+		err = repo.clone()
 		if err != nil {
-			goto postAudit
+			return NoLeaks, err
 		}
-		err = repoInfo.clone()
+		err = repo.audit()
 		if err != nil {
-			goto postAudit
+			return NoLeaks, err
 		}
-		leaks, err = repoInfo.audit()
+		repo.report()
+		leaks = repo.leaks
 	} else if opts.OwnerPath != "" {
-		var repoDs []*RepoInfo
-		repoDs, err = discoverRepos(opts.OwnerPath)
+		var repos []*Repo
+		repos, err = discoverRepos(opts.OwnerPath)
 		if err != nil {
-			goto postAudit
+			return NoLeaks, err
 		}
-		for _, repoInfo := range repoDs {
-			err = repoInfo.clone()
+		for _, repo := range repos {
+			err = repo.clone()
 			if err != nil {
+				log.Warnf("error occurred cloning repo: %s, continuing to next repo", repo.name)
 				continue
 			}
-			leaksFromRepo, err := repoInfo.audit()
-
+			err = repo.audit()
 			if err != nil {
-				log.Warnf("error occured auditing repo: %s, continuing", repoInfo.name)
+				log.Warnf("error occurred auditing repo: %s, continuing to next repo", repo.name)
+				continue
 			}
-			leaks = append(leaksFromRepo, leaks...)
+			repo.report()
+			leaks = append(leaks, repo.leaks...)
 		}
 	} else if opts.GithubOrg != "" || opts.GithubUser != "" {
-		leaks, err = auditGithubRepos()
+		return auditGithubRepos()
 	} else if opts.GitLabOrg != "" || opts.GitLabUser != "" {
-		leaks, err = auditGitlabRepos()
+		return auditGitlabRepos()
 	} else if opts.GithubPR != "" {
-		leaks, err = auditGithubPR()
-	}
-
-postAudit:
-	if err != nil {
-		return &Report{}, err
+		return auditGithubPR()
 	}
 
 	if opts.Report != "" {
-		writeReport(leaks)
+		err = writeReport(leaks)
+		if err != nil {
+			return NoLeaks, err
+		}
 	}
 
-	return &Report{
-		Leaks:    leaks,
-		Duration: durafmt.Parse(time.Now().Sub(now)).String(),
-		Commits:  totalCommits,
-	}, err
+	return len(leaks), nil
 }

+ 37 - 20
src/github.go

@@ -13,7 +13,7 @@ import (
 	"github.com/google/go-github/github"
 	log "github.com/sirupsen/logrus"
 	"golang.org/x/oauth2"
-	git "gopkg.in/src-d/go-git.v4"
+	"gopkg.in/src-d/go-git.v4"
 	gitHttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
 	"gopkg.in/src-d/go-git.v4/storage/memory"
 )
@@ -21,7 +21,7 @@ import (
 var githubPages = 100
 
 // auditPR audits a single github PR
-func auditGithubPR() ([]Leak, error) {
+func auditGithubPR() (int, error) {
 	var leaks []Leak
 	ctx := context.Background()
 	githubClient := github.NewClient(githubToken())
@@ -30,7 +30,7 @@ func auditGithubPR() ([]Leak, error) {
 	repo := splits[len(splits)-3]
 	prNum, err := strconv.Atoi(splits[len(splits)-1])
 	if err != nil {
-		return nil, err
+		return NoLeaks, err
 	}
 
 	page := 1
@@ -40,7 +40,7 @@ func auditGithubPR() ([]Leak, error) {
 			Page:    page,
 		})
 		if err != nil {
-			return leaks, err
+			return NoLeaks, err
 		}
 
 		for _, c := range commits {
@@ -66,7 +66,7 @@ func auditGithubPR() ([]Leak, error) {
 					continue
 				}
 
-				commit := &commitInfo{
+				commit := &Commit{
 					sha:      c.GetSHA(),
 					content:  *f.Patch,
 					filePath: *f.Filename,
@@ -84,14 +84,25 @@ func auditGithubPR() ([]Leak, error) {
 		}
 	}
 
-	return leaks, nil
+	if len(leaks) != 0 {
+		log.Warnf("%d leaks detected. %d commits inspected for PR: %s", len(leaks), totalCommits, opts.GithubPR)
+	}
+
+	if opts.Report != "" {
+		err = writeReport(leaks)
+		if err != nil {
+			return NoLeaks, err
+		}
+	}
+
+	return len(leaks), nil
 }
 
 // auditGithubRepos kicks off audits if --github-user or --github-org options are set.
 // First, we gather all the github repositories from the github api (this doesnt actually clone the repo).
 // After all the repos have been pulled from github's api we proceed to audit the repos by calling auditGithubRepo.
 // If an error occurs during an audit of a repo, that error is logged but won't break the execution cycle.
-func auditGithubRepos() ([]Leak, error) {
+func auditGithubRepos() (int, error) {
 	var (
 		err              error
 		githubRepos      []*github.Repository
@@ -100,8 +111,8 @@ func auditGithubRepos() ([]Leak, error) {
 		githubOrgOptions *github.RepositoryListByOrgOptions
 		githubOptions    *github.RepositoryListOptions
 		done             bool
-		leaks            []Leak
 		ownerDir         string
+		leaks            []Leak
 	)
 	ctx := context.Background()
 	githubClient := github.NewClient(githubToken())
@@ -163,31 +174,37 @@ func auditGithubRepos() ([]Leak, error) {
 		ownerDir, _ = ioutil.TempDir(dir, opts.GithubUser)
 	}
 	for _, githubRepo := range githubRepos {
-		repoD, err := cloneGithubRepo(githubRepo)
+		repo, err := cloneGithubRepo(githubRepo)
 		if err != nil {
 			log.Warn(err)
 			continue
 		}
-		leaksFromRepo, err := repoD.audit()
+		err = repo.audit()
+		if err != nil {
+			log.Warnf("error occurred during audit of repo: %s, err: %v, continuing github audit", repo.name, err)
+		}
 		if opts.Disk {
 			os.RemoveAll(fmt.Sprintf("%s/%s", ownerDir, *githubRepo.Name))
 		}
-		if len(leaksFromRepo) == 0 {
-			log.Infof("no leaks found for repo %s", *githubRepo.Name)
-		} else {
-			log.Warnf("leaks found for repo %s", *githubRepo.Name)
-		}
+
+		repo.report()
+
+		leaks = append(leaks, repo.leaks...)
+	}
+
+	if opts.Report != "" {
+		err = writeReport(leaks)
 		if err != nil {
-			log.Warn(err)
+			return NoLeaks, err
 		}
-		leaks = append(leaks, leaksFromRepo...)
 	}
-	return leaks, nil
+
+	return len(leaks), nil
 }
 
 // cloneGithubRepo clones a repo from the url parsed from a github repo. The repo
 // will be cloned to disk if --disk is set.
-func cloneGithubRepo(githubRepo *github.Repository) (*RepoInfo, error) {
+func cloneGithubRepo(githubRepo *github.Repository) (*Repo, error) {
 	var (
 		repo *git.Repository
 		err  error
@@ -248,7 +265,7 @@ func cloneGithubRepo(githubRepo *github.Repository) (*RepoInfo, error) {
 	if err != nil {
 		return nil, err
 	}
-	return &RepoInfo{
+	return &Repo{
 		repository: repo,
 		name:       *githubRepo.Name,
 	}, nil

+ 16 - 13
src/gitlab.go

@@ -17,13 +17,13 @@ const gitlabPages = 100
 // auditGitlabRepos kicks off audits if --gitlab-user or --gitlab-org options are set.
 // Getting all repositories from the GitLab API and run audit. If an error occurs during an audit of a repo,
 // that error is logged.
-func auditGitlabRepos() ([]Leak, error) {
+func auditGitlabRepos() (int, error) {
 	var (
 		ps      []*gitlab.Project
 		resp    *gitlab.Response
-		leaks   []Leak
 		tempDir string
 		err     error
+		leaks   []Leak
 	)
 
 	repos := make([]*gitlab.Project, 0, gitlabPages)
@@ -80,31 +80,34 @@ func auditGitlabRepos() ([]Leak, error) {
 	}
 
 	for _, p := range repos {
-		repoInfo, err := cloneGitlabRepo(tempDir, p)
+		repo, err := cloneGitlabRepo(tempDir, p)
 		if err != nil {
 			log.Warn(err)
 			continue
 		}
 
-		leaksFromRepo, err := repoInfo.audit()
+		err = repo.audit()
 		if err != nil {
 			log.Warn(err)
+			continue
 		}
 
 		if opts.Disk {
 			os.RemoveAll(fmt.Sprintf("%s/%d", tempDir, p.ID))
 		}
 
-		if len(leaksFromRepo) == 0 {
-			log.Infof("no leaks found for repo %s", p.Name)
-		} else {
-			log.Warnf("leaks found for repo %s", p.Name)
-		}
+		repo.report()
+		leaks = append(leaks, repo.leaks...)
+	}
 
-		leaks = append(leaks, leaksFromRepo...)
+	if opts.Report != "" {
+		err = writeReport(leaks)
+		if err != nil {
+			return NoLeaks, err
+		}
 	}
 
-	return leaks, nil
+	return len(leaks), nil
 }
 
 func createGitlabTempDir() (string, error) {
@@ -123,7 +126,7 @@ func createGitlabTempDir() (string, error) {
 	return ownerDir, nil
 }
 
-func cloneGitlabRepo(tempDir string, p *gitlab.Project) (*RepoInfo, error) {
+func cloneGitlabRepo(tempDir string, p *gitlab.Project) (*Repo, error) {
 	var (
 		repo *git.Repository
 		err  error
@@ -159,7 +162,7 @@ func cloneGitlabRepo(tempDir string, p *gitlab.Project) (*RepoInfo, error) {
 		return nil, err
 	}
 
-	return &RepoInfo{
+	return &Repo{
 		repository: repo,
 		name:       p.Name,
 	}, nil

+ 18 - 11
src/gitleaks_test.go

@@ -91,7 +91,7 @@ func TestGetRepo(t *testing.T) {
 				if err != nil {
 					log.Fatal(err)
 				}
-				repo, _ := newRepoInfo()
+				repo, _ := newRepo()
 				err := repo.clone()
 				if err != nil {
 					g.Assert(err.Error()).Equal(test.expectedErrMsg)
@@ -126,6 +126,14 @@ func TestRun(t *testing.T) {
 		configPath     string
 		commitPerPage  int
 	}{
+		{
+			testOpts: &Options{
+				Repo: "https://github.com/gitleakstest/gronit.git",
+			},
+			description:    "test leak",
+			numLeaks:       2,
+			expectedErrMsg: "",
+		},
 		{
 			testOpts: &Options{
 				GitLabUser: "gitleakstest",
@@ -274,11 +282,11 @@ func TestRun(t *testing.T) {
 				if test.commitPerPage != 0 {
 					githubPages = test.commitPerPage
 				}
-				report, err := Run(test.testOpts)
+				numLeaks, err := Run(test.testOpts)
 				if err != nil {
 					g.Assert(err.Error()).Equal(test.expectedErrMsg)
 				} else {
-					g.Assert(len(report.Leaks)).Equal(test.numLeaks)
+					g.Assert(numLeaks).Equal(test.numLeaks)
 				}
 				githubPages = 100
 			})
@@ -373,7 +381,6 @@ func TestWriteReport(t *testing.T) {
 }
 
 func TestAuditRepo(t *testing.T) {
-	var leaks []Leak
 	configsDir := testTomlLoader()
 	defer os.RemoveAll(configsDir)
 
@@ -383,7 +390,7 @@ func TestAuditRepo(t *testing.T) {
 	if err != nil {
 		panic(err)
 	}
-	leaksRepo := &RepoInfo{
+	leaksRepo := &Repo{
 		repository: leaksR,
 		name:       "gronit",
 	}
@@ -394,7 +401,7 @@ func TestAuditRepo(t *testing.T) {
 	if err != nil {
 		panic(err)
 	}
-	cleanRepo := &RepoInfo{
+	cleanRepo := &Repo{
 		repository: cleanR,
 		name:       "h1domains",
 	}
@@ -404,7 +411,7 @@ func TestAuditRepo(t *testing.T) {
 		description      string
 		expectedErrMsg   string
 		numLeaks         int
-		repo             *RepoInfo
+		repo             *Repo
 		whiteListFiles   []*regexp.Regexp
 		whiteListCommits map[string]bool
 		whiteListRepos   []*regexp.Regexp
@@ -616,7 +623,6 @@ func TestAuditRepo(t *testing.T) {
 	for _, test := range tests {
 		g.Describe("TestAuditRepo", func() {
 			g.It(test.description, func() {
-				auditDone = false
 				opts = test.testOpts
 
 				config, err = newConfig()
@@ -629,13 +635,14 @@ func TestAuditRepo(t *testing.T) {
 						goto next
 					}
 				}
-				leaks, err = test.repo.audit()
+				err = test.repo.audit()
 				if opts.Redact {
-					g.Assert(leaks[0].Offender).Equal("REDACTED")
+					g.Assert(test.repo.leaks[0].Offender).Equal("REDACTED")
 				}
-				g.Assert(len(leaks)).Equal(test.numLeaks)
+				g.Assert(len(test.repo.leaks)).Equal(test.numLeaks)
 			next:
 				os.Setenv("GITLEAKS_CONFIG", "")
+				test.repo.leaks = []Leak{}
 			})
 		})
 	}

+ 109 - 75
src/repo.go

@@ -3,6 +3,7 @@ package gitleaks
 import (
 	"crypto/md5"
 	"fmt"
+	"github.com/hako/durafmt"
 	"os"
 	"path/filepath"
 	"strings"
@@ -10,7 +11,7 @@ import (
 	"time"
 
 	log "github.com/sirupsen/logrus"
-	git "gopkg.in/src-d/go-git.v4"
+	"gopkg.in/src-d/go-git.v4"
 	"gopkg.in/src-d/go-git.v4/plumbing"
 	diffType "gopkg.in/src-d/go-git.v4/plumbing/format/diff"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
@@ -20,6 +21,20 @@ import (
 	"gopkg.in/src-d/go-git.v4/utils/merkletrie"
 )
 
+// Commit represents a git commit
+type Commit struct {
+	content  string
+	commit   *object.Commit
+	filePath string
+	repoName string
+	sha      string
+	message  string
+	author   string
+	email    string
+	date     time.Time
+}
+
+
 // Leak represents a leaked secret or regex match.
 type Leak struct {
 	Line     string    `json:"line"`
@@ -37,22 +52,25 @@ type Leak struct {
 	Severity string    `json:"severity"`
 }
 
-// RepoInfo contains a src-d git repository and other data about the repo
-type RepoInfo struct {
-	path       string
-	url        string
-	name       string
-	repository *git.Repository
-	err        error
+// Repo contains a src-d git repository and other data about the repo
+type Repo struct {
+	leaks         []Leak
+	path          string
+	url           string
+	name          string
+	repository    *git.Repository
+	err           error
+	auditDuration string
+	numCommits    int64
 }
 
-func newRepoInfo() (*RepoInfo, error) {
+func newRepo() (*Repo, error) {
 	for _, re := range config.WhiteList.repos {
 		if re.FindString(opts.Repo) != "" {
 			return nil, fmt.Errorf("skipping %s, whitelisted", opts.Repo)
 		}
 	}
-	return &RepoInfo{
+	return &Repo{
 		path: opts.RepoPath,
 		url:  opts.Repo,
 		name: filepath.Base(opts.Repo),
@@ -60,10 +78,10 @@ func newRepoInfo() (*RepoInfo, error) {
 }
 
 // clone will clone a repo
-func (repoInfo *RepoInfo) clone() error {
+func (repo *Repo) clone() error {
 	var (
-		err  error
-		repo *git.Repository
+		err        error
+		repository *git.Repository
 	)
 
 	// check if cloning to disk
@@ -72,7 +90,7 @@ func (repoInfo *RepoInfo) clone() error {
 		cloneTarget := fmt.Sprintf("%s/%x", dir, md5.Sum([]byte(fmt.Sprintf("%s%s", opts.GithubUser, opts.Repo))))
 		if strings.HasPrefix(opts.Repo, "git") {
 			// private
-			repo, err = git.PlainClone(cloneTarget, false, &git.CloneOptions{
+			repository, err = git.PlainClone(cloneTarget, false, &git.CloneOptions{
 				URL:      opts.Repo,
 				Progress: os.Stdout,
 				Auth:     config.sshAuth,
@@ -89,19 +107,19 @@ func (repoInfo *RepoInfo) clone() error {
 					Password: os.Getenv("GITHUB_TOKEN"),
 				}
 			}
-			repo, err = git.PlainClone(cloneTarget, false, options)
+			repository, err = git.PlainClone(cloneTarget, false, options)
 		}
-	} else if repoInfo.path != "" {
-		log.Infof("opening %s", repoInfo.path)
-		repo, err = git.PlainOpen(repoInfo.path)
+	} else if repo.path != "" {
+		log.Infof("opening %s", repo.path)
+		repository, err = git.PlainOpen(repo.path)
 		if err != nil {
-			log.Errorf("unable to open %s", repoInfo.path)
+			log.Errorf("unable to open %s", repo.path)
 		}
 	} else {
 		// cloning to memory
 		log.Infof("cloning %s", opts.Repo)
 		if strings.HasPrefix(opts.Repo, "git") {
-			repo, err = git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
+			repository, err = git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
 				URL:      opts.Repo,
 				Progress: os.Stdout,
 				Auth:     config.sshAuth,
@@ -117,56 +135,53 @@ func (repoInfo *RepoInfo) clone() error {
 					Password: os.Getenv("GITHUB_TOKEN"),
 				}
 			}
-			repo, err = git.Clone(memory.NewStorage(), nil, options)
+			repository, err = git.Clone(memory.NewStorage(), nil, options)
 		}
 	}
-	repoInfo.repository = repo
-	repoInfo.err = err
+	repo.repository = repository
+	repo.err = err
 	return err
 }
 
 // audit performs an audit
-func (repoInfo *RepoInfo) audit() ([]Leak, error) {
+func (repo *Repo) audit() error {
 	var (
 		err         error
-		leaks       []Leak
 		commitCount int64
 		commitWg    sync.WaitGroup
 		semaphore   chan bool
 		logOpts     git.LogOptions
 	)
 	for _, re := range config.WhiteList.repos {
-		if re.FindString(repoInfo.name) != "" {
-			return leaks, fmt.Errorf("skipping %s, whitelisted", repoInfo.name)
+		if re.FindString(repo.name) != "" {
+			return fmt.Errorf("skipping %s, whitelisted", repo.name)
 		}
 	}
 
+	start := time.Now()
+
 	// check if target contains an external gitleaks toml
 	if opts.RepoConfig {
-		err := config.updateFromRepo(repoInfo)
+		err := config.updateFromRepo(repo)
 		if err != nil {
-			return leaks, nil
+			return err
 		}
 	}
 
 	if opts.Commit != "" {
 		h := plumbing.NewHash(opts.Commit)
-		c, err := repoInfo.repository.CommitObject(h)
+		c, err := repo.repository.CommitObject(h)
 		if err != nil {
-			return leaks, nil
+			return err
 		}
 
-		commitCount = commitCount + 1
 		totalCommits = totalCommits + 1
-		leaksFromSingleCommit := repoInfo.auditSingleCommit(c)
-		mutex.Lock()
-		leaks = append(leaksFromSingleCommit, leaks...)
-		mutex.Unlock()
-		return leaks, err
+		repo.numCommits = 1
+		return repo.auditSingleCommit(c)
 	} else if opts.Branch != "" {
-		refs, err := repoInfo.repository.Storer.IterReferences()
+		refs, err := repo.repository.Storer.IterReferences()
 		if err != nil {
-			return leaks, err
+			return err
 		}
 		err = refs.ForEach(func(ref *plumbing.Reference) error {
 			if ref.Name().IsTag() {
@@ -193,10 +208,9 @@ func (repoInfo *RepoInfo) audit() ([]Leak, error) {
 	}
 
 	// iterate all through commits
-	cIter, err := repoInfo.repository.Log(&logOpts)
-
+	cIter, err := repo.repository.Log(&logOpts)
 	if err != nil {
-		return leaks, nil
+		return err
 	}
 
 	if opts.Threads != 0 {
@@ -221,10 +235,10 @@ func (repoInfo *RepoInfo) audit() ([]Leak, error) {
 		if len(c.ParentHashes) == 0 {
 			commitCount = commitCount + 1
 			totalCommits = totalCommits + 1
-			leaksFromSingleCommit := repoInfo.auditSingleCommit(c)
-			mutex.Lock()
-			leaks = append(leaksFromSingleCommit, leaks...)
-			mutex.Unlock()
+			err := repo.auditSingleCommit(c)
+			if err != nil {
+				return err
+			}
 			return nil
 		}
 
@@ -268,8 +282,8 @@ func (repoInfo *RepoInfo) audit() ([]Leak, error) {
 					for _, fr := range config.FileRules {
 						for _, r := range fr.fileTypes {
 							if r.FindString(filePath) != "" {
-								commitInfo := &commitInfo{
-									repoName: repoInfo.name,
+								commitInfo := &Commit{
+									repoName: repo.name,
 									filePath: filePath,
 									sha:      c.Hash.String(),
 									author:   c.Author.Name,
@@ -279,7 +293,7 @@ func (repoInfo *RepoInfo) audit() ([]Leak, error) {
 								}
 								leak := *newLeak("N/A", fmt.Sprintf("filetype %s found", r.String()), r.String(), fr, commitInfo)
 								mutex.Lock()
-								leaks = append(leaks, leak)
+								repo.leaks = append(repo.leaks, leak)
 								mutex.Unlock()
 							}
 						}
@@ -298,8 +312,8 @@ func (repoInfo *RepoInfo) audit() ([]Leak, error) {
 					chunks := f.Chunks()
 					for _, chunk := range chunks {
 						if chunk.Type() == diffType.Add || chunk.Type() == diffType.Delete {
-							diff := &commitInfo{
-								repoName: repoInfo.name,
+							diff := &Commit{
+								repoName: repo.name,
 								filePath: filePath,
 								content:  chunk.Content(),
 								sha:      c.Hash.String(),
@@ -311,7 +325,7 @@ func (repoInfo *RepoInfo) audit() ([]Leak, error) {
 							chunkLeaks := inspect(diff)
 							for _, leak := range chunkLeaks {
 								mutex.Lock()
-								leaks = append(leaks, leak)
+								repo.leaks = append(repo.leaks, leak)
 								mutex.Unlock()
 							}
 						}
@@ -326,22 +340,26 @@ func (repoInfo *RepoInfo) audit() ([]Leak, error) {
 	})
 
 	commitWg.Wait()
-	return leaks, nil
+	repo.numCommits = commitCount
+	repo.auditDuration = durafmt.Parse(time.Now().Sub(start)).String()
+
+	return nil
 }
 
-func (repoInfo *RepoInfo) auditSingleCommit(c *object.Commit) []Leak {
-	var leaks []Leak
-	var prevCommitObject *object.Commit
+func (repo *Repo) auditSingleCommit(c *object.Commit) error {
 	fIter, err := c.Files()
 	if err != nil {
-		return nil
+		return err
 	}
 
 	// If current commit has parents then search for leaks in tree change,
 	// that means scan in changed/modified files from one commit to another.
 	if len(c.ParentHashes) > 0 {
-		prevCommitObject, err = c.Parents().Next()
-		return repoInfo.auditTreeChange(prevCommitObject, c)
+		prevCommitObject, err := c.Parents().Next()
+		if err != nil {
+			return err
+		}
+		return repo.auditTreeChange(prevCommitObject, c)
 	}
 
 	// Scan for leaks in files related to current commit
@@ -360,8 +378,8 @@ func (repoInfo *RepoInfo) auditSingleCommit(c *object.Commit) []Leak {
 		if err != nil {
 			return nil
 		}
-		diff := &commitInfo{
-			repoName: repoInfo.name,
+		diff := &Commit{
+			repoName: repo.name,
 			filePath: f.Name,
 			content:  content,
 			sha:      c.Hash.String(),
@@ -372,28 +390,38 @@ func (repoInfo *RepoInfo) auditSingleCommit(c *object.Commit) []Leak {
 		}
 		fileLeaks := inspect(diff)
 		mutex.Lock()
-		leaks = append(leaks, fileLeaks...)
+		repo.leaks = append(repo.leaks, fileLeaks...)
 		mutex.Unlock()
 		return nil
 	})
-	return leaks
+	return err
+}
+
+func (repo *Repo) report() {
+	if len(repo.leaks) != 0 {
+		log.Warnf("%d leaks detected. %d commits inspected in %s", len(repo.leaks), repo.numCommits, repo.auditDuration)
+	} else {
+		log.Infof("No leaks detected. %d commits inspected in %s", repo.numCommits, repo.auditDuration)
+	}
 }
 
 // auditTreeChange will search for leaks in changed/modified files from one
 // commit to another
-func (repoInfo *RepoInfo) auditTreeChange(src, dst *object.Commit) []Leak {
-	var leaks []Leak
+func (repo *Repo) auditTreeChange(src, dst *object.Commit) error {
+	var (
+		skip bool
+	)
 
 	// Get state of src commit
 	srcState, err := src.Tree()
 	if err != nil {
-		return nil
+		return err
 	}
 
 	// Get state of destination commit
 	dstState, err := dst.Tree()
 	if err != nil {
-		return nil
+		return err
 	}
 	changes, err := srcState.Diff(dstState)
 
@@ -403,7 +431,7 @@ func (repoInfo *RepoInfo) auditTreeChange(src, dst *object.Commit) []Leak {
 		// Ignore deleted files
 		action, err := change.Action()
 		if err != nil {
-			return nil
+			return err
 		}
 		if action == merkletrie.Delete {
 			continue
@@ -413,22 +441,28 @@ func (repoInfo *RepoInfo) auditTreeChange(src, dst *object.Commit) []Leak {
 		_, to, err := change.Files()
 		bin, err := to.IsBinary()
 		if bin || err != nil {
-			return nil
+			continue
 		}
 
 		for _, re := range config.WhiteList.files {
 			if re.FindString(to.Name) != "" {
 				log.Debugf("skipping whitelisted file (matched regex '%s'): %s", re.String(), to.Name)
-				return nil
+				skip = true
 			}
 		}
+
+		if skip {
+			skip = false
+			continue
+		}
+
 		content, err := to.Contents()
 		if err != nil {
-			return nil
+			return err
 		}
 
-		diff := &commitInfo{
-			repoName: repoInfo.name,
+		diff := &Commit{
+			repoName: repo.name,
 			filePath: to.Name,
 			content:  content,
 			sha:      dst.Hash.String(),
@@ -439,9 +473,9 @@ func (repoInfo *RepoInfo) auditTreeChange(src, dst *object.Commit) []Leak {
 		}
 		fileLeaks := inspect(diff)
 		mutex.Lock()
-		leaks = append(leaks, fileLeaks...)
+		repo.leaks = append(repo.leaks, fileLeaks...)
 		mutex.Unlock()
 	}
-	return leaks
+	return nil
 
 }

+ 6 - 19
src/utils.go

@@ -11,21 +11,8 @@ import (
 	"time"
 
 	log "github.com/sirupsen/logrus"
-	"gopkg.in/src-d/go-git.v4/plumbing/object"
 )
 
-type commitInfo struct {
-	content  string
-	commit   *object.Commit
-	filePath string
-	repoName string
-	sha      string
-	message  string
-	author   string
-	email    string
-	date     time.Time
-}
-
 // writeReport writes a report to a file specified in the --report= option.
 // Default format for report is JSON. You can use the --csv option to write the report as a csv
 func writeReport(leaks []Leak) error {
@@ -83,7 +70,7 @@ func writeReport(leaks []Leak) error {
 }
 
 // check rule will inspect a single line and return a leak if it encounters one
-func (rule *Rule) check(line string, commit *commitInfo) (*Leak, error) {
+func (rule *Rule) check(line string, commit *Commit) (*Leak, error) {
 	var (
 		match       string
 		fileMatch   string
@@ -146,7 +133,7 @@ postEntropy:
 // a set of regexes set by the config (see gitleaks.toml for example). This function
 // will skip lines that include a whitelisted regex. A list of leaks is returned.
 // If verbose mode (-v/--verbose) is set, then checkDiff will log leaks as they are discovered.
-func inspect(commit *commitInfo) []Leak {
+func inspect(commit *Commit) []Leak {
 	var leaks []Leak
 	lines := strings.Split(commit.content, "\n")
 
@@ -176,7 +163,7 @@ func isLineWhitelisted(line string) bool {
 	return false
 }
 
-func newLeak(line string, info string, offender string, rule *Rule, commit *commitInfo) *Leak {
+func newLeak(line string, info string, offender string, rule *Rule, commit *Commit) *Leak {
 	leak := &Leak{
 		Line:     line,
 		Commit:   commit.sha,
@@ -205,10 +192,10 @@ func newLeak(line string, info string, offender string, rule *Rule, commit *comm
 
 // discoverRepos walks all the children of `path`. If a child directory
 // contain a .git subdirectory then that repo will be added to the list of repos returned
-func discoverRepos(ownerPath string) ([]*RepoInfo, error) {
+func discoverRepos(ownerPath string) ([]*Repo, error) {
 	var (
 		err    error
-		repoDs []*RepoInfo
+		repoDs []*Repo
 	)
 	files, err := ioutil.ReadDir(ownerPath)
 	if err != nil {
@@ -217,7 +204,7 @@ func discoverRepos(ownerPath string) ([]*RepoInfo, error) {
 	for _, f := range files {
 		repoPath := path.Join(ownerPath, f.Name())
 		if f.IsDir() && containsGit(repoPath) {
-			repoDs = append(repoDs, &RepoInfo{
+			repoDs = append(repoDs, &Repo{
 				name: f.Name(),
 				path: repoPath,
 			})