Fix the sonar report to follow the latest schema (#1507)

Signed-off-by: Cosmin Cojocar <cosmin@cojocar.ch>
This commit is contained in:
Cosmin Cojocar
2026-02-14 12:59:24 +01:00
committed by GitHub
parent 000384e510
commit 514225c8cb
5 changed files with 254 additions and 46 deletions
+50 -16
View File
@@ -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,
},
},
+21 -4
View File
@@ -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,
}
}
+60 -7
View File
@@ -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]
}
+106 -16
View File
@@ -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,
},
},
+17 -3
View File
@@ -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"`
}