9
9
package ostest
10
10
11
11
import (
12
+ "bytes"
12
13
"context"
13
14
"encoding/json"
14
15
std_errors "errors"
15
16
"fmt"
16
17
"math"
17
18
"os"
18
19
"sort"
20
+ "time"
19
21
20
22
"github.com/rs/zerolog"
21
23
"github.com/snyk/go-application-framework/pkg/apiclients/testapi"
@@ -29,6 +31,7 @@ import (
29
31
service "github.com/snyk/cli-extension-os-flows/internal/common"
30
32
"github.com/snyk/cli-extension-os-flows/internal/errors"
31
33
"github.com/snyk/cli-extension-os-flows/internal/flags"
34
+ "github.com/snyk/cli-extension-os-flows/internal/legacy/definitions"
32
35
"github.com/snyk/cli-extension-os-flows/internal/legacy/transform"
33
36
"github.com/snyk/cli-extension-os-flows/internal/snykclient"
34
37
)
@@ -57,6 +60,9 @@ const LogFieldCount = "count"
57
60
// ErrNoSummaryData is returned when a test summary cannot be generated due to lack of data.
58
61
var ErrNoSummaryData = std_errors .New ("no summary data to create" )
59
62
63
+ // PollInterval is the polling interval for the test API. It is exported to be configurable in tests.
64
+ var PollInterval = 8 * time .Second
65
+
60
66
// RegisterWorkflows registers the "test" workflow.
61
67
func RegisterWorkflows (e workflow.Engine ) error {
62
68
// Check if workflow already exists
@@ -140,12 +146,6 @@ func OSWorkflow(
140
146
141
147
// Unified test flow
142
148
143
- filename := config .GetString (flags .FlagFile )
144
- if filename == "" {
145
- logger .Error ().Msg ("No file specified for testing" )
146
- return nil , errFactory .NewMissingFilenameFlagError ()
147
- }
148
-
149
149
var riskScorePtr * uint16
150
150
if riskScoreThreshold >= math .MaxUint16 {
151
151
// the API will enforce a range from the test spec
@@ -164,14 +164,29 @@ func OSWorkflow(
164
164
severityThresholdPtr = & st
165
165
}
166
166
167
- return runUnifiedTestFlow (ctx , filename , riskScorePtr , severityThresholdPtr , ictx , config , orgID , errFactory , logger )
167
+ return runUnifiedTestFlow (ctx , riskScorePtr , severityThresholdPtr , ictx , config , orgID , errFactory , logger )
168
168
}
169
169
}
170
170
171
+ // Create local policy only if risk score or severity threshold are specified.
172
+ func createLocalPolicy (riskScoreThreshold * uint16 , severityThreshold * testapi.Severity ) * testapi.LocalPolicy {
173
+ if riskScoreThreshold == nil && severityThreshold == nil {
174
+ return nil
175
+ }
176
+
177
+ localPolicy := & testapi.LocalPolicy {}
178
+ if riskScoreThreshold != nil {
179
+ localPolicy .RiskScoreThreshold = riskScoreThreshold
180
+ }
181
+ if severityThreshold != nil {
182
+ localPolicy .SeverityThreshold = severityThreshold
183
+ }
184
+ return localPolicy
185
+ }
186
+
171
187
// runUnifiedTestFlow handles the unified test API flow.
172
188
func runUnifiedTestFlow (
173
189
ctx context.Context ,
174
- filename string ,
175
190
riskScoreThreshold * uint16 ,
176
191
severityThreshold * testapi.Severity ,
177
192
ictx workflow.InvocationContext ,
@@ -182,56 +197,90 @@ func runUnifiedTestFlow(
182
197
) ([]workflow.Data , error ) {
183
198
logger .Info ().Msg ("Starting open source test" )
184
199
185
- // Create depgraph
186
- depGraph , err := createDepGraph (ictx )
200
+ // Create depgraphs and get their associated target files
201
+ depGraphs , displayTargetFiles , err := createDepGraphs (ictx )
187
202
if err != nil {
188
- return nil , fmt . Errorf ( "failed to create depgraph: %w" , err )
203
+ return nil , err
189
204
}
205
+ var allFindings []definitions.LegacyVulnerabilityResponse
206
+ var allSummaries []workflow.Data
190
207
191
- // Create depgraph subject
192
- depGraphSubject := testapi.DepGraphSubjectCreate {
193
- Type : testapi .DepGraphSubjectCreateTypeDepGraph ,
194
- DepGraph : depGraph ,
195
- Locator : testapi.LocalPathLocator {
196
- Paths : []string {filename },
197
- Type : testapi .LocalPath ,
198
- },
199
- }
208
+ localPolicy := createLocalPolicy (riskScoreThreshold , severityThreshold )
200
209
201
- // Create test subject with depgraph
202
- var subject testapi.TestSubjectCreate
203
- err = subject .FromDepGraphSubjectCreate (depGraphSubject )
204
- if err != nil {
205
- return nil , fmt .Errorf ("failed to create test subject: %w" , err )
206
- }
210
+ for i , depGraph := range depGraphs {
211
+ displayTargetFile := ""
212
+ if i < len (displayTargetFiles ) {
213
+ displayTargetFile = displayTargetFiles [i ]
214
+ }
215
+
216
+ // Create depgraph subject
217
+ depGraphSubject := testapi.DepGraphSubjectCreate {
218
+ Type : testapi .DepGraphSubjectCreateTypeDepGraph ,
219
+ DepGraph : depGraph ,
220
+ Locator : testapi.LocalPathLocator {
221
+ Paths : []string {displayTargetFile },
222
+ Type : testapi .LocalPath ,
223
+ },
224
+ }
225
+
226
+ // Create test subject with depgraph
227
+ var subject testapi.TestSubjectCreate
228
+ err = subject .FromDepGraphSubjectCreate (depGraphSubject )
229
+ if err != nil {
230
+ return nil , fmt .Errorf ("failed to create test subject: %w" , err )
231
+ }
232
+
233
+ // Project name assigned as follows: --project-name || config project name || scannedProject?.depTree?.name
234
+ // TODO: use project name from Config file
235
+ config := ictx .GetConfiguration ()
236
+ projectName := config .GetString (flags .FlagProjectName )
237
+ if projectName == "" && len (depGraph .Pkgs ) > 0 {
238
+ projectName = depGraph .Pkgs [0 ].Info .Name
239
+ }
240
+
241
+ packageManager := depGraph .PkgManager .Name
242
+ depCount := max (0 , len (depGraph .Pkgs )- 1 )
207
243
208
- // Only create local policy if risk score or severity threshold are specified
209
- var localPolicy * testapi.LocalPolicy
210
- if riskScoreThreshold != nil || severityThreshold != nil {
211
- localPolicy = & testapi.LocalPolicy {}
212
- if riskScoreThreshold != nil {
213
- localPolicy .RiskScoreThreshold = riskScoreThreshold
244
+ // Run the test with the depgraph subject
245
+ findings , summary , err := runTest (ctx , subject , projectName , packageManager , depCount , displayTargetFile , ictx , orgID , errFactory , logger , localPolicy )
246
+ if err != nil {
247
+ return nil , err
248
+ }
249
+
250
+ if findings != nil {
251
+ allFindings = append (allFindings , * findings )
214
252
}
215
- if severityThreshold != nil {
216
- localPolicy . SeverityThreshold = severityThreshold
253
+ if summary != nil {
254
+ allSummaries = append ( allSummaries , summary )
217
255
}
218
256
}
219
257
220
- // Project name assigned as follows: --project-name || config project name || scannedProject?.depTree?.name
221
- // TODO: use project name from Config file
222
- // TODO: verify - depTree is a legacy depgraph concept that I don't see in cli-extension-dep-graph, but the name
223
- // appears to come from the first Pkg item.
224
- config := ictx .GetConfiguration ()
225
- projectName := config .GetString (flags .FlagProjectName )
226
- if projectName == "" && len (depGraph .Pkgs ) > 0 {
227
- projectName = depGraph .Pkgs [0 ].Info .Name
258
+ var finalOutput []workflow.Data
259
+ if len (allFindings ) > 0 {
260
+ var findingsData any
261
+ if len (allFindings ) == 1 {
262
+ findingsData = allFindings [0 ]
263
+ } else {
264
+ findingsData = allFindings
265
+ }
266
+
267
+ var buffer bytes.Buffer
268
+ encoder := json .NewEncoder (& buffer )
269
+ encoder .SetEscapeHTML (false )
270
+ encoder .SetIndent ("" , " " )
271
+ err := encoder .Encode (findingsData )
272
+ if err != nil {
273
+ return nil , errFactory .NewLegacyJSONTransformerError (fmt .Errorf ("marshaling to json: %w" , err ))
274
+ }
275
+ // encoder.Encode adds a newline, which we trim to match Marshal's behavior.
276
+ findingsBytes := bytes .TrimRight (buffer .Bytes (), "\n " )
277
+
278
+ finalOutput = append (finalOutput , NewWorkflowData (ApplicationJSONContentType , findingsBytes ))
228
279
}
229
280
230
- packageManager := depGraph .PkgManager .Name
231
- depCount := max (0 , len (depGraph .Pkgs )- 1 )
281
+ finalOutput = append (finalOutput , allSummaries ... )
232
282
233
- // Run the test with the depgraph subject
234
- return runTest (ctx , subject , projectName , packageManager , depCount , ictx , orgID , errFactory , logger , localPolicy )
283
+ return finalOutput , nil
235
284
}
236
285
237
286
// runTest executes the common test flow with the provided test subject.
@@ -241,12 +290,13 @@ func runTest(
241
290
projectName string ,
242
291
packageManager string ,
243
292
depCount int ,
293
+ displayTargetFile string ,
244
294
ictx workflow.InvocationContext ,
245
295
orgID string ,
246
296
errFactory * errors.ErrorFactory ,
247
297
logger * zerolog.Logger ,
248
298
localPolicy * testapi.LocalPolicy ,
249
- ) ([] workflow.Data , error ) {
299
+ ) (* definitions. LegacyVulnerabilityResponse , workflow.Data , error ) {
250
300
// Create Snyk client
251
301
httpClient := ictx .GetNetworkAccess ().GetHttpClient ()
252
302
snykClient := snykclient .NewSnykClient (httpClient , ictx .GetConfiguration ().GetString (configuration .API_URL ), orgID )
@@ -260,24 +310,25 @@ func runTest(
260
310
// Create and execute test client
261
311
testClient , err := testapi .NewTestClient (
262
312
snykClient .GetAPIBaseURL (),
313
+ testapi .WithPollInterval (PollInterval ),
263
314
testapi .WithCustomHTTPClient (snykClient .GetClient ()),
264
315
)
265
316
if err != nil {
266
- return nil , fmt .Errorf ("failed to create test client: %w" , err )
317
+ return nil , nil , fmt .Errorf ("failed to create test client: %w" , err )
267
318
}
268
319
269
320
handle , err := testClient .StartTest (ctx , startParams )
270
321
if err != nil {
271
- return nil , fmt .Errorf ("failed to start test: %w" , err )
322
+ return nil , nil , fmt .Errorf ("failed to start test: %w" , err )
272
323
}
273
324
274
325
if waitErr := handle .Wait (ctx ); waitErr != nil {
275
- return nil , fmt .Errorf ("test run failed: %w" , waitErr )
326
+ return nil , nil , fmt .Errorf ("test run failed: %w" , waitErr )
276
327
}
277
328
278
329
finalResult := handle .Result ()
279
330
if finalResult == nil {
280
- return nil , fmt .Errorf ("test completed but no result was returned" )
331
+ return nil , nil , fmt .Errorf ("test completed but no result was returned" )
281
332
}
282
333
283
334
// Get findings for the test
@@ -300,7 +351,7 @@ func runTest(
300
351
currentDir , err := os .Getwd ()
301
352
if err != nil {
302
353
logger .Error ().Err (err ).Msg ("Failed to get current working directory" )
303
- return nil , fmt .Errorf ("failed to get current working directory: %w" , err )
354
+ return nil , nil , fmt .Errorf ("failed to get current working directory: %w" , err )
304
355
}
305
356
306
357
var uniqueCount int32
@@ -314,35 +365,32 @@ func runTest(
314
365
}
315
366
}
316
367
317
- legacyJSON , err := transform .ConvertSnykSchemaFindingsToLegacyJSON (
368
+ legacyVulnResponse , err := transform .ConvertSnykSchemaFindingsToLegacy (
318
369
& transform.SnykSchemaToLegacyParams {
319
- Findings : findingsData ,
320
- TestResult : finalResult ,
321
- ProjectName : projectName ,
322
- PackageManager : packageManager ,
323
- CurrentDir : currentDir ,
324
- UniqueCount : uniqueCount ,
325
- DepCount : depCount ,
326
- ErrFactory : errFactory ,
327
- Logger : logger ,
370
+ Findings : findingsData ,
371
+ TestResult : finalResult ,
372
+ ProjectName : projectName ,
373
+ PackageManager : packageManager ,
374
+ CurrentDir : currentDir ,
375
+ UniqueCount : uniqueCount ,
376
+ DepCount : depCount ,
377
+ DisplayTargetFile : displayTargetFile ,
378
+ ErrFactory : errFactory ,
379
+ Logger : logger ,
328
380
})
329
381
if err != nil {
330
- return nil , fmt .Errorf ("error converting snyk schema findings to legacy json: %w" , err )
382
+ return nil , nil , fmt .Errorf ("error converting snyk schema findings to legacy json: %w" , err )
331
383
}
332
384
333
- legacyData := NewWorkflowData (ApplicationJSONContentType , legacyJSON )
334
- outputData := []workflow.Data {legacyData }
335
-
336
385
summaryData , err := NewSummaryData (finalResult , logger , currentDir )
337
386
if err != nil {
338
387
if ! std_errors .Is (err , ErrNoSummaryData ) {
339
388
logger .Warn ().Err (err ).Msg ("Failed to create test summary for exit code handling" )
340
389
}
341
- } else {
342
- outputData = append (outputData , summaryData )
390
+ return legacyVulnResponse , nil , nil
343
391
}
344
392
345
- return outputData , nil
393
+ return legacyVulnResponse , summaryData , nil
346
394
}
347
395
348
396
// extractSeverityKeys returns a map of severity keys present in the summaries.
@@ -382,13 +430,13 @@ func NewSummaryData(testResult testapi.TestResult, logger *zerolog.Logger, path
382
430
return nil , fmt .Errorf ("test result missing summary information" )
383
431
}
384
432
385
- severityKeys := extractSeverityKeys (rawSummary , effectiveSummary )
386
-
387
- if len (severityKeys ) == 0 && rawSummary .Count == 0 {
433
+ if rawSummary .Count == 0 {
388
434
logger .Debug ().Msg ("No findings in summary, skipping summary creation." )
389
435
return nil , fmt .Errorf ("no findings in summary: %w" , ErrNoSummaryData )
390
436
}
391
437
438
+ severityKeys := extractSeverityKeys (rawSummary , effectiveSummary )
439
+
392
440
var summaryResults []json_schemas.TestSummaryResult
393
441
for severity := range severityKeys {
394
442
total := getSeverityCount (rawSummary , severity )
@@ -444,31 +492,29 @@ func runReachabilityFlow(
444
492
return sbomTestReachability (ctx , config , errFactory , ictx , logger , sbomPath , sourceCodePath )
445
493
}
446
494
447
- // createDepGraph creates a depgraph from the file parameter in the context.
448
- func createDepGraph (ictx workflow.InvocationContext ) (testapi.IoSnykApiV1testdepgraphRequestDepGraph , error ) {
449
- var contents []byte
450
- var err error
451
-
495
+ // createDepGraphs creates depgraphs from the file parameter in the context.
496
+ func createDepGraphs (ictx workflow.InvocationContext ) ([]testapi.IoSnykApiV1testdepgraphRequestDepGraph , []string , error ) {
452
497
depGraphResult , err := service .GetDepGraph (ictx )
453
498
if err != nil {
454
- return testapi. IoSnykApiV1testdepgraphRequestDepGraph {} , fmt .Errorf ("failed to get dependency graph: %w" , err )
499
+ return nil , nil , fmt .Errorf ("failed to get dependency graph: %w" , err )
455
500
}
456
501
457
- if len (depGraphResult .DepGraphBytes ) > 1 {
458
- err = fmt .Errorf ("multiple depgraphs found, but only one is currently supported" )
459
- return testapi.IoSnykApiV1testdepgraphRequestDepGraph {}, err
502
+ if len (depGraphResult .DepGraphBytes ) == 0 {
503
+ return nil , nil , fmt .Errorf ("no dependency graphs found" )
460
504
}
461
- // TODO revisit handling multiple depgraphs
462
- contents = depGraphResult .DepGraphBytes [0 ]
463
505
464
- var depGraphStruct testapi.IoSnykApiV1testdepgraphRequestDepGraph
465
- err = json .Unmarshal (contents , & depGraphStruct )
466
- if err != nil {
467
- return testapi.IoSnykApiV1testdepgraphRequestDepGraph {},
468
- fmt .Errorf ("unmarshaling depGraph from args failed: %w" , err )
506
+ depGraphs := make ([]testapi.IoSnykApiV1testdepgraphRequestDepGraph , len (depGraphResult .DepGraphBytes ))
507
+ for i , depGraphBytes := range depGraphResult .DepGraphBytes {
508
+ var depGraphStruct testapi.IoSnykApiV1testdepgraphRequestDepGraph
509
+ err = json .Unmarshal (depGraphBytes , & depGraphStruct )
510
+ if err != nil {
511
+ return nil , nil ,
512
+ fmt .Errorf ("unmarshaling depGraph from args failed: %w" , err )
513
+ }
514
+ depGraphs [i ] = depGraphStruct
469
515
}
470
516
471
- return depGraphStruct , nil
517
+ return depGraphs , depGraphResult . DisplayTargetFiles , nil
472
518
}
473
519
474
520
// NewWorkflowData creates a workflow.Data object with the given content type and data.
0 commit comments