From 654aa8edcfad6007b1d15ac323c37bc2c140418c Mon Sep 17 00:00:00 2001
From: "stonezdj(Daojun Zhang)" <stonezdj@gmail.com>
Date: Tue, 16 Apr 2024 21:34:19 +0800
Subject: [PATCH] Add generate SBOM feature (#20251)

* Add SBOM scan feature

  Add scan handler for sbom
  Delete previous sbom accessory before the job service

Signed-off-by: stonezdj <daojunz@vmware.com>

* fix issue

Signed-off-by: stonezdj <stone.zhang@broadcom.com>

---------

Signed-off-by: stonezdj <daojunz@vmware.com>
Signed-off-by: stonezdj <stone.zhang@broadcom.com>
Co-authored-by: stonezdj <daojunz@vmware.com>
---
 api/v2.0/swagger.yaml                         |  15 +-
 src/common/rbac/const.go                      |   5 +
 src/common/rbac/project/rbac_role.go          |   9 +
 src/controller/artifact/controller.go         |   2 +
 .../artifact/processor/sbom/sbom.go           |   6 +-
 .../artifact/processor/sbom/sbom_test.go      |   2 +-
 src/controller/scan/base_controller.go        |  70 +++++++-
 src/controller/scan/base_controller_test.go   |  32 +++-
 src/controller/scanner/base_controller.go     |  23 ++-
 src/core/main.go                              |   1 +
 src/jobservice/main.go                        |   1 +
 src/pkg/scan/dao/scanner/model.go             |  20 ++-
 src/pkg/scan/handler.go                       |   7 +
 src/pkg/scan/job.go                           |  29 +++-
 src/pkg/scan/job_test.go                      |   5 +-
 src/pkg/scan/rest/v1/client.go                |  13 +-
 src/pkg/scan/rest/v1/client_test.go           |   6 +-
 src/pkg/scan/rest/v1/models.go                |  19 ++
 src/pkg/scan/sbom/model/summary.go            |  43 +++++
 src/pkg/scan/sbom/sbom.go                     | 162 ++++++++++++++++++
 src/pkg/scan/sbom/sbom_test.go                | 139 +++++++++++++++
 src/pkg/scan/vulnerability/vul.go             |  15 ++
 src/pkg/scan/vulnerability/vul_test.go        |  64 +++++++
 src/server/v2.0/handler/model/scanner.go      |   1 +
 src/server/v2.0/handler/project.go            |   8 +-
 src/server/v2.0/handler/project_test.go       |  14 +-
 src/server/v2.0/handler/scan.go               |  37 ++--
 src/testing/pkg/scan/rest/v1/client.go        |  18 +-
 28 files changed, 697 insertions(+), 69 deletions(-)
 create mode 100644 src/pkg/scan/sbom/model/summary.go
 create mode 100644 src/pkg/scan/sbom/sbom.go
 create mode 100644 src/pkg/scan/sbom/sbom_test.go

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)
 	}