From 46e55b908d61018e866dfc0f1b480d060e4f4778 Mon Sep 17 00:00:00 2001 From: Cosmin Cojocar Date: Mon, 24 Jun 2019 14:10:51 +0200 Subject: [PATCH 1/2] Fix the file path in the Sonarqube report Add some test to validate the Sonarqube formatter. Signed-off-by: Cosmin Cojocar --- cmd/gosec/main.go | 6 +- helpers.go | 8 +++ helpers_test.go | 20 ++++++ output/formatter.go | 24 ++++--- output/formatter_suite_test.go | 13 ++++ output/formatter_test.go | 110 +++++++++++++++++++++++++++++++++ 6 files changed, 172 insertions(+), 9 deletions(-) create mode 100644 output/formatter_suite_test.go create mode 100644 output/formatter_test.go diff --git a/cmd/gosec/main.go b/cmd/gosec/main.go index d740aa0..83e7692 100644 --- a/cmd/gosec/main.go +++ b/cmd/gosec/main.go @@ -318,7 +318,11 @@ func main() { os.Exit(0) } - rootPath := packages[0] + rootPath, err := gosec.RootPath(flag.Args()[0]) + if err != nil { + logger.Fatalf("Failed to get the root path of the project: %s", err) + } + // Create output report if err := saveOutput(*flagOutput, *flagFormat, rootPath, issues, metrics, errors); err != nil { logger.Fatal(err) diff --git a/helpers.go b/helpers.go index b2e5176..3fddae3 100644 --- a/helpers.go +++ b/helpers.go @@ -387,3 +387,11 @@ func PackagePaths(root string, exclude *regexp.Regexp) ([]string, error) { } return result, nil } + +// RootPath returns the absolute root path of a scan +func RootPath(root string) (string, error) { + if strings.HasSuffix(root, "...") { + root = root[0 : len(root)-3] + } + return filepath.Abs(root) +} diff --git a/helpers_test.go b/helpers_test.go index 636ef6d..f797203 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -3,6 +3,7 @@ package gosec_test import ( "io/ioutil" "os" + "path/filepath" "regexp" . "github.com/onsi/ginkgo" @@ -53,4 +54,23 @@ var _ = Describe("Helpers", func() { Expect(paths).Should(BeEmpty()) }) }) + + Context("when getting the root path", func() { + It("should return the absolute path from relative path", func() { + base := "test" + cwd, err := os.Getwd() + Expect(err).ShouldNot(HaveOccurred()) + root, err := gosec.RootPath(base) + Expect(err).ShouldNot(HaveOccurred()) + Expect(root).Should(Equal(filepath.Join(cwd, base))) + }) + It("should retrun the absolute path from ellipsis path", func() { + base := "test" + cwd, err := os.Getwd() + Expect(err).ShouldNot(HaveOccurred()) + root, err := gosec.RootPath(filepath.Join(base, "...")) + Expect(err).ShouldNot(HaveOccurred()) + Expect(root).Should(Equal(filepath.Join(cwd, base))) + }) + }) }) diff --git a/output/formatter.go b/output/formatter.go index e29664c..a63b51f 100644 --- a/output/formatter.go +++ b/output/formatter.go @@ -105,19 +105,32 @@ func CreateReport(w io.Writer, format, rootPath string, issues []*gosec.Issue, m } func reportSonarqube(rootPath string, w io.Writer, data *reportInfo) error { + si, err := convertToSonarIssues(rootPath, data) + if err != nil { + return err + } + raw, err := json.MarshalIndent(si, "", "\t") + if err != nil { + return err + } + _, err = w.Write(raw) + return err +} + +func convertToSonarIssues(rootPath string, data *reportInfo) (sonarIssues, error) { var si sonarIssues for _, issue := range data.Issues { lines := strings.Split(issue.Line, "-") startLine, err := strconv.Atoi(lines[0]) if err != nil { - return err + return si, err } endLine := startLine if len(lines) > 1 { endLine, err = strconv.Atoi(lines[1]) if err != nil { - return err + return si, err } } s := sonarIssue{ @@ -134,12 +147,7 @@ func reportSonarqube(rootPath string, w io.Writer, data *reportInfo) error { } si.SonarIssues = append(si.SonarIssues, s) } - raw, err := json.MarshalIndent(si, "", "\t") - if err != nil { - return err - } - _, err = w.Write(raw) - return err + return si, nil } func reportJSON(w io.Writer, data *reportInfo) error { diff --git a/output/formatter_suite_test.go b/output/formatter_suite_test.go new file mode 100644 index 0000000..62b1404 --- /dev/null +++ b/output/formatter_suite_test.go @@ -0,0 +1,13 @@ +package output + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestRules(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Formatters Suite") +} diff --git a/output/formatter_test.go b/output/formatter_test.go new file mode 100644 index 0000000..1d016f6 --- /dev/null +++ b/output/formatter_test.go @@ -0,0 +1,110 @@ +package output + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/securego/gosec" +) + +var _ = Describe("Formatter", func() { + BeforeEach(func() { + }) + Context("when converting to Sonarqube issues", func() { + It("it should parse the report info", func() { + data := &reportInfo{ + Errors: map[string][]gosec.Error{}, + Issues: []*gosec.Issue{ + &gosec.Issue{ + Severity: 2, + Confidence: 0, + RuleID: "test", + What: "test", + File: "/home/src/project/test.go", + Code: "", + Line: "1-2", + }, + }, + Stats: &gosec.Metrics{ + NumFiles: 0, + NumLines: 0, + NumNosec: 0, + NumFound: 0, + }, + } + want := sonarIssues{ + SonarIssues: []sonarIssue{ + { + EngineID: "gosec", + RuleID: "test", + PrimaryLocation: location{ + Message: "test", + FilePath: "test.go", + TextRange: textRange{ + StartLine: 1, + EndLine: 2, + }, + }, + Type: "VULNERABILITY", + Severity: "BLOCKER", + EffortMinutes: SonarqubeEffortMinutes, + }, + }, + } + + rootPath := "/home/src/project" + + issues, err := convertToSonarIssues(rootPath, data) + Expect(err).ShouldNot(HaveOccurred()) + Expect(issues).To(Equal(want)) + }) + + It("it should parse the report info with files in subfolders", func() { + data := &reportInfo{ + Errors: map[string][]gosec.Error{}, + Issues: []*gosec.Issue{ + &gosec.Issue{ + Severity: 2, + Confidence: 0, + RuleID: "test", + What: "test", + File: "/home/src/project/subfolder/test.go", + Code: "", + Line: "1-2", + }, + }, + Stats: &gosec.Metrics{ + NumFiles: 0, + NumLines: 0, + NumNosec: 0, + NumFound: 0, + }, + } + want := sonarIssues{ + SonarIssues: []sonarIssue{ + { + EngineID: "gosec", + RuleID: "test", + PrimaryLocation: location{ + Message: "test", + FilePath: "subfolder/test.go", + TextRange: textRange{ + StartLine: 1, + EndLine: 2, + }, + }, + Type: "VULNERABILITY", + Severity: "BLOCKER", + EffortMinutes: SonarqubeEffortMinutes, + }, + }, + } + + rootPath := "/home/src/project" + + issues, err := convertToSonarIssues(rootPath, data) + Expect(err).ShouldNot(HaveOccurred()) + Expect(issues).To(Equal(want)) + }) + }) +}) From 020479a8325a8fe974de5e11e2bb0dd0ab21afa2 Mon Sep 17 00:00:00 2001 From: Cosmin Cojocar Date: Mon, 24 Jun 2019 14:35:11 +0200 Subject: [PATCH 2/2] Support multiple root paths when generating the Sonarqube report Signed-off-by: Cosmin Cojocar --- cmd/gosec/main.go | 21 ++++---- output/formatter.go | 24 ++++++--- output/formatter_test.go | 107 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 134 insertions(+), 18 deletions(-) diff --git a/cmd/gosec/main.go b/cmd/gosec/main.go index 83e7692..7b2a886 100644 --- a/cmd/gosec/main.go +++ b/cmd/gosec/main.go @@ -171,19 +171,27 @@ func loadRules(include, exclude string) rules.RuleList { return rules.Generate(filters...) } -func saveOutput(filename, format, rootPath string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error { +func saveOutput(filename, format string, paths []string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error { + rootPaths := []string{} + for _, path := range paths { + rootPath, err := gosec.RootPath(path) + if err != nil { + return fmt.Errorf("failed to get the root path of the projects: %s", err) + } + rootPaths = append(rootPaths, rootPath) + } if filename != "" { outfile, err := os.Create(filename) if err != nil { return err } defer outfile.Close() - err = output.CreateReport(outfile, format, rootPath, issues, metrics, errors) + err = output.CreateReport(outfile, format, rootPaths, issues, metrics, errors) if err != nil { return err } } else { - err := output.CreateReport(os.Stdout, format, rootPath, issues, metrics, errors) + err := output.CreateReport(os.Stdout, format, rootPaths, issues, metrics, errors) if err != nil { return err } @@ -318,13 +326,8 @@ func main() { os.Exit(0) } - rootPath, err := gosec.RootPath(flag.Args()[0]) - if err != nil { - logger.Fatalf("Failed to get the root path of the project: %s", err) - } - // Create output report - if err := saveOutput(*flagOutput, *flagFormat, rootPath, issues, metrics, errors); err != nil { + if err := saveOutput(*flagOutput, *flagFormat, flag.Args(), issues, metrics, errors); err != nil { logger.Fatal(err) } diff --git a/output/formatter.go b/output/formatter.go index a63b51f..e66015b 100644 --- a/output/formatter.go +++ b/output/formatter.go @@ -76,7 +76,7 @@ type reportInfo struct { // CreateReport generates a report based for the supplied issues and metrics given // the specified format. The formats currently accepted are: json, csv, html and text. -func CreateReport(w io.Writer, format, rootPath string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error { +func CreateReport(w io.Writer, format string, rootPaths []string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error { data := &reportInfo{ Errors: errors, Issues: issues, @@ -97,15 +97,15 @@ func CreateReport(w io.Writer, format, rootPath string, issues []*gosec.Issue, m case "text": err = reportFromPlaintextTemplate(w, text, data) case "sonarqube": - err = reportSonarqube(rootPath, w, data) + err = reportSonarqube(rootPaths, w, data) default: err = reportFromPlaintextTemplate(w, text, data) } return err } -func reportSonarqube(rootPath string, w io.Writer, data *reportInfo) error { - si, err := convertToSonarIssues(rootPath, data) +func reportSonarqube(rootPaths []string, w io.Writer, data *reportInfo) error { + si, err := convertToSonarIssues(rootPaths, data) if err != nil { return err } @@ -117,11 +117,20 @@ func reportSonarqube(rootPath string, w io.Writer, data *reportInfo) error { return err } -func convertToSonarIssues(rootPath string, data *reportInfo) (sonarIssues, error) { +func convertToSonarIssues(rootPaths []string, data *reportInfo) (sonarIssues, error) { var si sonarIssues for _, issue := range data.Issues { - lines := strings.Split(issue.Line, "-") + var sonarFilePath string + for _, rootPath := range rootPaths { + if strings.HasPrefix(issue.File, rootPath) { + sonarFilePath = strings.Replace(issue.File, rootPath+"/", "", 1) + } + } + if sonarFilePath == "" { + continue + } + lines := strings.Split(issue.Line, "-") startLine, err := strconv.Atoi(lines[0]) if err != nil { return si, err @@ -133,12 +142,13 @@ func convertToSonarIssues(rootPath string, data *reportInfo) (sonarIssues, error return si, err } } + s := sonarIssue{ EngineID: "gosec", RuleID: issue.RuleID, PrimaryLocation: location{ Message: issue.What, - FilePath: strings.Replace(issue.File, rootPath+"/", "", 1), + FilePath: sonarFilePath, TextRange: textRange{StartLine: startLine, EndLine: endLine}, }, Type: "VULNERABILITY", diff --git a/output/formatter_test.go b/output/formatter_test.go index 1d016f6..339d166 100644 --- a/output/formatter_test.go +++ b/output/formatter_test.go @@ -54,7 +54,7 @@ var _ = Describe("Formatter", func() { rootPath := "/home/src/project" - issues, err := convertToSonarIssues(rootPath, data) + issues, err := convertToSonarIssues([]string{rootPath}, data) Expect(err).ShouldNot(HaveOccurred()) Expect(issues).To(Equal(want)) }) @@ -102,7 +102,110 @@ var _ = Describe("Formatter", func() { rootPath := "/home/src/project" - issues, err := convertToSonarIssues(rootPath, data) + issues, err := convertToSonarIssues([]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 := &reportInfo{ + Errors: map[string][]gosec.Error{}, + Issues: []*gosec.Issue{ + &gosec.Issue{ + Severity: 2, + Confidence: 0, + RuleID: "test", + What: "test", + File: "/home/src/project1/test.go", + Code: "", + Line: "1-2", + }, + }, + Stats: &gosec.Metrics{ + NumFiles: 0, + NumLines: 0, + NumNosec: 0, + NumFound: 0, + }, + } + want := sonarIssues{ + SonarIssues: nil, + } + + rootPath := "/home/src/project2" + + issues, err := convertToSonarIssues([]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 := &reportInfo{ + Errors: map[string][]gosec.Error{}, + Issues: []*gosec.Issue{ + &gosec.Issue{ + Severity: 2, + Confidence: 0, + RuleID: "test", + What: "test", + File: "/home/src/project1/test-project1.go", + Code: "", + Line: "1-2", + }, + &gosec.Issue{ + Severity: 2, + Confidence: 0, + RuleID: "test", + What: "test", + File: "/home/src/project2/test-project2.go", + Code: "", + Line: "1-2", + }, + }, + Stats: &gosec.Metrics{ + NumFiles: 0, + NumLines: 0, + NumNosec: 0, + NumFound: 0, + }, + } + want := sonarIssues{ + SonarIssues: []sonarIssue{ + { + EngineID: "gosec", + RuleID: "test", + PrimaryLocation: location{ + Message: "test", + FilePath: "test-project1.go", + TextRange: textRange{ + StartLine: 1, + EndLine: 2, + }, + }, + Type: "VULNERABILITY", + Severity: "BLOCKER", + EffortMinutes: SonarqubeEffortMinutes, + }, + { + EngineID: "gosec", + RuleID: "test", + PrimaryLocation: location{ + Message: "test", + FilePath: "test-project2.go", + TextRange: textRange{ + StartLine: 1, + EndLine: 2, + }, + }, + Type: "VULNERABILITY", + Severity: "BLOCKER", + EffortMinutes: SonarqubeEffortMinutes, + }, + }, + } + + rootPaths := []string{"/home/src/project1", "/home/src/project2"} + + issues, err := convertToSonarIssues(rootPaths, data) Expect(err).ShouldNot(HaveOccurred()) Expect(issues).To(Equal(want)) })