diff --git a/README.md b/README.md index 9c6c140cdd..cc3b160f2a 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ $ gas -nosec=true ./... ### Output formats -Gas currently supports text, json and csv output formats. By default +Gas currently supports text, json, csv and JUnit XML output formats. By default results will be reported to stdout, but can also be written to an output file. The output format is controlled by the '-fmt' flag, and the output file is controlled by the '-out' flag as follows: diff --git a/cmd/gas/main.go b/cmd/gas/main.go index 68a62775e8..deef06e1b8 100644 --- a/cmd/gas/main.go +++ b/cmd/gas/main.go @@ -59,7 +59,7 @@ var ( flagIgnoreNoSec = flag.Bool("nosec", false, "Ignores #nosec comments when set") // format output - flagFormat = flag.String("fmt", "text", "Set output format. Valid options are: json, csv, html, or text") + flagFormat = flag.String("fmt", "text", "Set output format. Valid options are: json, csv, junit-xml, html, or text") // output file flagOutput = flag.String("out", "", "Set output file for results") diff --git a/output/formatter.go b/output/formatter.go index 39955c096b..d829c53e79 100644 --- a/output/formatter.go +++ b/output/formatter.go @@ -17,6 +17,7 @@ package output import ( "encoding/csv" "encoding/json" + "encoding/xml" htmlTemplate "html/template" "io" plainTemplate "text/template" @@ -36,6 +37,9 @@ const ( // ReportCSV set the output format to csv ReportCSV // CSV format + + // ReportJUnitXML set the output format to junit xml + ReportJUnitXML // JUnit XML format ) var text = `Results: @@ -70,6 +74,8 @@ func CreateReport(w io.Writer, format string, issues []*gas.Issue, metrics *gas. err = reportJSON(w, data) case "csv": err = reportCSV(w, data) + case "junit-xml": + err = reportJUnitXML(w, data) case "html": err = reportFromHTMLTemplate(w, html, data) case "text": @@ -112,6 +118,25 @@ func reportCSV(w io.Writer, data *reportInfo) error { return nil } +func reportJUnitXML(w io.Writer, data *reportInfo) error { + groupedData := groupDataByRules(data) + junitXMLStruct := createJUnitXMLStruct(groupedData) + + raw, err := xml.MarshalIndent(junitXMLStruct, "", "\t") + if err != nil { + return err + } + + xmlHeader := []byte("\n") + raw = append(xmlHeader, raw...) + _, err = w.Write(raw) + if err != nil { + return err + } + + return nil +} + func reportFromPlaintextTemplate(w io.Writer, reportTemplate string, data *reportInfo) error { t, e := plainTemplate.New("gas").Parse(reportTemplate) if e != nil { diff --git a/output/junit_xml_format.go b/output/junit_xml_format.go new file mode 100644 index 0000000000..97960791c3 --- /dev/null +++ b/output/junit_xml_format.go @@ -0,0 +1,73 @@ +package output + +import ( + "encoding/xml" + "strconv" + + "github.com/GoASTScanner/gas" +) + +type junitXMLReport struct { + XMLName xml.Name `xml:"testsuites"` + Testsuites []testsuite `xml:"testsuite"` +} + +type testsuite struct { + XMLName xml.Name `xml:"testsuite"` + Name string `xml:"name,attr"` + Tests int `xml:"tests,attr"` + Testcases []testcase `xml:"testcase"` +} + +type testcase struct { + XMLName xml.Name `xml:"testcase"` + Name string `xml:"name,attr"` + Failure failure `xml:"failure"` +} + +type failure struct { + XMLName xml.Name `xml:"failure"` + Message string `xml:"message,attr"` + Text string `xml:",innerxml"` +} + +func generatePlaintext(issue *gas.Issue) string { + return "Results:\n" + + "[" + issue.File + ":" + issue.Line + "] - " + + issue.What + " (Confidence: " + strconv.Itoa(int(issue.Confidence)) + + ", Severity: " + strconv.Itoa(int(issue.Severity)) + ")\n" + "> " + issue.Code +} + +func groupDataByRules(data *reportInfo) map[string][]*gas.Issue { + groupedData := make(map[string][]*gas.Issue) + for _, issue := range data.Issues { + if _, ok := groupedData[issue.What]; ok { + groupedData[issue.What] = append(groupedData[issue.What], issue) + } else { + groupedData[issue.What] = []*gas.Issue{issue} + } + } + return groupedData +} + +func createJUnitXMLStruct(groupedData map[string][]*gas.Issue) junitXMLReport { + var xmlReport junitXMLReport + for what, issues := range groupedData { + testsuite := testsuite{ + Name: what, + Tests: len(issues), + } + for _, issue := range issues { + testcase := testcase{ + Name: issue.File, + Failure: failure{ + Message: "Found 1 vulnerability. See stacktrace for details.", + Text: generatePlaintext(issue), + }, + } + testsuite.Testcases = append(testsuite.Testcases, testcase) + } + xmlReport.Testsuites = append(xmlReport.Testsuites, testsuite) + } + return xmlReport +}