Skip to content

Commit

Permalink
Add junit report format (#920)
Browse files Browse the repository at this point in the history
* add junit report format

* add fingerprint to expected test result to fix tests

* fix expected junit test report
  • Loading branch information
maltemorgenstern committed Jun 13, 2023
1 parent bc59944 commit 0dbdde8
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ build
.gitleaks.toml
cmd/generate/config/gitleaks.toml

# test results
testdata/expected/report/*.got.*

# Test binary
*.out

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
102 changes: 102 additions & 0 deletions report/junit.go
Original file line number Diff line number Diff line change
@@ -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 changes: 107 additions & 0 deletions report/junit_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions report/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
16 changes: 16 additions & 0 deletions report/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
4 changes: 4 additions & 0 deletions testdata/expected/report/junit_empty.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite failures="0" name="gitleaks" tests="0" time=""></testsuite>
</testsuites>
11 changes: 11 additions & 0 deletions testdata/expected/report/junit_simple.xml
Original file line number Diff line number Diff line change
@@ -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>

0 comments on commit 0dbdde8

Please sign in to comment.