diff --git a/report/formatter_test.go b/report/formatter_test.go index 6f1b3f9..759dad5 100644 --- a/report/formatter_test.go +++ b/report/formatter_test.go @@ -83,10 +83,24 @@ var _ = Describe("Formatter", func() { }, } want := &sonar.Report{ + Rules: []*sonar.Rule{ + { + ID: "test", + Name: "test", + Description: "test", + EngineID: "gosec", + CleanCodeAttribute: "TRUSTWORTHY", + Impacts: []*sonar.Impact{ + { + SoftwareQuality: "SECURITY", + Severity: "HIGH", + }, + }, + }, + }, Issues: []*sonar.Issue{ { - EngineID: "gosec", - RuleID: "test", + RuleID: "test", PrimaryLocation: &sonar.Location{ Message: "test", FilePath: "test.go", @@ -95,8 +109,6 @@ var _ = Describe("Formatter", func() { EndLine: 2, }, }, - Type: "VULNERABILITY", - Severity: "BLOCKER", EffortMinutes: sonar.EffortMinutes, }, }, @@ -131,10 +143,24 @@ var _ = Describe("Formatter", func() { }, } want := &sonar.Report{ + Rules: []*sonar.Rule{ + { + ID: "test", + Name: "test", + Description: "test", + EngineID: "gosec", + CleanCodeAttribute: "TRUSTWORTHY", + Impacts: []*sonar.Impact{ + { + SoftwareQuality: "SECURITY", + Severity: "HIGH", + }, + }, + }, + }, Issues: []*sonar.Issue{ { - EngineID: "gosec", - RuleID: "test", + RuleID: "test", PrimaryLocation: &sonar.Location{ Message: "test", FilePath: "subfolder/test.go", @@ -143,8 +169,6 @@ var _ = Describe("Formatter", func() { EndLine: 2, }, }, - Type: "VULNERABILITY", - Severity: "BLOCKER", EffortMinutes: sonar.EffortMinutes, }, }, @@ -178,6 +202,7 @@ var _ = Describe("Formatter", func() { }, } want := &sonar.Report{ + Rules: []*sonar.Rule{}, Issues: []*sonar.Issue{}, } @@ -219,10 +244,24 @@ var _ = Describe("Formatter", func() { }, } want := &sonar.Report{ + Rules: []*sonar.Rule{ + { + ID: "test", + Name: "test", + Description: "test", + EngineID: "gosec", + CleanCodeAttribute: "TRUSTWORTHY", + Impacts: []*sonar.Impact{ + { + SoftwareQuality: "SECURITY", + Severity: "HIGH", + }, + }, + }, + }, Issues: []*sonar.Issue{ { - EngineID: "gosec", - RuleID: "test", + RuleID: "test", PrimaryLocation: &sonar.Location{ Message: "test", FilePath: "test-project1.go", @@ -231,13 +270,10 @@ var _ = Describe("Formatter", func() { EndLine: 2, }, }, - Type: "VULNERABILITY", - Severity: "BLOCKER", EffortMinutes: sonar.EffortMinutes, }, { - EngineID: "gosec", - RuleID: "test", + RuleID: "test", PrimaryLocation: &sonar.Location{ Message: "test", FilePath: "test-project2.go", @@ -246,8 +282,6 @@ var _ = Describe("Formatter", func() { EndLine: 2, }, }, - Type: "VULNERABILITY", - Severity: "BLOCKER", EffortMinutes: sonar.EffortMinutes, }, }, diff --git a/report/sonar/builder.go b/report/sonar/builder.go index a55c8ca..2679ddd 100644 --- a/report/sonar/builder.go +++ b/report/sonar/builder.go @@ -18,13 +18,30 @@ func NewTextRange(startLine int, endLine int) *TextRange { } // NewIssue instantiate an Issue -func NewIssue(engineID string, ruleID string, primaryLocation *Location, issueType string, severity string, effortMinutes int) *Issue { +func NewIssue(ruleID string, primaryLocation *Location, effortMinutes int) *Issue { return &Issue{ - EngineID: engineID, RuleID: ruleID, PrimaryLocation: primaryLocation, - Type: issueType, - Severity: severity, EffortMinutes: effortMinutes, } } + +// NewImpact instantiate an Impact. +func NewImpact(softwareQuality string, severity string) *Impact { + return &Impact{ + SoftwareQuality: softwareQuality, + Severity: severity, + } +} + +// NewRule instantiate a Rule. +func NewRule(id string, name string, description string, engineID string, cleanCodeAttribute string, impacts []*Impact) *Rule { + return &Rule{ + ID: id, + Name: name, + Description: description, + EngineID: engineID, + CleanCodeAttribute: cleanCodeAttribute, + Impacts: impacts, + } +} diff --git a/report/sonar/formatter.go b/report/sonar/formatter.go index d635d9d..a150929 100644 --- a/report/sonar/formatter.go +++ b/report/sonar/formatter.go @@ -6,16 +6,24 @@ import ( "github.com/securego/gosec/v2" "github.com/securego/gosec/v2/issue" + "github.com/securego/gosec/v2/rules" ) const ( // EffortMinutes effort to fix in minutes EffortMinutes = 5 + + sonarEngineID = "gosec" + sonarSoftwareQuality = "SECURITY" + sonarCleanCodeAttribute = "TRUSTWORTHY" ) // GenerateReport Convert a gosec report to a Sonar Report func GenerateReport(rootPaths []string, data *gosec.ReportInfo) (*Report, error) { - si := &Report{Issues: []*Issue{}} + si := &Report{Rules: []*Rule{}, Issues: []*Issue{}} + ruleDefinitions := rules.Generate(false).Rules + ruleIndex := make(map[string]*Rule) + for _, issue := range data.Issues { sonarFilePath := parseFilePath(issue, rootPaths) @@ -29,9 +37,28 @@ func GenerateReport(rootPaths []string, data *gosec.ReportInfo) (*Report, error) } primaryLocation := NewLocation(issue.What, sonarFilePath, textRange) - severity := getSonarSeverity(issue.Severity.String()) + severity := getImpactSeverity(issue.Severity.String()) - s := NewIssue("gosec", issue.RuleID, primaryLocation, "VULNERABILITY", severity, EffortMinutes) + if rule, ok := ruleIndex[issue.RuleID]; ok { + rule.Impacts = mergeRuleImpacts(rule.Impacts, severity) + } else { + description := issue.What + if def, found := ruleDefinitions[issue.RuleID]; found && def.Description != "" { + description = def.Description + } + newRule := NewRule( + issue.RuleID, + issue.RuleID, + description, + sonarEngineID, + sonarCleanCodeAttribute, + []*Impact{NewImpact(sonarSoftwareQuality, severity)}, + ) + ruleIndex[issue.RuleID] = newRule + si.Rules = append(si.Rules, newRule) + } + + s := NewIssue(issue.RuleID, primaryLocation, EffortMinutes) si.Issues = append(si.Issues, s) } return si, nil @@ -63,15 +90,41 @@ func parseTextRange(issue *issue.Issue) (*TextRange, error) { return NewTextRange(startLine, endLine), nil } -func getSonarSeverity(s string) string { +func getImpactSeverity(s string) string { switch s { case "LOW": - return "MINOR" + return "LOW" case "MEDIUM": - return "MAJOR" + return "MEDIUM" case "HIGH": - return "BLOCKER" + return "HIGH" default: return "INFO" } } + +func mergeRuleImpacts(existing []*Impact, severity string) []*Impact { + if len(existing) == 0 { + return []*Impact{NewImpact(sonarSoftwareQuality, severity)} + } + for _, impact := range existing { + if impact.SoftwareQuality == sonarSoftwareQuality { + if compareImpactSeverity(severity, impact.Severity) > 0 { + impact.Severity = severity + } + return existing + } + } + return append(existing, NewImpact(sonarSoftwareQuality, severity)) +} + +func compareImpactSeverity(a string, b string) int { + severityRank := map[string]int{ + "BLOCKER": 5, + "HIGH": 4, + "MEDIUM": 3, + "LOW": 2, + "INFO": 1, + } + return severityRank[a] - severityRank[b] +} diff --git a/report/sonar/sonar_test.go b/report/sonar/sonar_test.go index 1db9ed5..735442e 100644 --- a/report/sonar/sonar_test.go +++ b/report/sonar/sonar_test.go @@ -35,10 +35,24 @@ var _ = Describe("Sonar Formatter", func() { }, } want := &sonar.Report{ + Rules: []*sonar.Rule{ + { + ID: "test", + Name: "test", + Description: "test", + EngineID: "gosec", + CleanCodeAttribute: "TRUSTWORTHY", + Impacts: []*sonar.Impact{ + { + SoftwareQuality: "SECURITY", + Severity: "HIGH", + }, + }, + }, + }, Issues: []*sonar.Issue{ { - EngineID: "gosec", - RuleID: "test", + RuleID: "test", PrimaryLocation: &sonar.Location{ Message: "test", FilePath: "test.go", @@ -47,8 +61,62 @@ var _ = Describe("Sonar Formatter", func() { EndLine: 2, }, }, - Type: "VULNERABILITY", - Severity: "BLOCKER", + EffortMinutes: sonar.EffortMinutes, + }, + }, + } + + rootPath := "/home/src/project" + + issues, err := sonar.GenerateReport([]string{rootPath}, data) + Expect(err).ShouldNot(HaveOccurred()) + Expect(*issues).To(Equal(*want)) + }) + + It("it should enrich rules with metadata when available", func() { + data := &gosec.ReportInfo{ + Errors: map[string][]gosec.Error{}, + Issues: []*issue.Issue{ + { + Severity: issue.Medium, + Confidence: 0, + RuleID: "G101", + What: "Potential hardcoded credentials", + File: "/home/src/project/test.go", + Code: "", + Line: "5", + }, + }, + Stats: &gosec.Metrics{}, + } + + want := &sonar.Report{ + Rules: []*sonar.Rule{ + { + ID: "G101", + Name: "G101", + Description: "Look for hardcoded credentials", + EngineID: "gosec", + CleanCodeAttribute: "TRUSTWORTHY", + Impacts: []*sonar.Impact{ + { + SoftwareQuality: "SECURITY", + Severity: "MEDIUM", + }, + }, + }, + }, + Issues: []*sonar.Issue{ + { + RuleID: "G101", + PrimaryLocation: &sonar.Location{ + Message: "Potential hardcoded credentials", + FilePath: "test.go", + TextRange: &sonar.TextRange{ + StartLine: 5, + EndLine: 5, + }, + }, EffortMinutes: sonar.EffortMinutes, }, }, @@ -83,10 +151,24 @@ var _ = Describe("Sonar Formatter", func() { }, } want := &sonar.Report{ + Rules: []*sonar.Rule{ + { + ID: "test", + Name: "test", + Description: "test", + EngineID: "gosec", + CleanCodeAttribute: "TRUSTWORTHY", + Impacts: []*sonar.Impact{ + { + SoftwareQuality: "SECURITY", + Severity: "HIGH", + }, + }, + }, + }, Issues: []*sonar.Issue{ { - EngineID: "gosec", - RuleID: "test", + RuleID: "test", PrimaryLocation: &sonar.Location{ Message: "test", FilePath: "subfolder/test.go", @@ -95,8 +177,6 @@ var _ = Describe("Sonar Formatter", func() { EndLine: 2, }, }, - Type: "VULNERABILITY", - Severity: "BLOCKER", EffortMinutes: sonar.EffortMinutes, }, }, @@ -130,6 +210,7 @@ var _ = Describe("Sonar Formatter", func() { }, } want := &sonar.Report{ + Rules: []*sonar.Rule{}, Issues: []*sonar.Issue{}, } @@ -171,10 +252,24 @@ var _ = Describe("Sonar Formatter", func() { }, } want := &sonar.Report{ + Rules: []*sonar.Rule{ + { + ID: "test", + Name: "test", + Description: "test", + EngineID: "gosec", + CleanCodeAttribute: "TRUSTWORTHY", + Impacts: []*sonar.Impact{ + { + SoftwareQuality: "SECURITY", + Severity: "HIGH", + }, + }, + }, + }, Issues: []*sonar.Issue{ { - EngineID: "gosec", - RuleID: "test", + RuleID: "test", PrimaryLocation: &sonar.Location{ Message: "test", FilePath: "test-project1.go", @@ -183,13 +278,10 @@ var _ = Describe("Sonar Formatter", func() { EndLine: 2, }, }, - Type: "VULNERABILITY", - Severity: "BLOCKER", EffortMinutes: sonar.EffortMinutes, }, { - EngineID: "gosec", - RuleID: "test", + RuleID: "test", PrimaryLocation: &sonar.Location{ Message: "test", FilePath: "test-project2.go", @@ -198,8 +290,6 @@ var _ = Describe("Sonar Formatter", func() { EndLine: 2, }, }, - Type: "VULNERABILITY", - Severity: "BLOCKER", EffortMinutes: sonar.EffortMinutes, }, }, diff --git a/report/sonar/types.go b/report/sonar/types.go index c29eb7e..bdd4be7 100644 --- a/report/sonar/types.go +++ b/report/sonar/types.go @@ -17,16 +17,30 @@ type Location struct { // Issue defines a sonar issue type Issue struct { - EngineID string `json:"engineId"` RuleID string `json:"ruleId"` PrimaryLocation *Location `json:"primaryLocation"` - Type string `json:"type"` - Severity string `json:"severity"` EffortMinutes int `json:"effortMinutes"` SecondaryLocations []*Location `json:"secondaryLocations,omitempty"` } +// Impact defines the impact for a rule. +type Impact struct { + SoftwareQuality string `json:"softwareQuality"` + Severity string `json:"severity"` +} + +// Rule defines a sonar rule. +type Rule struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + EngineID string `json:"engineId"` + CleanCodeAttribute string `json:"cleanCodeAttribute,omitempty"` + Impacts []*Impact `json:"impacts"` +} + // Report defines a sonar report type Report struct { + Rules []*Rule `json:"rules"` Issues []*Issue `json:"issues"` }