diff --git a/cmd/gosec/main.go b/cmd/gosec/main.go index e954e53..72dca57 100644 --- a/cmd/gosec/main.go +++ b/cmd/gosec/main.go @@ -24,7 +24,7 @@ import ( "strings" "github.com/securego/gosec/v2" - "github.com/securego/gosec/v2/output" + "github.com/securego/gosec/v2/report" "github.com/securego/gosec/v2/rules" ) @@ -202,12 +202,12 @@ func saveOutput(filename, format string, color bool, paths []string, issues []*g return err } defer outfile.Close() // #nosec G307 - err = output.CreateReport(outfile, format, color, rootPaths, issues, metrics, errors) + err = report.CreateReport(outfile, format, color, rootPaths, issues, metrics, errors) if err != nil { return err } } else { - err := output.CreateReport(os.Stdout, format, color, rootPaths, issues, metrics, errors) + err := report.CreateReport(os.Stdout, format, color, rootPaths, issues, metrics, errors) if err != nil { return err } diff --git a/output/formatter.go b/output/formatter.go deleted file mode 100644 index 6b12a10..0000000 --- a/output/formatter.go +++ /dev/null @@ -1,326 +0,0 @@ -// (c) Copyright 2016 Hewlett Packard Enterprise Development LP -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package output - -import ( - "bufio" - "bytes" - "encoding/csv" - "encoding/json" - "encoding/xml" - "fmt" - htmlTemplate "html/template" - "io" - "strconv" - "strings" - plainTemplate "text/template" - - color "github.com/gookit/color" - "github.com/securego/gosec/v2" - "github.com/securego/gosec/v2/formatter" - "github.com/securego/gosec/v2/sarif" - "gopkg.in/yaml.v2" -) - -// ReportFormat enumerates the output format for reported issues -type ReportFormat int - -const ( - // ReportText is the default format that writes to stdout - ReportText ReportFormat = iota // Plain text format - - // ReportJSON set the output format to json - ReportJSON // Json format - - // ReportCSV set the output format to csv - ReportCSV // CSV format - - // ReportJUnitXML set the output format to junit xml - ReportJUnitXML // JUnit XML format - - // ReportSARIF set the output format to SARIF - ReportSARIF // SARIF format -) - -var text = `Results: -{{range $filePath,$fileErrors := .Errors}} -Golang errors in file: [{{ $filePath }}]: -{{range $index, $error := $fileErrors}} - > [line {{$error.Line}} : column {{$error.Column}}] - {{$error.Err}} -{{end}} -{{end}} -{{ range $index, $issue := .Issues }} -[{{ highlight $issue.FileLocation $issue.Severity }}] - {{ $issue.RuleID }} (CWE-{{ $issue.Cwe.ID }}): {{ $issue.What }} (Confidence: {{ $issue.Confidence}}, Severity: {{ $issue.Severity }}) -{{ printCode $issue }} - -{{ end }} -{{ notice "Summary:" }} - Files: {{.Stats.NumFiles}} - Lines: {{.Stats.NumLines}} - Nosec: {{.Stats.NumNosec}} - Issues: {{ if eq .Stats.NumFound 0 }} - {{- success .Stats.NumFound }} - {{- else }} - {{- danger .Stats.NumFound }} - {{- end }} - -` - -// CreateReport generates a report based for the supplied issues and metrics given -// the specified format. The formats currently accepted are: json, yaml, csv, junit-xml, html, sonarqube, golint and text. -func CreateReport(w io.Writer, format string, enableColor bool, rootPaths []string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error { - data := &formatter.ReportInfo{ - Errors: errors, - Issues: issues, - Stats: metrics, - } - var err error - switch format { - case "json": - err = reportJSON(w, data) - case "yaml": - err = reportYAML(w, data) - case "csv": - err = reportCSV(w, data) - case "junit-xml": - err = reportJUnitXML(w, data) - case "html": - err = reportFromHTMLTemplate(w, html, data) - case "text": - err = reportFromPlaintextTemplate(w, text, enableColor, data) - case "sonarqube": - err = reportSonarqube(rootPaths, w, data) - case "golint": - err = reportGolint(w, data) - case "sarif": - err = reportSARIFTemplate(rootPaths, w, data) - default: - err = reportFromPlaintextTemplate(w, text, enableColor, data) - } - return err -} - -func reportSonarqube(rootPaths []string, w io.Writer, data *formatter.ReportInfo) error { - si, err := convertToSonarIssues(rootPaths, data) - if err != nil { - return err - } - raw, err := json.MarshalIndent(si, "", "\t") - if err != nil { - return err - } - _, err = w.Write(raw) - return err -} - -func reportJSON(w io.Writer, data *formatter.ReportInfo) error { - raw, err := json.MarshalIndent(data, "", "\t") - if err != nil { - return err - } - - _, err = w.Write(raw) - return err -} - -func reportYAML(w io.Writer, data *formatter.ReportInfo) error { - raw, err := yaml.Marshal(data) - if err != nil { - return err - } - _, err = w.Write(raw) - return err -} - -func reportCSV(w io.Writer, data *formatter.ReportInfo) error { - out := csv.NewWriter(w) - defer out.Flush() - for _, issue := range data.Issues { - err := out.Write([]string{ - issue.File, - issue.Line, - issue.What, - issue.Severity.String(), - issue.Confidence.String(), - issue.Code, - fmt.Sprintf("CWE-%s", issue.Cwe.ID), - }) - if err != nil { - return err - } - } - return nil -} - -func reportGolint(w io.Writer, data *formatter.ReportInfo) error { - // Output Sample: - // /tmp/main.go:11:14: [CWE-310] RSA keys should be at least 2048 bits (Rule:G403, Severity:MEDIUM, Confidence:HIGH) - - for _, issue := range data.Issues { - what := issue.What - if issue.Cwe.ID != "" { - what = fmt.Sprintf("[CWE-%s] %s", issue.Cwe.ID, issue.What) - } - - // issue.Line uses "start-end" format for multiple line detection. - lines := strings.Split(issue.Line, "-") - start := lines[0] - - _, err := fmt.Fprintf(w, "%s:%s:%s: %s (Rule:%s, Severity:%s, Confidence:%s)\n", - issue.File, - start, - issue.Col, - what, - issue.RuleID, - issue.Severity.String(), - issue.Confidence.String(), - ) - if err != nil { - return err - } - } - return nil -} - -func reportJUnitXML(w io.Writer, data *formatter.ReportInfo) error { - junitXMLStruct := createJUnitXMLStruct(data) - 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 reportSARIFTemplate(rootPaths []string, w io.Writer, data *formatter.ReportInfo) error { - sr, err := sarif.ConvertToSarifReport(rootPaths, data) - if err != nil { - return err - } - raw, err := json.MarshalIndent(sr, "", "\t") - if err != nil { - return err - } - - _, err = w.Write(raw) - return err -} - -func reportFromPlaintextTemplate(w io.Writer, reportTemplate string, enableColor bool, data *formatter.ReportInfo) error { - t, e := plainTemplate. - New("gosec"). - Funcs(plainTextFuncMap(enableColor)). - Parse(reportTemplate) - if e != nil { - return e - } - - return t.Execute(w, data) -} - -func reportFromHTMLTemplate(w io.Writer, reportTemplate string, data *formatter.ReportInfo) error { - t, e := htmlTemplate.New("gosec").Parse(reportTemplate) - if e != nil { - return e - } - - return t.Execute(w, data) -} - -func plainTextFuncMap(enableColor bool) plainTemplate.FuncMap { - if enableColor { - return plainTemplate.FuncMap{ - "highlight": highlight, - "danger": color.Danger.Render, - "notice": color.Notice.Render, - "success": color.Success.Render, - "printCode": printCodeSnippet, - } - } - - // by default those functions return the given content untouched - return plainTemplate.FuncMap{ - "highlight": func(t string, s gosec.Score) string { - return t - }, - "danger": fmt.Sprint, - "notice": fmt.Sprint, - "success": fmt.Sprint, - "printCode": printCodeSnippet, - } -} - -var ( - errorTheme = color.New(color.FgLightWhite, color.BgRed) - warningTheme = color.New(color.FgBlack, color.BgYellow) - defaultTheme = color.New(color.FgWhite, color.BgBlack) -) - -// highlight returns content t colored based on Score -func highlight(t string, s gosec.Score) string { - switch s { - case gosec.High: - return errorTheme.Sprint(t) - case gosec.Medium: - return warningTheme.Sprint(t) - default: - return defaultTheme.Sprint(t) - } -} - -// printCodeSnippet prints the code snippet from the issue by adding a marker to the affected line -func printCodeSnippet(issue *gosec.Issue) string { - start, end := parseLine(issue.Line) - scanner := bufio.NewScanner(strings.NewReader(issue.Code)) - var buf bytes.Buffer - line := start - for scanner.Scan() { - codeLine := scanner.Text() - if strings.HasPrefix(codeLine, strconv.Itoa(line)) && line <= end { - codeLine = " > " + codeLine + "\n" - line++ - } else { - codeLine = " " + codeLine + "\n" - } - buf.WriteString(codeLine) - } - return buf.String() -} - -// parseLine extract the start and the end line numbers from a issue line -func parseLine(line string) (int, int) { - parts := strings.Split(line, "-") - start := parts[0] - end := start - if len(parts) > 1 { - end = parts[1] - } - s, err := strconv.Atoi(start) - if err != nil { - return -1, -1 - } - e, err := strconv.Atoi(end) - if err != nil { - return -1, -1 - } - return s, e -} diff --git a/output/junit_xml_format.go b/output/junit_xml_format.go deleted file mode 100644 index cbf4c3e..0000000 --- a/output/junit_xml_format.go +++ /dev/null @@ -1,70 +0,0 @@ -package output - -import ( - "encoding/xml" - htmlLib "html" - "strconv" - - "github.com/securego/gosec/v2" - "github.com/securego/gosec/v2/formatter" -) - -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 *gosec.Issue) string { - return "Results:\n" + - "[" + issue.File + ":" + issue.Line + "] - " + - issue.What + " (Confidence: " + strconv.Itoa(int(issue.Confidence)) + - ", Severity: " + strconv.Itoa(int(issue.Severity)) + - ", CWE: " + issue.Cwe.ID + ")\n" + "> " + htmlLib.EscapeString(issue.Code) -} - -func createJUnitXMLStruct(data *formatter.ReportInfo) junitXMLReport { - var xmlReport junitXMLReport - testsuites := map[string]int{} - - for _, issue := range data.Issues { - index, ok := testsuites[issue.What] - if !ok { - xmlReport.Testsuites = append(xmlReport.Testsuites, testsuite{ - Name: issue.What, - }) - index = len(xmlReport.Testsuites) - 1 - testsuites[issue.What] = index - } - testcase := testcase{ - Name: issue.File, - Failure: failure{ - Message: "Found 1 vulnerability. See stacktrace for details.", - Text: generatePlaintext(issue), - }, - } - - xmlReport.Testsuites[index].Testcases = append(xmlReport.Testsuites[index].Testcases, testcase) - xmlReport.Testsuites[index].Tests++ - } - - return xmlReport -} diff --git a/formatter/types.go b/report/core/types.go similarity index 92% rename from formatter/types.go rename to report/core/types.go index 32285ed..540891b 100644 --- a/formatter/types.go +++ b/report/core/types.go @@ -1,4 +1,4 @@ -package formatter +package core import ( "github.com/securego/gosec/v2" diff --git a/report/csv/writer.go b/report/csv/writer.go new file mode 100644 index 0000000..799be87 --- /dev/null +++ b/report/csv/writer.go @@ -0,0 +1,29 @@ +package csv + +import ( + "encoding/csv" + "fmt" + "github.com/securego/gosec/v2/report/core" + "io" +) + +//WriteReport write a report in csv format to the output writer +func WriteReport(w io.Writer, data *core.ReportInfo) error { + out := csv.NewWriter(w) + defer out.Flush() + for _, issue := range data.Issues { + err := out.Write([]string{ + issue.File, + issue.Line, + issue.What, + issue.Severity.String(), + issue.Confidence.String(), + issue.Code, + fmt.Sprintf("CWE-%s", issue.Cwe.ID), + }) + if err != nil { + return err + } + } + return nil +} diff --git a/report/formatter.go b/report/formatter.go new file mode 100644 index 0000000..eb1eaa3 --- /dev/null +++ b/report/formatter.go @@ -0,0 +1,84 @@ +// (c) Copyright 2016 Hewlett Packard Enterprise Development LP +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package report + +import ( + "github.com/securego/gosec/v2" + "github.com/securego/gosec/v2/report/core" + "github.com/securego/gosec/v2/report/csv" + "github.com/securego/gosec/v2/report/golint" + "github.com/securego/gosec/v2/report/html" + "github.com/securego/gosec/v2/report/json" + "github.com/securego/gosec/v2/report/junit" + "github.com/securego/gosec/v2/report/sarif" + "github.com/securego/gosec/v2/report/sonar" + "github.com/securego/gosec/v2/report/text" + "github.com/securego/gosec/v2/report/yaml" + "io" +) + +// Format enumerates the output format for reported issues +type Format int + +const ( + // ReportText is the default format that writes to stdout + ReportText Format = iota // Plain text format + + // ReportJSON set the output format to json + ReportJSON // Json format + + // ReportCSV set the output format to csv + ReportCSV // CSV format + + // ReportJUnitXML set the output format to junit xml + ReportJUnitXML // JUnit XML format + + // ReportSARIF set the output format to SARIF + ReportSARIF // SARIF format +) + +// CreateReport generates a report based for the supplied issues and metrics given +// the specified format. The formats currently accepted are: json, yaml, csv, junit-xml, html, sonarqube, golint and text. +func CreateReport(w io.Writer, format string, enableColor bool, rootPaths []string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error { + data := &core.ReportInfo{ + Errors: errors, + Issues: issues, + Stats: metrics, + } + var err error + switch format { + case "json": + err = json.WriteReport(w, data) + case "yaml": + err = yaml.WriteReport(w, data) + case "csv": + err = csv.WriteReport(w, data) + case "junit-xml": + err = junit.WriteReport(w, data) + case "html": + err = html.WriteReport(w, data) + case "text": + err = text.WriteReport(w, data, enableColor) + case "sonarqube": + err = sonar.WriteReport(w, data, rootPaths) + case "golint": + err = golint.WriteReport(w, data) + case "sarif": + err = sarif.WriteReport(w, data, rootPaths) + default: + err = text.WriteReport(w, data, enableColor) + } + return err +} diff --git a/output/formatter_suite_test.go b/report/formatter_suite_test.go similarity index 92% rename from output/formatter_suite_test.go rename to report/formatter_suite_test.go index 62b1404..6ffb744 100644 --- a/output/formatter_suite_test.go +++ b/report/formatter_suite_test.go @@ -1,4 +1,4 @@ -package output +package report import ( . "github.com/onsi/ginkgo" diff --git a/output/formatter_test.go b/report/formatter_test.go similarity index 94% rename from output/formatter_test.go rename to report/formatter_test.go index c51e410..f501693 100644 --- a/output/formatter_test.go +++ b/report/formatter_test.go @@ -1,4 +1,4 @@ -package output +package report import ( "bytes" @@ -9,8 +9,9 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/securego/gosec/v2" - "github.com/securego/gosec/v2/formatter" - "github.com/securego/gosec/v2/sonar" + "github.com/securego/gosec/v2/report/core" + "github.com/securego/gosec/v2/report/junit" + "github.com/securego/gosec/v2/report/sonar" "gopkg.in/yaml.v2" ) @@ -35,10 +36,10 @@ func createIssue(ruleID string, cwe gosec.Cwe) gosec.Issue { } } -func createReportInfo(rule string, cwe gosec.Cwe) formatter.ReportInfo { +func createReportInfo(rule string, cwe gosec.Cwe) core.ReportInfo { issue := createIssue(rule, cwe) metrics := gosec.Metrics{} - return formatter.ReportInfo{ + return core.ReportInfo{ Errors: map[string][]gosec.Error{}, Issues: []*gosec.Issue{ &issue, @@ -59,7 +60,7 @@ var _ = Describe("Formatter", func() { }) Context("when converting to Sonarqube issues", func() { It("it should parse the report info", func() { - data := &formatter.ReportInfo{ + data := &core.ReportInfo{ Errors: map[string][]gosec.Error{}, Issues: []*gosec.Issue{ { @@ -94,20 +95,20 @@ var _ = Describe("Formatter", func() { }, Type: "VULNERABILITY", Severity: "BLOCKER", - EffortMinutes: SonarqubeEffortMinutes, + EffortMinutes: sonar.EffortMinutes, }, }, } rootPath := "/home/src/project" - issues, err := convertToSonarIssues([]string{rootPath}, data) + issues, err := sonar.GenerateReport([]string{rootPath}, data) Expect(err).ShouldNot(HaveOccurred()) Expect(*issues).To(Equal(*want)) }) It("it should parse the report info with files in subfolders", func() { - data := &formatter.ReportInfo{ + data := &core.ReportInfo{ Errors: map[string][]gosec.Error{}, Issues: []*gosec.Issue{ { @@ -142,19 +143,19 @@ var _ = Describe("Formatter", func() { }, Type: "VULNERABILITY", Severity: "BLOCKER", - EffortMinutes: SonarqubeEffortMinutes, + EffortMinutes: sonar.EffortMinutes, }, }, } rootPath := "/home/src/project" - issues, err := convertToSonarIssues([]string{rootPath}, data) + issues, err := sonar.GenerateReport([]string{rootPath}, data) Expect(err).ShouldNot(HaveOccurred()) Expect(*issues).To(Equal(*want)) }) It("it should not parse the report info for files from other projects", func() { - data := &formatter.ReportInfo{ + data := &core.ReportInfo{ Errors: map[string][]gosec.Error{}, Issues: []*gosec.Issue{ { @@ -180,13 +181,13 @@ var _ = Describe("Formatter", func() { rootPath := "/home/src/project2" - issues, err := convertToSonarIssues([]string{rootPath}, data) + issues, err := sonar.GenerateReport([]string{rootPath}, data) Expect(err).ShouldNot(HaveOccurred()) Expect(*issues).To(Equal(*want)) }) It("it should parse the report info for multiple projects projects", func() { - data := &formatter.ReportInfo{ + data := &core.ReportInfo{ Errors: map[string][]gosec.Error{}, Issues: []*gosec.Issue{ { @@ -230,7 +231,7 @@ var _ = Describe("Formatter", func() { }, Type: "VULNERABILITY", Severity: "BLOCKER", - EffortMinutes: SonarqubeEffortMinutes, + EffortMinutes: sonar.EffortMinutes, }, { EngineID: "gosec", @@ -245,14 +246,14 @@ var _ = Describe("Formatter", func() { }, Type: "VULNERABILITY", Severity: "BLOCKER", - EffortMinutes: SonarqubeEffortMinutes, + EffortMinutes: sonar.EffortMinutes, }, }, } rootPaths := []string{"/home/src/project1", "/home/src/project2"} - issues, err := convertToSonarIssues(rootPaths, data) + issues, err := sonar.GenerateReport(rootPaths, data) Expect(err).ShouldNot(HaveOccurred()) Expect(*issues).To(Equal(*want)) }) @@ -262,7 +263,7 @@ var _ = Describe("Formatter", func() { It("preserves order of issues", func() { issues := []*gosec.Issue{createIssueWithFileWhat("i1", "1"), createIssueWithFileWhat("i2", "2"), createIssueWithFileWhat("i3", "1")} - junitReport := createJUnitXMLStruct(&formatter.ReportInfo{Issues: issues}) + junitReport := junit.GenerateReport(&core.ReportInfo{Issues: issues}) testSuite := junitReport.Testsuites[0] diff --git a/report/golint/writer.go b/report/golint/writer.go new file mode 100644 index 0000000..39aa400 --- /dev/null +++ b/report/golint/writer.go @@ -0,0 +1,39 @@ +package golint + +import ( + "fmt" + "github.com/securego/gosec/v2/report/core" + "io" + "strings" +) + +//WriteReport write a report in golint format to the output writer +func WriteReport(w io.Writer, data *core.ReportInfo) error { + // Output Sample: + // /tmp/main.go:11:14: [CWE-310] RSA keys should be at least 2048 bits (Rule:G403, Severity:MEDIUM, Confidence:HIGH) + + for _, issue := range data.Issues { + what := issue.What + if issue.Cwe.ID != "" { + what = fmt.Sprintf("[CWE-%s] %s", issue.Cwe.ID, issue.What) + } + + // issue.Line uses "start-end" format for multiple line detection. + lines := strings.Split(issue.Line, "-") + start := lines[0] + + _, err := fmt.Fprintf(w, "%s:%s:%s: %s (Rule:%s, Severity:%s, Confidence:%s)\n", + issue.File, + start, + issue.Col, + what, + issue.RuleID, + issue.Severity.String(), + issue.Confidence.String(), + ) + if err != nil { + return err + } + } + return nil +} diff --git a/output/template.go b/report/html/template.go similarity index 99% rename from output/template.go rename to report/html/template.go index c69f586..07d15cb 100644 --- a/output/template.go +++ b/report/html/template.go @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -package output +package html -const html = ` +const templateContent = ` diff --git a/report/html/writer.go b/report/html/writer.go new file mode 100644 index 0000000..3bdfa44 --- /dev/null +++ b/report/html/writer.go @@ -0,0 +1,17 @@ +package html + +import ( + "github.com/securego/gosec/v2/report/core" + "html/template" + "io" +) + +//WriteReport write a report in html format to the output writer +func WriteReport(w io.Writer, data *core.ReportInfo) error { + t, e := template.New("gosec").Parse(templateContent) + if e != nil { + return e + } + + return t.Execute(w, data) +} diff --git a/report/json/writer.go b/report/json/writer.go new file mode 100644 index 0000000..1809569 --- /dev/null +++ b/report/json/writer.go @@ -0,0 +1,18 @@ +package json + +import ( + "encoding/json" + "github.com/securego/gosec/v2/report/core" + "io" +) + +//WriteReport write a report in json format to the output writer +func WriteReport(w io.Writer, data *core.ReportInfo) error { + raw, err := json.MarshalIndent(data, "", "\t") + if err != nil { + return err + } + + _, err = w.Write(raw) + return err +} diff --git a/report/junit/formatter.go b/report/junit/formatter.go new file mode 100644 index 0000000..c44b1ce --- /dev/null +++ b/report/junit/formatter.go @@ -0,0 +1,46 @@ +package junit + +import ( + "html" + "strconv" + + "github.com/securego/gosec/v2" + "github.com/securego/gosec/v2/report/core" +) + +func generatePlaintext(issue *gosec.Issue) string { + return "Results:\n" + + "[" + issue.File + ":" + issue.Line + "] - " + + issue.What + " (Confidence: " + strconv.Itoa(int(issue.Confidence)) + + ", Severity: " + strconv.Itoa(int(issue.Severity)) + + ", CWE: " + issue.Cwe.ID + ")\n" + "> " + html.EscapeString(issue.Code) +} + +//GenerateReport Convert a gosec report to a JUnit Report +func GenerateReport(data *core.ReportInfo) Report { + var xmlReport Report + testsuites := map[string]int{} + + for _, issue := range data.Issues { + index, ok := testsuites[issue.What] + if !ok { + xmlReport.Testsuites = append(xmlReport.Testsuites, &Testsuite{ + Name: issue.What, + }) + index = len(xmlReport.Testsuites) - 1 + testsuites[issue.What] = index + } + testcase := &Testcase{ + Name: issue.File, + Failure: &Failure{ + Message: "Found 1 vulnerability. See stacktrace for details.", + Text: generatePlaintext(issue), + }, + } + + xmlReport.Testsuites[index].Testcases = append(xmlReport.Testsuites[index].Testcases, testcase) + xmlReport.Testsuites[index].Tests++ + } + + return xmlReport +} diff --git a/report/junit/types.go b/report/junit/types.go new file mode 100644 index 0000000..cc36e92 --- /dev/null +++ b/report/junit/types.go @@ -0,0 +1,33 @@ +package junit + +import ( + "encoding/xml" +) + +//Report defines a JUnit XML report +type Report struct { + XMLName xml.Name `xml:"testsuites"` + Testsuites []*Testsuite `xml:"testsuite"` +} + +//Testsuite defines a JUnit testsuite +type Testsuite struct { + XMLName xml.Name `xml:"testsuite"` + Name string `xml:"name,attr"` + Tests int `xml:"tests,attr"` + Testcases []*Testcase `xml:"testcase"` +} + +//Testcase defines a JUnit testcase +type Testcase struct { + XMLName xml.Name `xml:"testcase"` + Name string `xml:"name,attr"` + Failure *Failure `xml:"failure"` +} + +//Failure defines a JUnit failure +type Failure struct { + XMLName xml.Name `xml:"failure"` + Message string `xml:"message,attr"` + Text string `xml:",innerxml"` +} diff --git a/report/junit/writer.go b/report/junit/writer.go new file mode 100644 index 0000000..318f9ec --- /dev/null +++ b/report/junit/writer.go @@ -0,0 +1,25 @@ +package junit + +import ( + "encoding/xml" + "github.com/securego/gosec/v2/report/core" + "io" +) + +//WriteReport write a report in JUnit format to the output writer +func WriteReport(w io.Writer, data *core.ReportInfo) error { + junitXMLStruct := GenerateReport(data) + 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 +} diff --git a/sarif/formatter.go b/report/sarif/formatter.go similarity index 97% rename from sarif/formatter.go rename to report/sarif/formatter.go index cdea442..6b94474 100644 --- a/sarif/formatter.go +++ b/report/sarif/formatter.go @@ -9,7 +9,7 @@ import ( "github.com/securego/gosec/v2" "github.com/securego/gosec/v2/cwe" - "github.com/securego/gosec/v2/formatter" + "github.com/securego/gosec/v2/report/core" ) type sarifLevel string @@ -22,8 +22,8 @@ const ( cweAcronym = "CWE" ) -//ConvertToSarifReport Convert a gosec report to a Sarif Report -func ConvertToSarifReport(rootPaths []string, data *formatter.ReportInfo) (*Report, error) { +//GenerateReport Convert a gosec report to a Sarif Report +func GenerateReport(rootPaths []string, data *core.ReportInfo) (*Report, error) { type rule struct { index int diff --git a/sarif/types.go b/report/sarif/types.go similarity index 100% rename from sarif/types.go rename to report/sarif/types.go diff --git a/report/sarif/writer.go b/report/sarif/writer.go new file mode 100644 index 0000000..3b9902e --- /dev/null +++ b/report/sarif/writer.go @@ -0,0 +1,22 @@ +package sarif + +import ( + "encoding/json" + "github.com/securego/gosec/v2/report/core" + "io" +) + +//WriteReport write a report in SARIF format to the output writer +func WriteReport(w io.Writer, data *core.ReportInfo,rootPaths []string) error { + sr, err := GenerateReport(rootPaths, data) + if err != nil { + return err + } + raw, err := json.MarshalIndent(sr, "", "\t") + if err != nil { + return err + } + + _, err = w.Write(raw) + return err +} diff --git a/output/sonarqube_format.go b/report/sonar/formatter.go similarity index 71% rename from output/sonarqube_format.go rename to report/sonar/formatter.go index 8692d5b..ba71830 100644 --- a/output/sonarqube_format.go +++ b/report/sonar/formatter.go @@ -1,20 +1,20 @@ -package output +package sonar import ( "github.com/securego/gosec/v2" - "github.com/securego/gosec/v2/formatter" - "github.com/securego/gosec/v2/sonar" + "github.com/securego/gosec/v2/report/core" "strconv" "strings" ) const ( - //SonarqubeEffortMinutes effort to fix in minutes - SonarqubeEffortMinutes = 5 + //EffortMinutes effort to fix in minutes + EffortMinutes = 5 ) -func convertToSonarIssues(rootPaths []string, data *formatter.ReportInfo) (*sonar.Report, error) { - si := &sonar.Report{Issues: []*sonar.Issue{}} +//GenerateReport Convert a gosec report to a Sonar Report +func GenerateReport(rootPaths []string, data *core.ReportInfo) (*Report, error) { + si := &Report{Issues: []*Issue{}} for _, issue := range data.Issues { sonarFilePath := parseFilePath(issue, rootPaths) @@ -30,21 +30,21 @@ func convertToSonarIssues(rootPaths []string, data *formatter.ReportInfo) (*sona primaryLocation := buildPrimaryLocation(issue.What, sonarFilePath, textRange) severity := getSonarSeverity(issue.Severity.String()) - s := &sonar.Issue{ + s := &Issue{ EngineID: "gosec", RuleID: issue.RuleID, PrimaryLocation: primaryLocation, Type: "VULNERABILITY", Severity: severity, - EffortMinutes: SonarqubeEffortMinutes, + EffortMinutes: EffortMinutes, } si.Issues = append(si.Issues, s) } return si, nil } -func buildPrimaryLocation(message string, filePath string, textRange *sonar.TextRange) *sonar.Location { - return &sonar.Location{ +func buildPrimaryLocation(message string, filePath string, textRange *TextRange) *Location { + return &Location{ Message: message, FilePath: filePath, TextRange: textRange, @@ -61,7 +61,7 @@ func parseFilePath(issue *gosec.Issue, rootPaths []string) string { return sonarFilePath } -func parseTextRange(issue *gosec.Issue) (*sonar.TextRange, error) { +func parseTextRange(issue *gosec.Issue) (*TextRange, error) { lines := strings.Split(issue.Line, "-") startLine, err := strconv.Atoi(lines[0]) if err != nil { @@ -74,7 +74,7 @@ func parseTextRange(issue *gosec.Issue) (*sonar.TextRange, error) { return nil, err } } - return &sonar.TextRange{StartLine: startLine, EndLine: endLine}, nil + return &TextRange{StartLine: startLine, EndLine: endLine}, nil } func getSonarSeverity(s string) string { diff --git a/sonar/types.go b/report/sonar/types.go similarity index 100% rename from sonar/types.go rename to report/sonar/types.go diff --git a/report/sonar/writer.go b/report/sonar/writer.go new file mode 100644 index 0000000..ca49516 --- /dev/null +++ b/report/sonar/writer.go @@ -0,0 +1,21 @@ +package sonar + +import ( + "encoding/json" + "github.com/securego/gosec/v2/report/core" + "io" +) + +//WriteReport write a report in sonar format to the output writer +func WriteReport(w io.Writer, data *core.ReportInfo, rootPaths []string) error { + si, err := GenerateReport(rootPaths, data) + if err != nil { + return err + } + raw, err := json.MarshalIndent(si, "", "\t") + if err != nil { + return err + } + _, err = w.Write(raw) + return err +} diff --git a/report/text/template.go b/report/text/template.go new file mode 100644 index 0000000..33f44d4 --- /dev/null +++ b/report/text/template.go @@ -0,0 +1,25 @@ +package text + +const templateContent = `Results: +{{range $filePath,$fileErrors := .Errors}} +Golang errors in file: [{{ $filePath }}]: +{{range $index, $error := $fileErrors}} + > [line {{$error.Line}} : column {{$error.Column}}] - {{$error.Err}} +{{end}} +{{end}} +{{ range $index, $issue := .Issues }} +[{{ highlight $issue.FileLocation $issue.Severity }}] - {{ $issue.RuleID }} (CWE-{{ $issue.Cwe.ID }}): {{ $issue.What }} (Confidence: {{ $issue.Confidence}}, Severity: {{ $issue.Severity }}) +{{ printCode $issue }} + +{{ end }} +{{ notice "Summary:" }} + Files: {{.Stats.NumFiles}} + Lines: {{.Stats.NumLines}} + Nosec: {{.Stats.NumNosec}} + Issues: {{ if eq .Stats.NumFound 0 }} + {{- success .Stats.NumFound }} + {{- else }} + {{- danger .Stats.NumFound }} + {{- end }} + +` diff --git a/report/text/writer.go b/report/text/writer.go new file mode 100644 index 0000000..43dfc65 --- /dev/null +++ b/report/text/writer.go @@ -0,0 +1,106 @@ +package text + +import ( + "bufio" + "bytes" + "fmt" + "github.com/gookit/color" + "github.com/securego/gosec/v2" + "github.com/securego/gosec/v2/report/core" + "io" + "strconv" + "strings" + "text/template" +) + +var ( + errorTheme = color.New(color.FgLightWhite, color.BgRed) + warningTheme = color.New(color.FgBlack, color.BgYellow) + defaultTheme = color.New(color.FgWhite, color.BgBlack) +) + +//WriteReport write a (colorized) report in text format +func WriteReport(w io.Writer, data *core.ReportInfo, enableColor bool) error { + t, e := template. + New("gosec"). + Funcs(plainTextFuncMap(enableColor)). + Parse(templateContent) + if e != nil { + return e + } + + return t.Execute(w, data) +} + +func plainTextFuncMap(enableColor bool) template.FuncMap { + if enableColor { + return template.FuncMap{ + "highlight": highlight, + "danger": color.Danger.Render, + "notice": color.Notice.Render, + "success": color.Success.Render, + "printCode": printCodeSnippet, + } + } + + // by default those functions return the given content untouched + return template.FuncMap{ + "highlight": func(t string, s gosec.Score) string { + return t + }, + "danger": fmt.Sprint, + "notice": fmt.Sprint, + "success": fmt.Sprint, + "printCode": printCodeSnippet, + } +} + +// highlight returns content t colored based on Score +func highlight(t string, s gosec.Score) string { + switch s { + case gosec.High: + return errorTheme.Sprint(t) + case gosec.Medium: + return warningTheme.Sprint(t) + default: + return defaultTheme.Sprint(t) + } +} + +// printCodeSnippet prints the code snippet from the issue by adding a marker to the affected line +func printCodeSnippet(issue *gosec.Issue) string { + start, end := parseLine(issue.Line) + scanner := bufio.NewScanner(strings.NewReader(issue.Code)) + var buf bytes.Buffer + line := start + for scanner.Scan() { + codeLine := scanner.Text() + if strings.HasPrefix(codeLine, strconv.Itoa(line)) && line <= end { + codeLine = " > " + codeLine + "\n" + line++ + } else { + codeLine = " " + codeLine + "\n" + } + buf.WriteString(codeLine) + } + return buf.String() +} + +// parseLine extract the start and the end line numbers from a issue line +func parseLine(line string) (int, int) { + parts := strings.Split(line, "-") + start := parts[0] + end := start + if len(parts) > 1 { + end = parts[1] + } + s, err := strconv.Atoi(start) + if err != nil { + return -1, -1 + } + e, err := strconv.Atoi(end) + if err != nil { + return -1, -1 + } + return s, e +} diff --git a/report/yaml/writer.go b/report/yaml/writer.go new file mode 100644 index 0000000..a67f851 --- /dev/null +++ b/report/yaml/writer.go @@ -0,0 +1,17 @@ +package yaml + +import ( + "github.com/securego/gosec/v2/report/core" + "gopkg.in/yaml.v2" + "io" +) + +//WriteReport write a report in yaml format to the output writer +func WriteReport(w io.Writer, data *core.ReportInfo) error { + raw, err := yaml.Marshal(data) + if err != nil { + return err + } + _, err = w.Write(raw) + return err +}