Skip to content

Commit 4efdd02

Browse files
ackebastiandoetschteodora-sandu
authored
Added the call for RunAnalysis to Trigger Analysis API (#30)
Signed-off-by: Knut Funkel <[email protected]> Co-authored-by: Bastian Doetsch <[email protected]> Co-authored-by: Teodora Sandu <[email protected]>
1 parent 54250ef commit 4efdd02

File tree

7 files changed

+435
-111
lines changed

7 files changed

+435
-111
lines changed

internal/analysis/analysis.go

Lines changed: 205 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -23,33 +23,44 @@ import (
2323
"encoding/json"
2424
"fmt"
2525
"github.com/google/uuid"
26+
openapi_types "github.com/oapi-codegen/runtime/types"
2627
"github.com/pkg/errors"
2728
"github.com/rs/zerolog"
2829
"github.com/snyk/code-client-go/config"
2930
codeClientHTTP "github.com/snyk/code-client-go/http"
31+
orchestrationClient "github.com/snyk/code-client-go/internal/orchestration/2024-02-16"
32+
scans "github.com/snyk/code-client-go/internal/orchestration/2024-02-16/scans"
3033
"github.com/snyk/code-client-go/internal/util"
3134
workspaceClient "github.com/snyk/code-client-go/internal/workspace/2024-03-12"
32-
externalRef3 "github.com/snyk/code-client-go/internal/workspace/2024-03-12/workspaces"
35+
workspaces "github.com/snyk/code-client-go/internal/workspace/2024-03-12/workspaces"
3336
"github.com/snyk/code-client-go/observability"
3437
"github.com/snyk/code-client-go/sarif"
38+
3539
"strings"
40+
"time"
3641
)
3742

38-
//go:embed fake.json
39-
var fakeResponse []byte
43+
//go:generate mockgen -destination=mocks/analysis.go -source=analysis.go -package mocks
44+
type AnalysisOrchestrator interface {
45+
CreateWorkspace(ctx context.Context, orgId string, requestId string, path string, bundleHash string) (string, error)
46+
RunAnalysis(ctx context.Context, orgId string, workspaceId string) (*sarif.SarifResponse, error)
47+
}
4048

4149
type analysisOrchestrator struct {
42-
httpClient codeClientHTTP.HTTPClient
43-
instrumentor observability.Instrumentor
44-
errorReporter observability.ErrorReporter
45-
logger *zerolog.Logger
46-
config config.Config
50+
httpClient codeClientHTTP.HTTPClient
51+
instrumentor observability.Instrumentor
52+
errorReporter observability.ErrorReporter
53+
logger *zerolog.Logger
54+
config config.Config
55+
timeoutInSeconds time.Duration
4756
}
4857

49-
//go:generate mockgen -destination=mocks/analysis.go -source=analysis.go -package mocks
50-
type AnalysisOrchestrator interface {
51-
CreateWorkspace(ctx context.Context, orgId string, requestId string, path string, bundleHash string) (string, error)
52-
RunAnalysis() (*sarif.SarifResponse, error)
58+
type OptionFunc func(*analysisOrchestrator)
59+
60+
func WithTimeoutInSeconds(timeoutInSeconds time.Duration) func(*analysisOrchestrator) {
61+
return func(a *analysisOrchestrator) {
62+
a.timeoutInSeconds = timeoutInSeconds
63+
}
5364
}
5465

5566
func NewAnalysisOrchestrator(
@@ -58,20 +69,27 @@ func NewAnalysisOrchestrator(
5869
httpClient codeClientHTTP.HTTPClient,
5970
instrumentor observability.Instrumentor,
6071
errorReporter observability.ErrorReporter,
61-
) *analysisOrchestrator {
62-
return &analysisOrchestrator{
63-
httpClient,
64-
instrumentor,
65-
errorReporter,
66-
logger,
67-
config,
72+
options ...OptionFunc,
73+
) AnalysisOrchestrator {
74+
a := &analysisOrchestrator{
75+
httpClient: httpClient,
76+
instrumentor: instrumentor,
77+
errorReporter: errorReporter,
78+
logger: logger,
79+
config: config,
80+
timeoutInSeconds: 120 * time.Second,
81+
}
82+
for _, option := range options {
83+
option(a)
6884
}
85+
86+
return a
6987
}
7088

7189
func (a *analysisOrchestrator) CreateWorkspace(ctx context.Context, orgId string, requestId string, path string, bundleHash string) (string, error) {
7290
method := "analysis.CreateWorkspace"
73-
log := a.logger.With().Str("method", method).Logger()
74-
log.Debug().Msg("API: Creating the workspace")
91+
logger := a.logger.With().Str("method", method).Logger()
92+
logger.Debug().Msg("API: Creating the workspace")
7593

7694
span := a.instrumentor.StartSpan(ctx, method)
7795
defer a.instrumentor.Finish(span)
@@ -84,7 +102,7 @@ func (a *analysisOrchestrator) CreateWorkspace(ctx context.Context, orgId string
84102
return "", fmt.Errorf("workspace is not a repository, cannot scan, %w", err)
85103
}
86104

87-
host := a.host()
105+
host := a.host(true)
88106
a.logger.Info().Str("host", host).Str("path", path).Str("repositoryUri", repositoryUri).Msg("creating workspace")
89107

90108
workspace, err := workspaceClient.NewClientWithResponses(host, workspaceClient.WithHTTPClient(a.httpClient))
@@ -101,26 +119,26 @@ func (a *analysisOrchestrator) CreateWorkspace(ctx context.Context, orgId string
101119
}, workspaceClient.CreateWorkspaceApplicationVndAPIPlusJSONRequestBody{
102120
Data: struct {
103121
Attributes struct {
104-
BundleId string `json:"bundle_id"`
105-
RepositoryUri string `json:"repository_uri"`
106-
WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
122+
BundleId string `json:"bundle_id"`
123+
RepositoryUri string `json:"repository_uri"`
124+
WorkspaceType workspaces.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
107125
} `json:"attributes"`
108-
Type externalRef3.WorkspacePostRequestDataType `json:"type"`
126+
Type workspaces.WorkspacePostRequestDataType `json:"type"`
109127
}(struct {
110128
Attributes struct {
111-
BundleId string `json:"bundle_id"`
112-
RepositoryUri string `json:"repository_uri"`
113-
WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
129+
BundleId string `json:"bundle_id"`
130+
RepositoryUri string `json:"repository_uri"`
131+
WorkspaceType workspaces.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
114132
}
115-
Type externalRef3.WorkspacePostRequestDataType
133+
Type workspaces.WorkspacePostRequestDataType
116134
}{Attributes: struct {
117-
BundleId string `json:"bundle_id"`
118-
RepositoryUri string `json:"repository_uri"`
119-
WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
135+
BundleId string `json:"bundle_id"`
136+
RepositoryUri string `json:"repository_uri"`
137+
WorkspaceType workspaces.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
120138
}(struct {
121139
BundleId string
122140
RepositoryUri string
123-
WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType
141+
WorkspaceType workspaces.WorkspacePostRequestDataAttributesWorkspaceType
124142
}{
125143
BundleId: bundleHash,
126144
RepositoryUri: repositoryUri,
@@ -152,17 +170,163 @@ func (a *analysisOrchestrator) CreateWorkspace(ctx context.Context, orgId string
152170
return workspaceResponse.ApplicationvndApiJSON201.Data.Id.String(), nil
153171
}
154172

155-
func (*analysisOrchestrator) RunAnalysis() (*sarif.SarifResponse, error) {
156-
var response sarif.SarifResponse
173+
//go:embed fake.json
174+
var fakeResponse []byte
175+
176+
func (a *analysisOrchestrator) RunAnalysis(ctx context.Context, orgId string, workspaceId string) (*sarif.SarifResponse, error) {
177+
method := "analysis.RunAnalysis"
178+
logger := a.logger.With().Str("method", method).Logger()
179+
logger.Debug().Msg("API: Creating the scan")
180+
org := uuid.MustParse(orgId)
181+
182+
host := a.host(false)
183+
a.logger.Debug().Str("host", host).Str("workspaceId", workspaceId).Msg("starting scan")
184+
185+
client, err := orchestrationClient.NewClientWithResponses(host, orchestrationClient.WithHTTPClient(a.httpClient))
157186

158-
err := json.Unmarshal(fakeResponse, &response)
159187
if err != nil {
160-
return nil, fmt.Errorf("failed to create SARIF response: %w", err)
188+
return nil, fmt.Errorf("failed to create orchestrationClient: %w", err)
189+
}
190+
191+
flow := scans.Flow{}
192+
err = flow.UnmarshalJSON([]byte(`{"name": "cli_test"}`))
193+
if err != nil {
194+
return nil, fmt.Errorf("failed to create scan request: %w", err)
195+
}
196+
createScanResponse, err := client.CreateScanWorkspaceJobForUserWithApplicationVndAPIPlusJSONBodyWithResponse(
197+
ctx,
198+
org,
199+
&orchestrationClient.CreateScanWorkspaceJobForUserParams{Version: "2024-02-16~experimental"},
200+
orchestrationClient.CreateScanWorkspaceJobForUserApplicationVndAPIPlusJSONRequestBody{Data: struct {
201+
Attributes struct {
202+
Flow scans.Flow `json:"flow"`
203+
WorkspaceUrl string `json:"workspace_url"`
204+
} `json:"attributes"`
205+
Id *openapi_types.UUID `json:"id,omitempty"`
206+
Type scans.PostScanRequestDataType `json:"type"`
207+
}(struct {
208+
Attributes struct {
209+
Flow scans.Flow `json:"flow"`
210+
WorkspaceUrl string `json:"workspace_url"`
211+
}
212+
Id *openapi_types.UUID
213+
Type scans.PostScanRequestDataType
214+
}{
215+
Attributes: struct {
216+
Flow scans.Flow `json:"flow"`
217+
WorkspaceUrl string `json:"workspace_url"`
218+
}(struct {
219+
Flow scans.Flow
220+
WorkspaceUrl string
221+
}{
222+
Flow: flow,
223+
WorkspaceUrl: fmt.Sprintf("http://workspace-service/workspaces/%s", workspaceId),
224+
}),
225+
Type: "workspace",
226+
})})
227+
228+
if err != nil {
229+
return nil, fmt.Errorf("failed to trigger scan: %w", err)
230+
}
231+
232+
if createScanResponse.ApplicationvndApiJSON201 == nil {
233+
msg := a.getStatusCode(createScanResponse)
234+
return nil, errors.New(msg)
235+
}
236+
237+
scanJobId := createScanResponse.ApplicationvndApiJSON201.Data.Id
238+
a.logger.Debug().Str("host", host).Str("workspaceId", workspaceId).Msg("starting scan")
239+
240+
// Actual polling loop.
241+
pollingTicker := time.NewTicker(1 * time.Second)
242+
defer pollingTicker.Stop()
243+
timeoutTimer := time.NewTimer(a.timeoutInSeconds)
244+
defer timeoutTimer.Stop()
245+
for {
246+
select {
247+
case <-timeoutTimer.C:
248+
msg := "timeout requesting the ScanJobResult"
249+
logger.Error().Str("scanJobId", scanJobId.String()).Msg(msg)
250+
return nil, errors.New(msg)
251+
case <-pollingTicker.C:
252+
_, complete, err := a.poller(ctx, logger, client, org, scanJobId, method) // todo add processing of the response with the findings
253+
if err != nil {
254+
return nil, err
255+
}
256+
if !complete {
257+
continue
258+
}
259+
260+
var response sarif.SarifResponse
261+
_ = json.Unmarshal(fakeResponse, &response)
262+
263+
return &response, nil
264+
}
161265
}
162-
return &response, nil
163266
}
164267

165-
func (a *analysisOrchestrator) host() string {
268+
func (a *analysisOrchestrator) poller(ctx context.Context, logger zerolog.Logger, client *orchestrationClient.ClientWithResponses, org uuid.UUID, scanJobId openapi_types.UUID, method string) (response *orchestrationClient.GetScanWorkspaceJobForUserResponse, complete bool, err error) {
269+
logger.Debug().Msg("polling for ScanJobResult")
270+
httpResponse, err := client.GetScanWorkspaceJobForUserWithResponse(
271+
ctx,
272+
org,
273+
scanJobId,
274+
&orchestrationClient.GetScanWorkspaceJobForUserParams{Version: "2024-02-16~experimental"},
275+
)
276+
if err != nil {
277+
logger.Err(err).Str("method", method).Str("scanJobId", scanJobId.String()).Msg("error requesting the ScanJobResult")
278+
return httpResponse, true, err
279+
}
280+
281+
var msg string
282+
switch httpResponse.StatusCode() {
283+
case 200:
284+
scanJobStatus := httpResponse.ApplicationvndApiJSON200.Data.Attributes.Status
285+
if scanJobStatus == scans.ScanJobResultsAttributesStatusInProgress {
286+
return httpResponse, false, nil
287+
} else {
288+
return httpResponse, true, nil
289+
}
290+
case 400:
291+
msg = httpResponse.ApplicationvndApiJSON400.Errors[0].Detail
292+
case 401:
293+
msg = httpResponse.ApplicationvndApiJSON401.Errors[0].Detail
294+
case 403:
295+
msg = httpResponse.ApplicationvndApiJSON403.Errors[0].Detail
296+
case 404:
297+
msg = httpResponse.ApplicationvndApiJSON404.Errors[0].Detail
298+
case 429:
299+
msg = httpResponse.ApplicationvndApiJSON429.Errors[0].Detail
300+
case 500:
301+
msg = httpResponse.ApplicationvndApiJSON500.Errors[0].Detail
302+
}
303+
return nil, true, errors.New(msg)
304+
}
305+
306+
func (a *analysisOrchestrator) getStatusCode(createScanResponse *orchestrationClient.CreateScanWorkspaceJobForUserResponse) string {
307+
var msg string
308+
switch createScanResponse.StatusCode() {
309+
case 400:
310+
msg = createScanResponse.ApplicationvndApiJSON400.Errors[0].Detail
311+
case 401:
312+
msg = createScanResponse.ApplicationvndApiJSON401.Errors[0].Detail
313+
case 403:
314+
msg = createScanResponse.ApplicationvndApiJSON403.Errors[0].Detail
315+
case 404:
316+
msg = createScanResponse.ApplicationvndApiJSON404.Errors[0].Detail
317+
case 429:
318+
msg = createScanResponse.ApplicationvndApiJSON429.Errors[0].Detail
319+
case 500:
320+
msg = createScanResponse.ApplicationvndApiJSON500.Errors[0].Detail
321+
}
322+
return msg
323+
}
324+
325+
func (a *analysisOrchestrator) host(isHidden bool) string {
166326
apiUrl := strings.TrimRight(a.config.SnykApi(), "/")
167-
return fmt.Sprintf("%s/hidden", apiUrl)
327+
path := "rest"
328+
if isHidden {
329+
path = "hidden"
330+
}
331+
return fmt.Sprintf("%s/%s", apiUrl, path)
168332
}

0 commit comments

Comments
 (0)