Prechádzať zdrojové kódy

Merge branch 'master' of github.com:gitleaks/gitleaks

Zachary Rice 2 rokov pred
rodič
commit
fab4f4e97d
35 zmenil súbory, kde vykonal 411 pridanie a 21 odobranie
  1. 3 0
      .gitignore
  2. 6 5
      .gitleaksignore
  3. 1 1
      README.md
  4. 1 1
      cmd/root.go
  5. 15 6
      detect/detect.go
  6. 40 1
      detect/detect_test.go
  7. 5 4
      go.mod
  8. 11 0
      go.sum
  9. 102 0
      report/junit.go
  10. 107 0
      report/junit_test.go
  11. 2 0
      report/report.go
  12. 16 0
      report/report_test.go
  13. 4 0
      testdata/expected/report/junit_empty.xml
  14. 11 0
      testdata/expected/report/junit_simple.xml
  15. 1 0
      testdata/repos/nogit/.gitleaksignore
  16. 24 0
      testdata/repos/nogit/api.go
  17. 2 0
      testdata/repos/small/.gitleaksignore
  18. 24 0
      testdata/repos/small/api/ignoreCommit.go
  19. 24 0
      testdata/repos/small/api/ignoreGlobal.go
  20. 1 1
      testdata/repos/small/dotGit/COMMIT_EDITMSG
  21. 1 1
      testdata/repos/small/dotGit/ORIG_HEAD
  22. BIN
      testdata/repos/small/dotGit/index
  23. 4 0
      testdata/repos/small/dotGit/logs/HEAD
  24. 4 0
      testdata/repos/small/dotGit/logs/refs/heads/main
  25. BIN
      testdata/repos/small/dotGit/objects/32/bfaec6697c1a693f71b183d3389ad2547bbb3c
  26. BIN
      testdata/repos/small/dotGit/objects/4f/77f1b3cc39d4b17e4cf4ba0a38f5daed9875b4
  27. BIN
      testdata/repos/small/dotGit/objects/53/cd7a3c6eb4937f413e3c25e4a9f39289afa69e
  28. BIN
      testdata/repos/small/dotGit/objects/8d/c20a62e189d3446cfb6ec328dbd379d64feb20
  29. BIN
      testdata/repos/small/dotGit/objects/98/1ab8417104c91bb9a4657f5d436c760c0aff50
  30. BIN
      testdata/repos/small/dotGit/objects/ac/bef43fdb053ec01e8e16697536d47853460cbc
  31. 1 0
      testdata/repos/small/dotGit/objects/ca/da78a9bf157ec05573f19e682d211f811c2e2d
  32. BIN
      testdata/repos/small/dotGit/objects/da/eb160a13200a3422e73296a6892784a113e0d6
  33. BIN
      testdata/repos/small/dotGit/objects/df/7eaa59fc89b3e7258167605db2e582f1a78e6f
  34. BIN
      testdata/repos/small/dotGit/objects/fa/468655086f149d41a8e7db14ed054bebec7687
  35. 1 1
      testdata/repos/small/dotGit/refs/heads/main

+ 3 - 0
.gitignore

@@ -14,6 +14,9 @@ build
 .gitleaks.toml
 cmd/generate/config/gitleaks.toml
 
