Skip to content

Commit 8cb0a57

Browse files
committed
fixes pipeline failing even if cve is accepted
1 parent 581be71 commit 8cb0a57

2 files changed

Lines changed: 303 additions & 5 deletions

File tree

cmd/devguard-scanner/commands/sca.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,15 @@ func generateSBOM(path string) (*os.File, error) {
115115

116116
// Function to dynamically change the format of the table row depending on the input parameters
117117
func dependencyVulnToTableRow(pURL packageurl.PackageURL, v vuln.DependencyVulnDTO) table.Row {
118+
var cvss float32 = 0.0
119+
if v.CVE != nil {
120+
cvss = v.CVE.CVSS
121+
}
122+
118123
if pURL.Namespace == "" { //Remove the second slash if the second parameter is empty to avoid double slashes
119-
return table.Row{fmt.Sprintf("pkg:%s/%s", pURL.Type, pURL.Name), utils.SafeDereference(v.CVEID), utils.OrDefault(v.RawRiskAssessment, 0), v.CVE.CVSS, strings.TrimPrefix(pURL.Version, "v"), utils.SafeDereference(v.ComponentFixedVersion), v.State}
124+
return table.Row{fmt.Sprintf("pkg:%s/%s", pURL.Type, pURL.Name), utils.SafeDereference(v.CVEID), utils.OrDefault(v.RawRiskAssessment, 0), cvss, strings.TrimPrefix(pURL.Version, "v"), utils.SafeDereference(v.ComponentFixedVersion), v.State}
120125
} else {
121-
return table.Row{fmt.Sprintf("pkg:%s/%s/%s", pURL.Type, pURL.Namespace, pURL.Name), utils.SafeDereference(v.CVEID), utils.OrDefault(v.RawRiskAssessment, 0), v.CVE.CVSS, strings.TrimPrefix(pURL.Version, "v"), utils.SafeDereference(v.ComponentFixedVersion), v.State}
126+
return table.Row{fmt.Sprintf("pkg:%s/%s/%s", pURL.Type, pURL.Namespace, pURL.Name), utils.SafeDereference(v.CVEID), utils.OrDefault(v.RawRiskAssessment, 0), cvss, strings.TrimPrefix(pURL.Version, "v"), utils.SafeDereference(v.ComponentFixedVersion), v.State}
122127
}
123128
}
124129

@@ -171,6 +176,12 @@ func printScaResults(scanResponse scan.ScanResponse, failOnRisk, failOnCVSS, ass
171176
return utils.OrDefault(f.RawRiskAssessment, 0)
172177
})
173178

