9
9
"io"
10
10
"log/slog"
11
11
"slices"
12
+ "sort"
12
13
"strings"
13
14
14
15
"github.com/gatecheckdev/gatecheck/pkg/archive"
@@ -65,6 +66,34 @@ func Validate(config *Config, reportSrc io.Reader, targetFilename string, option
65
66
}
66
67
}
67
68
69
+ func removeIgnoredSeverityCVEs (config * Config , report * artifacts.GrypeReportMin , data * epss.Data ) {
70
+ hasLimits := map [string ]bool {
71
+ "critical" : config .Grype .SeverityLimit .Critical .Enabled ,
72
+ "high" : config .Grype .SeverityLimit .High .Enabled ,
73
+ "medium" : config .Grype .SeverityLimit .Medium .Enabled ,
74
+ "low" : config .Grype .SeverityLimit .Low .Enabled ,
75
+ "unknown" : false ,
76
+ "negligible" : false ,
77
+ }
78
+
79
+ for severity , hasLimit := range hasLimits {
80
+ if hasLimit {
81
+ continue
82
+ }
83
+
84
+ if config .Grype .EPSSLimit .Enabled {
85
+ report .Matches = slices .DeleteFunc (report .Matches , func (match artifacts.GrypeMatch ) bool {
86
+ epssCVE , ok := data .CVEs [match .Vulnerability .ID ]
87
+ return strings .ToLower (match .Vulnerability .Severity ) == severity && (! ok || epssCVE .EPSSValue () < config .Grype .EPSSLimit .Score )
88
+ })
89
+ } else {
90
+ report .Matches = slices .DeleteFunc (report .Matches , func (match artifacts.GrypeMatch ) bool {
91
+ return strings .ToLower (match .Vulnerability .Severity ) == severity
92
+ })
93
+ }
94
+ }
95
+ }
96
+
68
97
func ruleGrypeSeverityLimit (config * Config , report * artifacts.GrypeReportMin ) bool {
69
98
validationPass := true
70
99
@@ -86,6 +115,9 @@ func ruleGrypeSeverityLimit(config *Config, report *artifacts.GrypeReportMin) bo
86
115
}
87
116
if matchCount > int (configuredLimit .Limit ) {
88
117
slog .Error ("grype severity limit exceeded" , "severity" , severity , "report" , matchCount , "limit" , configuredLimit .Limit )
118
+ for _ , match := range matches {
119
+ slog .Info ("vulnerability detected" , "id" , match .Vulnerability .ID , "severity" , match .Vulnerability .Severity )
120
+ }
89
121
validationPass = false
90
122
continue
91
123
}
@@ -359,7 +391,7 @@ func ruleGrypeEPSSLimit(config *Config, report *artifacts.GrypeReportMin, data *
359
391
slog .Debug ("run epss limit rule" ,
360
392
"artifact" , "grype" ,
361
393
"vulnerabilities" , len (report .Matches ),
362
- "epss_risk_acceptance_score " , config .Grype .EPSSRiskAcceptance .Score ,
394
+ "epss_limit_score " , config .Grype .EPSSLimit .Score ,
363
395
)
364
396
for _ , match := range report .Matches {
365
397
epssCVE , ok := data .CVEs [match .Vulnerability .ID ]
@@ -402,7 +434,7 @@ func ruleCyclonedxEPSSLimit(config *Config, report *artifacts.CyclonedxReportMin
402
434
slog .Debug ("run epss limit rule" ,
403
435
"artifact" , "cyclonedx" ,
404
436
"vulnerabilities" , len (report .Vulnerabilities ),
405
- "epss_risk_acceptance_score " , config .Cyclonedx .EPSSRiskAcceptance .Score ,
437
+ "epss_limit_score " , config .Cyclonedx .EPSSLimit .Score ,
406
438
)
407
439
408
440
for _ , vulnerability := range report .Vulnerabilities {
@@ -431,6 +463,24 @@ func ruleCyclonedxEPSSLimit(config *Config, report *artifacts.CyclonedxReportMin
431
463
return true
432
464
}
433
465
466
+ func removeIgnoredSemgrepIssues (config * Config , report * artifacts.SemgrepReportMin ) {
467
+ hasLimits := map [string ]bool {
468
+ "error" : config .Semgrep .SeverityLimit .Error .Enabled ,
469
+ "warning" : config .Semgrep .SeverityLimit .Warning .Enabled ,
470
+ "info" : config .Semgrep .SeverityLimit .Info .Enabled ,
471
+ }
472
+
473
+ for severity , hasLimit := range hasLimits {
474
+ if hasLimit {
475
+ continue
476
+ }
477
+
478
+ report .Results = slices .DeleteFunc (report .Results , func (result artifacts.SemgrepResults ) bool {
479
+ return strings .EqualFold (result .Extra .Severity , severity )
480
+ })
481
+ }
482
+ }
483
+
434
484
func ruleSemgrepSeverityLimit (config * Config , report * artifacts.SemgrepReportMin ) bool {
435
485
slog .Debug (
436
486
"severity limit rule" , "artifact" , "semgrep" ,
@@ -458,6 +508,9 @@ func ruleSemgrepSeverityLimit(config *Config, report *artifacts.SemgrepReportMin
458
508
}
459
509
if matchCount > int (configuredLimit .Limit ) {
460
510
slog .Error ("severity limit exceeded" , "artifact" , "semgrep" , "severity" , severity , "report" , matchCount , "limit" , configuredLimit .Limit )
511
+ for _ , match := range matches {
512
+ slog .Info ("Potential issue detected" , "severity" , match .Extra .Severity , "check_id" , match .CheckID , "message" , match .Extra .Message )
513
+ }
461
514
validationPass = false
462
515
continue
463
516
}
@@ -483,6 +536,7 @@ func ruleSemgrepImpactRiskAccept(config *Config, report *artifacts.SemgrepReport
483
536
484
537
results := slices .DeleteFunc (report .Results , func (result artifacts.SemgrepResults ) bool {
485
538
riskAccepted := false
539
+ // TODO: make the configuration for risk acceptance less dumb (what would you accept high medium impact and not accept low impact)
486
540
switch {
487
541
case config .Semgrep .ImpactRiskAcceptance .High && strings .EqualFold (result .Extra .Metadata .Impact , "high" ):
488
542
riskAccepted = true
@@ -494,7 +548,7 @@ func ruleSemgrepImpactRiskAccept(config *Config, report *artifacts.SemgrepReport
494
548
495
549
if riskAccepted {
496
550
slog .Info (
497
- "risk accepted: epss score is below risk acceptance threshold" ,
551
+ "risk accepted: Semgrep issue impact is below acceptance threshold" ,
498
552
"check_id" , result .CheckID ,
499
553
"severity" , result .Extra .Severity ,
500
554
"impact" , result .Extra .Metadata .Impact ,
@@ -647,19 +701,29 @@ func validateCoverage(src io.Reader, targetFilename string, config *Config) erro
647
701
functionCoverage := float32 (report .CoveredFunctions ) / float32 (report .TotalFunctions )
648
702
branchCoverage := float32 (report .CoveredBranches ) / float32 (report .TotalBranches )
649
703
704
+ slog .Info (
705
+ "validate coverage" ,
706
+ "line_coverage" , lineCoverage ,
707
+ "function_coverage" , functionCoverage ,
708
+ "branch_coverage" , branchCoverage ,
709
+ )
710
+
650
711
var errs error
651
712
652
713
if lineCoverage < config .Coverage .LineThreshold {
714
+ slog .Error ("line coverage below threshold" , "line_coverage" , lineCoverage , "threshold" , config .Coverage .LineThreshold )
653
715
coverageErr := newValidationErr ("Coverage: Line coverage below threshold" )
654
716
errs = errors .Join (errs , coverageErr )
655
717
}
656
718
657
719
if functionCoverage < config .Coverage .FunctionThreshold {
720
+ slog .Error ("function coverage below threshold" , "function_coverage" , functionCoverage , "threshold" , config .Coverage .FunctionThreshold )
658
721
coverageErr := newValidationErr ("Coverage: Function coverage below threshold" )
659
722
errs = errors .Join (errs , coverageErr )
660
723
}
661
724
662
725
if branchCoverage < config .Coverage .BranchThreshold {
726
+ slog .Error ("branch coverage below threshold" , "branch_coverage" , branchCoverage , "threshold" , config .Coverage .BranchThreshold )
663
727
coverageErr := newValidationErr ("Coverage: Branch coverage below threshold" )
664
728
errs = errors .Join (errs , coverageErr )
665
729
}
@@ -713,11 +777,34 @@ func validateBundle(r io.Reader, config *Config, options *fetchOptions) error {
713
777
// Validate Rules
714
778
715
779
func validateGrypeRules (config * Config , report * artifacts.GrypeReportMin , catalog * kev.Catalog , data * epss.Data ) error {
780
+ severityRank := []string {
781
+ "critical" ,
782
+ "high" ,
783
+ "medium" ,
784
+ "low" ,
785
+ "negligible" ,
786
+ "unknown" ,
787
+ }
788
+ sort .Slice (report .Matches , func (i , j int ) bool {
789
+ if report .Matches [i ].Vulnerability .Severity == report .Matches [j ].Vulnerability .Severity {
790
+ epssi , oki := data .CVEs [report .Matches [i ].Vulnerability .ID ]
791
+ epssj , okj := data .CVEs [report .Matches [j ].Vulnerability .ID ]
792
+
793
+ // Sort EPPS from highest to lowest
794
+ return ! okj || oki && epssi .EPSSValue () > epssj .EPSSValue ()
795
+ }
796
+ ranki := slices .Index (severityRank , strings .ToLower (report .Matches [i ].Vulnerability .Severity ))
797
+ rankj := slices .Index (severityRank , strings .ToLower (report .Matches [j ].Vulnerability .Severity ))
798
+ return ranki < rankj
799
+ })
716
800
// 1. Deny List - Fail Matching
717
801
if ! ruleGrypeCVEDeny (config , report ) {
718
802
return newValidationErr ("Grype: CVE explicitly denied" )
719
803
}
720
804
805
+ // Ignore any CVEs that don't meet the vulnerability threshold or the EPPS threshold
806
+ removeIgnoredSeverityCVEs (config , report , data )
807
+
721
808
// 2. CVE Allowance - remove from matches
722
809
ruleGrypeCVEAllow (config , report )
723
810
@@ -773,6 +860,10 @@ func validateCyclonedxRules(config *Config, report *artifacts.CyclonedxReportMin
773
860
}
774
861
775
862
func validateSemgrepRules (config * Config , report * artifacts.SemgrepReportMin ) error {
863
+ slog .Info ("validating semgrep rules" , "findings" , len (report .Results ))
864
+ // Ignore issues for which there is no severity limit
865
+ removeIgnoredSemgrepIssues (config , report )
866
+
776
867
// 1. Impact Allowance - remove result
777
868
ruleSemgrepImpactRiskAccept (config , report )
778
869
0 commit comments