diff --git a/api/v2.0/swagger.yaml b/api/v2.0/swagger.yaml index ce4210ec8..0f5c18e73 100644 --- a/api/v2.0/swagger.yaml +++ b/api/v2.0/swagger.yaml @@ -996,7 +996,7 @@ paths: description: Specify whether the SBOM overview is included in returning artifacts, when this option is true, the SBOM overview will be included in the response type: boolean required: false - default: false + default: false - name: with_signature in: query description: Specify whether the signature is included inside the tags of the returning artifacts. Only works when setting "with_tag=true" @@ -1179,7 +1179,7 @@ paths: - name: scan_request_type in: body required: false - schema: + schema: $ref: '#/definitions/ScanRequestType' responses: '202': @@ -6769,7 +6769,7 @@ definitions: ScanRequestType: type: object properties: - scan_type: + scan_type: type: string description: 'The scan type for the scan request. Two options are currently supported, vulnerability and sbom' enum: [vulnerability, sbom] @@ -6797,12 +6797,12 @@ definitions: description: 'The status of the generating SBOM task' sbom_digest: type: string - description: 'The digest of the generated SBOM accessory' + description: 'The digest of the generated SBOM accessory' report_id: type: string description: 'id of the native scan report' - example: '5f62c830-f996-11e9-957f-0242c0a89008' - duration: + example: '5f62c830-f996-11e9-957f-0242c0a89008' + duration: type: integer format: int64 description: 'Time in seconds required to create the report' @@ -8437,7 +8437,7 @@ definitions: description: Indicates the capabilities of the scanner, e.g. support_vulnerability or support_sbom. additionalProperties: True example: {"support_vulnerability": true, "support_sbom": true} - + ScannerRegistrationReq: type: object required: @@ -9986,7 +9986,6 @@ definitions: items: type: string description: Links of the vulnerability - ScanType: type: object properties: diff --git a/src/common/rbac/const.go b/src/common/rbac/const.go index ff49ec3fd..a783e71d4 100644 --- a/src/common/rbac/const.go +++ b/src/common/rbac/const.go @@ -51,6 +51,7 @@ const ( ResourceRobot = Resource("robot") ResourceNotificationPolicy = Resource("notification-policy") ResourceScan = Resource("scan") + ResourceSBOM = Resource("sbom") ResourceScanner = Resource("scanner") ResourceArtifact = Resource("artifact") ResourceTag = Resource("tag") @@ -182,6 +183,10 @@ var ( {Resource: ResourceScan, Action: ActionRead}, {Resource: ResourceScan, Action: ActionStop}, + {Resource: ResourceSBOM, Action: ActionCreate}, + {Resource: ResourceSBOM, Action: ActionStop}, + {Resource: ResourceSBOM, Action: ActionRead}, + {Resource: ResourceTag, Action: ActionCreate}, {Resource: ResourceTag, Action: ActionList}, {Resource: ResourceTag, Action: ActionDelete}, diff --git a/src/common/rbac/project/rbac_role.go b/src/common/rbac/project/rbac_role.go index fa618b982..5ef773e9a 100644 --- a/src/common/rbac/project/rbac_role.go +++ b/src/common/rbac/project/rbac_role.go @@ -86,6 +86,9 @@ var ( {Resource: rbac.ResourceScan, Action: rbac.ActionCreate}, {Resource: rbac.ResourceScan, Action: rbac.ActionRead}, {Resource: rbac.ResourceScan, Action: rbac.ActionStop}, + {Resource: rbac.ResourceSBOM, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceSBOM, Action: rbac.ActionStop}, + {Resource: rbac.ResourceSBOM, Action: rbac.ActionRead}, {Resource: rbac.ResourceScanner, Action: rbac.ActionRead}, {Resource: rbac.ResourceScanner, Action: rbac.ActionCreate}, @@ -169,6 +172,9 @@ var ( {Resource: rbac.ResourceScan, Action: rbac.ActionCreate}, {Resource: rbac.ResourceScan, Action: rbac.ActionRead}, {Resource: rbac.ResourceScan, Action: rbac.ActionStop}, + {Resource: rbac.ResourceSBOM, Action: rbac.ActionCreate}, + {Resource: rbac.ResourceSBOM, Action: rbac.ActionStop}, + {Resource: rbac.ResourceSBOM, Action: rbac.ActionRead}, {Resource: rbac.ResourceScanner, Action: rbac.ActionRead}, @@ -223,6 +229,7 @@ var ( {Resource: rbac.ResourceRobot, Action: rbac.ActionList}, {Resource: rbac.ResourceScan, Action: rbac.ActionRead}, + {Resource: rbac.ResourceSBOM, Action: rbac.ActionRead}, {Resource: rbac.ResourceScanner, Action: rbac.ActionRead}, @@ -267,6 +274,7 @@ var ( {Resource: rbac.ResourceRobot, Action: rbac.ActionList}, {Resource: rbac.ResourceScan, Action: rbac.ActionRead}, + {Resource: rbac.ResourceSBOM, Action: rbac.ActionRead}, {Resource: rbac.ResourceScanner, Action: rbac.ActionRead}, @@ -290,6 +298,7 @@ var ( {Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead}, {Resource: rbac.ResourceScan, Action: rbac.ActionRead}, + {Resource: rbac.ResourceSBOM, Action: rbac.ActionRead}, {Resource: rbac.ResourceScanner, Action: rbac.ActionRead}, diff --git a/src/controller/artifact/controller.go b/src/controller/artifact/controller.go index cc100211f..34ea29077 100644 --- a/src/controller/artifact/controller.go +++ b/src/controller/artifact/controller.go @@ -29,6 +29,7 @@ import ( "github.com/goharbor/harbor/src/controller/artifact/processor/chart" "github.com/goharbor/harbor/src/controller/artifact/processor/cnab" "github.com/goharbor/harbor/src/controller/artifact/processor/image" + "github.com/goharbor/harbor/src/controller/artifact/processor/sbom" "github.com/goharbor/harbor/src/controller/artifact/processor/wasm" "github.com/goharbor/harbor/src/controller/event/metadata" "github.com/goharbor/harbor/src/controller/tag" @@ -73,6 +74,7 @@ var ( chart.ArtifactTypeChart: icon.DigestOfIconChart, cnab.ArtifactTypeCNAB: icon.DigestOfIconCNAB, wasm.ArtifactTypeWASM: icon.DigestOfIconWASM, + sbom.ArtifactTypeSBOM: icon.DigestOfIconAccSBOM, } ) diff --git a/src/controller/artifact/processor/sbom/sbom.go b/src/controller/artifact/processor/sbom/sbom.go index ec0222fb9..4eb11f4bd 100644 --- a/src/controller/artifact/processor/sbom/sbom.go +++ b/src/controller/artifact/processor/sbom/sbom.go @@ -29,8 +29,8 @@ import ( ) const ( - // processorArtifactTypeSBOM is the artifact type for SBOM, it's scope is only used in the processor - processorArtifactTypeSBOM = "SBOM" + // ArtifactTypeSBOM is the artifact type for SBOM, it's scope is only used in the processor + ArtifactTypeSBOM = "SBOM" // processorMediaType is the media type for SBOM, it's scope is only used to register the processor processorMediaType = "application/vnd.goharbor.harbor.sbom.v1" ) @@ -85,5 +85,5 @@ func (m *Processor) AbstractAddition(_ context.Context, art *artifact.Artifact, // GetArtifactType the artifact type is used to display the artifact type in the UI func (m *Processor) GetArtifactType(_ context.Context, _ *artifact.Artifact) string { - return processorArtifactTypeSBOM + return ArtifactTypeSBOM } diff --git a/src/controller/artifact/processor/sbom/sbom_test.go b/src/controller/artifact/processor/sbom/sbom_test.go index 6128c550f..33889591a 100644 --- a/src/controller/artifact/processor/sbom/sbom_test.go +++ b/src/controller/artifact/processor/sbom/sbom_test.go @@ -158,7 +158,7 @@ func (suite *SBOMProcessorTestSuite) TestAbstractAdditionPullManifestError() { } func (suite *SBOMProcessorTestSuite) TestGetArtifactType() { - suite.Equal(processorArtifactTypeSBOM, suite.processor.GetArtifactType(context.Background(), &artifact.Artifact{})) + suite.Equal(ArtifactTypeSBOM, suite.processor.GetArtifactType(context.Background(), &artifact.Artifact{})) } func TestSBOMProcessorTestSuite(t *testing.T) { diff --git a/src/controller/scan/base_controller.go b/src/controller/scan/base_controller.go index a1627af30..be168098a 100644 --- a/src/controller/scan/base_controller.go +++ b/src/controller/scan/base_controller.go @@ -49,8 +49,10 @@ import ( "github.com/goharbor/harbor/src/pkg/scan/postprocessors" "github.com/goharbor/harbor/src/pkg/scan/report" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" + sbomModel "github.com/goharbor/harbor/src/pkg/scan/sbom/model" "github.com/goharbor/harbor/src/pkg/scan/vuln" "github.com/goharbor/harbor/src/pkg/task" + "github.com/goharbor/harbor/src/testing/controller/artifact" ) var ( @@ -108,6 +110,8 @@ type basicController struct { rc robot.Controller // Tag controller tagCtl tag.Controller + // Artifact controller + artCtl artifact.Controller // UUID generator uuid uuidGenerator // Configuration getter func @@ -259,7 +263,7 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti launchScanJobParams []*launchScanJobParam ) for _, art := range artifacts { - reports, err := bc.makeReportPlaceholder(ctx, r, art) + reports, err := bc.makeReportPlaceholder(ctx, r, art, opts) if err != nil { if errors.IsConflictErr(err) { errs = append(errs, err) @@ -326,7 +330,7 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti for _, launchScanJobParam := range launchScanJobParams { launchScanJobParam.ExecutionID = opts.ExecutionID - if err := bc.launchScanJob(ctx, launchScanJobParam); err != nil { + if err := bc.launchScanJob(ctx, launchScanJobParam, opts); err != nil { log.G(ctx).Warningf("scan artifact %s@%s failed, error: %v", artifact.RepositoryName, artifact.Digest, err) errs = append(errs, err) } @@ -546,13 +550,15 @@ func (bc *basicController) startScanAll(ctx context.Context, executionID int64) return nil } -func (bc *basicController) makeReportPlaceholder(ctx context.Context, r *scanner.Registration, art *ar.Artifact) ([]*scan.Report, error) { - mimeTypes := r.GetProducesMimeTypes(art.ManifestMediaType) - +func (bc *basicController) makeReportPlaceholder(ctx context.Context, r *scanner.Registration, art *ar.Artifact, opts *Options) ([]*scan.Report, error) { + mimeTypes := r.GetProducesMimeTypes(art.ManifestMediaType, opts.GetScanType()) oldReports, err := bc.manager.GetBy(bc.cloneCtx(ctx), art.Digest, r.UUID, mimeTypes) if err != nil { return nil, err } + if err := bc.deleteArtifactAccessories(ctx, oldReports); err != nil { + return nil, err + } if err := bc.assembleReports(ctx, oldReports...); err != nil { return nil, err @@ -574,7 +580,7 @@ func (bc *basicController) makeReportPlaceholder(ctx context.Context, r *scanner var reports []*scan.Report - for _, pm := range r.GetProducesMimeTypes(art.ManifestMediaType) { + for _, pm := range r.GetProducesMimeTypes(art.ManifestMediaType, opts.GetScanType()) { report := &scan.Report{ Digest: art.Digest, RegistrationUUID: r.UUID, @@ -991,7 +997,7 @@ func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64 } // launchScanJob launches a job to run scan -func (bc *basicController) launchScanJob(ctx context.Context, param *launchScanJobParam) error { +func (bc *basicController) launchScanJob(ctx context.Context, param *launchScanJobParam, opts *Options) error { // don't launch scan job for the artifact which is not supported by the scanner if !hasCapability(param.Registration, param.Artifact) { return nil @@ -1032,6 +1038,11 @@ func (bc *basicController) launchScanJob(ctx context.Context, param *launchScanJ MimeType: param.Artifact.ManifestMediaType, Size: param.Artifact.Size, }, + RequestType: []*v1.ScanType{ + { + Type: opts.GetScanType(), + }, + }, } rJSON, err := param.Registration.ToJSON() @@ -1265,3 +1276,48 @@ func parseOptions(options ...Option) (*Options, error) { return ops, nil } + +// deleteArtifactAccessories delete the accessory in reports, only delete sbom accessory +func (bc *basicController) deleteArtifactAccessories(ctx context.Context, reports []*scan.Report) error { + for _, rpt := range reports { + if rpt.MimeType != v1.MimeTypeSBOMReport { + continue + } + if err := bc.deleteArtifactAccessory(ctx, rpt.Report); err != nil { + return err + } + } + return nil +} + +// deleteArtifactAccessory check if current report has accessory info, if there is, delete it +func (bc *basicController) deleteArtifactAccessory(ctx context.Context, report string) error { + if len(report) == 0 { + return nil + } + sbomSummary := sbomModel.Summary{} + if err := json.Unmarshal([]byte(report), &sbomSummary); err != nil { + // it could be a non sbom report, just skip + log.Debugf("fail to unmarshal %v, skip to delete sbom report", err) + return nil + } + repo, dgst := sbomSummary.SBOMAccArt() + if len(repo) == 0 || len(dgst) == 0 { + return nil + } + art, err := bc.ar.GetByReference(ctx, repo, dgst, nil) + if err != nil { + if errors.IsNotFoundErr(err) { + return nil + } + return err + } + if art == nil { + return nil + } + err = bc.ar.Delete(ctx, art.ID) + if errors.IsNotFoundErr(err) { + return nil + } + return err +} diff --git a/src/controller/scan/base_controller_test.go b/src/controller/scan/base_controller_test.go index 9d485f16c..521325d79 100644 --- a/src/controller/scan/base_controller_test.go +++ b/src/controller/scan/base_controller_test.go @@ -108,6 +108,7 @@ func (suite *ControllerTestSuite) SetupSuite() { Version: "0.1.0", }, Capabilities: []*v1.ScannerCapability{{ + Type: v1.ScanTypeVulnerability, ConsumesMimeTypes: []string{ v1.MimeTypeOCIArtifact, v1.MimeTypeDockerArtifact, @@ -115,7 +116,17 @@ func (suite *ControllerTestSuite) SetupSuite() { ProducesMimeTypes: []string{ v1.MimeTypeNativeReport, }, - }}, + }, + { + Type: v1.ScanTypeSbom, + ConsumesMimeTypes: []string{ + v1.MimeTypeOCIArtifact, + }, + ProducesMimeTypes: []string{ + v1.MimeTypeSBOMReport, + }, + }, + }, Properties: v1.ScannerProperties{ "extra": "testing", }, @@ -655,3 +666,22 @@ func TestIsSBOMMimeTypes(t *testing.T) { // Test with an empty slice assert.False(t, isSBOMMimeTypes([]string{})) } + +func (suite *ControllerTestSuite) TestDeleteArtifactAccessories() { + // artifact not provided + suite.Nil(suite.c.deleteArtifactAccessories(context.TODO(), nil)) + + // artifact is provided + art := &artifact.Artifact{Artifact: art.Artifact{ID: 1, ProjectID: 1, RepositoryName: "library/photon"}} + mock.OnAnything(suite.ar, "GetByReference").Return(art, nil).Once() + mock.OnAnything(suite.ar, "Delete").Return(nil).Once() + reportContent := `{"sbom_digest":"sha256:12345", "scan_status":"Success", "duration":3, "sbom_repository":"library/photon"}` + emptyReportContent := `` + reports := []*scan.Report{ + {Report: reportContent}, + {Report: emptyReportContent}, + } + ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{}) + suite.NoError(suite.c.deleteArtifactAccessories(ctx, reports)) + +} diff --git a/src/controller/scanner/base_controller.go b/src/controller/scanner/base_controller.go index 2028da355..068424a1e 100644 --- a/src/controller/scanner/base_controller.go +++ b/src/controller/scanner/base_controller.go @@ -79,7 +79,11 @@ func (bc *basicController) ListRegistrations(ctx context.Context, query *q.Query if err != nil { return nil, errors.Wrap(err, "api controller: list registrations") } - + for _, r := range l { + if err := bc.appendCap(ctx, r); err != nil { + return nil, err + } + } return l, nil } @@ -122,10 +126,25 @@ func (bc *basicController) GetRegistration(ctx context.Context, registrationUUID if err != nil { return nil, errors.Wrap(err, "api controller: get registration") } - + if r == nil { + return nil, nil + } + if err := bc.appendCap(ctx, r); err != nil { + return nil, err + } return r, nil } +func (bc *basicController) appendCap(ctx context.Context, r *scanner.Registration) error { + mt, err := bc.Ping(ctx, r) + if err != nil { + logger.Errorf("Get registration error: %s", err) + return err + } + r.Capabilities = mt.ConvertCapability() + return nil +} + // RegistrationExists ... func (bc *basicController) RegistrationExists(ctx context.Context, registrationUUID string) bool { registration, err := bc.manager.Get(ctx, registrationUUID) diff --git a/src/core/main.go b/src/core/main.go index ebc786d7e..f0bc96564 100644 --- a/src/core/main.go +++ b/src/core/main.go @@ -70,6 +70,7 @@ import ( "github.com/goharbor/harbor/src/pkg/oidc" "github.com/goharbor/harbor/src/pkg/scan" "github.com/goharbor/harbor/src/pkg/scan/dao/scanner" + _ "github.com/goharbor/harbor/src/pkg/scan/sbom" _ "github.com/goharbor/harbor/src/pkg/scan/vulnerability" pkguser "github.com/goharbor/harbor/src/pkg/user" "github.com/goharbor/harbor/src/pkg/version" diff --git a/src/jobservice/main.go b/src/jobservice/main.go index f288efea7..e00dd4b20 100644 --- a/src/jobservice/main.go +++ b/src/jobservice/main.go @@ -36,6 +36,7 @@ import ( _ "github.com/goharbor/harbor/src/pkg/accessory/model/subject" _ "github.com/goharbor/harbor/src/pkg/config/inmemory" _ "github.com/goharbor/harbor/src/pkg/config/rest" + _ "github.com/goharbor/harbor/src/pkg/scan/sbom" _ "github.com/goharbor/harbor/src/pkg/scan/vulnerability" ) diff --git a/src/pkg/scan/dao/scanner/model.go b/src/pkg/scan/dao/scanner/model.go index dbdcf8b1d..cc418e624 100644 --- a/src/pkg/scan/dao/scanner/model.go +++ b/src/pkg/scan/dao/scanner/model.go @@ -66,8 +66,9 @@ type Registration struct { Metadata *v1.ScannerAdapterMetadata `orm:"-" json:"-"` // Timestamps - CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` - UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"` + Capabilities map[string]interface{} `orm:"-" json:"capabilities,omitempty"` } // TableName for Endpoint @@ -151,15 +152,20 @@ func (r *Registration) HasCapability(manifestMimeType string) bool { } // GetProducesMimeTypes returns produces mime types for the artifact -func (r *Registration) GetProducesMimeTypes(mimeType string) []string { +func (r *Registration) GetProducesMimeTypes(mimeType string, scanType string) []string { if r.Metadata == nil { return nil } - for _, capability := range r.Metadata.Capabilities { - for _, mt := range capability.ConsumesMimeTypes { - if mt == mimeType { - return capability.ProducesMimeTypes + capType := capability.Type + if len(capType) == 0 { + capType = v1.ScanTypeVulnerability + } + if scanType == capType { + for _, mt := range capability.ConsumesMimeTypes { + if mt == mimeType { + return capability.ProducesMimeTypes + } } } } diff --git a/src/pkg/scan/handler.go b/src/pkg/scan/handler.go index 7ddba595d..f402107c7 100644 --- a/src/pkg/scan/handler.go +++ b/src/pkg/scan/handler.go @@ -38,7 +38,14 @@ func GetScanHandler(requestType string) Handler { // Handler handler for scan job, it could be implement by different scan type, such as vulnerability, sbom type Handler interface { + // RequestProducesMineTypes returns the produces mime types + RequestProducesMineTypes() []string + // RequiredPermissions defines the permission used by the scan robot account RequiredPermissions() []*types.Policy + // RequestParameters defines the parameters for scan request + RequestParameters() map[string]interface{} + // ReportURLParameter defines the parameters for scan report + ReportURLParameter(sr *v1.ScanRequest) (string, error) // PostScan defines the operation after scan PostScan(ctx job.Context, sr *v1.ScanRequest, rp *scan.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error) } diff --git a/src/pkg/scan/job.go b/src/pkg/scan/job.go index c21b48cb2..171e0c307 100644 --- a/src/pkg/scan/job.go +++ b/src/pkg/scan/job.go @@ -242,7 +242,13 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error { } myLogger.Debugf("check scan report for mime %s at %s", m, t.Format("2006/01/02 15:04:05")) - rawReport, err := fetchScanReportFromScanner(client, resp.ID, m) + + reportURLParameter, err := handler.ReportURLParameter(req) + if err != nil { + errs[i] = errors.Wrap(err, "scan job: get report url") + return + } + rawReport, err := fetchScanReportFromScanner(client, resp.ID, m, reportURLParameter) if err != nil { // Not ready yet if notReadyErr, ok := err.(*v1.ReportNotReadyError); ok { @@ -332,13 +338,13 @@ func getReportPlaceholder(ctx context.Context, digest string, reportUUID string, return reports[0], nil } -func fetchScanReportFromScanner(client v1.Client, requestID string, m string) (rawReport string, err error) { - rawReport, err = client.GetScanReport(requestID, m) +func fetchScanReportFromScanner(client v1.Client, requestID string, mimType string, urlParameter string) (rawReport string, err error) { + rawReport, err = client.GetScanReport(requestID, mimType, urlParameter) if err != nil { return "", err } // Make sure the data is aligned with the v1 spec. - if _, err = report.ResolveData(m, []byte(rawReport)); err != nil { + if _, err = report.ResolveData(mimType, []byte(rawReport)); err != nil { return "", err } return rawReport, nil @@ -367,7 +373,20 @@ func ExtractScanReq(params job.Parameters) (*v1.ScanRequest, error) { if err := req.Validate(); err != nil { return nil, err } - + reqType := v1.ScanTypeVulnerability + // attach the request with ProducesMimeTypes and Parameters + if len(req.RequestType) > 0 { + // current only support requestType with one element for each request + if len(req.RequestType[0].Type) > 0 { + reqType = req.RequestType[0].Type + } + handler := GetScanHandler(reqType) + if handler == nil { + return nil, errors.Errorf("failed to get scan handler, request type %v", reqType) + } + req.RequestType[0].ProducesMimeTypes = handler.RequestProducesMineTypes() + req.RequestType[0].Parameters = handler.RequestParameters() + } return req, nil } diff --git a/src/pkg/scan/job_test.go b/src/pkg/scan/job_test.go index 92571285d..ff00dd20f 100644 --- a/src/pkg/scan/job_test.go +++ b/src/pkg/scan/job_test.go @@ -211,8 +211,9 @@ func (suite *JobTestSuite) TestfetchScanReportFromScanner() { suite.reportIDs = append(suite.reportIDs, rptID) require.NoError(suite.T(), err) client := &v1testing.Client{} - client.On("GetScanReport", mock.Anything, v1.MimeTypeGenericVulnerabilityReport).Return(rawContent, nil) - rawRept, err := fetchScanReportFromScanner(client, "abc", v1.MimeTypeGenericVulnerabilityReport) + client.On("GetScanReport", mock.Anything, v1.MimeTypeGenericVulnerabilityReport, mock.Anything).Return(rawContent, nil) + parameters := "sbom_media_type=application/spdx+json" + rawRept, err := fetchScanReportFromScanner(client, "abc", v1.MimeTypeGenericVulnerabilityReport, parameters) require.NoError(suite.T(), err) require.Equal(suite.T(), rawContent, rawRept) } diff --git a/src/pkg/scan/rest/v1/client.go b/src/pkg/scan/rest/v1/client.go index 251ccef1f..a5ae04075 100644 --- a/src/pkg/scan/rest/v1/client.go +++ b/src/pkg/scan/rest/v1/client.go @@ -68,7 +68,7 @@ type Client interface { // Returns: // string : the scan report of the given artifact // error : non nil error if any errors occurred - GetScanReport(scanRequestID, reportMIMEType string) (string, error) + GetScanReport(scanRequestID, reportMIMEType string, urlParameter string) (string, error) } // basicClient is default implementation of the Client interface @@ -97,7 +97,7 @@ func NewClient(url, authType, accessCredential string, skipCertVerify bool) (Cli httpClient: &http.Client{ Timeout: time.Second * 5, Transport: transport, - CheckRedirect: func(req *http.Request, via []*http.Request) error { + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }, }, @@ -167,7 +167,7 @@ func (c *basicClient) SubmitScan(req *ScanRequest) (*ScanResponse, error) { } // GetScanReport ... -func (c *basicClient) GetScanReport(scanRequestID, reportMIMEType string) (string, error) { +func (c *basicClient) GetScanReport(scanRequestID, reportMIMEType string, urlParameter string) (string, error) { if len(scanRequestID) == 0 { return "", errors.New("empty scan request ID") } @@ -177,8 +177,11 @@ func (c *basicClient) GetScanReport(scanRequestID, reportMIMEType string) (strin } def := c.spec.GetScanReport(scanRequestID, reportMIMEType) - - req, err := http.NewRequest(http.MethodGet, def.URL, nil) + reportURL := def.URL + if len(urlParameter) > 0 { + reportURL = fmt.Sprintf("%s?%s", def.URL, urlParameter) + } + req, err := http.NewRequest(http.MethodGet, reportURL, nil) if err != nil { return "", errors.Wrap(err, "v1 client: get scan report") } diff --git a/src/pkg/scan/rest/v1/client_test.go b/src/pkg/scan/rest/v1/client_test.go index ee3435066..5893514d6 100644 --- a/src/pkg/scan/rest/v1/client_test.go +++ b/src/pkg/scan/rest/v1/client_test.go @@ -72,7 +72,7 @@ func (suite *ClientTestSuite) TestClientSubmitScan() { // TestClientGetScanReportError tests getting report failed func (suite *ClientTestSuite) TestClientGetScanReportError() { - _, err := suite.client.GetScanReport("id1", MimeTypeNativeReport) + _, err := suite.client.GetScanReport("id1", MimeTypeNativeReport, "") require.Error(suite.T(), err) assert.Condition(suite.T(), func() (success bool) { success = strings.Index(err.Error(), "error") != -1 @@ -82,14 +82,14 @@ func (suite *ClientTestSuite) TestClientGetScanReportError() { // TestClientGetScanReport tests getting report func (suite *ClientTestSuite) TestClientGetScanReport() { - res, err := suite.client.GetScanReport("id2", MimeTypeNativeReport) + res, err := suite.client.GetScanReport("id2", MimeTypeNativeReport, "") require.NoError(suite.T(), err) require.NotEmpty(suite.T(), res) } // TestClientGetScanReportNotReady tests the case that the report is not ready func (suite *ClientTestSuite) TestClientGetScanReportNotReady() { - _, err := suite.client.GetScanReport("id3", MimeTypeNativeReport) + _, err := suite.client.GetScanReport("id3", MimeTypeNativeReport, "") require.Error(suite.T(), err) require.Condition(suite.T(), func() (success bool) { _, success = err.(*ReportNotReadyError) diff --git a/src/pkg/scan/rest/v1/models.go b/src/pkg/scan/rest/v1/models.go index c31edb93b..21352c749 100644 --- a/src/pkg/scan/rest/v1/models.go +++ b/src/pkg/scan/rest/v1/models.go @@ -21,6 +21,11 @@ import ( "github.com/goharbor/harbor/src/lib/errors" ) +const ( + supportVulnerability = "support_vulnerability" + supportSBOM = "support_sbom" +) + var supportedMimeTypes = []string{ MimeTypeNativeReport, MimeTypeGenericVulnerabilityReport, @@ -153,6 +158,20 @@ func (md *ScannerAdapterMetadata) GetCapability(mimeType string) *ScannerCapabil return nil } +// ConvertCapability converts the capability to map, used in get scanner API +func (md *ScannerAdapterMetadata) ConvertCapability() map[string]interface{} { + capabilities := make(map[string]interface{}) + for _, c := range md.Capabilities { + if c.Type == ScanTypeVulnerability { + capabilities[supportVulnerability] = true + } + if c.Type == ScanTypeSbom { + capabilities[supportSBOM] = true + } + } + return capabilities +} + // Artifact represents an artifact stored in Registry. type Artifact struct { // ID of the namespace (project). It will not be sent to scanner adapter. diff --git a/src/pkg/scan/sbom/model/summary.go b/src/pkg/scan/sbom/model/summary.go new file mode 100644 index 000000000..46c870f97 --- /dev/null +++ b/src/pkg/scan/sbom/model/summary.go @@ -0,0 +1,43 @@ +// 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 model + +const ( + // SBOMRepository ... + SBOMRepository = "sbom_repository" + // SBOMDigest ... + SBOMDigest = "sbom_digest" + // StartTime ... + StartTime = "start_time" + // EndTime ... + EndTime = "end_time" + // Duration ... + Duration = "duration" + // ScanStatus ... + ScanStatus = "scan_status" +) + +// Summary includes the sbom summary information +type Summary map[string]interface{} + +// SBOMAccArt returns the repository and digest of the SBOM +func (s Summary) SBOMAccArt() (repo, digest string) { + if repo, ok := s[SBOMRepository].(string); ok { + if digest, ok := s[SBOMDigest].(string); ok { + return repo, digest + } + } + return "", "" +} diff --git a/src/pkg/scan/sbom/sbom.go b/src/pkg/scan/sbom/sbom.go new file mode 100644 index 000000000..bbf405571 --- /dev/null +++ b/src/pkg/scan/sbom/sbom.go @@ -0,0 +1,162 @@ +// 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 sbom + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strings" + "time" + + "github.com/goharbor/harbor/src/common" + "github.com/goharbor/harbor/src/lib/config" + scanModel "github.com/goharbor/harbor/src/pkg/scan/dao/scan" + sbom "github.com/goharbor/harbor/src/pkg/scan/sbom/model" + + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/jobservice/job" + "github.com/goharbor/harbor/src/pkg/permission/types" + "github.com/goharbor/harbor/src/pkg/robot/model" + "github.com/goharbor/harbor/src/pkg/scan" + + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" + "github.com/goharbor/harbor/src/pkg/scan/vuln" +) + +const ( + sbomMimeType = "application/vnd.goharbor.harbor.sbom.v1" + sbomMediaTypeSpdx = "application/spdx+json" +) + +func init() { + scan.RegisterScanHanlder(v1.ScanTypeSbom, &scanHandler{GenAccessoryFunc: scan.GenAccessoryArt, RegistryServer: registryFQDN}) +} + +// ScanHandler defines the Handler to generate sbom +type scanHandler struct { + GenAccessoryFunc func(scanRep v1.ScanRequest, sbomContent []byte, labels map[string]string, mediaType string, robot *model.Robot) (string, error) + RegistryServer func(ctx context.Context) string +} + +// RequestProducesMineTypes defines the mine types produced by the scan handler +func (v *scanHandler) RequestProducesMineTypes() []string { + return []string{v1.MimeTypeSBOMReport} +} + +// RequestParameters defines the parameters for scan request +func (v *scanHandler) RequestParameters() map[string]interface{} { + return map[string]interface{}{"sbom_media_types": []string{sbomMediaTypeSpdx}} +} + +// ReportURLParameter defines the parameters for scan report url +func (v *scanHandler) ReportURLParameter(_ *v1.ScanRequest) (string, error) { + return fmt.Sprintf("sbom_media_type=%s", url.QueryEscape(sbomMediaTypeSpdx)), nil +} + +// RequiredPermissions defines the permission used by the scan robot account +func (v *scanHandler) RequiredPermissions() []*types.Policy { + return []*types.Policy{ + { + Resource: rbac.ResourceRepository, + Action: rbac.ActionPull, + }, + { + Resource: rbac.ResourceRepository, + Action: rbac.ActionScannerPull, + }, + { + Resource: rbac.ResourceRepository, + Action: rbac.ActionPush, + }, + } +} + +// PostScan defines task specific operations after the scan is complete +func (v *scanHandler) PostScan(ctx job.Context, sr *v1.ScanRequest, _ *scanModel.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error) { + sbomContent, err := retrieveSBOMContent(rawReport) + if err != nil { + return "", err + } + scanReq := v1.ScanRequest{ + Registry: sr.Registry, + Artifact: sr.Artifact, + } + // the registry server url is core by default, need to replace it with real registry server url + scanReq.Registry.URL = v.RegistryServer(ctx.SystemContext()) + if len(scanReq.Registry.URL) == 0 { + return "", fmt.Errorf("empty registry server") + } + myLogger := ctx.GetLogger() + myLogger.Debugf("Pushing accessory artifact to %s/%s", scanReq.Registry.URL, scanReq.Artifact.Repository) + dgst, err := v.GenAccessoryFunc(scanReq, sbomContent, v.annotations(), sbomMimeType, robot) + if err != nil { + myLogger.Errorf("error when create accessory from image %v", err) + return "", err + } + return v.generateReport(startTime, sr.Artifact.Repository, dgst, "Success") +} + +// annotations defines the annotations for the accessory artifact +func (v *scanHandler) annotations() map[string]string { + return map[string]string{ + "created-by": "Harbor", + "org.opencontainers.artifact.created": time.Now().Format(time.RFC3339), + "org.opencontainers.artifact.description": "SPDX JSON SBOM", + } +} + +func (v *scanHandler) generateReport(startTime time.Time, repository, digest, status string) (string, error) { + summary := sbom.Summary{} + endTime := time.Now() + summary[sbom.StartTime] = startTime + summary[sbom.EndTime] = endTime + summary[sbom.Duration] = int64(endTime.Sub(startTime).Seconds()) + summary[sbom.SBOMRepository] = repository + summary[sbom.SBOMDigest] = digest + summary[sbom.ScanStatus] = status + rep, err := json.Marshal(summary) + if err != nil { + return "", err + } + return string(rep), nil +} + +// extract server name from config, and remove the protocol prefix +func registryFQDN(ctx context.Context) string { + cfgMgr, ok := config.FromContext(ctx) + if ok { + extURL := cfgMgr.Get(context.Background(), common.ExtEndpoint).GetString() + server := strings.TrimPrefix(extURL, "https://") + server = strings.TrimPrefix(server, "http://") + return server + } + return "" +} + +// retrieveSBOMContent retrieves the "sbom" field from the raw report +func retrieveSBOMContent(rawReport string) ([]byte, error) { + rpt := vuln.Report{} + err := json.Unmarshal([]byte(rawReport), &rpt) + if err != nil { + return nil, err + } + sbomContent, err := json.Marshal(rpt.SBOM) + if err != nil { + return nil, err + } + return sbomContent, nil +} diff --git a/src/pkg/scan/sbom/sbom_test.go b/src/pkg/scan/sbom/sbom_test.go new file mode 100644 index 000000000..cf56b3bbb --- /dev/null +++ b/src/pkg/scan/sbom/sbom_test.go @@ -0,0 +1,139 @@ +package sbom + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/pkg/permission/types" + "github.com/goharbor/harbor/src/pkg/robot/model" + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" + "github.com/goharbor/harbor/src/testing/jobservice" + + "github.com/stretchr/testify/suite" +) + +func Test_scanHandler_ReportURLParameter(t *testing.T) { + type args struct { + in0 *v1.ScanRequest + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + {"normal test", args{&v1.ScanRequest{}}, "sbom_media_type=application%2Fspdx%2Bjson", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &scanHandler{} + got, err := v.ReportURLParameter(tt.args.in0) + if (err != nil) != tt.wantErr { + t.Errorf("ReportURLParameter() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ReportURLParameter() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_scanHandler_RequiredPermissions(t *testing.T) { + tests := []struct { + name string + want []*types.Policy + }{ + {"normal test", []*types.Policy{ + { + Resource: rbac.ResourceRepository, + Action: rbac.ActionPull, + }, + { + Resource: rbac.ResourceRepository, + Action: rbac.ActionScannerPull, + }, + { + Resource: rbac.ResourceRepository, + Action: rbac.ActionPush, + }, + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &scanHandler{} + if got := v.RequiredPermissions(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("RequiredPermissions() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_scanHandler_RequestProducesMineTypes(t *testing.T) { + tests := []struct { + name string + want []string + }{ + {"normal test", []string{v1.MimeTypeSBOMReport}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &scanHandler{} + if got := v.RequestProducesMineTypes(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("RequestProducesMineTypes() = %v, want %v", got, tt.want) + } + }) + } +} + +func mockGetRegistry(ctx context.Context) string { + return "myharbor.example.com" +} + +func mockGenAccessory(scanRep v1.ScanRequest, sbomContent []byte, labels map[string]string, mediaType string, robot *model.Robot) (string, error) { + return "sha256:1234567890", nil +} + +type ExampleTestSuite struct { + handler *scanHandler + suite.Suite +} + +func (suite *ExampleTestSuite) SetupSuite() { + suite.handler = &scanHandler{ + GenAccessoryFunc: mockGenAccessory, + RegistryServer: mockGetRegistry, + } +} + +func (suite *ExampleTestSuite) TearDownSuite() { +} + +func (suite *ExampleTestSuite) TestPostScan() { + req := &v1.ScanRequest{ + Registry: &v1.Registry{ + URL: "myregistry.example.com", + }, + Artifact: &v1.Artifact{ + Repository: "library/nosql", + }, + } + robot := &model.Robot{ + Name: "robot", + Secret: "mysecret", + } + startTime := time.Now() + rawReport := `{"sbom": { "key": "value" }}` + ctx := &jobservice.MockJobContext{} + ctx.On("GetLogger").Return(&jobservice.MockJobLogger{}) + accessory, err := suite.handler.PostScan(ctx, req, nil, rawReport, startTime, robot) + suite.Require().NoError(err) + suite.Require().NotEmpty(accessory) +} + +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, &ExampleTestSuite{}) +} diff --git a/src/pkg/scan/vulnerability/vul.go b/src/pkg/scan/vulnerability/vul.go index 804659c09..2e9194c4a 100644 --- a/src/pkg/scan/vulnerability/vul.go +++ b/src/pkg/scan/vulnerability/vul.go @@ -35,6 +35,16 @@ func init() { type ScanHandler struct { } +// RequestProducesMineTypes returns the produces mime types +func (v *ScanHandler) RequestProducesMineTypes() []string { + return []string{v1.MimeTypeGenericVulnerabilityReport} +} + +// RequestParameters defines the parameters for scan request +func (v *ScanHandler) RequestParameters() map[string]interface{} { + return nil +} + // RequiredPermissions defines the permission used by the scan robot account func (v *ScanHandler) RequiredPermissions() []*types.Policy { return []*types.Policy{ @@ -49,6 +59,11 @@ func (v *ScanHandler) RequiredPermissions() []*types.Policy { } } +// ReportURLParameter vulnerability doesn't require any scan report parameters +func (v *ScanHandler) ReportURLParameter(_ *v1.ScanRequest) (string, error) { + return "", nil +} + // PostScan ... func (v *ScanHandler) PostScan(ctx job.Context, _ *v1.ScanRequest, origRp *scan.Report, rawReport string, _ time.Time, _ *model.Robot) (string, error) { // use a new ormer here to use the short db connection diff --git a/src/pkg/scan/vulnerability/vul_test.go b/src/pkg/scan/vulnerability/vul_test.go index 50d84287e..003e15a0d 100644 --- a/src/pkg/scan/vulnerability/vul_test.go +++ b/src/pkg/scan/vulnerability/vul_test.go @@ -1,6 +1,7 @@ package vulnerability import ( + "fmt" "testing" "time" @@ -50,3 +51,66 @@ func TestPostScan(t *testing.T) { assert.Equal(t, "", refreshedReport, "PostScan should return the refreshed report") assert.Nil(t, err, "PostScan should not return an error") } + +func TestScanHandler_RequiredPermissions(t *testing.T) { + tests := []struct { + name string + want []*types.Policy + }{ + {"normal", []*types.Policy{ + { + Resource: rbac.ResourceRepository, + Action: rbac.ActionPull, + }, + { + Resource: rbac.ResourceRepository, + Action: rbac.ActionScannerPull, + }, + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &ScanHandler{} + assert.Equalf(t, tt.want, v.RequiredPermissions(), "RequiredPermissions()") + }) + } +} + +func TestScanHandler_ReportURLParameter(t *testing.T) { + type args struct { + in0 *v1.ScanRequest + } + tests := []struct { + name string + args args + want string + wantErr assert.ErrorAssertionFunc + }{ + {"normal", args{&v1.ScanRequest{}}, "", assert.NoError}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &ScanHandler{} + got, err := v.ReportURLParameter(tt.args.in0) + if !tt.wantErr(t, err, fmt.Sprintf("ReportURLParameter(%v)", tt.args.in0)) { + return + } + assert.Equalf(t, tt.want, got, "ReportURLParameter(%v)", tt.args.in0) + }) + } +} + +func TestScanHandler_RequestProducesMineTypes(t *testing.T) { + tests := []struct { + name string + want []string + }{ + {"normal", []string{v1.MimeTypeGenericVulnerabilityReport}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &ScanHandler{} + assert.Equalf(t, tt.want, v.RequestProducesMineTypes(), "RequestProducesMineTypes()") + }) + } +} diff --git a/src/server/v2.0/handler/model/scanner.go b/src/server/v2.0/handler/model/scanner.go index bb140937f..a2fd45ce9 100644 --- a/src/server/v2.0/handler/model/scanner.go +++ b/src/server/v2.0/handler/model/scanner.go @@ -52,6 +52,7 @@ func (s *ScannerRegistration) ToSwagger(_ context.Context) *models.ScannerRegist Vendor: s.Vendor, Version: s.Version, Health: s.Health, + Capabilities: s.Capabilities, } } diff --git a/src/server/v2.0/handler/project.go b/src/server/v2.0/handler/project.go index 692a26d6f..f9848345f 100644 --- a/src/server/v2.0/handler/project.go +++ b/src/server/v2.0/handler/project.go @@ -594,7 +594,13 @@ func (a *projectAPI) GetScannerOfProject(ctx context.Context, params operation.G if err != nil { return a.SendError(ctx, err) } - + if scanner != nil { + metadata, err := a.scannerCtl.GetMetadata(ctx, scanner.UUID) + if err != nil { + return a.SendError(ctx, err) + } + scanner.Capabilities = metadata.ConvertCapability() + } return operation.NewGetScannerOfProjectOK().WithPayload(model.NewScannerRegistration(scanner).ToSwagger(ctx)) } diff --git a/src/server/v2.0/handler/project_test.go b/src/server/v2.0/handler/project_test.go index 21ba79a0a..9289829b8 100644 --- a/src/server/v2.0/handler/project_test.go +++ b/src/server/v2.0/handler/project_test.go @@ -22,6 +22,7 @@ import ( "github.com/goharbor/harbor/src/pkg/project/models" "github.com/goharbor/harbor/src/pkg/scan/dao/scanner" + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" "github.com/goharbor/harbor/src/server/v2.0/restapi" projecttesting "github.com/goharbor/harbor/src/testing/controller/project" scannertesting "github.com/goharbor/harbor/src/testing/controller/scanner" @@ -36,6 +37,7 @@ type ProjectTestSuite struct { scannerCtl *scannertesting.Controller project *models.Project reg *scanner.Registration + metadata *v1.ScannerAdapterMetadata } func (suite *ProjectTestSuite) SetupSuite() { @@ -59,7 +61,12 @@ func (suite *ProjectTestSuite) SetupSuite() { scannerCtl: suite.scannerCtl, }, } - + suite.metadata = &v1.ScannerAdapterMetadata{ + Capabilities: []*v1.ScannerCapability{ + {Type: "vulnerability", ProducesMimeTypes: []string{v1.MimeTypeScanResponse}}, + {Type: "sbom", ProducesMimeTypes: []string{v1.MimeTypeSBOMReport}}, + }, + } suite.Suite.SetupSuite() } @@ -81,7 +88,7 @@ func (suite *ProjectTestSuite) TestGetScannerOfProject() { // scanner not found mock.OnAnything(suite.projectCtl, "Get").Return(suite.project, nil).Once() mock.OnAnything(suite.scannerCtl, "GetRegistrationByProject").Return(nil, nil).Once() - + mock.OnAnything(suite.scannerCtl, "GetMetadata").Return(suite.metadata, nil).Once() res, err := suite.Get("/projects/1/scanner") suite.NoError(err) suite.Equal(200, res.StatusCode) @@ -90,7 +97,7 @@ func (suite *ProjectTestSuite) TestGetScannerOfProject() { { mock.OnAnything(suite.projectCtl, "Get").Return(suite.project, nil).Once() mock.OnAnything(suite.scannerCtl, "GetRegistrationByProject").Return(suite.reg, nil).Once() - + mock.OnAnything(suite.scannerCtl, "GetMetadata").Return(suite.metadata, nil).Once() var scanner scanner.Registration res, err := suite.GetJSON("/projects/1/scanner", &scanner) suite.NoError(err) @@ -101,6 +108,7 @@ func (suite *ProjectTestSuite) TestGetScannerOfProject() { { mock.OnAnything(projectCtlMock, "GetByName").Return(suite.project, nil).Once() mock.OnAnything(suite.projectCtl, "Get").Return(suite.project, nil).Once() + mock.OnAnything(suite.scannerCtl, "GetMetadata").Return(suite.metadata, nil).Once() mock.OnAnything(suite.scannerCtl, "GetRegistrationByProject").Return(suite.reg, nil).Once() var scanner scanner.Registration diff --git a/src/server/v2.0/handler/scan.go b/src/server/v2.0/handler/scan.go index cca0092e8..80b70131f 100644 --- a/src/server/v2.0/handler/scan.go +++ b/src/server/v2.0/handler/scan.go @@ -25,6 +25,7 @@ import ( "github.com/goharbor/harbor/src/controller/scan" "github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/pkg/distribution" + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/scan" ) @@ -50,7 +51,15 @@ func (s *scanAPI) Prepare(ctx context.Context, _ string, params interface{}) mid } func (s *scanAPI) StopScanArtifact(ctx context.Context, params operation.StopScanArtifactParams) middleware.Responder { - if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionStop, rbac.ResourceScan); err != nil { + scanType := v1.ScanTypeVulnerability + if params.ScanType != nil && validScanType(params.ScanType.ScanType) { + scanType = params.ScanType.ScanType + } + res := rbac.ResourceScan + if scanType == v1.ScanTypeSbom { + res = rbac.ResourceSBOM + } + if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionStop, res); err != nil { return s.SendError(ctx, err) } @@ -68,22 +77,26 @@ func (s *scanAPI) StopScanArtifact(ctx context.Context, params operation.StopSca } func (s *scanAPI) ScanArtifact(ctx context.Context, params operation.ScanArtifactParams) middleware.Responder { - if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionCreate, rbac.ResourceScan); err != nil { - return s.SendError(ctx, err) - } - - repository := fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName) - artifact, err := s.artCtl.GetByReference(ctx, repository, params.Reference, nil) - if err != nil { - return s.SendError(ctx, err) - } - + scanType := v1.ScanTypeVulnerability options := []scan.Option{} if !distribution.IsDigest(params.Reference) { options = append(options, scan.WithTag(params.Reference)) } if params.ScanRequestType != nil && validScanType(params.ScanRequestType.ScanType) { - options = append(options, scan.WithScanType(params.ScanRequestType.ScanType)) + scanType = params.ScanRequestType.ScanType + options = append(options, scan.WithScanType(scanType)) + } + res := rbac.ResourceScan + if scanType == v1.ScanTypeSbom { + res = rbac.ResourceSBOM + } + if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionCreate, res); err != nil { + return s.SendError(ctx, err) + } + repository := fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName) + artifact, err := s.artCtl.GetByReference(ctx, repository, params.Reference, nil) + if err != nil { + return s.SendError(ctx, err) } if err := s.scanCtl.Scan(ctx, artifact, options...); err != nil { diff --git a/src/testing/pkg/scan/rest/v1/client.go b/src/testing/pkg/scan/rest/v1/client.go index b17c21e71..1d15012c7 100644 --- a/src/testing/pkg/scan/rest/v1/client.go +++ b/src/testing/pkg/scan/rest/v1/client.go @@ -42,9 +42,9 @@ func (_m *Client) GetMetadata() (*v1.ScannerAdapterMetadata, error) { return r0, r1 } -// GetScanReport provides a mock function with given fields: scanRequestID, reportMIMEType -func (_m *Client) GetScanReport(scanRequestID string, reportMIMEType string) (string, error) { - ret := _m.Called(scanRequestID, reportMIMEType) +// GetScanReport provides a mock function with given fields: scanRequestID, reportMIMEType, urlParameter +func (_m *Client) GetScanReport(scanRequestID string, reportMIMEType string, urlParameter string) (string, error) { + ret := _m.Called(scanRequestID, reportMIMEType, urlParameter) if len(ret) == 0 { panic("no return value specified for GetScanReport") @@ -52,17 +52,17 @@ func (_m *Client) GetScanReport(scanRequestID string, reportMIMEType string) (st var r0 string var r1 error - if rf, ok := ret.Get(0).(func(string, string) (string, error)); ok { - return rf(scanRequestID, reportMIMEType) + if rf, ok := ret.Get(0).(func(string, string, string) (string, error)); ok { + return rf(scanRequestID, reportMIMEType, urlParameter) } - if rf, ok := ret.Get(0).(func(string, string) string); ok { - r0 = rf(scanRequestID, reportMIMEType) + if rf, ok := ret.Get(0).(func(string, string, string) string); ok { + r0 = rf(scanRequestID, reportMIMEType, urlParameter) } else { r0 = ret.Get(0).(string) } - if rf, ok := ret.Get(1).(func(string, string) error); ok { - r1 = rf(scanRequestID, reportMIMEType) + if rf, ok := ret.Get(1).(func(string, string, string) error); ok { + r1 = rf(scanRequestID, reportMIMEType, urlParameter) } else { r1 = ret.Error(1) }