From fd9e456e483c05aa0f7318896666199d6f202dc4 Mon Sep 17 00:00:00 2001 From: Omri SirComp Date: Wed, 20 May 2026 20:11:22 +0300 Subject: [PATCH] fix(report): update SonarQube issue format --- pkg/report/model/sonarqube.go | 97 ++++++++++++++++++++++++++---- pkg/report/model/sonarqube_test.go | 84 +++++++++++++++++++++----- pkg/report/sonarqube_test.go | 17 +++++- 3 files changed, 172 insertions(+), 26 deletions(-) diff --git a/pkg/report/model/sonarqube.go b/pkg/report/model/sonarqube.go index 4ee9d225386..3523b2b8c7b 100644 --- a/pkg/report/model/sonarqube.go +++ b/pkg/report/model/sonarqube.go @@ -32,25 +32,60 @@ var categorySonarQubeEquivalence = map[string]string{ "Structure and Semantics": "CODE_SMELL", } +var cleanCodeAttributeSonarQubeEquivalence = map[string]string{ + "BUG": "LOGICAL", + "CODE_SMELL": "CONVENTIONAL", + "VULNERABILITY": "TRUSTWORTHY", +} + +var softwareQualitySonarQubeEquivalence = map[string]string{ + "BUG": "RELIABILITY", + "CODE_SMELL": "MAINTAINABILITY", + "VULNERABILITY": "SECURITY", +} + +var impactSeveritySonarQubeEquivalence = map[string]string{ + "BLOCKER": "BLOCKER", + "CRITICAL": "HIGH", + "MAJOR": "MEDIUM", + "MINOR": "LOW", + "INFO": "INFO", +} + // SonarQubeReportBuilder is the builder for the SonarQubeReport struct type SonarQubeReportBuilder struct { version string report *SonarQubeReport } -// SonarQubeReport is a list of issues for SonarQube Report +// SonarQubeReport is a list of rules and issues for SonarQube Report type SonarQubeReport struct { + Rules []Rule `json:"rules"` Issues []Issue `json:"issues"` } +// Rule is a single rule for SonarQube Report +type Rule struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + EngineID string `json:"engineId"` + CleanCodeAttribute string `json:"cleanCodeAttribute"` + Type string `json:"type"` + Severity string `json:"severity"` + Impacts []Impact `json:"impacts"` +} + +// Impact is a software quality impact for SonarQube Report +type Impact struct { + SoftwareQuality string `json:"softwareQuality"` + Severity string `json:"severity"` +} + // Issue is a single issue for SonarQube Report type Issue struct { - EngineID string `json:"engineId"` RuleID string `json:"ruleId"` - Severity string `json:"severity"` - CWE string `json:"cwe,omitempty"` - RiskScore string `json:"riskScore,omitempty"` - Type string `json:"type"` + EffortMinutes int `json:"effortMinutes,omitempty"` PrimaryLocation *Location `json:"primaryLocation"` SecondaryLocations []*Location `json:"secondaryLocations,omitempty"` } @@ -72,6 +107,7 @@ func NewSonarQubeRepory() *SonarQubeReportBuilder { return &SonarQubeReportBuilder{ version: "KICS " + constants.Version, report: &SonarQubeReport{ + Rules: make([]Rule, 0), Issues: make([]Issue, 0), }, } @@ -80,26 +116,65 @@ func NewSonarQubeRepory() *SonarQubeReportBuilder { // BuildReport builds the SonarQubeReport from the given QueryResults func (s *SonarQubeReportBuilder) BuildReport(summary *model.Summary) *SonarQubeReport { for i := range summary.Queries { + s.buildRule(&summary.Queries[i]) s.buildIssue(&summary.Queries[i]) } return s.report } +func (s *SonarQubeReportBuilder) buildRule(query *model.QueryResult) { + ruleType := getRuleType(query.Category) + severity := severitySonarQubeEquivalence[query.Severity] + + s.report.Rules = append(s.report.Rules, Rule{ + ID: query.QueryID, + Name: getRuleName(query), + Description: getRuleDescription(query), + EngineID: s.version, + CleanCodeAttribute: cleanCodeAttributeSonarQubeEquivalence[ruleType], + Type: ruleType, + Severity: severity, + Impacts: []Impact{ + { + SoftwareQuality: softwareQualitySonarQubeEquivalence[ruleType], + Severity: impactSeveritySonarQubeEquivalence[severity], + }, + }, + }) +} + // buildIssue builds the issue from the given QueryResult and adds it to the SonarQubeReport func (s *SonarQubeReportBuilder) buildIssue(query *model.QueryResult) { issue := Issue{ - EngineID: s.version, RuleID: query.QueryID, - Severity: severitySonarQubeEquivalence[query.Severity], - CWE: query.CWE, - RiskScore: query.RiskScore, - Type: categorySonarQubeEquivalence[query.Category], PrimaryLocation: buildLocation(0, query), SecondaryLocations: buildSecondaryLocation(query), } s.report.Issues = append(s.report.Issues, issue) } +func getRuleType(category string) string { + ruleType := categorySonarQubeEquivalence[category] + if ruleType == "" { + return "VULNERABILITY" + } + return ruleType +} + +func getRuleName(query *model.QueryResult) string { + if query.QueryName != "" { + return query.QueryName + } + return query.QueryID +} + +func getRuleDescription(query *model.QueryResult) string { + if query.Description != "" { + return query.Description + } + return getRuleName(query) +} + // buildSecondaryLocation builds the secondary location for the SonarQube Report func buildSecondaryLocation(query *model.QueryResult) []*Location { locations := make([]*Location, 0) diff --git a/pkg/report/model/sonarqube_test.go b/pkg/report/model/sonarqube_test.go index 7f150f47cd9..536ea9dbb16 100644 --- a/pkg/report/model/sonarqube_test.go +++ b/pkg/report/model/sonarqube_test.go @@ -20,6 +20,7 @@ func TestNewSonarQubeRepory(t *testing.T) { want: &SonarQubeReportBuilder{ version: "KICS " + constants.Version, report: &SonarQubeReport{ + Rules: make([]Rule, 0), Issues: make([]Issue, 0), }, }, @@ -54,6 +55,7 @@ func TestSonarQubeReportBuilder_BuildReport(t *testing.T) { fields: fields{ version: "KICS " + constants.Version, report: &SonarQubeReport{ + Rules: make([]Rule, 0), Issues: make([]Issue, 0), }, }, @@ -61,13 +63,26 @@ func TestSonarQubeReportBuilder_BuildReport(t *testing.T) { summary: &test.SummaryMock, }, want: &SonarQubeReport{ + Rules: []Rule{ + { + ID: "de7f5e83-da88-4046-871f-ea18504b1d43", + Name: "ALB protocol is HTTP", + Description: "ALB protocol is HTTP Description", + EngineID: "KICS " + constants.Version, + CleanCodeAttribute: "TRUSTWORTHY", + Type: "VULNERABILITY", + Severity: "CRITICAL", + Impacts: []Impact{ + { + SoftwareQuality: "SECURITY", + Severity: "HIGH", + }, + }, + }, + }, Issues: []Issue{ { - EngineID: "KICS " + constants.Version, - RuleID: "de7f5e83-da88-4046-871f-ea18504b1d43", - Severity: "CRITICAL", - CWE: "", - Type: "", + RuleID: "de7f5e83-da88-4046-871f-ea18504b1d43", PrimaryLocation: &Location{ Message: "ALB protocol is HTTP Description", FilePath: "positive.tf", @@ -93,6 +108,7 @@ func TestSonarQubeReportBuilder_BuildReport(t *testing.T) { fields: fields{ version: "KICS " + constants.Version, report: &SonarQubeReport{ + Rules: make([]Rule, 0), Issues: make([]Issue, 0), }, }, @@ -100,13 +116,26 @@ func TestSonarQubeReportBuilder_BuildReport(t *testing.T) { summary: &test.SummaryMockCWE, }, want: &SonarQubeReport{ + Rules: []Rule{ + { + ID: "97707503-a22c-4cd7-b7c0-f088fa7cf830", + Name: "AMI Not Encrypted", + Description: "AWS AMI Encryption is not enabled", + EngineID: "KICS " + constants.Version, + CleanCodeAttribute: "TRUSTWORTHY", + Type: "VULNERABILITY", + Severity: "CRITICAL", + Impacts: []Impact{ + { + SoftwareQuality: "SECURITY", + Severity: "HIGH", + }, + }, + }, + }, Issues: []Issue{ { - EngineID: "KICS " + constants.Version, - RuleID: "97707503-a22c-4cd7-b7c0-f088fa7cf830", - Severity: "CRITICAL", - CWE: "22", - Type: "", + RuleID: "97707503-a22c-4cd7-b7c0-f088fa7cf830", PrimaryLocation: &Location{ Message: "AWS AMI Encryption is not enabled", FilePath: "positive.tf", @@ -132,6 +161,7 @@ func TestSonarQubeReportBuilder_BuildReport(t *testing.T) { fields: fields{ version: "KICS " + constants.Version, report: &SonarQubeReport{ + Rules: make([]Rule, 0), Issues: make([]Issue, 0), }, }, @@ -139,12 +169,26 @@ func TestSonarQubeReportBuilder_BuildReport(t *testing.T) { summary: &test.SummaryMockCriticalSonar, }, want: &SonarQubeReport{ + Rules: []Rule{ + { + ID: "316278b3-87ac-444c-8f8f-a733a28da609", + Name: "AmazonMQ Broker Encryption Disabled", + Description: "AmazonMQ Broker should have Encryption Options defined", + EngineID: "KICS " + constants.Version, + CleanCodeAttribute: "TRUSTWORTHY", + Type: "VULNERABILITY", + Severity: "BLOCKER", + Impacts: []Impact{ + { + SoftwareQuality: "SECURITY", + Severity: "BLOCKER", + }, + }, + }, + }, Issues: []Issue{ { - EngineID: "KICS " + constants.Version, - RuleID: "316278b3-87ac-444c-8f8f-a733a28da609", - Severity: "BLOCKER", - Type: "VULNERABILITY", + RuleID: "316278b3-87ac-444c-8f8f-a733a28da609", PrimaryLocation: &Location{ Message: "AmazonMQ Broker should have Encryption Options defined", FilePath: "../../../test/fixtures/test_critical_custom_queries/amazon_mq_broker_encryption_disabled/test/positive1.yaml", @@ -165,11 +209,23 @@ func TestSonarQubeReportBuilder_BuildReport(t *testing.T) { report: tt.fields.report, } got := s.BuildReport(tt.args.summary) + if len(got.Rules) != len(tt.want.Rules) { + t.Errorf("Number of rules mismatch: got %d, want %d", len(got.Rules), len(tt.want.Rules)) + return + } + for i := range got.Rules { + if !reflect.DeepEqual(got.Rules[i], tt.want.Rules[i]) { + t.Errorf("Rule mismatch at index %d: got %+v, want %+v", i, got.Rules[i], tt.want.Rules[i]) + } + } if len(got.Issues) != len(tt.want.Issues) { t.Errorf("Number of issues mismatch: got %d, want %d", len(got.Issues), len(tt.want.Issues)) return } for i := range got.Issues { + if got.Issues[i].RuleID != tt.want.Issues[i].RuleID { + t.Errorf("RuleID mismatch at index %d: got %s, want %s", i, got.Issues[i].RuleID, tt.want.Issues[i].RuleID) + } if !reflect.DeepEqual(got.Issues[i].PrimaryLocation, tt.want.Issues[i].PrimaryLocation) { t.Errorf("PrimaryLocation mismatch at index %d: got %+v, want %+v", i, got.Issues[i].PrimaryLocation, tt.want.Issues[i].PrimaryLocation) } diff --git a/pkg/report/sonarqube_test.go b/pkg/report/sonarqube_test.go index 597d4848c0c..219c9d555c7 100644 --- a/pkg/report/sonarqube_test.go +++ b/pkg/report/sonarqube_test.go @@ -1,6 +1,7 @@ package report import ( + "encoding/json" "os" "path/filepath" "testing" @@ -57,7 +58,21 @@ func TestPrintSonarQubeReport(t *testing.T) { if err := PrintSonarQubeReport(tt.args.path, tt.args.filename, tt.args.body); (err != nil) != tt.wantErr { t.Errorf("PrintSonarQubeReport() error = %v, wantErr %v", err, tt.wantErr) } - require.FileExists(t, filepath.Join(tt.args.path, "sonarqube-"+tt.args.filename+".json")) + reportPath := filepath.Join(tt.args.path, "sonarqube-"+tt.args.filename+".json") + require.FileExists(t, reportPath) + if tt.args.body != "" { + reportBytes, err := os.ReadFile(reportPath) + require.NoError(t, err) + + var reportBody map[string][]map[string]interface{} + require.NoError(t, json.Unmarshal(reportBytes, &reportBody)) + + require.NotEmpty(t, reportBody["rules"]) + require.NotEmpty(t, reportBody["issues"]) + require.NotContains(t, reportBody["issues"][0], "engineId") + require.NotContains(t, reportBody["issues"][0], "severity") + require.NotContains(t, reportBody["issues"][0], "type") + } os.RemoveAll(tt.args.path) }) }