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
4 changes: 2 additions & 2 deletions finding/Finding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
func TestCreateFindingID(t *testing.T) {
// Given
occurrence := rules.Occurrence{
FileName: "TestMessageChannel.messageChannel-meta.xml",
LineContent: " <isExposed>true</isExposed>",
FileName: "TestMessageChannel.messageChannel-meta.xml",
LineContent: " <isExposed>true</isExposed>",
LineNumber: 10,
ColumnRange: []int{1, 28},
IsFalsePositive: false,
Expand Down
4 changes: 4 additions & 0 deletions message/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,7 @@ func GetThresholdViolation(ruleId string, count int, max int) string {
func GetThresholdViolationSummary(count int) string {
return fmt.Sprintf("%d rule(s) exceeded their cicdmaxissues threshold.", count)
}

func GetNoThresholdViolationSummary() string {
return "No rules exceeded their cicdmaxissues threshold."
}
98 changes: 68 additions & 30 deletions output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,47 +43,74 @@ func ListRules(ruleInstances []*rules.Rule) {
}

/**
* CheckThresholdViolations checks if any rule exceeds its configured cicdmaxissues.
* Default cicdmaxissues is 0 (no issues allowed).
* Returns true if any threshold is violated, false otherwise.
* countFindingsPerRule counts the findings of each rule.
*/
func CheckThresholdViolations(w io.Writer, finalResult *finding.Output, configFile *config.Config) bool {
func countFindingsPerRule(finalResult *finding.Output) map[rules.RuleID]int {
findingsPerRule := make(map[rules.RuleID]int)

for _, finding := range finalResult.Results {
findingsPerRule[finding.ID]++
}
return findingsPerRule
}

// Sort rule IDs for deterministic output
sortedRuleIds := make([]rules.RuleID, 0, len(findingsPerRule))
for ruleId := range findingsPerRule {
sortedRuleIds = append(sortedRuleIds, ruleId)
/**
* getViolatedRuleIds returns the IDs of rules whose finding count exceeds their
* configured cicdmaxissues, sorted for deterministic output.
* Default cicdmaxissues is 0 (no issues allowed).
*/
func getViolatedRuleIds(findingsPerRule map[rules.RuleID]int, configFile *config.Config) []rules.RuleID {
violatedRuleIds := []rules.RuleID{}
for ruleId, count := range findingsPerRule {
if count > configFile.GetRuleCicdMaxIssues(ruleId) {
violatedRuleIds = append(violatedRuleIds, ruleId)
}
}
sort.Slice(sortedRuleIds, func(i, j int) bool {
return string(sortedRuleIds[i]) < string(sortedRuleIds[j])
sort.Slice(violatedRuleIds, func(i, j int) bool {
return string(violatedRuleIds[i]) < string(violatedRuleIds[j])
})
return violatedRuleIds
}

// Check each rule that has findings
violationCount := 0

for _, ruleId := range sortedRuleIds {
count := findingsPerRule[ruleId]
maxIssuesAllowed := configFile.GetRuleCicdMaxIssues(ruleId)
/**
* filterFindingsByRules removes findings that do not belong to the given rules
* and updates the result count accordingly.
*/
func filterFindingsByRules(finalResult *finding.Output, ruleIds []rules.RuleID) {
includedRuleIds := make(map[rules.RuleID]bool, len(ruleIds))
for _, ruleId := range ruleIds {
includedRuleIds[ruleId] = true
}

if count > maxIssuesAllowed {
if violationCount == 0 {
fmt.Fprintf(w, "\n%s\n", message.GetThresholdViolationHeader())
}
fmt.Fprintf(w, "%s\n", message.GetThresholdViolation(string(ruleId), count, maxIssuesAllowed))
violationCount++
filteredResults := []finding.Finding{}
for _, finding := range finalResult.Results {
if includedRuleIds[finding.ID] {
filteredResults = append(filteredResults, finding)
}
}
finalResult.Results = filteredResults
finalResult.Count = len(filteredResults)
}

/**
* CheckThresholdViolations checks if any rule exceeds its configured cicdmaxissues.
* Default cicdmaxissues is 0 (no issues allowed).
* Returns true if any threshold is violated, false otherwise.
*/
func CheckThresholdViolations(w io.Writer, finalResult *finding.Output, configFile *config.Config) bool {
findingsPerRule := countFindingsPerRule(finalResult)
violatedRuleIds := getViolatedRuleIds(findingsPerRule, configFile)

if len(violatedRuleIds) == 0 {
return false
}

if violationCount > 0 {
fmt.Fprintf(w, "\n%s\n", message.GetThresholdViolationSummary(violationCount))
fmt.Fprintf(w, "\n%s\n", message.GetThresholdViolationHeader())
for _, ruleId := range violatedRuleIds {
fmt.Fprintf(w, "%s\n", message.GetThresholdViolation(string(ruleId), findingsPerRule[ruleId], configFile.GetRuleCicdMaxIssues(ruleId)))
}
fmt.Fprintf(w, "\n%s\n", message.GetThresholdViolationSummary(len(violatedRuleIds)))

return violationCount > 0
return true
}

/**
Expand All @@ -100,13 +127,24 @@ func DisplayOutput(finalResult *finding.Output, scanTime *ScanTime) {
finalResult.ScanStartedTime = scanTime.StartedTime
finalResult.ScanEndingTime = scanTime.EndingTime
finalResult.Count = len(finalResult.Results)
displayOutput(finalResult)

configFile := config.GetConfigInstance()
if options.IsCICDScan() {
configFile := config.GetConfigInstance()
violatedRuleIds := getViolatedRuleIds(countFindingsPerRule(finalResult), configFile)

if options.IsCICDScan() && CheckThresholdViolations(os.Stderr, finalResult, configFile) {
os.Exit(int(errorhandler.ExitCodeOccurrence))
if len(violatedRuleIds) > 0 {
filterFindingsByRules(finalResult, violatedRuleIds)
displayOutput(finalResult)
CheckThresholdViolations(os.Stdout, finalResult, configFile)
os.Exit(int(errorhandler.ExitCodeOccurrence))
}

displayOutput(finalResult)
fmt.Printf("\n%s\n", message.GetNoThresholdViolationSummary())
return
}

displayOutput(finalResult)
}
}

Expand Down
82 changes: 82 additions & 0 deletions output/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,88 @@ func TestCheckThresholdViolations_WhenConfigNil_DefaultsToZero(t *testing.T) {
}
}

func TestGetViolatedRuleIds_ReturnsOnlyBreachedRulesSorted(t *testing.T) {
//Given
finalResult := &finding.Output{
Results: []finding.Finding{
{ID: "ZRule"},
{ID: "ZRule"},
{ID: "ARule"},
{ID: "ARule"},
{ID: "MRule"},
},
}
configFile := &config.Config{
RuleOverrides: map[string]rules.RuleMetadataOverride{
"ZRule": {CicdMaxIssues: intPtr(1)}, // 2 > 1, violation
"ARule": {CicdMaxIssues: intPtr(1)}, // 2 > 1, violation
"MRule": {CicdMaxIssues: intPtr(5)}, // 1 <= 5, no violation
},
}

//When
violatedRuleIds := getViolatedRuleIds(countFindingsPerRule(finalResult), configFile)

//Then
expectedRuleIds := []rules.RuleID{"ARule", "ZRule"}
if !reflect.DeepEqual(violatedRuleIds, expectedRuleIds) {
t.Errorf("Expected violated rules %v, got %v", expectedRuleIds, violatedRuleIds)
}
}

func TestFilterFindingsByRules_KeepsOnlyGivenRulesAndUpdatesCount(t *testing.T) {
//Given
finalResult := &finding.Output{
Count: 5,
Results: []finding.Finding{
{ID: "RuleA", Name: "First A"},
{ID: "RuleB"},
{ID: "RuleA", Name: "Second A"},
{ID: "RuleC"},
{ID: "RuleC"},
},
}

//When
filterFindingsByRules(finalResult, []rules.RuleID{"RuleA", "RuleC"})

//Then
expectedResults := []finding.Finding{
{ID: "RuleA", Name: "First A"},
{ID: "RuleA", Name: "Second A"},
{ID: "RuleC"},
{ID: "RuleC"},
}
if !reflect.DeepEqual(finalResult.Results, expectedResults) {
t.Errorf("Expected filtered results %+v, got %+v", expectedResults, finalResult.Results)
}
if finalResult.Count != 4 {
t.Errorf("Expected count 4 after filtering, got %d", finalResult.Count)
}
}

func TestFilterFindingsByRules_WhenNoRulesGiven_RemovesAllFindings(t *testing.T) {
//Given
finalResult := &finding.Output{
Count: 2,
Results: []finding.Finding{
{ID: "RuleA"},
{ID: "RuleB"},
},
}

//When
filterFindingsByRules(finalResult, []rules.RuleID{})

//Then
if len(finalResult.Results) != 0 {
t.Errorf("Expected no results after filtering, got %+v", finalResult.Results)
}
if finalResult.Count != 0 {
t.Errorf("Expected count 0 after filtering, got %d", finalResult.Count)
}
}

func TestCheckThresholdViolations_OutputIsSortedByRuleID(t *testing.T) {
//Given
finalResult := &finding.Output{
Expand Down
Loading