179+
openCVSS := utils.Map(utils.Filter(scanResponse.DependencyVulns, func(f vuln.DependencyVulnDTO) bool {
180+
return f.State == "open" && f.CVE != nil
181+
}), func(f vuln.DependencyVulnDTO) float32 {
182+
return f.CVE.CVSS
183+
})
184+
174185
maxRisk := 0.
175186
for _, risk := range openRisks {
176187
if risk > maxRisk {
@@ -179,9 +190,9 @@ func printScaResults(scanResponse scan.ScanResponse, failOnRisk, failOnCVSS, ass
179190
}
180191

181192
var maxCVSS float32
182-
for _, v := range scanResponse.DependencyVulns {
183-
if v.CVE != nil && v.CVE.CVSS > maxCVSS {
184-
maxCVSS = v.CVE.CVSS
193+
for _, v := range openCVSS {
194+
if v > maxCVSS {
195+
maxCVSS = v
185196
}
186197
}
187198

cmd/devguard-scanner/commands/sca_test.go

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import (
1919
"testing"
2020

2121
"github.com/l3montree-dev/devguard/internal/core/vuln"
22+
"github.com/l3montree-dev/devguard/internal/core/vulndb/scan"
2223
"github.com/l3montree-dev/devguard/internal/database/models"
24+
"github.com/l3montree-dev/devguard/internal/utils"
2325
"github.com/package-url/packageurl-go"
2426
"github.com/stretchr/testify/assert"
2527
)
@@ -79,3 +81,288 @@ func TestDependencyVulnToTableRow(t *testing.T) {
7981
})
8082

8183
}
84+
85+
func TestPrintScaResults(t *testing.T) {
86+
assetName := "test-asset"
87+
webUI := "https://app.devguard.org"
88+
89+
t.Run("should return nil when no vulnerabilities found", func(t *testing.T) {
90+
scanResponse := scan.ScanResponse{
91+
DependencyVulns: []vuln.DependencyVulnDTO{},
92+
AmountOpened: 0,
93+
AmountClosed: 0,
94+
}
95+
96+
err := printScaResults(scanResponse, "critical", "critical", assetName, webUI)
97+
assert.Nil(t, err)
98+
})
99+
100+
t.Run("should not fail when all vulnerabilities are closed/accepted - even with high risk/CVSS", func(t *testing.T) {
101+
scanResponse := scan.ScanResponse{
102+
DependencyVulns: []vuln.DependencyVulnDTO{
103+
{
104+
CVEID: utils.Ptr("CVE-2023-12345"),
105+
ComponentPurl: utils.Ptr("pkg:golang/github.com/example/lib@v1.0.0"),
106+
State: "closed", // CLOSED vulnerability should not cause failure
107+
RawRiskAssessment: utils.Ptr(9.5), // High risk but closed
108+
AssetVersionName: "main",
109+
CVE: &models.CVE{
110+
CVE: "CVE-2023-12345",
111+
CVSS: 9.0, // High CVSS but closed
112+
},
113+
},
114+
{
115+
CVEID: utils.Ptr("CVE-2023-67890"),
116+
ComponentPurl: utils.Ptr("pkg:golang/github.com/example/lib2@v1.0.0"),
117+
State: "accepted", // ACCEPTED vulnerability should not cause failure
118+
RawRiskAssessment: utils.Ptr(10.0), // High risk but accepted
119+
AssetVersionName: "main",
120+
CVE: &models.CVE{
121+
CVE: "CVE-2023-67890",
122+
CVSS: 9.8, // High CVSS but accepted
123+
},
124+
},
125+
},
126+
AmountOpened: 0,
127+
AmountClosed: 2,
128+
}
129+
130+
// Should pass even with low thresholds because vulnerabilities are closed/accepted
131+
err := printScaResults(scanResponse, "low", "low", assetName, webUI)
132+
assert.Nil(t, err)
133+
})
134+
135+
// Test failOnRisk conditions - consolidated table-driven test
136+
t.Run("failOnRisk thresholds", func(t *testing.T) {
137+
testCases := []struct {
138+
name string
139+
risk float64
140+
threshold string
141+
shouldFail bool
142+
expectedError string
143+
}{
144+
{"low threshold pass", 0.05, "low", false, ""},
145+
{"low threshold fail", 0.2, "low", true, "max risk exceeds threshold 0.20"},
146+
{"medium threshold pass", 3.9, "medium", false, ""},
147+
{"medium threshold fail", 4.5, "medium", true, "max risk exceeds threshold 4.50"},
148+
{"high threshold pass", 6.9, "high", false, ""},
149+
{"high threshold fail", 7.2, "high", true, "max risk exceeds threshold 7.20"},
150+
{"critical threshold pass", 8.9, "critical", false, ""},
151+
{"critical threshold fail", 9.5, "critical", true, "max risk exceeds threshold 9.50"},
152+
}
153+
154+
for _, tc := range testCases {
155+
t.Run(tc.name, func(t *testing.T) {
156+
scanResponse := scan.ScanResponse{
157+
DependencyVulns: []vuln.DependencyVulnDTO{
158+
{
159+
CVEID: utils.Ptr("CVE-2023-12345"),
160+
ComponentPurl: utils.Ptr("pkg:golang/github.com/example/lib@v1.0.0"),
161+
State: "open",
162+
RawRiskAssessment: utils.Ptr(tc.risk),
163+
AssetVersionName: "main",
164+
CVE: &models.CVE{
165+
CVE: "CVE-2023-12345",
166+
CVSS: 5.0,
167+
},
168+
},
169+
},
170+
AmountOpened: 1,
171+
AmountClosed: 0,
172+
}
173+
174+
err := printScaResults(scanResponse, tc.threshold, "critical", assetName, webUI)
175+
if tc.shouldFail {
176+
assert.NotNil(t, err)
177+
assert.Contains(t, err.Error(), tc.expectedError)
178+
} else {
179+
assert.Nil(t, err)
180+
}
181+
})
182+
}
183+
})
184+
185+
// Test failOnCVSS conditions - consolidated table-driven test
186+
t.Run("failOnCVSS thresholds", func(t *testing.T) {
187+
testCases := []struct {
188+
name string
189+
cvss float32
190+
threshold string
191+
shouldFail bool
192+
expectedError string
193+
}{
194+
{"low threshold pass", 0.05, "low", false, ""},
195+
{"low threshold fail", 0.2, "low", true, "max CVSS exceeds threshold 0.20"},
196+
{"medium threshold pass", 3.9, "medium", false, ""},
197+
{"medium threshold fail", 4.5, "medium", true, "max CVSS exceeds threshold 4.50"},
198+
{"high threshold pass", 6.9, "high", false, ""},
199+
{"high threshold fail", 7.2, "high", true, "max CVSS exceeds threshold 7.20"},
200+
{"critical threshold pass", 8.9, "critical", false, ""},
201+
{"critical threshold fail", 9.5, "critical", true, "max CVSS exceeds threshold 9.50"},
202+
}
203+
204+
for _, tc := range testCases {
205+
t.Run(tc.name, func(t *testing.T) {
206+
scanResponse := scan.ScanResponse{
207+
DependencyVulns: []vuln.DependencyVulnDTO{
208+
{
209+
CVEID: utils.Ptr("CVE-2023-12345"),
210+
ComponentPurl: utils.Ptr("pkg:golang/github.com/example/lib@v1.0.0"),
211+
State: "open",
212+
RawRiskAssessment: utils.Ptr(1.0),
213+
AssetVersionName: "main",
214+
CVE: &models.CVE{
215+
CVE: "CVE-2023-12345",
216+
CVSS: tc.cvss,
217+
},
218+
},
219+
},
220+
AmountOpened: 1,
221+
AmountClosed: 0,
222+
}
223+
224+
err := printScaResults(scanResponse, "critical", tc.threshold, assetName, webUI)
225+
if tc.shouldFail {
226+
assert.NotNil(t, err)
227+
assert.Contains(t, err.Error(), tc.expectedError)
228+
} else {
229+
assert.Nil(t, err)
230+
}
231+
})
232+
}
233+
})
234+
235+
// Test edge cases
236+
t.Run("should handle vulnerabilities without CVE (no CVSS)", func(t *testing.T) {
237+
scanResponse := scan.ScanResponse{
238+
DependencyVulns: []vuln.DependencyVulnDTO{
239+
{
240+
CVEID: nil, // No CVE ID
241+
ComponentPurl: utils.Ptr("pkg:golang/github.com/example/lib@v1.0.0"),
242+
State: "open",
243+
RawRiskAssessment: utils.Ptr(5.0),
244+
AssetVersionName: "main",
245+
CVE: nil, // No CVE data - should default CVSS to 0.0
246+
},
247+
},
248+
AmountOpened: 1,
249+
AmountClosed: 0,
250+
}
251+
252+
// Should fail on risk but pass on CVSS (defaults to 0.0)
253+
err := printScaResults(scanResponse, "medium", "critical", assetName, webUI)
254+
assert.NotNil(t, err)
255+
assert.Contains(t, err.Error(), "max risk exceeds threshold 5.00")
256+
})
257+
258+
t.Run("should only consider OPEN vulnerabilities - mixed states scenario", func(t *testing.T) {
259+
scanResponse := scan.ScanResponse{
260+
DependencyVulns: []vuln.DependencyVulnDTO{
261+
{
262+
CVEID: utils.Ptr("CVE-2023-12345"),
263+
ComponentPurl: utils.Ptr("pkg:golang/github.com/example/lib1@v1.0.0"),
264+
State: "open", // OPEN - should be considered
265+
RawRiskAssessment: utils.Ptr(3.0),
266+
AssetVersionName: "main",
267+
CVE: &models.CVE{
268+
CVE: "CVE-2023-12345",
269+
CVSS: 5.0,
270+
},
271+
},
272+
{
273+
CVEID: utils.Ptr("CVE-2023-67890"),
274+
ComponentPurl: utils.Ptr("pkg:golang/github.com/example/lib2@v2.0.0"),
275+
State: "open", // OPEN - should be considered (highest values)
276+
RawRiskAssessment: utils.Ptr(8.5), // Higher risk
277+
AssetVersionName: "main",
278+
CVE: &models.CVE{
279+
CVE: "CVE-2023-67890",
280+
CVSS: 7.8, // Higher CVSS
281+
},
282+
},
283+
{
284+
CVEID: utils.Ptr("CVE-2023-11111"),
285+
ComponentPurl: utils.Ptr("pkg:golang/github.com/example/lib3@v3.0.0"),
286+
State: "closed", // CLOSED - should be IGNORED even though it has highest values
287+
RawRiskAssessment: utils.Ptr(10.0), // Highest risk but closed
288+
AssetVersionName: "main",
289+
CVE: &models.CVE{
290+
CVE: "CVE-2023-11111",
291+
CVSS: 10.0, // Highest CVSS but closed
292+
},
293+
},
294+
{
295+
CVEID: utils.Ptr("CVE-2023-22222"),
296+
ComponentPurl: utils.Ptr("pkg:golang/github.com/example/lib4@v4.0.0"),
297+
State: "accepted", // ACCEPTED - should be IGNORED even though it has highest values
298+
RawRiskAssessment: utils.Ptr(9.8), // Very high risk but accepted
299+
AssetVersionName: "main",
300+
CVE: &models.CVE{
301+
CVE: "CVE-2023-22222",
302+
CVSS: 9.9, // Very high CVSS but accepted
303+
},
304+
},
305+
},
306+
AmountOpened: 2,
307+
AmountClosed: 2,
308+
}
309+
310+
// Should fail on high risk threshold (8.5 >= 7) - only considering open vulns
311+
err := printScaResults(scanResponse, "high", "critical", assetName, webUI)
312+
assert.NotNil(t, err)
313+
assert.Contains(t, err.Error(), "max risk exceeds threshold 8.50")
314+
315+
// Should fail on high CVSS threshold (7.8 >= 7) - only considering open vulns
316+
err = printScaResults(scanResponse, "critical", "high", assetName, webUI)
317+
assert.NotNil(t, err)
318+
assert.Contains(t, err.Error(), "max CVSS exceeds threshold 7.80")
319+
})
320+
321+
t.Run("should handle nil RawRiskAssessment gracefully", func(t *testing.T) {
322+
scanResponse := scan.ScanResponse{
323+
DependencyVulns: []vuln.DependencyVulnDTO{
324+
{
325+
CVEID: utils.Ptr("CVE-2023-12345"),
326+
ComponentPurl: utils.Ptr("pkg:golang/github.com/example/lib@v1.0.0"),
327+
State: "open",
328+
RawRiskAssessment: nil, // Should default to 0
329+
AssetVersionName: "main",
330+
CVE: &models.CVE{
331+
CVE: "CVE-2023-12345",
332+
CVSS: 5.0,
333+
},
334+
},
335+
},
336+
AmountOpened: 1,
337+
AmountClosed: 0,
338+
}
339+
340+
// Should pass all risk thresholds (defaults to 0)
341+
err := printScaResults(scanResponse, "low", "critical", assetName, webUI)
342+
assert.Nil(t, err)
343+
})
344+
345+
t.Run("should handle unknown failOn values gracefully", func(t *testing.T) {
346+
scanResponse := scan.ScanResponse{
347+
DependencyVulns: []vuln.DependencyVulnDTO{
348+
{
349+
CVEID: utils.Ptr("CVE-2023-12345"),
350+
ComponentPurl: utils.Ptr("pkg:golang/github.com/example/lib@v1.0.0"),
351+
State: "open",
352+
RawRiskAssessment: utils.Ptr(10.0),
353+
AssetVersionName: "main",
354+
CVE: &models.CVE{
355+
CVE: "CVE-2023-12345",
356+
CVSS: 10.0,
357+
},
358+
},
359+
},
360+
AmountOpened: 1,
361+
AmountClosed: 0,
362+
}
363+
364+
// Unknown failOn values should not cause failures
365+
err := printScaResults(scanResponse, "unknown", "invalid", assetName, webUI)
366+
assert.Nil(t, err)
367+
})
368+
}

0 commit comments

Comments
 (0)