@@ -34,6 +34,9 @@ import (
3434
3535 "github.com/snyk/code-client-go/config"
3636 codeClientHTTP "github.com/snyk/code-client-go/http"
37+ testApi "github.com/snyk/code-client-go/internal/api/test/2024-12-21"
38+ testModels "github.com/snyk/code-client-go/internal/api/test/2024-12-21/models"
39+ "github.com/snyk/code-client-go/internal/bundle"
3740 orchestrationClient "github.com/snyk/code-client-go/internal/orchestration/2024-02-16"
3841 scans "github.com/snyk/code-client-go/internal/orchestration/2024-02-16/scans"
3942 workspaceClient "github.com/snyk/code-client-go/internal/workspace/2024-05-14"
@@ -48,6 +51,8 @@ type AnalysisOrchestrator interface {
4851 CreateWorkspace (ctx context.Context , orgId string , requestId string , path scan.Target , bundleHash string ) (string , error )
4952 RunAnalysis (ctx context.Context , orgId string , rootPath string , workspaceId string ) (* sarif.SarifResponse , error )
5053 RunIncrementalAnalysis (ctx context.Context , orgId string , rootPath string , workspaceId string , limitToFiles []string ) (* sarif.SarifResponse , error )
54+
55+ RunTest (ctx context.Context , orgId string , b bundle.Bundle , target scan.Target ) (* sarif.SarifResponse , error )
5156}
5257
5358type analysisOrchestrator struct {
@@ -58,6 +63,7 @@ type analysisOrchestrator struct {
5863 trackerFactory scan.TrackerFactory
5964 config config.Config
6065 flow scans.Flow
66+ testType testModels.Scan
6167}
6268
6369type OptionFunc func (* analysisOrchestrator )
@@ -86,10 +92,9 @@ func WithTrackerFactory(factory scan.TrackerFactory) func(*analysisOrchestrator)
8692 }
8793}
8894
89- func WithFlow ( flow string ) func (* analysisOrchestrator ) {
95+ func WithResultType ( t testModels. Scan ) func (* analysisOrchestrator ) {
9096 return func (a * analysisOrchestrator ) {
91- a .flow = scans.Flow {}
92- _ = a .flow .UnmarshalJSON ([]byte (fmt .Sprintf (`{"name": "%s"}` , flow )))
97+ a .testType = t
9398 }
9499}
95100
@@ -109,7 +114,7 @@ func NewAnalysisOrchestrator(
109114 trackerFactory : scan .NewNoopTrackerFactory (),
110115 errorReporter : observability .NewErrorReporter (& nopLogger ),
111116 logger : & nopLogger ,
112- flow : flow ,
117+ testType : testModels . CodeSecurityCodeQuality ,
113118 }
114119
115120 for _ , option := range options {
@@ -428,11 +433,11 @@ func (a *analysisOrchestrator) retrieveFindings(ctx context.Context, scanJobId u
428433 return nil , errors .New ("do not have a findings URL" )
429434 }
430435 req , err := http .NewRequest (http .MethodGet , findingsUrl , nil )
431- req = req .WithContext (ctx )
432-
433436 if err != nil {
434437 return nil , err
435438 }
439+ req = req .WithContext (ctx )
440+
436441 rsp , err := a .httpClient .Do (req )
437442 if err != nil {
438443 return nil , err
@@ -471,3 +476,136 @@ func (a *analysisOrchestrator) host(isHidden bool) string {
471476 }
472477 return fmt .Sprintf ("%s/%s" , apiUrl , path )
473478}
479+
480+ func (a * analysisOrchestrator ) RunTest (ctx context.Context , orgId string , b bundle.Bundle , target scan.Target ) (* sarif.SarifResponse , error ) {
481+ tracker := a .trackerFactory .GenerateTracker ()
482+ tracker .Begin ("Snyk Code analysis for " + target .GetPath (), "Retrieving results..." )
483+
484+ orgUuid := uuid .MustParse (orgId )
485+ host := a .host (true )
486+ var repoUrl * string = nil
487+ if repoTarget , ok := target .(* scan.RepositoryTarget ); ok {
488+ tmp := repoTarget .GetRepositoryUrl ()
489+ repoUrl = & tmp
490+ }
491+
492+ client , err := testApi .NewClient (host , testApi .WithHTTPClient (a .httpClient ))
493+ if err != nil {
494+ return nil , err
495+ }
496+
497+ params := testApi.CreateTestParams {Version : testApi .ApiVersion }
498+ body := testApi .NewCreateTestApplicationBody (
499+ testApi .WithInputBundle (b .GetBundleHash (), target .GetPath (), repoUrl , b .GetLimitToFiles ()),
500+ testApi .WithScanType (a .testType ),
501+ )
502+
503+ // create test
504+ resp , err := client .CreateTestWithApplicationVndAPIPlusJSONBody (ctx , orgUuid , & params , * body )
505+ if err != nil {
506+ return nil , err
507+ }
508+
509+ parsedResponse , err := testApi .ParseCreateTestResponse (resp )
510+ defer func () {
511+ closeErr := resp .Body .Close ()
512+ a .logger .Err (closeErr ).Msg ("failed to close response body" )
513+ }()
514+ if err != nil {
515+ a .logger .Debug ().Msg (err .Error ())
516+ return nil , err
517+ }
518+
519+ switch parsedResponse .StatusCode () {
520+ case http .StatusCreated :
521+ // poll results
522+ result , pollErr := a .pollTestForFindings (ctx , client , orgUuid , parsedResponse .ApplicationvndApiJSON201 .Data .Id )
523+ tracker .End ("Analysis complete." )
524+ return result , pollErr
525+ default :
526+ return nil , fmt .Errorf ("failed to create test: %s" , parsedResponse .Status ())
527+ }
528+ }
529+
530+ func (a * analysisOrchestrator ) pollTestForFindings (ctx context.Context , client * testApi.Client , org uuid.UUID , testId openapi_types.UUID ) (* sarif.SarifResponse , error ) {
531+ method := "analysis.pollTestForFindings"
532+ logger := a .logger .With ().Str ("method" , method ).Logger ()
533+
534+ pollingTicker := time .NewTicker (1 * time .Second )
535+ defer pollingTicker .Stop ()
536+ timeoutTimer := time .NewTimer (a .config .SnykCodeAnalysisTimeout ())
537+ defer timeoutTimer .Stop ()
538+ for {
539+ select {
540+ case <- timeoutTimer .C :
541+ msg := "Snyk Code analysis timed out"
542+ logger .Error ().Str ("scanJobId" , testId .String ()).Msg (msg )
543+ return nil , errors .New (msg )
544+ case <- pollingTicker .C :
545+ findingsUrl , complete , err := a .retrieveTestURL (ctx , client , org , testId )
546+ if err != nil {
547+ return nil , err
548+ }
549+ if complete {
550+ findings , findingsErr := a .retrieveFindings (ctx , testId , findingsUrl )
551+ if findingsErr != nil {
552+ return nil , findingsErr
553+ }
554+ return findings , nil
555+ }
556+ }
557+ }
558+ }
559+
560+ func (a * analysisOrchestrator ) retrieveTestURL (ctx context.Context , client * testApi.Client , org uuid.UUID , testId openapi_types.UUID ) (url string , completed bool , err error ) {
561+ method := "analysis.retrieveTestURL"
562+ logger := a .logger .With ().Str ("method" , method ).Logger ()
563+ logger .Debug ().Msg ("retrieving Test URL" )
564+
565+ httpResponse , err := client .GetTestResult (
566+ ctx ,
567+ org ,
568+ testId ,
569+ & testApi.GetTestResultParams {Version : testApi .ApiVersion },
570+ )
571+ if err != nil {
572+ logger .Err (err ).Str ("testId" , testId .String ()).Msg ("error requesting the ScanJobResult" )
573+ return "" , false , err
574+ }
575+ defer func () {
576+ closeErr := httpResponse .Body .Close ()
577+ a .logger .Err (closeErr ).Msg ("failed to close response body" )
578+ }()
579+
580+ parsedResponse , err := testApi .ParseGetTestResultResponse (httpResponse )
581+ if err != nil {
582+ return "" , false , err
583+ }
584+
585+ switch parsedResponse .StatusCode () {
586+ case 200 :
587+ stateDiscriminator , stateError := parsedResponse .ApplicationvndApiJSON200 .Data .Attributes .Discriminator ()
588+ if stateError != nil {
589+ return "" , false , stateError
590+ }
591+
592+ switch stateDiscriminator {
593+ case string (testModels .TestAcceptedStateStatusAccepted ):
594+ fallthrough
595+ case string (testModels .TestInProgressStateStatusInProgress ):
596+ return "" , false , nil
597+ case string (testModels .TestCompletedStateStatusCompleted ):
598+ testCompleted , stateCompleteError := parsedResponse .ApplicationvndApiJSON200 .Data .Attributes .AsTestCompletedState ()
599+ if stateCompleteError != nil {
600+ return "" , false , stateCompleteError
601+ }
602+
603+ findingsUrl := a .host (true ) + testCompleted .Documents .EnrichedSarif + "?version=" + testApi .DocumentApiVersion
604+ return findingsUrl , true , nil
605+ default :
606+ return "" , false , fmt .Errorf ("unexpected test status \" %s\" " , stateDiscriminator )
607+ }
608+ default :
609+ return "" , false , fmt .Errorf ("unexpected response status \" %d\" " , parsedResponse .StatusCode ())
610+ }
611+ }
0 commit comments