5
5
"encoding/json"
6
6
std_errors "errors"
7
7
"fmt"
8
- "math"
9
8
"sort"
10
9
"strings"
11
10
@@ -61,17 +60,33 @@ func RunTest(
61
60
orgSlugOrID = orgID
62
61
}
63
62
64
- uniqueCount := calculateUniqueIssueCount (findingsData )
63
+ allFindingsData := findingsData
64
+ vulnerablePathsCount := calculateVulnerablePathsCount (allFindingsData )
65
+
66
+ // Deep copy findings for consolidation to avoid modifying the original slice,
67
+ // which would affect the JSON output.
68
+ b , err := json .Marshal (allFindingsData )
69
+ if err != nil {
70
+ return nil , nil , fmt .Errorf ("failed to marshal findings for deep copy: %w" , err )
71
+ }
72
+ var findingsForConsolidation []testapi.FindingData
73
+ if err := json .Unmarshal (b , & findingsForConsolidation ); err != nil {
74
+ return nil , nil , fmt .Errorf ("failed to unmarshal findings for deep copy: %w" , err )
75
+ }
76
+
77
+ consolidatedFindings := consolidateFindings (findingsForConsolidation , logger )
78
+ //nolint:gosec // G115: integer overflow is not a concern here
79
+ uniqueCount := int32 (len (consolidatedFindings ))
65
80
66
81
// The summary is always needed for the exit code calculation.
67
- standardSummary , summaryData , summaryErr := NewSummaryData ( finalResult , logger , targetDir )
68
- if summaryErr != nil && ! std_errors . Is ( summaryErr , ErrNoSummaryData ) {
69
- // Log the error but continue, as this is not fatal.
82
+ standardSummary , summaryData , summaryErr := NewSummaryDataFromFindings ( consolidatedFindings , logger , targetDir )
83
+ if summaryErr != nil {
84
+ // Log other errors but continue, as this is not fatal.
70
85
logger .Warn ().Err (summaryErr ).Msg ("Failed to create test summary for exit code handling" )
71
86
}
72
87
73
88
legacyParams := & transform.SnykSchemaToLegacyParams {
74
- Findings : findingsData ,
89
+ Findings : allFindingsData ,
75
90
TestResult : finalResult ,
76
91
OrgSlugOrID : orgSlugOrID ,
77
92
ProjectName : projectName ,
@@ -84,7 +99,7 @@ func RunTest(
84
99
Logger : logger ,
85
100
}
86
101
87
- return prepareOutput (ictx , findingsData , standardSummary , summaryData , legacyParams )
102
+ return prepareOutput (ictx , consolidatedFindings , standardSummary , summaryData , legacyParams , vulnerablePathsCount )
88
103
}
89
104
90
105
// executeTest runs the test and returns the results.
@@ -147,6 +162,7 @@ func prepareOutput(
147
162
standardSummary * json_schemas.TestSummary ,
148
163
summaryData workflow.Data ,
149
164
params * transform.SnykSchemaToLegacyParams ,
165
+ vulnerablePathsCount int ,
150
166
) (* definitions.LegacyVulnerabilityResponse , []workflow.Data , error ) {
151
167
config := ictx .GetConfiguration ()
152
168
var outputData []workflow.Data
@@ -180,12 +196,13 @@ func prepareOutput(
180
196
181
197
if standardSummary != nil {
182
198
extendedPayload := presenters.SummaryPayload {
183
- Summary : standardSummary ,
184
- DependencyCount : params .DepCount ,
185
- PackageManager : params .PackageManager ,
186
- ProjectName : params .ProjectName ,
187
- DisplayTargetFile : params .DisplayTargetFile ,
188
- UniqueCount : params .UniqueCount ,
199
+ Summary : standardSummary ,
200
+ DependencyCount : params .DepCount ,
201
+ PackageManager : params .PackageManager ,
202
+ ProjectName : params .ProjectName ,
203
+ DisplayTargetFile : params .DisplayTargetFile ,
204
+ UniqueCount : params .UniqueCount ,
205
+ VulnerablePathsCount : vulnerablePathsCount ,
189
206
}
190
207
191
208
extendedPayloadBytes , marshalErr := json .Marshal (extendedPayload )
@@ -198,62 +215,42 @@ func prepareOutput(
198
215
return legacyVulnResponse , outputData , nil
199
216
}
200
217
201
- // getSeverityCount safely retrieves the count for a given severity from a summary.
202
- func getSeverityCount (summary * testapi.FindingSummary , severity string ) uint32 {
203
- if summary == nil || summary .CountBy == nil {
204
- return 0
205
- }
206
- if severityCounts , ok := (* summary .CountBy )["severity" ]; ok {
207
- return severityCounts [severity ]
208
- }
209
- return 0
210
- }
211
-
212
- // extractSeverityKeys returns a map of severity keys present in the summaries.
213
- func extractSeverityKeys (summaries ... * testapi.FindingSummary ) map [string ]bool {
214
- keys := make (map [string ]bool )
215
- for _ , summary := range summaries {
216
- if summary != nil && summary .CountBy != nil {
217
- if severityCounts , ok := (* summary .CountBy )["severity" ]; ok {
218
- for severity := range severityCounts {
219
- keys [severity ] = true
220
- }
221
- }
218
+ // NewSummaryDataFromFindings creates a workflow.Data object containing a json_schemas.TestSummary
219
+ // from a list of findings. This is used for downstream processing, like determining
220
+ // the CLI exit code.
221
+ func NewSummaryDataFromFindings (
222
+ findings []testapi.FindingData ,
223
+ _ * zerolog.Logger ,
224
+ path string ,
225
+ ) (* json_schemas.TestSummary , workflow.Data , error ) {
226
+ if len (findings ) == 0 {
227
+ testSummary := json_schemas .NewTestSummary ("open-source" , path )
228
+ testSummary .Results = []json_schemas.TestSummaryResult {}
229
+ testSummary .SeverityOrderAsc = []string {"low" , "medium" , "high" , "critical" }
230
+ summaryBytes , err := json .Marshal (testSummary )
231
+ if err != nil {
232
+ return nil , nil , fmt .Errorf ("failed to marshal empty test summary: %w" , err )
222
233
}
234
+ return testSummary , NewWorkflowData (content_type .TEST_SUMMARY , summaryBytes ), nil
223
235
}
224
- return keys
225
- }
226
-
227
- // NewSummaryData creates a workflow.Data object containing a json_schemas.TestSummary
228
- // from a testapi.TestResult. This is used for downstream processing, like determining
229
- // the CLI exit code.
230
- func NewSummaryData (testResult testapi.TestResult , _ * zerolog.Logger , path string ) (* json_schemas.TestSummary , workflow.Data , error ) {
231
- rawSummary := testResult .GetRawSummary ()
232
- effectiveSummary := testResult .GetEffectiveSummary ()
233
236
234
- if rawSummary == nil || effectiveSummary == nil {
235
- return nil , nil , fmt .Errorf ("test result missing summary information" )
237
+ severityCounts := make (map [string ]int )
238
+ for _ , finding := range findings {
239
+ if finding .Attributes == nil {
240
+ continue
241
+ }
242
+ severity := string (finding .Attributes .Rating .Severity )
243
+ severityCounts [severity ]++
236
244
}
237
245
238
- severityKeys := extractSeverityKeys (rawSummary , effectiveSummary )
239
-
240
- var summaryResults []json_schemas.TestSummaryResult
241
- for severity := range severityKeys {
242
- total := getSeverityCount (rawSummary , severity )
243
- open := getSeverityCount (effectiveSummary , severity )
244
-
245
- if total > 0 || open > 0 {
246
- ignored := 0
247
- if total > open {
248
- ignored = int (total - open )
249
- }
250
- summaryResults = append (summaryResults , json_schemas.TestSummaryResult {
251
- Severity : severity ,
252
- Total : int (total ),
253
- Open : int (open ),
254
- Ignored : ignored ,
255
- })
256
- }
246
+ summaryResults := make ([]json_schemas.TestSummaryResult , 0 , len (severityCounts ))
247
+ for severity , count := range severityCounts {
248
+ summaryResults = append (summaryResults , json_schemas.TestSummaryResult {
249
+ Severity : severity ,
250
+ Total : count ,
251
+ Open : count , // For this summary, all found issues are considered open.
252
+ Ignored : 0 ,
253
+ })
257
254
}
258
255
259
256
// Sort results for consistent output, matching the standard CLI order.
@@ -275,40 +272,92 @@ func NewSummaryData(testResult testapi.TestResult, _ *zerolog.Logger, path strin
275
272
return testSummary , summaryWorkflowData , nil
276
273
}
277
274
278
- // calculateUniqueIssueCount iterates through findings to determine the number of unique issues.
279
- // A unique issue is identified by its Snyk ID (e.g., SNYK-JS-LODASH-12345).
280
- func calculateUniqueIssueCount (findings []testapi.FindingData ) int32 {
281
- issueIDs := make (map [string ]bool )
275
+ // consolidateFindings consolidates findings with the same Snyk ID into a single finding
276
+ // with all the evidence and locations from the original findings.
277
+ func consolidateFindings (findings []testapi.FindingData , logger * zerolog.Logger ) []testapi.FindingData {
278
+ consolidatedFindings := make (map [string ]testapi.FindingData )
279
+ var orderedKeys []string
280
+
282
281
for _ , finding := range findings {
283
- var snykID string
284
- // A finding can have multiple problems (e.g., a CVE and a Snyk ID).
285
- // We iterate through them to find the canonical Snyk ID for uniqueness.
286
- for _ , problem := range finding .Attributes .Problems {
287
- var id string
282
+ snykID := getSnykID (finding )
283
+ if snykID == "" {
284
+ // If a finding has no Snyk ID, treat it as unique.
285
+ if finding .Id == nil {
286
+ logger .Error ().Msg ("finding is missing an ID" )
287
+ continue
288
+ }
289
+ snykID = finding .Id .String ()
290
+ }
291
+
292
+ if existingFinding , exists := consolidatedFindings [snykID ]; ! exists {
293
+ consolidatedFindings [snykID ] = finding
294
+ orderedKeys = append (orderedKeys , snykID )
295
+ } else {
296
+ // Deep copy to avoid modifying the map's shared object directly
297
+ newFinding := existingFinding
298
+ if newFinding .Attributes == nil && finding .Attributes != nil {
299
+ newFinding .Attributes = & testapi.FindingAttributes {}
300
+ }
301
+
302
+ if finding .Attributes != nil {
303
+ newFinding .Attributes .Evidence = append (newFinding .Attributes .Evidence , finding .Attributes .Evidence ... )
304
+ newFinding .Attributes .Locations = append (newFinding .Attributes .Locations , finding .Attributes .Locations ... )
305
+ }
306
+ consolidatedFindings [snykID ] = newFinding
307
+ }
308
+ }
309
+
310
+ result := make ([]testapi.FindingData , len (orderedKeys ))
311
+ for i , key := range orderedKeys {
312
+ result [i ] = consolidatedFindings [key ]
313
+ }
314
+ return result
315
+ }
316
+
317
+ // getSnykID extracts the canonical Snyk ID from a finding.
318
+ // TODO This needs to use attributes.Key.
319
+ func getSnykID (finding testapi.FindingData ) string {
320
+ if finding .Attributes == nil || len (finding .Attributes .Problems ) == 0 {
321
+ return ""
322
+ }
288
323
289
- // The problem is a union type, so we need to check which type it is.
324
+ for _ , problem := range finding .Attributes .Problems {
325
+ var id string
326
+ disc , err := problem .Discriminator ()
327
+ if err != nil {
328
+ continue
329
+ }
330
+
331
+ if disc == string (testapi .SnykVuln ) {
290
332
if p , err := problem .AsSnykVulnProblem (); err == nil {
291
333
id = p .Id
292
- } else if p , err := problem .AsSnykLicenseProblem (); err == nil {
293
- id = p .Id
294
334
}
295
-
296
- if id != "" {
297
- snykID = id
298
- break // Found a Snyk ID, we can stop searching for this finding.
335
+ } else if disc == string (testapi .SnykLicense ) {
336
+ if p , err := problem .AsSnykLicenseProblem (); err == nil {
337
+ id = p .Id
299
338
}
300
339
}
301
340
302
- if snykID != "" {
303
- issueIDs [ snykID ] = true
341
+ if id != "" {
342
+ return id
304
343
}
305
344
}
345
+ return ""
346
+ }
306
347
307
- count := len (issueIDs )
308
- if count > math .MaxInt32 {
309
- return math .MaxInt32
348
+ func calculateVulnerablePathsCount (findings []testapi.FindingData ) int {
349
+ var count int
350
+ for _ , finding := range findings {
351
+ if finding .Attributes == nil {
352
+ continue
353
+ }
354
+ for _ , evidence := range finding .Attributes .Evidence {
355
+ if disc , err := evidence .Discriminator (); err == nil && disc == string (testapi .DependencyPath ) {
356
+ count ++
357
+ }
358
+ }
310
359
}
311
- return int32 ( count )
360
+ return count
312
361
}
313
362
314
363
// NewWorkflowData creates a workflow.Data object with the given content type and data.
0 commit comments