Skip to content

Commit

Permalink
Added the call for RunAnalysis to Trigger Analysis API (#30)
Browse files Browse the repository at this point in the history
Signed-off-by: Knut Funkel <knut.funkel@snyk.io>
Co-authored-by: Bastian Doetsch <bastian.doetsch@snyk.io>
Co-authored-by: Teodora Sandu <teodora.sandu@snyk.io>
  • Loading branch information
3 people authored Apr 23, 2024
1 parent 54250ef commit 4efdd02
Show file tree
Hide file tree
Showing 7 changed files with 435 additions and 111 deletions.
246 changes: 205 additions & 41 deletions internal/analysis/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,33 +23,44 @@ import (
"encoding/json"
"fmt"
"github.com/google/uuid"
openapi_types "github.com/oapi-codegen/runtime/types"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/snyk/code-client-go/config"
codeClientHTTP "github.com/snyk/code-client-go/http"
orchestrationClient "github.com/snyk/code-client-go/internal/orchestration/2024-02-16"
scans "github.com/snyk/code-client-go/internal/orchestration/2024-02-16/scans"
"github.com/snyk/code-client-go/internal/util"
workspaceClient "github.com/snyk/code-client-go/internal/workspace/2024-03-12"
externalRef3 "github.com/snyk/code-client-go/internal/workspace/2024-03-12/workspaces"
workspaces "github.com/snyk/code-client-go/internal/workspace/2024-03-12/workspaces"
"github.com/snyk/code-client-go/observability"
"github.com/snyk/code-client-go/sarif"

"strings"
"time"
)

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

type analysisOrchestrator struct {
httpClient codeClientHTTP.HTTPClient
instrumentor observability.Instrumentor
errorReporter observability.ErrorReporter
logger *zerolog.Logger
config config.Config
httpClient codeClientHTTP.HTTPClient
instrumentor observability.Instrumentor
errorReporter observability.ErrorReporter
logger *zerolog.Logger
config config.Config
timeoutInSeconds time.Duration
}

//go:generate mockgen -destination=mocks/analysis.go -source=analysis.go -package mocks
type AnalysisOrchestrator interface {
CreateWorkspace(ctx context.Context, orgId string, requestId string, path string, bundleHash string) (string, error)
RunAnalysis() (*sarif.SarifResponse, error)
type OptionFunc func(*analysisOrchestrator)

func WithTimeoutInSeconds(timeoutInSeconds time.Duration) func(*analysisOrchestrator) {
return func(a *analysisOrchestrator) {
a.timeoutInSeconds = timeoutInSeconds
}
}

func NewAnalysisOrchestrator(
Expand All @@ -58,20 +69,27 @@ func NewAnalysisOrchestrator(
httpClient codeClientHTTP.HTTPClient,
instrumentor observability.Instrumentor,
errorReporter observability.ErrorReporter,
) *analysisOrchestrator {
return &analysisOrchestrator{
httpClient,
instrumentor,
errorReporter,
logger,
config,
options ...OptionFunc,
) AnalysisOrchestrator {
a := &analysisOrchestrator{
httpClient: httpClient,
instrumentor: instrumentor,
errorReporter: errorReporter,
logger: logger,
config: config,
timeoutInSeconds: 120 * time.Second,
}
for _, option := range options {
option(a)
}

return a
}

func (a *analysisOrchestrator) CreateWorkspace(ctx context.Context, orgId string, requestId string, path string, bundleHash string) (string, error) {
method := "analysis.CreateWorkspace"
log := a.logger.With().Str("method", method).Logger()
log.Debug().Msg("API: Creating the workspace")
logger := a.logger.With().Str("method", method).Logger()
logger.Debug().Msg("API: Creating the workspace")

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

host := a.host()
host := a.host(true)
a.logger.Info().Str("host", host).Str("path", path).Str("repositoryUri", repositoryUri).Msg("creating workspace")

workspace, err := workspaceClient.NewClientWithResponses(host, workspaceClient.WithHTTPClient(a.httpClient))
Expand All @@ -101,26 +119,26 @@ func (a *analysisOrchestrator) CreateWorkspace(ctx context.Context, orgId string
}, workspaceClient.CreateWorkspaceApplicationVndAPIPlusJSONRequestBody{
Data: struct {
Attributes struct {
BundleId string `json:"bundle_id"`
RepositoryUri string `json:"repository_uri"`
WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
BundleId string `json:"bundle_id"`
RepositoryUri string `json:"repository_uri"`
WorkspaceType workspaces.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
} `json:"attributes"`
Type externalRef3.WorkspacePostRequestDataType `json:"type"`
Type workspaces.WorkspacePostRequestDataType `json:"type"`
}(struct {
Attributes struct {
BundleId string `json:"bundle_id"`
RepositoryUri string `json:"repository_uri"`
WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
BundleId string `json:"bundle_id"`
RepositoryUri string `json:"repository_uri"`
WorkspaceType workspaces.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
}
Type externalRef3.WorkspacePostRequestDataType
Type workspaces.WorkspacePostRequestDataType
}{Attributes: struct {
BundleId string `json:"bundle_id"`
RepositoryUri string `json:"repository_uri"`
WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
BundleId string `json:"bundle_id"`
RepositoryUri string `json:"repository_uri"`
WorkspaceType workspaces.WorkspacePostRequestDataAttributesWorkspaceType `json:"workspace_type"`
}(struct {
BundleId string
RepositoryUri string
WorkspaceType externalRef3.WorkspacePostRequestDataAttributesWorkspaceType
WorkspaceType workspaces.WorkspacePostRequestDataAttributesWorkspaceType
}{
BundleId: bundleHash,
RepositoryUri: repositoryUri,
Expand Down Expand Up @@ -152,17 +170,163 @@ func (a *analysisOrchestrator) CreateWorkspace(ctx context.Context, orgId string
return workspaceResponse.ApplicationvndApiJSON201.Data.Id.String(), nil
}

func (*analysisOrchestrator) RunAnalysis() (*sarif.SarifResponse, error) {
var response sarif.SarifResponse
//go:embed fake.json
var fakeResponse []byte

func (a *analysisOrchestrator) RunAnalysis(ctx context.Context, orgId string, workspaceId string) (*sarif.SarifResponse, error) {
method := "analysis.RunAnalysis"
logger := a.logger.With().Str("method", method).Logger()
logger.Debug().Msg("API: Creating the scan")
org := uuid.MustParse(orgId)

host := a.host(false)
a.logger.Debug().Str("host", host).Str("workspaceId", workspaceId).Msg("starting scan")

client, err := orchestrationClient.NewClientWithResponses(host, orchestrationClient.WithHTTPClient(a.httpClient))

err := json.Unmarshal(fakeResponse, &response)
if err != nil {
return nil, fmt.Errorf("failed to create SARIF response: %w", err)
return nil, fmt.Errorf("failed to create orchestrationClient: %w", err)
}

flow := scans.Flow{}
err = flow.UnmarshalJSON([]byte(`{"name": "cli_test"}`))
if err != nil {
return nil, fmt.Errorf("failed to create scan request: %w", err)
}
createScanResponse, err := client.CreateScanWorkspaceJobForUserWithApplicationVndAPIPlusJSONBodyWithResponse(
ctx,
org,
&orchestrationClient.CreateScanWorkspaceJobForUserParams{Version: "2024-02-16~experimental"},
orchestrationClient.CreateScanWorkspaceJobForUserApplicationVndAPIPlusJSONRequestBody{Data: struct {
Attributes struct {
Flow scans.Flow `json:"flow"`
WorkspaceUrl string `json:"workspace_url"`
} `json:"attributes"`
Id *openapi_types.UUID `json:"id,omitempty"`
Type scans.PostScanRequestDataType `json:"type"`
}(struct {
Attributes struct {
Flow scans.Flow `json:"flow"`
WorkspaceUrl string `json:"workspace_url"`
}
Id *openapi_types.UUID
Type scans.PostScanRequestDataType
}{
Attributes: struct {
Flow scans.Flow `json:"flow"`
WorkspaceUrl string `json:"workspace_url"`
}(struct {
Flow scans.Flow
WorkspaceUrl string
}{
Flow: flow,
WorkspaceUrl: fmt.Sprintf("http://workspace-service/workspaces/%s", workspaceId),
}),
Type: "workspace",
})})

if err != nil {
return nil, fmt.Errorf("failed to trigger scan: %w", err)
}

if createScanResponse.ApplicationvndApiJSON201 == nil {
msg := a.getStatusCode(createScanResponse)
return nil, errors.New(msg)
}

scanJobId := createScanResponse.ApplicationvndApiJSON201.Data.Id
a.logger.Debug().Str("host", host).Str("workspaceId", workspaceId).Msg("starting scan")

// Actual polling loop.
pollingTicker := time.NewTicker(1 * time.Second)
defer pollingTicker.Stop()
timeoutTimer := time.NewTimer(a.timeoutInSeconds)
defer timeoutTimer.Stop()
for {
select {
case <-timeoutTimer.C:
msg := "timeout requesting the ScanJobResult"
logger.Error().Str("scanJobId", scanJobId.String()).Msg(msg)
return nil, errors.New(msg)
case <-pollingTicker.C:
_, complete, err := a.poller(ctx, logger, client, org, scanJobId, method) // todo add processing of the response with the findings
if err != nil {
return nil, err
}
if !complete {
continue
}

var response sarif.SarifResponse
_ = json.Unmarshal(fakeResponse, &response)

return &response, nil
}
}
return &response, nil
}

func (a *analysisOrchestrator) host() string {
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) {
logger.Debug().Msg("polling for ScanJobResult")
httpResponse, err := client.GetScanWorkspaceJobForUserWithResponse(
ctx,
org,
scanJobId,
&orchestrationClient.GetScanWorkspaceJobForUserParams{Version: "2024-02-16~experimental"},
)
if err != nil {
logger.Err(err).Str("method", method).Str("scanJobId", scanJobId.String()).Msg("error requesting the ScanJobResult")
return httpResponse, true, err
}

var msg string
switch httpResponse.StatusCode() {
case 200:
scanJobStatus := httpResponse.ApplicationvndApiJSON200.Data.Attributes.Status
if scanJobStatus == scans.ScanJobResultsAttributesStatusInProgress {
return httpResponse, false, nil
} else {
return httpResponse, true, nil
}
case 400:
msg = httpResponse.ApplicationvndApiJSON400.Errors[0].Detail
case 401:
msg = httpResponse.ApplicationvndApiJSON401.Errors[0].Detail
case 403:
msg = httpResponse.ApplicationvndApiJSON403.Errors[0].Detail
case 404:
msg = httpResponse.ApplicationvndApiJSON404.Errors[0].Detail
case 429:
msg = httpResponse.ApplicationvndApiJSON429.Errors[0].Detail
case 500:
msg = httpResponse.ApplicationvndApiJSON500.Errors[0].Detail
}
return nil, true, errors.New(msg)
}

func (a *analysisOrchestrator) getStatusCode(createScanResponse *orchestrationClient.CreateScanWorkspaceJobForUserResponse) string {
var msg string
switch createScanResponse.StatusCode() {
case 400:
msg = createScanResponse.ApplicationvndApiJSON400.Errors[0].Detail
case 401:
msg = createScanResponse.ApplicationvndApiJSON401.Errors[0].Detail
case 403:
msg = createScanResponse.ApplicationvndApiJSON403.Errors[0].Detail
case 404:
msg = createScanResponse.ApplicationvndApiJSON404.Errors[0].Detail
case 429:
msg = createScanResponse.ApplicationvndApiJSON429.Errors[0].Detail
case 500:
msg = createScanResponse.ApplicationvndApiJSON500.Errors[0].Detail
}
return msg
}

func (a *analysisOrchestrator) host(isHidden bool) string {
apiUrl := strings.TrimRight(a.config.SnykApi(), "/")
return fmt.Sprintf("%s/hidden", apiUrl)
path := "rest"
if isHidden {
path = "hidden"
}
return fmt.Sprintf("%s/%s", apiUrl, path)
}
Loading

0 comments on commit 4efdd02

Please sign in to comment.