From f36152a5603151b98a38aa24bebdba22084a3789 Mon Sep 17 00:00:00 2001 From: He Weiwei Date: Sat, 22 Feb 2020 13:29:58 +0800 Subject: [PATCH] feat(vulnerability): assemble vulnerabilities info for artifact (#10800) 1. Assemble scan overview to artifact when scanner enabled in the project of the artifact. 2. Set addition link for vulnerabilities to artifact when scanner enabled in the project of the artifact. Signed-off-by: He Weiwei --- api/v2.0/swagger.yaml | 79 +++++- src/api/artifact/controller.go | 68 +++-- src/api/artifact/controller_test.go | 8 +- src/api/artifact/model.go | 15 ++ src/api/scan/base_controller.go | 3 +- src/pkg/artifact/model.go | 2 + src/server/v2.0/handler/artifact.go | 56 +++-- src/server/v2.0/handler/assembler/vul.go | 107 ++++++++ src/server/v2.0/handler/assembler/vul_test.go | 107 ++++++++ src/server/v2.0/handler/model/artifact.go | 2 +- src/server/v2.0/handler/util.go | 70 ++++++ src/testing/api/api.go | 18 ++ src/testing/api/scan/controller.go | 177 +++++++++++++ src/testing/api/scanner/controller.go | 232 ++++++++++++++++++ 14 files changed, 891 insertions(+), 53 deletions(-) create mode 100644 src/server/v2.0/handler/assembler/vul.go create mode 100644 src/server/v2.0/handler/assembler/vul_test.go create mode 100644 src/server/v2.0/handler/util.go create mode 100644 src/testing/api/api.go create mode 100644 src/testing/api/scan/controller.go create mode 100644 src/testing/api/scanner/controller.go diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index 5677fcd2c..54e808400 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -135,31 +135,31 @@ paths: required: false - name: with_tag in: query - description: Specify whether the tags are inclued inside the returning artifacts + description: Specify whether the tags are included inside the returning artifacts type: boolean required: false default: true - name: with_label in: query - description: Specify whether the labels are inclued inside the returning artifacts + description: Specify whether the labels are included inside the returning artifacts type: boolean required: false default: false - name: with_scan_overview in: query - description: Specify whether the scan overview is inclued inside the returning artifacts + description: Specify whether the scan overview is included inside the returning artifacts type: boolean required: false default: false - name: with_signature in: query - description: Specify whether the signature is inclued inside the tags of the returning artifacts. Only works when setting "with_tag=true" + description: Specify whether the signature is included inside the tags of the returning artifacts. Only works when setting "with_tag=true" type: boolean required: false default: false - name: with_immutable_status in: query - description: Specify whether the immutable status is inclued inside the tags of the returning artifacts. Only works when setting "with_tag=true" + description: Specify whether the immutable status is included inside the tags of the returning artifacts. Only works when setting "with_tag=true" type: boolean required: false default: false @@ -372,7 +372,7 @@ paths: in: path description: The type of addition. type: string - enum: [build_history, values.yaml, readme.md, dependencies] + enum: [build_history, values.yaml, readme.md, dependencies, vulnerabilities] required: true responses: '200': @@ -670,6 +670,9 @@ definitions: type: array items: $ref: '#/definitions/Label' + scan_overview: + $ref: '#/definitions/ScanOverview' + description: The overview of the scan result. Tag: type: object properties: @@ -796,4 +799,66 @@ definitions: deleted: type: boolean description: Whether the label is deleted or not - + ScanOverview: + type: object + description: 'The scan overview attached in the metadata of tag' + additionalProperties: + $ref: '#/definitions/NativeReportSummary' + NativeReportSummary: + type: object + description: 'The summary for the native report' + properties: + report_id: + type: string + description: 'id of the native scan report' + example: '5f62c830-f996-11e9-957f-0242c0a89008' + scan_status: + type: string + description: 'The status of the report generating process' + example: 'Success' + severity: + type: string + description: 'The overall severity' + example: 'High' + duration: + type: integer + format: int64 + description: 'The seconds spent for generating the report' + example: 300 + summary: + $ref: '#/definitions/VulnerabilitySummary' + start_time: + type: string + format: date-time + description: 'The start time of the scan process that generating report' + example: '2006-01-02T14:04:05' + end_time: + type: string + format: date-time + description: 'The end time of the scan process that generating report' + example: '2006-01-02T15:04:05' + VulnerabilitySummary: + type: object + description: | + VulnerabilitySummary contains the total number of the foun d vulnerabilities number and numbers of each severity level. + properties: + total: + type: integer + format: int + description: 'The total number of the found vulnerabilities' + example: 500 + fixable: + type: integer + format: int + description: 'The number of the fixable vulnerabilities' + example: 100 + summary: + type: object + description: 'Numbers of the vulnerabilities with different severity' + additionalProperties: + type: integer + format: int + example: 10 + example: + 'Critical': 5 + 'High': 5 diff --git a/src/api/artifact/controller.go b/src/api/artifact/controller.go index 4454223ad..a7df07a26 100644 --- a/src/api/artifact/controller.go +++ b/src/api/artifact/controller.go @@ -239,6 +239,11 @@ func (c *controller) List(ctx context.Context, query *q.Query, option *Option) ( if err != nil { return nil, err } + + if err := c.populateRepositoryName(ctx, arts...); err != nil { + return nil, err + } + var artifacts []*Artifact for _, art := range arts { artifacts = append(artifacts, c.assembleArtifact(ctx, art, option)) @@ -516,6 +521,17 @@ func (c *controller) assembleArtifact(ctx context.Context, art *artifact.Artifac artifact := &Artifact{ Artifact: *art, } + + if artifact.RepositoryName == "" { + repo, err := c.repoMgr.Get(ctx, artifact.RepositoryID) + if err != nil { + log.Errorf("get repository %d failed, error: %v", artifact.RepositoryID, err) + return artifact + } + + artifact.RepositoryName = repo.Name + } + // populate addition links c.populateAdditionLinks(ctx, artifact) if option == nil { @@ -532,6 +548,34 @@ func (c *controller) assembleArtifact(ctx context.Context, art *artifact.Artifac return artifact } +func (c *controller) populateRepositoryName(ctx context.Context, artifacts ...*artifact.Artifact) error { + var ids []int64 + for _, artifact := range artifacts { + ids = append(ids, artifact.RepositoryID) + } + + repositories, err := c.repoMgr.List(ctx, &q.Query{Keywords: map[string]interface{}{"repository_id__in": ids}}) + if err != nil { + return err + } + + mp := make(map[int64]string, len(repositories)) + for _, repository := range repositories { + mp[repository.RepositoryID] = repository.Name + } + + for _, artifact := range artifacts { + repositoryName, ok := mp[artifact.RepositoryID] + if !ok { + return ierror.NotFoundError(nil).WithMessage("repository %d not found", artifact.RepositoryID) + } + + artifact.RepositoryName = repositoryName + } + + return nil +} + func (c *controller) populateTags(ctx context.Context, art *Artifact, option *TagOption) { tags, err := c.tagMgr.List(ctx, &q.Query{ Keywords: map[string]interface{}{ @@ -619,25 +663,11 @@ func (c *controller) populateAdditionLinks(ctx context.Context, artifact *Artifa log.Error(err.Error()) return } - if len(types) == 0 { - return - } - repository, err := c.repoMgr.Get(ctx, artifact.RepositoryID) - if err != nil { - log.Error(err.Error()) - return - } - pro, repo := utils.ParseRepository(repository.Name) - version := internal.GetAPIVersion(ctx) - if artifact.AdditionLinks == nil { - artifact.AdditionLinks = make(map[string]*AdditionLink) - } - for _, t := range types { - t = strings.ToLower(t) - artifact.AdditionLinks[t] = &AdditionLink{ - HREF: fmt.Sprintf("/api/%s/projects/%s/repositories/%s/artifacts/%s/additions/%s", - version, pro, repo, artifact.Digest, t), - Absolute: false, + + if len(types) > 0 { + version := internal.GetAPIVersion(ctx) + for _, t := range types { + artifact.SetAdditionLink(strings.ToLower(t), version) } } } diff --git a/src/api/artifact/controller_test.go b/src/api/artifact/controller_test.go index dadd20ea1..25c9dfcfc 100644 --- a/src/api/artifact/controller_test.go +++ b/src/api/artifact/controller_test.go @@ -134,8 +134,9 @@ func (c *controllerTestSuite) TestAssembleTag() { func (c *controllerTestSuite) TestAssembleArtifact() { art := &artifact.Artifact{ - ID: 1, - Digest: "sha256:123", + ID: 1, + Digest: "sha256:123", + RepositoryName: "library/hello-world", } option := &Option{ WithTag: true, @@ -297,6 +298,9 @@ func (c *controllerTestSuite) TestList() { c.repoMgr.On("Get").Return(&models.RepoRecord{ Name: "library/hello-world", }, nil) + c.repoMgr.On("List").Return([]*models.RepoRecord{ + {RepositoryID: 1, Name: "library/hello-world"}, + }, nil) artifacts, err := c.ctl.List(nil, query, option) c.Require().Nil(err) c.Require().Len(artifacts, 1) diff --git a/src/api/artifact/model.go b/src/api/artifact/model.go index 7a1f5386d..2f2bee128 100644 --- a/src/api/artifact/model.go +++ b/src/api/artifact/model.go @@ -15,7 +15,10 @@ package artifact import ( + "fmt" + cmodels "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils" "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/signature" "github.com/goharbor/harbor/src/pkg/tag/model/tag" @@ -29,6 +32,18 @@ type Artifact struct { Labels []*cmodels.Label `json:"labels"` } +// SetAdditionLink set a addition link +func (artifact *Artifact) SetAdditionLink(addition, version string) { + if artifact.AdditionLinks == nil { + artifact.AdditionLinks = make(map[string]*AdditionLink) + } + + projectName, repo := utils.ParseRepository(artifact.RepositoryName) + href := fmt.Sprintf("/api/%s/projects/%s/repositories/%s/artifacts/%s/additions/%s", version, projectName, repo, artifact.Digest, addition) + + artifact.AdditionLinks[addition] = &AdditionLink{HREF: href, Absolute: false} +} + // Tag is the overall view of tag type Tag struct { tag.Tag diff --git a/src/api/scan/base_controller.go b/src/api/scan/base_controller.go index d856ba6da..10080da92 100644 --- a/src/api/scan/base_controller.go +++ b/src/api/scan/base_controller.go @@ -24,6 +24,7 @@ import ( "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils/log" "github.com/goharbor/harbor/src/core/config" + ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/jobservice/job" "github.com/goharbor/harbor/src/jobservice/logger" "github.com/goharbor/harbor/src/pkg/robot" @@ -261,7 +262,7 @@ func (bc *basicController) GetReport(artifact *v1.Artifact, mimeTypes []string) } if r == nil { - return nil, errs.WithCode(errs.PreconditionFailed, errs.Errorf("no scanner registration configured for project: %d", artifact.NamespaceID)) + return nil, ierror.NotFoundError(nil).WithMessage("no scanner registration configured for project: %d", artifact.NamespaceID) } return bc.manager.GetBy(artifact.Digest, r.UUID, mimes) diff --git a/src/pkg/artifact/model.go b/src/pkg/artifact/model.go index 6eb4787f2..ca659a8e9 100644 --- a/src/pkg/artifact/model.go +++ b/src/pkg/artifact/model.go @@ -40,6 +40,8 @@ type Artifact struct { ExtraAttrs map[string]interface{} `json:"extra_attrs"` // only contains the simple attributes specific for the different artifact type, most of them should come from the config layer Annotations map[string]string `json:"annotations"` References []*Reference `json:"references"` // child artifacts referenced by the parent artifact if the artifact is an index + + RepositoryName string `json:"-"` // repository name, eg: library/photon } // From converts the database level artifact to the business level object diff --git a/src/server/v2.0/handler/artifact.go b/src/server/v2.0/handler/artifact.go index a06679d19..c3b212f63 100644 --- a/src/server/v2.0/handler/artifact.go +++ b/src/server/v2.0/handler/artifact.go @@ -17,22 +17,29 @@ package handler import ( "context" "fmt" + "net/http" + "strings" + "time" + "github.com/docker/distribution/reference" "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/goharbor/harbor/src/api/artifact" + "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver" "github.com/goharbor/harbor/src/api/repository" "github.com/goharbor/harbor/src/common/rbac" "github.com/goharbor/harbor/src/common/utils" ierror "github.com/goharbor/harbor/src/internal/error" "github.com/goharbor/harbor/src/pkg/project" "github.com/goharbor/harbor/src/pkg/q" + "github.com/goharbor/harbor/src/server/v2.0/handler/assembler" "github.com/goharbor/harbor/src/server/v2.0/handler/model" operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/artifact" "github.com/opencontainers/go-digest" - "net/http" - "strings" - "time" +) + +const ( + vulnerabilitiesAddition = "vulnerabilities" ) func newArtifactAPI() *artifactAPI { @@ -95,10 +102,11 @@ func (a *artifactAPI) ListArtifacts(ctx context.Context, params operation.ListAr for _, art := range arts { artifact := &model.Artifact{} artifact.Artifact = *art - a.assembleArtifact(ctx, artifact, params.WithScanOverview) artifacts = append(artifacts, artifact) } + assembler.NewVulAssembler(boolValue(params.WithScanOverview)).WithArtifacts(artifacts...).Assemble(ctx) + // TODO add link header return operation.NewListArtifactsOK().WithXTotalCount(total).WithLink("").WithPayload(artifacts) } @@ -118,7 +126,9 @@ func (a *artifactAPI) GetArtifact(ctx context.Context, params operation.GetArtif } art := &model.Artifact{} art.Artifact = *artifact - a.assembleArtifact(ctx, art, params.WithScanOverview) + + assembler.NewVulAssembler(boolValue(params.WithScanOverview)).WithArtifacts(art).Assemble(ctx) + return operation.NewGetArtifactOK().WithPayload(art) } @@ -243,14 +253,23 @@ func (a *artifactAPI) GetAddition(ctx context.Context, params operation.GetAddit if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionRead, rbac.ResourceArtifactAddition); err != nil { return a.SendError(ctx, err) } + artifact, err := a.artCtl.GetByReference(ctx, fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName), params.Reference, nil) if err != nil { return a.SendError(ctx, err) } - addition, err := a.artCtl.GetAddition(ctx, artifact.ID, strings.ToUpper(params.Addition)) + + var addition *resolver.Addition + + if params.Addition == vulnerabilitiesAddition { + addition, err = resolveVulnerabilitiesAddition(ctx, artifact) + } else { + addition, err = a.artCtl.GetAddition(ctx, artifact.ID, strings.ToUpper(params.Addition)) + } if err != nil { return a.SendError(ctx, err) } + return middleware.ResponderFunc(func(w http.ResponseWriter, p runtime.Producer) { w.Header().Set("Content-Type", addition.ContentType) w.Write(addition.Content) @@ -285,31 +304,22 @@ func (a *artifactAPI) RemoveLabel(ctx context.Context, params operation.RemoveLa return operation.NewRemoveLabelOK() } -func (a *artifactAPI) assembleArtifact(ctx context.Context, artifact *model.Artifact, withScanOverview *bool) { - if withScanOverview != nil && *withScanOverview { - // TODO populate scan result - } - // TODO populate vulnerability link -} - func option(withTag, withImmutableStatus, withLabel, withSignature *bool) *artifact.Option { option := &artifact.Option{ - WithTag: true, // return the tag by default + WithTag: true, // return the tag by default + WithLabel: boolValue(withLabel), } + if withTag != nil { option.WithTag = *(withTag) } + if option.WithTag { - option.TagOption = &artifact.TagOption{} - if withImmutableStatus != nil { - option.TagOption.WithImmutableStatus = *(withImmutableStatus) - } - if withSignature != nil { - option.TagOption.WithSignature = *withSignature + option.TagOption = &artifact.TagOption{ + WithImmutableStatus: boolValue(withImmutableStatus), + WithSignature: boolValue(withSignature), } } - if withLabel != nil { - option.WithLabel = *(withLabel) - } + return option } diff --git a/src/server/v2.0/handler/assembler/vul.go b/src/server/v2.0/handler/assembler/vul.go new file mode 100644 index 000000000..129d8776e --- /dev/null +++ b/src/server/v2.0/handler/assembler/vul.go @@ -0,0 +1,107 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package assembler + +import ( + "context" + + "github.com/goharbor/harbor/src/api/scan" + "github.com/goharbor/harbor/src/api/scanner" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/internal" + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" + "github.com/goharbor/harbor/src/server/v2.0/handler/model" +) + +const ( + vulnerabilitiesAddition = "vulnerabilities" +) + +// NewVulAssembler returns vul assembler +func NewVulAssembler(withScanOverview bool) *VulAssembler { + return &VulAssembler{ + withScanOverview: withScanOverview, + scanCtl: scan.DefaultController, + scannerCtl: scanner.DefaultController, + scanners: map[int64]bool{}, + } +} + +// VulAssembler vul assembler +type VulAssembler struct { + artifacts []*model.Artifact + + scanCtl scan.Controller + scannerCtl scanner.Controller + scanners map[int64]bool + + withScanOverview bool +} + +func (assembler *VulAssembler) hasScanner(ctx context.Context, projectID int64) bool { + value, ok := assembler.scanners[projectID] + if !ok { + scanner, err := assembler.scannerCtl.GetRegistrationByProject(projectID) + if err != nil { + log.Warningf("get scanner for project %d failed, error: %v", projectID, err) + return false + } + + value = scanner != nil + assembler.scanners[projectID] = value + } + + return value +} + +// WithArtifacts set artifacts for the assembler +func (assembler *VulAssembler) WithArtifacts(artifacts ...*model.Artifact) *VulAssembler { + assembler.artifacts = artifacts + + return assembler +} + +// Assemble assemble vul for the artifacts +func (assembler *VulAssembler) Assemble(ctx context.Context) error { + version := internal.GetAPIVersion(ctx) + + for _, artifact := range assembler.artifacts { + hasScanner := assembler.hasScanner(ctx, artifact.ProjectID) + + if !hasScanner { + continue + } + + artifact.SetAdditionLink(vulnerabilitiesAddition, version) + + if assembler.withScanOverview { + art := &v1.Artifact{ + NamespaceID: artifact.ProjectID, + Repository: artifact.RepositoryName, + Digest: artifact.Digest, + MimeType: artifact.ManifestMediaType, + } + + overview, err := assembler.scanCtl.GetSummary(art, []string{v1.MimeTypeNativeReport}) + if err != nil { + log.Warningf("get scan summary of artifact %s failed, error:%v", artifact.Digest, err) + } else if len(overview) > 0 { + artifact.ScanOverview = overview + } + } + } + + return nil +} diff --git a/src/server/v2.0/handler/assembler/vul_test.go b/src/server/v2.0/handler/assembler/vul_test.go new file mode 100644 index 000000000..d84a989f1 --- /dev/null +++ b/src/server/v2.0/handler/assembler/vul_test.go @@ -0,0 +1,107 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package assembler + +import ( + "context" + "fmt" + "testing" + + models "github.com/goharbor/harbor/src/pkg/scan/dao/scanner" + "github.com/goharbor/harbor/src/server/v2.0/handler/model" + "github.com/goharbor/harbor/src/testing/api/scan" + "github.com/goharbor/harbor/src/testing/api/scanner" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +type VulAssemblerTestSuite struct { + suite.Suite +} + +func (suite *VulAssemblerTestSuite) newVulAssembler(withScanOverview bool) (*VulAssembler, *scan.Controller, *scanner.Controller) { + vulAssembler := NewVulAssembler(withScanOverview) + + scanCtl := &scan.Controller{} + scannerCtl := &scanner.Controller{} + + vulAssembler.scanCtl = scanCtl + vulAssembler.scannerCtl = scannerCtl + + return vulAssembler, scanCtl, scannerCtl +} + +func (suite *VulAssemblerTestSuite) TestNotHasScanner() { + { + assembler, _, scannerCtl := suite.newVulAssembler(true) + scannerCtl.On("GetRegistrationByProject", mock.AnythingOfType("int64")).Return(nil, nil) + + var artifact model.Artifact + suite.Nil(assembler.WithArtifacts(&artifact).Assemble(context.TODO())) + suite.Len(artifact.AdditionLinks, 0) + } + + { + assembler, _, scannerCtl := suite.newVulAssembler(true) + scannerCtl.On("GetRegistrationByProject", mock.AnythingOfType("int64")).Return(nil, fmt.Errorf("error")) + + var artifact model.Artifact + suite.Nil(assembler.WithArtifacts(&artifact).Assemble(context.TODO())) + suite.Len(artifact.AdditionLinks, 0) + } +} + +func (suite *VulAssemblerTestSuite) TestHasScanner() { + { + assembler, scanCtl, scannerCtl := suite.newVulAssembler(true) + scannerCtl.On("GetRegistrationByProject", mock.AnythingOfType("int64")).Return(&models.Registration{}, nil) + + summary := map[string]interface{}{"key": "value"} + scanCtl.On("GetSummary", mock.AnythingOfType("*v1.Artifact"), mock.AnythingOfType("[]string")).Return(summary, nil) + + var artifact model.Artifact + suite.Nil(assembler.WithArtifacts(&artifact).Assemble(context.TODO())) + suite.Len(artifact.AdditionLinks, 1) + suite.Equal(artifact.ScanOverview, summary) + } + + { + assembler, scanCtl, scannerCtl := suite.newVulAssembler(false) + scannerCtl.On("GetRegistrationByProject", mock.AnythingOfType("int64")).Return(&models.Registration{}, nil) + summary := map[string]interface{}{"key": "value"} + scanCtl.On("GetSummary", mock.AnythingOfType("*v1.Artifact"), mock.AnythingOfType("[]string")).Return(summary, nil) + + var artifact model.Artifact + suite.Nil(assembler.WithArtifacts(&artifact).Assemble(context.TODO())) + suite.Len(artifact.AdditionLinks, 1) + suite.Nil(artifact.ScanOverview) + } + + { + assembler, scanCtl, scannerCtl := suite.newVulAssembler(true) + scannerCtl.On("GetRegistrationByProject", mock.AnythingOfType("int64")).Return(&models.Registration{}, nil) + + scanCtl.On("GetSummary", mock.AnythingOfType("*v1.Artifact"), mock.AnythingOfType("[]string")).Return(nil, fmt.Errorf("error")) + + var artifact model.Artifact + suite.Nil(assembler.WithArtifacts(&artifact).Assemble(context.TODO())) + suite.Len(artifact.AdditionLinks, 1) + suite.Nil(artifact.ScanOverview) + } +} + +func TestVulAssemblerTestSuite(t *testing.T) { + suite.Run(t, &VulAssemblerTestSuite{}) +} diff --git a/src/server/v2.0/handler/model/artifact.go b/src/server/v2.0/handler/model/artifact.go index 09c4d08c3..a04ac0258 100644 --- a/src/server/v2.0/handler/model/artifact.go +++ b/src/server/v2.0/handler/model/artifact.go @@ -19,5 +19,5 @@ import "github.com/goharbor/harbor/src/api/artifact" // Artifact model type Artifact struct { artifact.Artifact - // TODO add other properties: scan result + ScanOverview map[string]interface{} `json:"scan_overview"` } diff --git a/src/server/v2.0/handler/util.go b/src/server/v2.0/handler/util.go new file mode 100644 index 000000000..28bddfc7d --- /dev/null +++ b/src/server/v2.0/handler/util.go @@ -0,0 +1,70 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package handler + +import ( + "context" + "encoding/json" + + "github.com/goharbor/harbor/src/api/artifact" + "github.com/goharbor/harbor/src/api/artifact/abstractor/resolver" + "github.com/goharbor/harbor/src/api/scan" + "github.com/goharbor/harbor/src/pkg/scan/report" + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" +) + +func boolValue(v *bool) bool { + if v != nil { + return *v + } + + return false +} + +func resolveVulnerabilitiesAddition(ctx context.Context, artifact *artifact.Artifact) (*resolver.Addition, error) { + art := &v1.Artifact{ + NamespaceID: artifact.ProjectID, + Repository: artifact.RepositoryName, + Digest: artifact.Digest, + MimeType: artifact.ManifestMediaType, + } + + reports, err := scan.DefaultController.GetReport(art, []string{v1.MimeTypeNativeReport}) + if err != nil { + return nil, err + } + + vulnerabilities := make(map[string]interface{}) + for _, rp := range reports { + // Resolve scan report data only when it is ready + if len(rp.Report) == 0 { + continue + } + + vrp, err := report.ResolveData(rp.MimeType, []byte(rp.Report)) + if err != nil { + return nil, err + } + + vulnerabilities[rp.MimeType] = vrp + } + + content, _ := json.Marshal(vulnerabilities) + + return &resolver.Addition{ + Content: content, + ContentType: "application/json", + }, nil +} diff --git a/src/testing/api/api.go b/src/testing/api/api.go new file mode 100644 index 000000000..1bb6f3cc9 --- /dev/null +++ b/src/testing/api/api.go @@ -0,0 +1,18 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +//go:generate mockery -case snake -dir ../../api/scan -name Controller -output ./scan -outpkg scan +//go:generate mockery -case snake -dir ../../api/scanner -name Controller -output ./scanner -outpkg scanner diff --git a/src/testing/api/scan/controller.go b/src/testing/api/scan/controller.go new file mode 100644 index 000000000..13ab0606a --- /dev/null +++ b/src/testing/api/scan/controller.go @@ -0,0 +1,177 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package scan + +import ( + all "github.com/goharbor/harbor/src/pkg/scan/all" + daoscan "github.com/goharbor/harbor/src/pkg/scan/dao/scan" + + job "github.com/goharbor/harbor/src/jobservice/job" + + mock "github.com/stretchr/testify/mock" + + report "github.com/goharbor/harbor/src/pkg/scan/report" + + scan "github.com/goharbor/harbor/src/api/scan" + + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" +) + +// Controller is an autogenerated mock type for the Controller type +type Controller struct { + mock.Mock +} + +// DeleteReports provides a mock function with given fields: digests +func (_m *Controller) DeleteReports(digests ...string) error { + _va := make([]interface{}, len(digests)) + for _i := range digests { + _va[_i] = digests[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(...string) error); ok { + r0 = rf(digests...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetReport provides a mock function with given fields: artifact, mimeTypes +func (_m *Controller) GetReport(artifact *v1.Artifact, mimeTypes []string) ([]*daoscan.Report, error) { + ret := _m.Called(artifact, mimeTypes) + + var r0 []*daoscan.Report + if rf, ok := ret.Get(0).(func(*v1.Artifact, []string) []*daoscan.Report); ok { + r0 = rf(artifact, mimeTypes) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*daoscan.Report) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*v1.Artifact, []string) error); ok { + r1 = rf(artifact, mimeTypes) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetScanLog provides a mock function with given fields: uuid +func (_m *Controller) GetScanLog(uuid string) ([]byte, error) { + ret := _m.Called(uuid) + + var r0 []byte + if rf, ok := ret.Get(0).(func(string) []byte); ok { + r0 = rf(uuid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(uuid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStats provides a mock function with given fields: requester +func (_m *Controller) GetStats(requester string) (*all.Stats, error) { + ret := _m.Called(requester) + + var r0 *all.Stats + if rf, ok := ret.Get(0).(func(string) *all.Stats); ok { + r0 = rf(requester) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*all.Stats) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(requester) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSummary provides a mock function with given fields: artifact, mimeTypes, options +func (_m *Controller) GetSummary(artifact *v1.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error) { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, artifact, mimeTypes) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 map[string]interface{} + if rf, ok := ret.Get(0).(func(*v1.Artifact, []string, ...report.Option) map[string]interface{}); ok { + r0 = rf(artifact, mimeTypes, options...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]interface{}) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*v1.Artifact, []string, ...report.Option) error); ok { + r1 = rf(artifact, mimeTypes, options...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// HandleJobHooks provides a mock function with given fields: trackID, change +func (_m *Controller) HandleJobHooks(trackID string, change *job.StatusChange) error { + ret := _m.Called(trackID, change) + + var r0 error + if rf, ok := ret.Get(0).(func(string, *job.StatusChange) error); ok { + r0 = rf(trackID, change) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Scan provides a mock function with given fields: artifact, options +func (_m *Controller) Scan(artifact *v1.Artifact, options ...scan.Option) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, artifact) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(*v1.Artifact, ...scan.Option) error); ok { + r0 = rf(artifact, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/src/testing/api/scanner/controller.go b/src/testing/api/scanner/controller.go new file mode 100644 index 000000000..2c68f563e --- /dev/null +++ b/src/testing/api/scanner/controller.go @@ -0,0 +1,232 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package scanner + +import ( + q "github.com/goharbor/harbor/src/pkg/q" + mock "github.com/stretchr/testify/mock" + + scanner "github.com/goharbor/harbor/src/pkg/scan/dao/scanner" + + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" +) + +// Controller is an autogenerated mock type for the Controller type +type Controller struct { + mock.Mock +} + +// CreateRegistration provides a mock function with given fields: registration +func (_m *Controller) CreateRegistration(registration *scanner.Registration) (string, error) { + ret := _m.Called(registration) + + var r0 string + if rf, ok := ret.Get(0).(func(*scanner.Registration) string); ok { + r0 = rf(registration) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(*scanner.Registration) error); ok { + r1 = rf(registration) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteRegistration provides a mock function with given fields: registrationUUID +func (_m *Controller) DeleteRegistration(registrationUUID string) (*scanner.Registration, error) { + ret := _m.Called(registrationUUID) + + var r0 *scanner.Registration + if rf, ok := ret.Get(0).(func(string) *scanner.Registration); ok { + r0 = rf(registrationUUID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*scanner.Registration) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(registrationUUID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetMetadata provides a mock function with given fields: registrationUUID +func (_m *Controller) GetMetadata(registrationUUID string) (*v1.ScannerAdapterMetadata, error) { + ret := _m.Called(registrationUUID) + + var r0 *v1.ScannerAdapterMetadata + if rf, ok := ret.Get(0).(func(string) *v1.ScannerAdapterMetadata); ok { + r0 = rf(registrationUUID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.ScannerAdapterMetadata) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(registrationUUID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRegistration provides a mock function with given fields: registrationUUID +func (_m *Controller) GetRegistration(registrationUUID string) (*scanner.Registration, error) { + ret := _m.Called(registrationUUID) + + var r0 *scanner.Registration + if rf, ok := ret.Get(0).(func(string) *scanner.Registration); ok { + r0 = rf(registrationUUID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*scanner.Registration) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(registrationUUID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRegistrationByProject provides a mock function with given fields: projectID +func (_m *Controller) GetRegistrationByProject(projectID int64) (*scanner.Registration, error) { + ret := _m.Called(projectID) + + var r0 *scanner.Registration + if rf, ok := ret.Get(0).(func(int64) *scanner.Registration); ok { + r0 = rf(projectID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*scanner.Registration) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(projectID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRegistrations provides a mock function with given fields: query +func (_m *Controller) ListRegistrations(query *q.Query) ([]*scanner.Registration, error) { + ret := _m.Called(query) + + var r0 []*scanner.Registration + if rf, ok := ret.Get(0).(func(*q.Query) []*scanner.Registration); ok { + r0 = rf(query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*scanner.Registration) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*q.Query) error); ok { + r1 = rf(query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Ping provides a mock function with given fields: registration +func (_m *Controller) Ping(registration *scanner.Registration) (*v1.ScannerAdapterMetadata, error) { + ret := _m.Called(registration) + + var r0 *v1.ScannerAdapterMetadata + if rf, ok := ret.Get(0).(func(*scanner.Registration) *v1.ScannerAdapterMetadata); ok { + r0 = rf(registration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.ScannerAdapterMetadata) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*scanner.Registration) error); ok { + r1 = rf(registration) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RegistrationExists provides a mock function with given fields: registrationUUID +func (_m *Controller) RegistrationExists(registrationUUID string) bool { + ret := _m.Called(registrationUUID) + + var r0 bool + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(registrationUUID) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// SetDefaultRegistration provides a mock function with given fields: registrationUUID +func (_m *Controller) SetDefaultRegistration(registrationUUID string) error { + ret := _m.Called(registrationUUID) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(registrationUUID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetRegistrationByProject provides a mock function with given fields: projectID, scannerID +func (_m *Controller) SetRegistrationByProject(projectID int64, scannerID string) error { + ret := _m.Called(projectID, scannerID) + + var r0 error + if rf, ok := ret.Get(0).(func(int64, string) error); ok { + r0 = rf(projectID, scannerID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateRegistration provides a mock function with given fields: registration +func (_m *Controller) UpdateRegistration(registration *scanner.Registration) error { + ret := _m.Called(registration) + + var r0 error + if rf, ok := ret.Get(0).(func(*scanner.Registration) error); ok { + r0 = rf(registration) + } else { + r0 = ret.Error(0) + } + + return r0 +}