+# test results
+testdata/expected/report/*.got.*
+
 # Test binary
 *.out
 

+ 6 - 5
.gitleaksignore

@@ -716,6 +716,9 @@ testdata/expected/git/small-branch-foo.txt:aws-access-token:15
 testdata/expected/git/small.txt:aws-access-token:15
 testdata/expected/git/small.txt:aws-access-token:44
 testdata/repos/nogit/main.go:aws-access-token:20
+testdata/repos/nogit/api.go:aws-access-token:20
+testdata/repos/small/api/ignoreCommit.go:aws-access-token:20
+testdata/repos/small/api/ignoreGlobal.go:aws-access-token:20
 3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:513
 3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:492
 3df8c3deb7bc1e34210bdbce114f1c6165bc6ac8:detect/detect_test.go:aws-access-token:414
@@ -759,9 +762,7 @@ acce01f2338434a78f6a4a06a097b0fd23280484:README.md:aws-access-token:220
 91ff8f9b7020a16dac9b8b9ef98560ea1cb464f8:testdata/repos/small/api/ignoreCommit.go:aws-access-token:20
 91ff8f9b7020a16dac9b8b9ef98560ea1cb464f8:testdata/repos/small/api/ignoreGlobal.go:aws-access-token:20
 91ff8f9b7020a16dac9b8b9ef98560ea1cb464f8:testdata/repos/nogit/api.go:aws-access-token:20
-
-
-
-
-
+adf617b3b4628e1160fa3d135b4c3dfd45c05e15:testdata/repos/nogit/api.go:aws-access-token:20
+adf617b3b4628e1160fa3d135b4c3dfd45c05e15:testdata/repos/small/api/ignoreGlobal.go:aws-access-token:20
+adf617b3b4628e1160fa3d135b4c3dfd45c05e15:testdata/repos/small/api/ignoreCommit.go:aws-access-token:20
 

+ 1 - 1
README.md

@@ -157,7 +157,7 @@ Flags:
       --no-color                   turn off color for verbose output
       --no-banner                  suppress banner
       --redact                     redact secrets from logs and stdout
-  -f, --report-format string       output format (json, csv, sarif) (default "json")
+  -f, --report-format string       output format (json, csv, junit, sarif) (default "json")
   -r, --report-path string         report file
   -s, --source string              path to source (default ".")
   -v, --verbose                    show verbose output from scan

+ 1 - 1
cmd/root.go

@@ -41,7 +41,7 @@ func init() {
 	rootCmd.PersistentFlags().Int("exit-code", 1, "exit code when leaks have been encountered")
 	rootCmd.PersistentFlags().StringP("source", "s", ".", "path to source")
 	rootCmd.PersistentFlags().StringP("report-path", "r", "", "report file")
-	rootCmd.PersistentFlags().StringP("report-format", "f", "json", "output format (json, csv, sarif)")
+	rootCmd.PersistentFlags().StringP("report-format", "f", "json", "output format (json, csv, junit, sarif)")
 	rootCmd.PersistentFlags().StringP("baseline-path", "b", "", "path to baseline with issues that can be ignored")
 	rootCmd.PersistentFlags().StringP("log-level", "l", "info", "log level (trace, debug, info, warn, error, fatal)")
 	rootCmd.PersistentFlags().BoolP("verbose", "v", false, "show verbose output from scan")

+ 15 - 6
detect/detect.go

@@ -147,7 +147,7 @@ func NewDetectorDefaultConfig() (*Detector, error) {
 }
 
 func (d *Detector) AddGitleaksIgnore(gitleaksIgnorePath string) error {
-	log.Debug().Msg("found .gitleaksignore file")
+	log.Debug().Msgf("found .gitleaksignore file: %s", gitleaksIgnorePath)
 	file, err := os.Open(gitleaksIgnorePath)
 
 	if err != nil {
@@ -598,16 +598,25 @@ func (d *Detector) Detect(fragment Fragment) []report.Finding {
 
 // addFinding synchronously adds a finding to the findings slice
 func (d *Detector) addFinding(finding report.Finding) {
-	if finding.Commit == "" {
-		finding.Fingerprint = fmt.Sprintf("%s:%s:%d", finding.File, finding.RuleID, finding.StartLine)
-	} else {
+	globalFingerprint := fmt.Sprintf("%s:%s:%d", finding.File, finding.RuleID, finding.StartLine)
+	if finding.Commit != "" {
 		finding.Fingerprint = fmt.Sprintf("%s:%s:%s:%d", finding.Commit, finding.File, finding.RuleID, finding.StartLine)
+	} else {
+		finding.Fingerprint = globalFingerprint
 	}
+
 	// check if we should ignore this finding
-	if _, ok := d.gitleaksIgnore[finding.Fingerprint]; ok {
-		log.Debug().Msgf("ignoring finding with Fingerprint %s",
+	if _, ok := d.gitleaksIgnore[globalFingerprint]; ok {
+		log.Debug().Msgf("ignoring finding with global Fingerprint %s",
 			finding.Fingerprint)
 		return
+	} else if finding.Commit != "" {
+		// Awkward nested if because I'm not sure how to chain these two conditions.
+		if _, ok := d.gitleaksIgnore[finding.Fingerprint]; ok {
+			log.Debug().Msgf("ignoring finding with Fingerprint %s",
+				finding.Fingerprint)
+			return
+		}
 	}
 
 	if d.baseline != nil && !IsNew(finding, d.baseline) {

+ 40 - 1
detect/detect_test.go

@@ -6,6 +6,7 @@ import (
 	"path/filepath"
 	"testing"
 
+	"github.com/rs/zerolog/log"
 	"github.com/spf13/viper"
 	"github.com/stretchr/testify/assert"
 
@@ -474,6 +475,22 @@ func TestFromGit(t *testing.T) {
 			t.Error(err)
 		}
 		detector := NewDetector(cfg)
+
+		var ignorePath string
+		info, err := os.Stat(tt.source)
+		if err != nil {
+			log.Fatal().Err(err).Msg("could not call AddGitleaksIgnore")
+		}
+
+		if info.IsDir() {
+			ignorePath = filepath.Join(tt.source, ".gitleaksignore")
+		} else {
+			ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore")
+		}
+		if err = detector.AddGitleaksIgnore(ignorePath); err != nil {
+			log.Fatal().Err(err).Msg("could not call AddGitleaksIgnore")
+		}
+
 		findings, err := detector.DetectGit(tt.source, tt.logOpts, DetectType)
 		if err != nil {
 			t.Error(err)
@@ -554,7 +571,9 @@ func TestFromGitStaged(t *testing.T) {
 			t.Error(err)
 		}
 		detector := NewDetector(cfg)
-		detector.AddGitleaksIgnore(filepath.Join(tt.source, ".gitleaksignore"))
+		if err = detector.AddGitleaksIgnore(filepath.Join(tt.source, ".gitleaksignore")); err != nil {
+			log.Fatal().Err(err).Msg("could not call AddGitleaksIgnore")
+		}
 		findings, err := detector.DetectGit(tt.source, tt.logOpts, ProtectStagedType)
 		if err != nil {
 			t.Error(err)
@@ -617,6 +636,11 @@ func TestFromFiles(t *testing.T) {
 				},
 			},
 		},
+		{
+			source:           filepath.Join(repoBasePath, "nogit", "api.go"),
+			cfgName:          "simple",
+			expectedFindings: []report.Finding{},
+		},
 	}
 
 	for _, tt := range tests {
@@ -635,6 +659,21 @@ func TestFromFiles(t *testing.T) {
 		}
 		cfg, _ := vc.Translate()
 		detector := NewDetector(cfg)
+
+		var ignorePath string
+		info, err := os.Stat(tt.source)
+		if err != nil {
+			log.Fatal().Err(err).Msg("could not call AddGitleaksIgnore")
+		}
+
+		if info.IsDir() {
+			ignorePath = filepath.Join(tt.source, ".gitleaksignore")
+		} else {
+			ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore")
+		}
+		if err = detector.AddGitleaksIgnore(ignorePath); err != nil {
+			log.Fatal().Err(err).Msg("could not call AddGitleaksIgnore")
+		}
 		detector.FollowSymlinks = true
 		findings, err := detector.DetectFiles(tt.source)
 		if err != nil {

+ 5 - 4
go.mod

@@ -14,11 +14,12 @@ require (
 )
 
 require (
+	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
-	github.com/mattn/go-isatty v0.0.14 // indirect
-	github.com/mattn/go-runewidth v0.0.13 // indirect
+	github.com/mattn/go-isatty v0.0.17 // indirect
+	github.com/mattn/go-runewidth v0.0.14 // indirect
 	github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 // indirect
-	github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 // indirect
+	github.com/muesli/termenv v0.15.1 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 )
 
@@ -39,7 +40,7 @@ require (
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
-	golang.org/x/sys v0.0.0-20211110154304-99a53858aa08 // indirect
+	golang.org/x/sys v0.6.0 // indirect
 	golang.org/x/text v0.3.6 // indirect
 	gopkg.in/ini.v1 v1.62.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect

+ 11 - 0
go.sum

@@ -43,6 +43,8 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -195,9 +197,13 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
+github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -215,6 +221,8 @@ github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9Fn
 github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
 github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 h1:STjmj0uFfRryL9fzRA/OupNppeAID6QJYPMavTL7jtY=
 github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
+github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
+github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
 github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
@@ -434,6 +442,9 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211110154304-99a53858aa08 h1:WecRHqgE09JBkh/584XIE6PMz5KKE/vER4izNUi30AQ=
 golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

+ 102 - 0
report/junit.go

@@ -0,0 +1,102 @@
+package report
+
+import (
+	"encoding/json"
+	"encoding/xml"
+	"fmt"
+	"io"
+	"strconv"
+)
+
+func writeJunit(findings []Finding, w io.WriteCloser) error {
+	testSuites := TestSuites{
+		TestSuites: getTestSuites(findings),
+	}
+
+	io.WriteString(w, xml.Header)
+	encoder := xml.NewEncoder(w)
+	encoder.Indent("", "\t")
+	return encoder.Encode(testSuites)
+}
+
+func getTestSuites(findings []Finding) []TestSuite {
+	return []TestSuite{
+		{
+			Failures:  strconv.Itoa(len(findings)),
+			Name:      "gitleaks",
+			Tests:     strconv.Itoa(len(findings)),
+			TestCases: getTestCases(findings),
+			Time:      "",
+		},
+	}
+}
+
+func getTestCases(findings []Finding) []TestCase {
+	testCases := []TestCase{}
+	for _, f := range findings {
+		testCase := TestCase{
+			Classname: f.Description,
+			Failure:   getFailure(f),
+			File:      f.File,
+			Name:      getMessage(f),
+			Time:      "",
+		}
+		testCases = append(testCases, testCase)
+	}
+	return testCases
+}
+
+func getFailure(f Finding) Failure {
+	return Failure{
+		Data:    getData(f),
+		Message: getMessage(f),
+		Type:    f.Description,
+	}
+}
+
+func getData(f Finding) string {
+	data, err := json.MarshalIndent(f, "", "\t")
+	if err != nil {
+		fmt.Println(err)
+		return ""
+	}
+	return string(data)
+}
+
+func getMessage(f Finding) string {
+	if f.Commit == "" {
+		return fmt.Sprintf("%s has detected a secret in file %s, line %s.", f.RuleID, f.File, strconv.Itoa(f.StartLine))
+	}
+
+	return fmt.Sprintf("%s has detected a secret in file %s, line %s, at commit %s.", f.RuleID, f.File, strconv.Itoa(f.StartLine), f.Commit)
+}
+
+type TestSuites struct {
+	XMLName    xml.Name `xml:"testsuites"`
+	TestSuites []TestSuite
+}
+
+type TestSuite struct {
+	XMLName   xml.Name   `xml:"testsuite"`
+	Failures  string     `xml:"failures,attr"`
+	Name      string     `xml:"name,attr"`
+	Tests     string     `xml:"tests,attr"`
+	TestCases []TestCase `xml:"testcase"`
+	Time      string     `xml:"time,attr"`
+}
+
+type TestCase struct {
+	XMLName   xml.Name `xml:"testcase"`
+	Classname string   `xml:"classname,attr"`
+	Failure   Failure  `xml:"failure"`
+	File      string   `xml:"file,attr"`
+	Name      string   `xml:"name,attr"`
+	Time      string   `xml:"time,attr"`
+}
+
+type Failure struct {
+	XMLName xml.Name `xml:"failure"`
+	Data    string   `xml:",chardata"`
+	Message string   `xml:"message,attr"`
+	Type    string   `xml:"type,attr"`
+}

+ 107 - 0
report/junit_test.go

@@ -0,0 +1,107 @@
+package report
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func TestWriteJunit(t *testing.T) {
+	tests := []struct {
+		findings       []Finding
+		testReportName string
+		expected       string
+		wantEmpty      bool
+	}{
+		{
+			testReportName: "simple",
+			expected:       filepath.Join(expectPath, "report", "junit_simple.xml"),
+			findings: []Finding{
+				{
+
+					Description: "Test Rule",
+					RuleID:      "test-rule",
+					Match:       "line containing secret",
+					Secret:      "a secret",
+					StartLine:   1,
+					EndLine:     2,
+					StartColumn: 1,
+					EndColumn:   2,
+					Message:     "opps",
+					File:        "auth.py",
+					Commit:      "0000000000000000",
+					Author:      "John Doe",
+					Email:       "johndoe@gmail.com",
+					Date:        "10-19-2003",
+					Tags:        []string{},
+				},
+				{
+
+					Description: "Test Rule",
+					RuleID:      "test-rule",
+					Match:       "line containing secret",
+					Secret:      "a secret",
+					StartLine:   2,
+					EndLine:     3,
+					StartColumn: 1,
+					EndColumn:   2,
+					Message:     "",
+					File:        "auth.py",
+					Commit:      "",
+					Author:      "",
+					Email:       "",
+					Date:        "",
+					Tags:        []string{},
+				},
+			},
+		},
+		{
+			testReportName: "empty",
+			expected:       filepath.Join(expectPath, "report", "junit_empty.xml"),
+			findings:       []Finding{},
+		},
+	}
+
+	for _, test := range tests {
+		// create tmp file using os.TempDir()
+		tmpfile, err := os.Create(filepath.Join(tmpPath, test.testReportName+".xml"))
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		err = writeJunit(test.findings, tmpfile)
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		got, err := os.ReadFile(tmpfile.Name())
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+		if test.wantEmpty {
+			if len(got) > 0 {
+				os.Remove(tmpfile.Name())
+				t.Errorf("Expected empty file, got %s", got)
+			}
+			os.Remove(tmpfile.Name())
+			continue
+		}
+		want, err := os.ReadFile(test.expected)
+		if err != nil {
+			os.Remove(tmpfile.Name())
+			t.Error(err)
+		}
+
+		if string(got) != string(want) {
+			err = os.WriteFile(strings.Replace(test.expected, ".xml", ".got.xml", 1), got, 0644)
+			if err != nil {
+				t.Error(err)
+			}
+			t.Errorf("got %s, want %s", string(got), string(want))
+		}
+
+		os.Remove(tmpfile.Name())
+	}
+}

+ 2 - 0
report/report.go

@@ -24,6 +24,8 @@ func Write(findings []Finding, cfg config.Config, ext string, reportPath string)
 		err = writeJson(findings, file)
 	case ".csv", "csv":
 		err = writeCsv(findings, file)
+	case ".xml", "junit":
+		err = writeJunit(findings, file)
 	case ".sarif", "sarif":
 		err = writeSarif(cfg, findings, file)
 	}

+ 16 - 0
report/report_test.go

@@ -68,6 +68,22 @@ func TestReport(t *testing.T) {
 				},
 			},
 		},
+		{
+			ext: ".xml",
+			findings: []Finding{
+				{
+					RuleID: "test-rule",
+				},
+			},
+		},
+		{
+			ext: "junit",
+			findings: []Finding{
+				{
+					RuleID: "test-rule",
+				},
+			},
+		},
 		// {
 		// 	ext: "SARIF",
 		// 	findings: []Finding{

+ 4 - 0
testdata/expected/report/junit_empty.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuites>
+	<testsuite failures="0" name="gitleaks" tests="0" time=""></testsuite>
+</testsuites>

+ 11 - 0
testdata/expected/report/junit_simple.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuites>
+	<testsuite failures="2" name="gitleaks" tests="2" time="">
+		<testcase classname="Test Rule" file="auth.py" name="test-rule has detected a secret in file auth.py, line 1, at commit 0000000000000000." time="">
+			<failure message="test-rule has detected a secret in file auth.py, line 1, at commit 0000000000000000." type="Test Rule">{&#xA;&#x9;&#34;Description&#34;: &#34;Test Rule&#34;,&#xA;&#x9;&#34;StartLine&#34;: 1,&#xA;&#x9;&#34;EndLine&#34;: 2,&#xA;&#x9;&#34;StartColumn&#34;: 1,&#xA;&#x9;&#34;EndColumn&#34;: 2,&#xA;&#x9;&#34;Match&#34;: &#34;line containing secret&#34;,&#xA;&#x9;&#34;Secret&#34;: &#34;a secret&#34;,&#xA;&#x9;&#34;File&#34;: &#34;auth.py&#34;,&#xA;&#x9;&#34;SymlinkFile&#34;: &#34;&#34;,&#xA;&#x9;&#34;Commit&#34;: &#34;0000000000000000&#34;,&#xA;&#x9;&#34;Entropy&#34;: 0,&#xA;&#x9;&#34;Author&#34;: &#34;John Doe&#34;,&#xA;&#x9;&#34;Email&#34;: &#34;johndoe@gmail.com&#34;,&#xA;&#x9;&#34;Date&#34;: &#34;10-19-2003&#34;,&#xA;&#x9;&#34;Message&#34;: &#34;opps&#34;,&#xA;&#x9;&#34;Tags&#34;: [],&#xA;&#x9;&#34;RuleID&#34;: &#34;test-rule&#34;,&#xA;&#x9;&#34;Fingerprint&#34;: &#34;&#34;&#xA;}</failure>
+		</testcase>
+		<testcase classname="Test Rule" file="auth.py" name="test-rule has detected a secret in file auth.py, line 2." time="">
+			<failure message="test-rule has detected a secret in file auth.py, line 2." type="Test Rule">{&#xA;&#x9;&#34;Description&#34;: &#34;Test Rule&#34;,&#xA;&#x9;&#34;StartLine&#34;: 2,&#xA;&#x9;&#34;EndLine&#34;: 3,&#xA;&#x9;&#34;StartColumn&#34;: 1,&#xA;&#x9;&#34;EndColumn&#34;: 2,&#xA;&#x9;&#34;Match&#34;: &#34;line containing secret&#34;,&#xA;&#x9;&#34;Secret&#34;: &#34;a secret&#34;,&#xA;&#x9;&#34;File&#34;: &#34;auth.py&#34;,&#xA;&#x9;&#34;SymlinkFile&#34;: &#34;&#34;,&#xA;&#x9;&#34;Commit&#34;: &#34;&#34;,&#xA;&#x9;&#34;Entropy&#34;: 0,&#xA;&#x9;&#34;Author&#34;: &#34;&#34;,&#xA;&#x9;&#34;Email&#34;: &#34;&#34;,&#xA;&#x9;&#34;Date&#34;: &#34;&#34;,&#xA;&#x9;&#34;Message&#34;: &#34;&#34;,&#xA;&#x9;&#34;Tags&#34;: [],&#xA;&#x9;&#34;RuleID&#34;: &#34;test-rule&#34;,&#xA;&#x9;&#34;Fingerprint&#34;: &#34;&#34;&#xA;}</failure>
+		</testcase>
+	</testsuite>
+</testsuites>

+ 1 - 0
testdata/repos/nogit/.gitleaksignore

@@ -0,0 +1 @@
+../testdata/repos/nogit/api.go:aws-access-key:20

+ 24 - 0
testdata/repos/nogit/api.go

@@ -0,0 +1,24 @@
+package main
+
+import "fmt"
+
+func main() {
+
+	var a = "initial"
+	fmt.Println(a)
+
+	var b, c int = 1, 2
+	fmt.Println(b, c)
+
+	var d = true
+	fmt.Println(d)
+
+	var e int
+	fmt.Println(e)
+
+	// opps I added a secret at line 20
+	awsToken := "AKIALALEMEL33243OLIA"
+
+	f := "apple"
+	fmt.Println(f)
+}

+ 2 - 0
testdata/repos/small/.gitleaksignore

@@ -0,0 +1,2 @@
+api/ignoreGlobal.go:aws-access-key:20
+53cd7a3c6eb4937f413e3c25e4a9f39289afa69e:api/ignoreCommit.go:aws-access-key:20

+ 24 - 0
testdata/repos/small/api/ignoreCommit.go

@@ -0,0 +1,24 @@
+package main
+
+import "fmt"
+
+func main() {
+
+	var a = "initial"
+	fmt.Println(a)
+
+	var b, c int = 1, 2
+	fmt.Println(b, c)
+
+	var d = true
+	fmt.Println(d)
+
+	var e int
+	fmt.Println(e)
+
+	// opps I added a secret at line 20
+	awsToken := "AKIALALEMEL33243OLIA"
+
+	f := "apple"
+	fmt.Println(f)
+}

+ 24 - 0
testdata/repos/small/api/ignoreGlobal.go

@@ -0,0 +1,24 @@
+package main
+
+import "fmt"
+
+func main() {
+
+	var a = "initial"
+	fmt.Println(a)
+
+	var b, c int = 1, 2
+	fmt.Println(b, c)
+
+	var d = true
+	fmt.Println(d)
+
+	var e int
+	fmt.Println(e)
+
+	// opps I added a secret at line 20
+	awsToken := "AKIALALEMEL33243OLIA"
+
+	f := "apple"
+	fmt.Println(f)
+}

+ 1 - 1
testdata/repos/small/dotGit/COMMIT_EDITMSG

@@ -1 +1 @@
-removing secret from foo package
+add .gitleaksignore test files

+ 1 - 1
testdata/repos/small/dotGit/ORIG_HEAD

@@ -1 +1 @@
-1b6da43b82b22e4eaa10bcf8ee591e91abbfc587
+cada78a9bf157ec05573f19e682d211f811c2e2d

BIN
testdata/repos/small/dotGit/index


+ 4 - 0
testdata/repos/small/dotGit/logs/HEAD

@@ -11,3 +11,7 @@ a122b33c6bad3ee54724f52f2caad385ab1982ab 1b6da43b82b22e4eaa10bcf8ee591e91abbfc58
 f1b58b97808f8e744f6a23c693859df5b5968901 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice <zricer@protonmail.com> 1635897009 -0500	checkout: moving from foo to main
 2e1db472eeba53f06c4026ae4566ea022e36598e f1b58b97808f8e744f6a23c693859df5b5968901 Zach Rice <zricer@protonmail.com> 1635897062 -0500	checkout: moving from main to foo
 f1b58b97808f8e744f6a23c693859df5b5968901 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice <zricer@protonmail.com> 1635897508 -0500	checkout: moving from foo to main
+2e1db472eeba53f06c4026ae4566ea022e36598e 4f77f1b3cc39d4b17e4cf4ba0a38f5daed9875b4 Richard Gomez <rmgomez368@gmail.com> 1681920523 -0400	commit: add .gitleaksignore test
+4f77f1b3cc39d4b17e4cf4ba0a38f5daed9875b4 cada78a9bf157ec05573f19e682d211f811c2e2d Richard Gomez <rmgomez368@gmail.com> 1681920658 -0400	commit (amend): add .gitleaksignore test
+cada78a9bf157ec05573f19e682d211f811c2e2d 2e1db472eeba53f06c4026ae4566ea022e36598e Richard Gomez <rmgomez368@gmail.com> 1681920730 -0400	reset: moving to HEAD~
+2e1db472eeba53f06c4026ae4566ea022e36598e 53cd7a3c6eb4937f413e3c25e4a9f39289afa69e Richard Gomez <rmgomez368@gmail.com> 1681920759 -0400	commit: add .gitleaksignore test files

+ 4 - 0
testdata/repos/small/dotGit/logs/refs/heads/main

@@ -1,2 +1,6 @@
 0000000000000000000000000000000000000000 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 Zach Rice <zricer@protonmail.com> 1635896329 -0500	clone: from github.com:gitleaks/test.git
 1b6da43b82b22e4eaa10bcf8ee591e91abbfc587 2e1db472eeba53f06c4026ae4566ea022e36598e Zach Rice <zricer@protonmail.com> 1635896648 -0500	pull origin main: Fast-forward
+2e1db472eeba53f06c4026ae4566ea022e36598e 4f77f1b3cc39d4b17e4cf4ba0a38f5daed9875b4 Richard Gomez <rmgomez368@gmail.com> 1681920523 -0400	commit: add .gitleaksignore test
+4f77f1b3cc39d4b17e4cf4ba0a38f5daed9875b4 cada78a9bf157ec05573f19e682d211f811c2e2d Richard Gomez <rmgomez368@gmail.com> 1681920658 -0400	commit (amend): add .gitleaksignore test
+cada78a9bf157ec05573f19e682d211f811c2e2d 2e1db472eeba53f06c4026ae4566ea022e36598e Richard Gomez <rmgomez368@gmail.com> 1681920730 -0400	reset: moving to HEAD~
+2e1db472eeba53f06c4026ae4566ea022e36598e 53cd7a3c6eb4937f413e3c25e4a9f39289afa69e Richard Gomez <rmgomez368@gmail.com> 1681920759 -0400	commit: add .gitleaksignore test files

BIN
testdata/repos/small/dotGit/objects/32/bfaec6697c1a693f71b183d3389ad2547bbb3c


BIN
testdata/repos/small/dotGit/objects/4f/77f1b3cc39d4b17e4cf4ba0a38f5daed9875b4


BIN
testdata/repos/small/dotGit/objects/53/cd7a3c6eb4937f413e3c25e4a9f39289afa69e


BIN
testdata/repos/small/dotGit/objects/8d/c20a62e189d3446cfb6ec328dbd379d64feb20


BIN
testdata/repos/small/dotGit/objects/98/1ab8417104c91bb9a4657f5d436c760c0aff50


BIN
testdata/repos/small/dotGit/objects/ac/bef43fdb053ec01e8e16697536d47853460cbc


+ 1 - 0
testdata/repos/small/dotGit/objects/ca/da78a9bf157ec05573f19e682d211f811c2e2d

@@ -0,0 +1 @@
+x•ÎAjÃ0…á®uй@Ãh$�e!»ì{ƒ±5±E-+(ʦ§¯½@v�>xs-%w >zS…$:YF±ŽÅy"�,G¢k�bbó�¦ûզɤ:IpwäÙ#±¨Ì*xpÇaŒjäÕ×Úà+Ï«´·Zôέ,Ãq¼.Eòvšk¹€åhGÂ@>Ñ#š£/»¾ç9Äo$%8-¹o*ßϼìµ)t}vó¸ÁLë

BIN
testdata/repos/small/dotGit/objects/da/eb160a13200a3422e73296a6892784a113e0d6


BIN
testdata/repos/small/dotGit/objects/df/7eaa59fc89b3e7258167605db2e582f1a78e6f


BIN
testdata/repos/small/dotGit/objects/fa/468655086f149d41a8e7db14ed054bebec7687


+ 1 - 1
testdata/repos/small/dotGit/refs/heads/main

@@ -1 +1 @@
-2e1db472eeba53f06c4026ae4566ea022e36598e
+53cd7a3c6eb4937f413e3c25e4a9f39289afa69e