Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 86 additions & 11 deletions pkg/report/model/sonarqube.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand All @@ -72,6 +107,7 @@ func NewSonarQubeRepory() *SonarQubeReportBuilder {
return &SonarQubeReportBuilder{
version: "KICS " + constants.Version,
report: &SonarQubeReport{
Rules: make([]Rule, 0),
Issues: make([]Issue, 0),
},
}
Expand All @@ -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)
Expand Down
84 changes: 70 additions & 14 deletions pkg/report/model/sonarqube_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
},
Expand Down Expand Up @@ -54,20 +55,34 @@ func TestSonarQubeReportBuilder_BuildReport(t *testing.T) {
fields: fields{
version: "KICS " + constants.Version,
report: &SonarQubeReport{
Rules: make([]Rule, 0),
Issues: make([]Issue, 0),
},
},
args: args{
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",
Expand All @@ -93,20 +108,34 @@ func TestSonarQubeReportBuilder_BuildReport(t *testing.T) {
fields: fields{
version: "KICS " + constants.Version,
report: &SonarQubeReport{
Rules: make([]Rule, 0),
Issues: make([]Issue, 0),
},
},
args: args{
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",
Expand All @@ -132,19 +161,34 @@ func TestSonarQubeReportBuilder_BuildReport(t *testing.T) {
fields: fields{
version: "KICS " + constants.Version,
report: &SonarQubeReport{
Rules: make([]Rule, 0),
Issues: make([]Issue, 0),
},
},
args: args{
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",
Expand All @@ -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)
}
Expand Down
17 changes: 16 additions & 1 deletion pkg/report/sonarqube_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package report

import (
"encoding/json"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -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)
})
}
Expand Down
Loading