diff --git a/pkg/adapter/anchore/adapter.go b/pkg/adapter/anchore/adapter.go index 1c72642..aa76eff 100644 --- a/pkg/adapter/anchore/adapter.go +++ b/pkg/adapter/anchore/adapter.go @@ -183,8 +183,8 @@ func (s *HarborScannerAdapter) EnsureRegistryCredentials( return fmt.Errorf("unexpected response on registry credential update from anchore api: %v", resp.StatusCode) } } else { - log.WithFields(log.Fields{"errorMessage": anchoreError.Message, "registry": registryURL, "repository": repository}).Error("unexpected response from anchore api. could not determine if update action is appropriate") - return fmt.Errorf("unexpected response from anchore api") + log.WithFields(log.Fields{"errorMessage": anchoreError.Message, "registry": registryURL, "repository": repository}).Error("unexpected response from anchore api could not determine if update action is appropriate for registry credentials") + return fmt.Errorf("unexpected response from anchore api could not determine if update action is appropriate for registry credentials") } } else if resp.StatusCode != http.StatusOK { // More handling @@ -206,42 +206,46 @@ func (s *HarborScannerAdapter) Scan(req harbor.ScanRequest) (harbor.ScanResponse log.WithFields(log.Fields{"scanId": scanID, "repository": req.Artifact.Repository, "artifactDigest": req.Artifact.Digest, "artifactTag": req.Artifact.Tag}). Debug("generated ScanId") - tokenLength := len(req.Registry.Authorization) - - if s.Configuration.UseAnchoreConfiguredCreds { - log.WithFields(log.Fields{"scanId": scanID, "UseAnchoreConfiguredCredentials": s.Configuration.UseAnchoreConfiguredCreds, "ScanRequestTokenLength": tokenLength}). - Debug("Skipping adding Harbor authz token to Anchore due to adapter configuration") - } else { - if req.Registry.Authorization == "" { - log.WithFields(log.Fields{"scanId": scanID, "UseAnchoreConfiguredCredentials": s.Configuration.UseAnchoreConfiguredCreds, "ScanRequestTokenLength": tokenLength}).Debug("Skipping adding Harbor authz token to Anchore due to no token provided in request") - } else { - log.WithFields(log.Fields{"scanId": scanID, "UseAnchoreConfiguredCredentials": s.Configuration.UseAnchoreConfiguredCreds}).Debug("ensuring Anchore deployment has credentials for retrieving the image to scan") - username, password, err2 := GetUsernamePassword(req.Registry.Authorization) - if err2 != nil { - log.WithFields(log.Fields{"scanId": scanID, "UseAnchoreConfiguredCredentials": s.Configuration.UseAnchoreConfiguredCreds}).Error("could not extract credentials for image pull access from the scan request authorization field") - return harbor.ScanResponse{}, err2 - } - - // Add the credentials for the repository to be scanned - err = s.EnsureRegistryCredentials(req.Registry.URL, req.Artifact.Repository, username, password) - if err != nil { - log.WithFields(log.Fields{"scanId": scanID}).Error("failed ensuring that Anchore has authorized access to pull the image from Harbor") - return harbor.ScanResponse{}, err - } - } - } - // Convert and submit the scan request anchoreScanRequest, err := ScanToAnchoreRequest(req) if err != nil { return harbor.ScanResponse{}, err } - err = client.AnalyzeImage(&s.Configuration.AnchoreClientConfig, *anchoreScanRequest) - if err != nil { - log.Error("Could not submit image for analysis ", err) - return harbor.ScanResponse{}, err + asyncCreateScan := func() (bool, error) { + tokenLength := len(req.Registry.Authorization) + + if s.Configuration.UseAnchoreConfiguredCreds { + log.WithFields(log.Fields{"scanId": scanID, "UseAnchoreConfiguredCredentials": s.Configuration.UseAnchoreConfiguredCreds, "ScanRequestTokenLength": tokenLength}). + Debug("Skipping adding Harbor authz token to Anchore due to adapter configuration") + } else { + if req.Registry.Authorization == "" { + log.WithFields(log.Fields{"scanId": scanID, "UseAnchoreConfiguredCredentials": s.Configuration.UseAnchoreConfiguredCreds, "ScanRequestTokenLength": tokenLength}).Debug("Skipping adding Harbor authz token to Anchore due to no token provided in request") + } else { + log.WithFields(log.Fields{"scanId": scanID, "UseAnchoreConfiguredCredentials": s.Configuration.UseAnchoreConfiguredCreds}).Debug("ensuring Anchore deployment has credentials for retrieving the image to scan") + username, password, err2 := GetUsernamePassword(req.Registry.Authorization) + if err2 != nil { + log.WithFields(log.Fields{"scanId": scanID, "UseAnchoreConfiguredCredentials": s.Configuration.UseAnchoreConfiguredCreds}).Error("could not extract credentials for image pull access from the scan request authorization field") + return false, err2 + } + + // Add the credentials for the repository to be scanned + err = s.EnsureRegistryCredentials(req.Registry.URL, req.Artifact.Repository, username, password) + if err != nil { + log.WithFields(log.Fields{"scanId": scanID}).Error("failed ensuring that Anchore has authorized access to pull the image from Harbor") + return false, err + } + } + } + + err = client.AnalyzeImage(&s.Configuration.AnchoreClientConfig, *anchoreScanRequest) + if err != nil { + log.Error("Could not submit image for analysis ", err) + return false, err + } + return true, nil } + resultStore.RequestCreateScan(scanID, asyncCreateScan) log.WithFields(log.Fields{"scanId": scanID, "repository": req.Artifact.Repository, "artifactDigest": req.Artifact.Digest, "artifactTag": req.Artifact.Tag}). Info("scan successfully initiated against Anchore") @@ -261,24 +265,23 @@ func (s *HarborScannerAdapter) GetHarborVulnerabilityReport( return nil, err } - imageState, err := GetImageState(imageDigest, &s.Configuration.AnchoreClientConfig) - if err != nil { - return nil, err + result, ok := resultStore.PopResult(scanID) + log.WithFields(log.Fields{"scanId": scanID, "resultRecordFound": ok}).Debug("checked result store for scan Id result") + if !ok { + // No result found, so only continue if the image is in Anchore Enterprise and analyzed. This can happen if the adapter was restarted during a scan. + log.WithFields(log.Fields{"scanId": scanID, "resultRecordFound": ok}). + Info("no result found in store checking image is analyzed in Anchore Enterprise") } - log.WithFields(log.Fields{"scanId": scanID, "imageState": imageState, "imageDigest": imageDigest, "imageRepository": imageRepository}). - Debug("image analysis state check") - - if imageState != Analyzed { - log.WithFields(log.Fields{"scanId": scanID, "imageState": imageState, "imageDigest": imageDigest, "imageRepository": imageRepository}). - Info("image analysis not completed yet") - return &harbor.VulnerabilityReport{}, fmt.Errorf("analysis not complete") + if !result.AnalysisComplete { + log.WithFields(log.Fields{"scanId": scanID}).Debug("checking image analysis state in Anchore Enterprise") + imageAnalsisFn := func() (bool, error) { + return IsImageAnalysed(imageDigest, scanID, &s.Configuration.AnchoreClientConfig) + } + resultStore.RequestAnalysisStatus(scanID, imageAnalsisFn) } - result, ok := resultStore.PopResult(scanID) - log.WithFields(log.Fields{"scanId": scanID, "resultRecordFound": ok}).Debug("checked result store for scan Id result") - - if ok { + if result.ReportBuildInProgress { log.WithFields(log.Fields{"scanId": scanID, "resultIsComplete": result.IsComplete, "resultError": result.Error}). Debug("checked result store for scan Id result") if result.IsComplete { @@ -286,6 +289,15 @@ func (s *HarborScannerAdapter) GetHarborVulnerabilityReport( } return nil, fmt.Errorf("result not ready") } + if !result.ScanCreated { + log.WithFields(log.Fields{"scanId": scanID, "scanCreated": result.ScanCreated, "resultIsComplete": result.IsComplete, "resultError": result.Error}). + Debug("scan not created yet") + if result.Error != nil { + return nil, result.Error + } + return nil, fmt.Errorf("create scan not ready") + } + fn := func() (*harbor.VulnerabilityReport, error) { rep, err := BuildHarborVulnerabilityReport( imageRepository, @@ -319,6 +331,35 @@ const ( Analyzed ImageState = 3 ) +func IsImageAnalysed(imageDigest, scanID string, clientConfig *client.Config) (bool, error) { + imageState, err := GetImageState(imageDigest, clientConfig) + if err != nil { + return false, err + } + + log.WithFields(log.Fields{"scanId": scanID, "imageState": imageState, "imageDigest": imageDigest}). + Debug("image analysis state check") + + switch imageState { + case Analyzed: + log.WithFields(log.Fields{"scanId": scanID, "imageState": imageState, "imageDigest": imageDigest}). + Debug("found analyzed image") + return true, nil + case AnalysisFailed: + log.WithFields(log.Fields{"scanId": scanID, "imageState": imageState, "imageDigest": imageDigest}). + Debug("analysis failed") + return false, fmt.Errorf("analysis failed") + case Analyzing: + log.WithFields(log.Fields{"scanId": scanID, "imageState": imageState, "imageDigest": imageDigest}). + Debug("analysis pending") + return false, fmt.Errorf("analysis pending") + default: + log.WithFields(log.Fields{"scanId": scanID, "imageState": imageState, "imageDigest": imageDigest}). + Debug("analysis incomplete but unknown state") + return false, fmt.Errorf("analysis in unknown state") + } +} + func GetImageState(imageDigest string, clientConfig *client.Config) (ImageState, error) { log.WithField("imageDigest", imageDigest).Debug("checking vulnerability report cache") _, ok := ReportCache.Get(imageDigest) @@ -443,27 +484,6 @@ func GetAnchoreVulnReport( clientConfig *client.Config, filterVendorIgnoredVulns bool, ) (anchore.ImageVulnerabilityReport, error) { - imageState, err := GetImageState(digest, clientConfig) - if err != nil { - return anchore.ImageVulnerabilityReport{}, err - } - - // Handle nice error messages - switch imageState { - case Analyzed: - log.Debug("found analyzed image") - // Continue - case AnalysisFailed: - log.Debug("analysis failed") - return anchore.ImageVulnerabilityReport{}, fmt.Errorf("analysis failed") - case Analyzing: - log.Debug("analysis pending") - return anchore.ImageVulnerabilityReport{}, fmt.Errorf("analysis pending") - default: - log.Debug("analysis incomplete but unknown state") - return anchore.ImageVulnerabilityReport{}, fmt.Errorf("analysis in unknown state") - } - report, err := client.GetImageVulnerabilities(clientConfig, digest, filterVendorIgnoredVulns) if err == nil { log.WithField("imageDigest", digest).Debug("caching result report") @@ -484,9 +504,45 @@ func (s *HarborScannerAdapter) GetRawVulnerabilityReport(scanID string) (harbor. return harbor.VulnerabilityReport{}, err } - log.WithFields(log.Fields{"repository": repository, "imageDigest": digest, "scanId": scanID}). - Info("Getting raw Anchore-formatted vulnerability report") - return GetAnchoreVulnReport(digest, &s.Configuration.AnchoreClientConfig, s.Configuration.FullVulnerabilityDescriptions) + rawScanID := fmt.Sprintf("%s-raw", scanID) // Used to store just the raw report results in the result store + result, _ := resultStore.PopResult(rawScanID) + + if !result.AnalysisComplete { + log.WithFields(log.Fields{"scanId": scanID}).Debug("checking image analysis state in Anchore Enterprise") + imageAnalsisFn := func() (bool, error) { + return IsImageAnalysed(digest, scanID, &s.Configuration.AnchoreClientConfig) + } + resultStore.RequestAnalysisStatus(rawScanID, imageAnalsisFn) + } + + if result.ReportBuildInProgress { + log.WithFields(log.Fields{"scanId": scanID, "resultIsComplete": result.IsComplete, "resultError": result.Error}). + Debug("checked result store for scan Id result") + if result.IsComplete { + return result.RawResult, result.Error + } + return nil, fmt.Errorf("result not ready") + } + + rawReportFn := func() (*anchore.ImageVulnerabilityReport, error) { + log.WithFields(log.Fields{"repository": repository, "imageDigest": digest, "scanId": scanID}). + Info("Getting raw Anchore-formatted vulnerability report") + rep, err := GetAnchoreVulnReport( + digest, + &s.Configuration.AnchoreClientConfig, + s.Configuration.FullVulnerabilityDescriptions, + ) + if err != nil { + return nil, err + } + return &rep, err + } + + requestResult := resultStore.RequestRawResult(rawScanID, rawReportFn) + if requestResult.Error != nil { + return nil, requestResult.Error + } + return requestResult.Result, nil } // ToHarborDescription Convert the Anchore Vulnerability record to a harbor description string diff --git a/pkg/adapter/anchore/result_store.go b/pkg/adapter/anchore/result_store.go index a2ccdcf..eb7b95c 100644 --- a/pkg/adapter/anchore/result_store.go +++ b/pkg/adapter/anchore/result_store.go @@ -5,6 +5,7 @@ import ( log "github.com/sirupsen/logrus" + "github.com/anchore/harbor-scanner-adapter/pkg/model/anchore" "github.com/anchore/harbor-scanner-adapter/pkg/model/harbor" ) @@ -15,20 +16,36 @@ type ResultStore interface { HasResult( scanID string, ) bool // Check if a result is available + RequestCreateScan( + scanID string, + buildFn func() (bool, error), + ) VulnerabilityResult // Request a result to be created and add image for analysis + RequestAnalysisStatus( + scanID string, + buildFn func() (bool, error), + ) VulnerabilityResult // Request a result to be created and add image for analysis RequestResult( scanID string, buildFn func() (*harbor.VulnerabilityReport, error), ) VulnerabilityResult // Request a result to be created + RequestRawResult( + scanID string, + buildFn func() (*anchore.ImageVulnerabilityReport, error), + ) VulnerabilityResult PopResult( scanID string, ) (VulnerabilityResult, bool) // Returns a result and true if found, false if not (e.g. like hash map interface) } type VulnerabilityResult struct { - ScanID string - IsComplete bool - Result *harbor.VulnerabilityReport - Error error + ScanID string + ScanCreated bool + AnalysisComplete bool + ReportBuildInProgress bool + IsComplete bool + Result *harbor.VulnerabilityReport + RawResult *anchore.ImageVulnerabilityReport + Error error } type MemoryResultStore struct { @@ -64,25 +81,194 @@ func (m MemoryResultStore) PopResult(scanID string) (VulnerabilityResult, bool) return found, ok } -func (m MemoryResultStore) RequestResult( +func (m MemoryResultStore) RequestCreateScan( //nolint scanID string, - buildFn func() (*harbor.VulnerabilityReport, error), + buildFn func() (bool, error), ) VulnerabilityResult { existing, ok := m.PopResult(scanID) if !ok { + // Result not found so begin the async fetch + go func() { + imageAdded, err := buildFn() + if err != nil { + log.Debugf("error creating scan for %v: %v", scanID, err) + // Set IsComplete to true to remove the scan from the store if the create scan fails so that it can be retried without using the cache. + resultChannel <- VulnerabilityResult{ + ScanID: scanID, + ScanCreated: false, + AnalysisComplete: false, + ReportBuildInProgress: false, + IsComplete: true, + Result: nil, + Error: err, + } + } else { + log.Debugf("create scan finished for %v", scanID) + resultChannel <- VulnerabilityResult{ + ScanID: scanID, + ScanCreated: imageAdded, + AnalysisComplete: false, + ReportBuildInProgress: false, + IsComplete: false, + Result: nil, + Error: nil, + } + } + }() + existing = VulnerabilityResult{ + ScanID: scanID, + ScanCreated: false, + AnalysisComplete: false, + ReportBuildInProgress: false, + IsComplete: false, + Result: nil, + Error: fmt.Errorf("create scan not ready"), + } + m.Results[scanID] = existing + } + return existing +} + +func (m MemoryResultStore) RequestAnalysisStatus( //nolint + scanID string, + buildFn func() (bool, error), +) VulnerabilityResult { + existing, ok := m.PopResult(scanID) + + if !ok { + // Result not found so begin the async fetch + go func() { + complete, err := buildFn() + if err != nil { + log.Debugf("error checking analysis state for %v: %v", scanID, err) + // Set IsComplete to true to remove the scan from the store if the create scan fails so that it can be retried without using the cache. + resultChannel <- VulnerabilityResult{ + ScanID: scanID, + ScanCreated: true, + AnalysisComplete: false, + ReportBuildInProgress: false, + IsComplete: true, + Result: nil, + Error: err, + } + } else { + log.Debugf("checking analysis state complete for %v", scanID) + resultChannel <- VulnerabilityResult{ + ScanID: scanID, + ScanCreated: true, + AnalysisComplete: complete, + ReportBuildInProgress: false, + IsComplete: false, + Result: nil, + Error: nil, + } + } + }() + existing = VulnerabilityResult{ + ScanID: scanID, + ScanCreated: true, + AnalysisComplete: false, + ReportBuildInProgress: false, + IsComplete: false, + Result: nil, + Error: fmt.Errorf("image analysis not ready"), + } + m.Results[scanID] = existing + } + return existing +} + +func (m MemoryResultStore) RequestResult( + scanID string, + buildFn func() (*harbor.VulnerabilityReport, error), +) VulnerabilityResult { + existing, _ := m.PopResult(scanID) + + if !existing.ReportBuildInProgress && existing.ScanCreated { + log.Debug("Scan created, beginning report build") + // Result not found so begin the async fetch + go func() { + result, err := buildFn() + if err != nil { + log.Debugf("error building result for %v: %v", scanID, err) + // Set IsComplete to true to remove the scan from the store so that it can be retried without using the cache. + resultChannel <- VulnerabilityResult{ + ScanID: scanID, + ScanCreated: true, + ReportBuildInProgress: true, + IsComplete: true, + Result: nil, + Error: err, + } + } else { + log.Debugf("result built for %v", scanID) + resultChannel <- VulnerabilityResult{ + ScanID: scanID, + ScanCreated: true, + ReportBuildInProgress: true, + IsComplete: true, + Result: result, + Error: nil, + } + } + }() + existing = VulnerabilityResult{ + ScanID: scanID, + ScanCreated: true, + ReportBuildInProgress: true, + IsComplete: false, + Result: nil, + Error: fmt.Errorf("result not ready"), + } + m.Results[scanID] = existing + } + return existing +} + +func (m MemoryResultStore) RequestRawResult( + scanID string, + buildFn func() (*anchore.ImageVulnerabilityReport, error), +) VulnerabilityResult { + existing, _ := m.PopResult(scanID) + + if !existing.ReportBuildInProgress && existing.ScanCreated { + log.Debug("Scan created, beginning raw report build") // Result not found so begin the async fetch go func() { result, err := buildFn() if err != nil { log.Debugf("error building result for %v: %v", scanID, err) - resultChannel <- VulnerabilityResult{scanID, true, nil, err} + // Set IsComplete to true to remove the scan from the store so that it can be retried without using the cache. + resultChannel <- VulnerabilityResult{ + ScanID: scanID, + ScanCreated: true, + ReportBuildInProgress: true, + IsComplete: true, + Result: nil, + Error: err, + } } else { log.Debugf("result built for %v", scanID) - resultChannel <- VulnerabilityResult{scanID, true, result, nil} + resultChannel <- VulnerabilityResult{ + ScanID: scanID, + ScanCreated: true, + ReportBuildInProgress: true, + IsComplete: true, + Result: nil, + RawResult: result, + Error: nil, + } } }() - existing = VulnerabilityResult{ScanID: scanID, IsComplete: false, Result: nil, Error: fmt.Errorf("result not ready")} + existing = VulnerabilityResult{ + ScanID: scanID, + ScanCreated: true, + ReportBuildInProgress: true, + IsComplete: false, + Result: nil, + Error: fmt.Errorf("result not ready"), + } m.Results[scanID] = existing } return existing @@ -91,7 +277,7 @@ func (m MemoryResultStore) RequestResult( func (m MemoryResultStore) resultRetriever() { for { report := <-resultChannel - log.WithFields(log.Fields{"scanId": report.ScanID, "isComplete": report.IsComplete, "reportError": report.Error}). + log.WithFields(log.Fields{"scanId": report.ScanID, "imageAdded": report.ScanCreated, "isComplete": report.IsComplete, "reportError": report.Error}). Debug("scan result added to result store") m.Results[report.ScanID] = report } diff --git a/pkg/adapter/anchore/result_store_test.go b/pkg/adapter/anchore/result_store_test.go index d1d442a..ad98828 100644 --- a/pkg/adapter/anchore/result_store_test.go +++ b/pkg/adapter/anchore/result_store_test.go @@ -138,8 +138,10 @@ func TestMemoryResultStore_RequestResult(t *testing.T) { name: "result found and scan complete", fields: fields{Results: map[string]VulnerabilityResult{ "test1": { - ScanID: "test1", - IsComplete: true, + ScanID: "test1", + ScanCreated: true, + ReportBuildInProgress: true, + IsComplete: true, Result: &harbor.VulnerabilityReport{ GeneratedAt: testTime, Artifact: harbor.Artifact{ @@ -174,8 +176,10 @@ func TestMemoryResultStore_RequestResult(t *testing.T) { }, }, want: VulnerabilityResult{ - ScanID: "test1", - IsComplete: true, + ScanID: "test1", + ScanCreated: true, + ReportBuildInProgress: true, + IsComplete: true, Result: &harbor.VulnerabilityReport{ GeneratedAt: testTime, Artifact: harbor.Artifact{ @@ -208,8 +212,9 @@ func TestMemoryResultStore_RequestResult(t *testing.T) { name: "result found and scan incomplete", fields: fields{Results: map[string]VulnerabilityResult{ "test1": { - ScanID: "test1", - IsComplete: false, + ScanID: "test1", + ScanCreated: false, + IsComplete: false, }, }}, args: args{ @@ -219,15 +224,22 @@ func TestMemoryResultStore_RequestResult(t *testing.T) { }, }, want: VulnerabilityResult{ - ScanID: "test1", - IsComplete: false, - Result: nil, + ScanID: "test1", + ScanCreated: false, + IsComplete: false, + Result: nil, }, wantResponseOnChannel: false, }, { - name: "error in build function", - fields: fields{Results: map[string]VulnerabilityResult{}}, + name: "error in build function", + fields: fields{Results: map[string]VulnerabilityResult{ + "test1": { + ScanID: "test1", + ScanCreated: true, + ReportBuildInProgress: false, + }, + }}, args: args{ scanID: "test1", buildFn: func() (*harbor.VulnerabilityReport, error) { @@ -235,22 +247,32 @@ func TestMemoryResultStore_RequestResult(t *testing.T) { }, }, want: VulnerabilityResult{ - ScanID: "test1", - IsComplete: false, - Result: nil, + ScanID: "test1", + ScanCreated: true, + ReportBuildInProgress: true, + IsComplete: false, + Result: nil, }, wantResponseOnChannel: true, wantErr: true, wantChannelResult: VulnerabilityResult{ - ScanID: "test1", - IsComplete: true, - Result: nil, - Error: fmt.Errorf("test error"), + ScanID: "test1", + ScanCreated: true, + ReportBuildInProgress: true, + IsComplete: true, + Result: nil, + Error: fmt.Errorf("test error"), }, }, { - name: "result not found", - fields: fields{Results: map[string]VulnerabilityResult{}}, + name: "result not in progress so start it", + fields: fields{Results: map[string]VulnerabilityResult{ + "test1": { + ScanID: "test1", + ScanCreated: true, + ReportBuildInProgress: false, + }, + }}, args: args{ scanID: "test1", buildFn: func() (*harbor.VulnerabilityReport, error) { @@ -282,16 +304,20 @@ func TestMemoryResultStore_RequestResult(t *testing.T) { }, }, want: VulnerabilityResult{ - ScanID: "test1", - IsComplete: false, - Result: nil, - Error: fmt.Errorf("result not ready"), + ScanID: "test1", + ScanCreated: true, + ReportBuildInProgress: true, + IsComplete: false, + Result: nil, + Error: fmt.Errorf("result not ready"), }, wantResponseOnChannel: true, wantErr: true, wantChannelResult: VulnerabilityResult{ - ScanID: "test1", - IsComplete: true, + ScanID: "test1", + ScanCreated: true, + ReportBuildInProgress: true, + IsComplete: true, Result: &harbor.VulnerabilityReport{ GeneratedAt: testTime, Artifact: harbor.Artifact{ diff --git a/pkg/http/api/v1/handler.go b/pkg/http/api/v1/handler.go index b3bfe2a..b9056a8 100644 --- a/pkg/http/api/v1/handler.go +++ b/pkg/http/api/v1/handler.go @@ -208,6 +208,10 @@ func (h *APIHandler) GetScanReport(res http.ResponseWriter, req *http.Request) { log.Info("valid scanId, but results not ready") res.Header().Set("Location", req.URL.String()) SendErrorResponse(&res, "scan pending", http.StatusFound) + case "create scan not ready": + log.Info("valid scanId, but create scan not ready") + res.Header().Set("Location", req.URL.String()) + SendErrorResponse(&res, "scan pending", http.StatusFound) default: log.Error("unknown internal error") SendErrorResponse(&res, err.Error(), http.StatusInternalServerError)