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>
This commit is contained in:
stonezdj(Daojun Zhang) 2024-04-16 21:34:19 +08:00 committed by GitHub
parent 67c03ddc4f
commit 654aa8edcf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 697 additions and 69 deletions

View File

@ -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 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 type: boolean
required: false required: false
default: false default: false
- name: with_signature - name: with_signature
in: query in: query
description: Specify whether the signature is included inside the tags of the returning artifacts. Only works when setting "with_tag=true" description: Specify whether the signature is included inside the tags of the returning artifacts. Only works when setting "with_tag=true"
@ -1179,7 +1179,7 @@ paths:
- name: scan_request_type - name: scan_request_type
in: body in: body
required: false required: false
schema: schema:
$ref: '#/definitions/ScanRequestType' $ref: '#/definitions/ScanRequestType'
responses: responses:
'202': '202':
@ -6769,7 +6769,7 @@ definitions:
ScanRequestType: ScanRequestType:
type: object type: object
properties: properties:
scan_type: scan_type:
type: string type: string
description: 'The scan type for the scan request. Two options are currently supported, vulnerability and sbom' description: 'The scan type for the scan request. Two options are currently supported, vulnerability and sbom'
enum: [vulnerability, sbom] enum: [vulnerability, sbom]
@ -6797,12 +6797,12 @@ definitions:
description: 'The status of the generating SBOM task' description: 'The status of the generating SBOM task'
sbom_digest: sbom_digest:
type: string type: string
description: 'The digest of the generated SBOM accessory' description: 'The digest of the generated SBOM accessory'
report_id: report_id:
type: string type: string
description: 'id of the native scan report' description: 'id of the native scan report'
example: '5f62c830-f996-11e9-957f-0242c0a89008' example: '5f62c830-f996-11e9-957f-0242c0a89008'
duration: duration:
type: integer type: integer
format: int64 format: int64
description: 'Time in seconds required to create the report' 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. description: Indicates the capabilities of the scanner, e.g. support_vulnerability or support_sbom.
additionalProperties: True additionalProperties: True
example: {"support_vulnerability": true, "support_sbom": true} example: {"support_vulnerability": true, "support_sbom": true}
ScannerRegistrationReq: ScannerRegistrationReq:
type: object type: object
required: required:
@ -9986,7 +9986,6 @@ definitions:
items: items:
type: string type: string
description: Links of the vulnerability description: Links of the vulnerability
ScanType: ScanType:
type: object type: object
properties: properties:

View File

@ -51,6 +51,7 @@ const (
ResourceRobot = Resource("robot") ResourceRobot = Resource("robot")
ResourceNotificationPolicy = Resource("notification-policy") ResourceNotificationPolicy = Resource("notification-policy")
ResourceScan = Resource("scan") ResourceScan = Resource("scan")
ResourceSBOM = Resource("sbom")
ResourceScanner = Resource("scanner") ResourceScanner = Resource("scanner")
ResourceArtifact = Resource("artifact") ResourceArtifact = Resource("artifact")
ResourceTag = Resource("tag") ResourceTag = Resource("tag")
@ -182,6 +183,10 @@ var (
{Resource: ResourceScan, Action: ActionRead}, {Resource: ResourceScan, Action: ActionRead},
{Resource: ResourceScan, Action: ActionStop}, {Resource: ResourceScan, Action: ActionStop},
{Resource: ResourceSBOM, Action: ActionCreate},
{Resource: ResourceSBOM, Action: ActionStop},
{Resource: ResourceSBOM, Action: ActionRead},
{Resource: ResourceTag, Action: ActionCreate}, {Resource: ResourceTag, Action: ActionCreate},
{Resource: ResourceTag, Action: ActionList}, {Resource: ResourceTag, Action: ActionList},
{Resource: ResourceTag, Action: ActionDelete}, {Resource: ResourceTag, Action: ActionDelete},

View File

@ -86,6 +86,9 @@ var (
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate}, {Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
{Resource: rbac.ResourceScan, Action: rbac.ActionRead}, {Resource: rbac.ResourceScan, Action: rbac.ActionRead},
{Resource: rbac.ResourceScan, Action: rbac.ActionStop}, {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.ActionRead},
{Resource: rbac.ResourceScanner, Action: rbac.ActionCreate}, {Resource: rbac.ResourceScanner, Action: rbac.ActionCreate},
@ -169,6 +172,9 @@ var (
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate}, {Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
{Resource: rbac.ResourceScan, Action: rbac.ActionRead}, {Resource: rbac.ResourceScan, Action: rbac.ActionRead},
{Resource: rbac.ResourceScan, Action: rbac.ActionStop}, {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.ActionRead},
@ -223,6 +229,7 @@ var (
{Resource: rbac.ResourceRobot, Action: rbac.ActionList}, {Resource: rbac.ResourceRobot, Action: rbac.ActionList},
{Resource: rbac.ResourceScan, Action: rbac.ActionRead}, {Resource: rbac.ResourceScan, Action: rbac.ActionRead},
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead}, {Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
@ -267,6 +274,7 @@ var (
{Resource: rbac.ResourceRobot, Action: rbac.ActionList}, {Resource: rbac.ResourceRobot, Action: rbac.ActionList},
{Resource: rbac.ResourceScan, Action: rbac.ActionRead}, {Resource: rbac.ResourceScan, Action: rbac.ActionRead},
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead}, {Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
@ -290,6 +298,7 @@ var (
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead}, {Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
{Resource: rbac.ResourceScan, Action: rbac.ActionRead}, {Resource: rbac.ResourceScan, Action: rbac.ActionRead},
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead}, {Resource: rbac.ResourceScanner, Action: rbac.ActionRead},

View File

@ -29,6 +29,7 @@ import (
"github.com/goharbor/harbor/src/controller/artifact/processor/chart" "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/cnab"
"github.com/goharbor/harbor/src/controller/artifact/processor/image" "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/artifact/processor/wasm"
"github.com/goharbor/harbor/src/controller/event/metadata" "github.com/goharbor/harbor/src/controller/event/metadata"
"github.com/goharbor/harbor/src/controller/tag" "github.com/goharbor/harbor/src/controller/tag"
@ -73,6 +74,7 @@ var (
chart.ArtifactTypeChart: icon.DigestOfIconChart, chart.ArtifactTypeChart: icon.DigestOfIconChart,
cnab.ArtifactTypeCNAB: icon.DigestOfIconCNAB, cnab.ArtifactTypeCNAB: icon.DigestOfIconCNAB,
wasm.ArtifactTypeWASM: icon.DigestOfIconWASM, wasm.ArtifactTypeWASM: icon.DigestOfIconWASM,
sbom.ArtifactTypeSBOM: icon.DigestOfIconAccSBOM,
} }
) )

View File

@ -29,8 +29,8 @@ import (
) )
const ( const (
// processorArtifactTypeSBOM is the artifact type for SBOM, it's scope is only used in the processor // ArtifactTypeSBOM is the artifact type for SBOM, it's scope is only used in the processor
processorArtifactTypeSBOM = "SBOM" ArtifactTypeSBOM = "SBOM"
// processorMediaType is the media type for SBOM, it's scope is only used to register the processor // processorMediaType is the media type for SBOM, it's scope is only used to register the processor
processorMediaType = "application/vnd.goharbor.harbor.sbom.v1" 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 // GetArtifactType the artifact type is used to display the artifact type in the UI
func (m *Processor) GetArtifactType(_ context.Context, _ *artifact.Artifact) string { func (m *Processor) GetArtifactType(_ context.Context, _ *artifact.Artifact) string {
return processorArtifactTypeSBOM return ArtifactTypeSBOM
} }

View File

@ -158,7 +158,7 @@ func (suite *SBOMProcessorTestSuite) TestAbstractAdditionPullManifestError() {
} }
func (suite *SBOMProcessorTestSuite) TestGetArtifactType() { 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) { func TestSBOMProcessorTestSuite(t *testing.T) {

View File

@ -49,8 +49,10 @@ import (
"github.com/goharbor/harbor/src/pkg/scan/postprocessors" "github.com/goharbor/harbor/src/pkg/scan/postprocessors"
"github.com/goharbor/harbor/src/pkg/scan/report" "github.com/goharbor/harbor/src/pkg/scan/report"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" 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/scan/vuln"
"github.com/goharbor/harbor/src/pkg/task" "github.com/goharbor/harbor/src/pkg/task"
"github.com/goharbor/harbor/src/testing/controller/artifact"
) )
var ( var (
@ -108,6 +110,8 @@ type basicController struct {
rc robot.Controller rc robot.Controller
// Tag controller // Tag controller
tagCtl tag.Controller tagCtl tag.Controller
// Artifact controller
artCtl artifact.Controller
// UUID generator // UUID generator
uuid uuidGenerator uuid uuidGenerator
// Configuration getter func // Configuration getter func
@ -259,7 +263,7 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti
launchScanJobParams []*launchScanJobParam launchScanJobParams []*launchScanJobParam
) )
for _, art := range artifacts { for _, art := range artifacts {
reports, err := bc.makeReportPlaceholder(ctx, r, art) reports, err := bc.makeReportPlaceholder(ctx, r, art, opts)
if err != nil { if err != nil {
if errors.IsConflictErr(err) { if errors.IsConflictErr(err) {
errs = append(errs, err) errs = append(errs, err)
@ -326,7 +330,7 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti
for _, launchScanJobParam := range launchScanJobParams { for _, launchScanJobParam := range launchScanJobParams {
launchScanJobParam.ExecutionID = opts.ExecutionID 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) log.G(ctx).Warningf("scan artifact %s@%s failed, error: %v", artifact.RepositoryName, artifact.Digest, err)
errs = append(errs, err) errs = append(errs, err)
} }
@ -546,13 +550,15 @@ func (bc *basicController) startScanAll(ctx context.Context, executionID int64)
return nil return nil
} }
func (bc *basicController) makeReportPlaceholder(ctx context.Context, r *scanner.Registration, art *ar.Artifact) ([]*scan.Report, error) { func (bc *basicController) makeReportPlaceholder(ctx context.Context, r *scanner.Registration, art *ar.Artifact, opts *Options) ([]*scan.Report, error) {
mimeTypes := r.GetProducesMimeTypes(art.ManifestMediaType) mimeTypes := r.GetProducesMimeTypes(art.ManifestMediaType, opts.GetScanType())
oldReports, err := bc.manager.GetBy(bc.cloneCtx(ctx), art.Digest, r.UUID, mimeTypes) oldReports, err := bc.manager.GetBy(bc.cloneCtx(ctx), art.Digest, r.UUID, mimeTypes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := bc.deleteArtifactAccessories(ctx, oldReports); err != nil {
return nil, err
}
if err := bc.assembleReports(ctx, oldReports...); err != nil { if err := bc.assembleReports(ctx, oldReports...); err != nil {
return nil, err return nil, err
@ -574,7 +580,7 @@ func (bc *basicController) makeReportPlaceholder(ctx context.Context, r *scanner
var reports []*scan.Report var reports []*scan.Report
for _, pm := range r.GetProducesMimeTypes(art.ManifestMediaType) { for _, pm := range r.GetProducesMimeTypes(art.ManifestMediaType, opts.GetScanType()) {
report := &scan.Report{ report := &scan.Report{
Digest: art.Digest, Digest: art.Digest,
RegistrationUUID: r.UUID, RegistrationUUID: r.UUID,
@ -991,7 +997,7 @@ func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64
} }
// launchScanJob launches a job to run scan // 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 // don't launch scan job for the artifact which is not supported by the scanner
if !hasCapability(param.Registration, param.Artifact) { if !hasCapability(param.Registration, param.Artifact) {
return nil return nil
@ -1032,6 +1038,11 @@ func (bc *basicController) launchScanJob(ctx context.Context, param *launchScanJ
MimeType: param.Artifact.ManifestMediaType, MimeType: param.Artifact.ManifestMediaType,
Size: param.Artifact.Size, Size: param.Artifact.Size,
}, },
RequestType: []*v1.ScanType{
{
Type: opts.GetScanType(),
},
},
} }
rJSON, err := param.Registration.ToJSON() rJSON, err := param.Registration.ToJSON()
@ -1265,3 +1276,48 @@ func parseOptions(options ...Option) (*Options, error) {
return ops, nil 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
}

View File

@ -108,6 +108,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
Version: "0.1.0", Version: "0.1.0",
}, },
Capabilities: []*v1.ScannerCapability{{ Capabilities: []*v1.ScannerCapability{{
Type: v1.ScanTypeVulnerability,
ConsumesMimeTypes: []string{ ConsumesMimeTypes: []string{
v1.MimeTypeOCIArtifact, v1.MimeTypeOCIArtifact,
v1.MimeTypeDockerArtifact, v1.MimeTypeDockerArtifact,
@ -115,7 +116,17 @@ func (suite *ControllerTestSuite) SetupSuite() {
ProducesMimeTypes: []string{ ProducesMimeTypes: []string{
v1.MimeTypeNativeReport, v1.MimeTypeNativeReport,
}, },
}}, },
{
Type: v1.ScanTypeSbom,
ConsumesMimeTypes: []string{
v1.MimeTypeOCIArtifact,
},
ProducesMimeTypes: []string{
v1.MimeTypeSBOMReport,
},
},
},
Properties: v1.ScannerProperties{ Properties: v1.ScannerProperties{
"extra": "testing", "extra": "testing",
}, },
@ -655,3 +666,22 @@ func TestIsSBOMMimeTypes(t *testing.T) {
// Test with an empty slice // Test with an empty slice
assert.False(t, isSBOMMimeTypes([]string{})) 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))
}

View File

@ -79,7 +79,11 @@ func (bc *basicController) ListRegistrations(ctx context.Context, query *q.Query
if err != nil { if err != nil {
return nil, errors.Wrap(err, "api controller: list registrations") 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 return l, nil
} }
@ -122,10 +126,25 @@ func (bc *basicController) GetRegistration(ctx context.Context, registrationUUID
if err != nil { if err != nil {
return nil, errors.Wrap(err, "api controller: get registration") 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 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 ... // RegistrationExists ...
func (bc *basicController) RegistrationExists(ctx context.Context, registrationUUID string) bool { func (bc *basicController) RegistrationExists(ctx context.Context, registrationUUID string) bool {
registration, err := bc.manager.Get(ctx, registrationUUID) registration, err := bc.manager.Get(ctx, registrationUUID)

View File

@ -70,6 +70,7 @@ import (
"github.com/goharbor/harbor/src/pkg/oidc" "github.com/goharbor/harbor/src/pkg/oidc"
"github.com/goharbor/harbor/src/pkg/scan" "github.com/goharbor/harbor/src/pkg/scan"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner" "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" _ "github.com/goharbor/harbor/src/pkg/scan/vulnerability"
pkguser "github.com/goharbor/harbor/src/pkg/user" pkguser "github.com/goharbor/harbor/src/pkg/user"
"github.com/goharbor/harbor/src/pkg/version" "github.com/goharbor/harbor/src/pkg/version"

View File

@ -36,6 +36,7 @@ import (
_ "github.com/goharbor/harbor/src/pkg/accessory/model/subject" _ "github.com/goharbor/harbor/src/pkg/accessory/model/subject"
_ "github.com/goharbor/harbor/src/pkg/config/inmemory" _ "github.com/goharbor/harbor/src/pkg/config/inmemory"
_ "github.com/goharbor/harbor/src/pkg/config/rest" _ "github.com/goharbor/harbor/src/pkg/config/rest"
_ "github.com/goharbor/harbor/src/pkg/scan/sbom"
_ "github.com/goharbor/harbor/src/pkg/scan/vulnerability" _ "github.com/goharbor/harbor/src/pkg/scan/vulnerability"
) )

View File

@ -66,8 +66,9 @@ type Registration struct {
Metadata *v1.ScannerAdapterMetadata `orm:"-" json:"-"` Metadata *v1.ScannerAdapterMetadata `orm:"-" json:"-"`
// Timestamps // Timestamps
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_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"` 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 // TableName for Endpoint
@ -151,15 +152,20 @@ func (r *Registration) HasCapability(manifestMimeType string) bool {
} }
// GetProducesMimeTypes returns produces mime types for the artifact // 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 { if r.Metadata == nil {
return nil return nil
} }
for _, capability := range r.Metadata.Capabilities { for _, capability := range r.Metadata.Capabilities {
for _, mt := range capability.ConsumesMimeTypes { capType := capability.Type
if mt == mimeType { if len(capType) == 0 {
return capability.ProducesMimeTypes capType = v1.ScanTypeVulnerability
}
if scanType == capType {
for _, mt := range capability.ConsumesMimeTypes {
if mt == mimeType {
return capability.ProducesMimeTypes
}
} }
} }
} }

View File

@ -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 // Handler handler for scan job, it could be implement by different scan type, such as vulnerability, sbom
type Handler interface { type Handler interface {
// RequestProducesMineTypes returns the produces mime types
RequestProducesMineTypes() []string
// RequiredPermissions defines the permission used by the scan robot account
RequiredPermissions() []*types.Policy 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 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) PostScan(ctx job.Context, sr *v1.ScanRequest, rp *scan.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error)
} }

View File

@ -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")) 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 { if err != nil {
// Not ready yet // Not ready yet
if notReadyErr, ok := err.(*v1.ReportNotReadyError); ok { if notReadyErr, ok := err.(*v1.ReportNotReadyError); ok {
@ -332,13 +338,13 @@ func getReportPlaceholder(ctx context.Context, digest string, reportUUID string,
return reports[0], nil return reports[0], nil
} }
func fetchScanReportFromScanner(client v1.Client, requestID string, m string) (rawReport string, err error) { func fetchScanReportFromScanner(client v1.Client, requestID string, mimType string, urlParameter string) (rawReport string, err error) {
rawReport, err = client.GetScanReport(requestID, m) rawReport, err = client.GetScanReport(requestID, mimType, urlParameter)
if err != nil { if err != nil {
return "", err return "", err
} }
// Make sure the data is aligned with the v1 spec. // 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 "", err
} }
return rawReport, nil return rawReport, nil
@ -367,7 +373,20 @@ func ExtractScanReq(params job.Parameters) (*v1.ScanRequest, error) {
if err := req.Validate(); err != nil { if err := req.Validate(); err != nil {
return nil, err 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 return req, nil
} }

View File

@ -211,8 +211,9 @@ func (suite *JobTestSuite) TestfetchScanReportFromScanner() {
suite.reportIDs = append(suite.reportIDs, rptID) suite.reportIDs = append(suite.reportIDs, rptID)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
client := &v1testing.Client{} client := &v1testing.Client{}
client.On("GetScanReport", mock.Anything, v1.MimeTypeGenericVulnerabilityReport).Return(rawContent, nil) client.On("GetScanReport", mock.Anything, v1.MimeTypeGenericVulnerabilityReport, mock.Anything).Return(rawContent, nil)
rawRept, err := fetchScanReportFromScanner(client, "abc", v1.MimeTypeGenericVulnerabilityReport) parameters := "sbom_media_type=application/spdx+json"
rawRept, err := fetchScanReportFromScanner(client, "abc", v1.MimeTypeGenericVulnerabilityReport, parameters)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
require.Equal(suite.T(), rawContent, rawRept) require.Equal(suite.T(), rawContent, rawRept)
} }

View File

@ -68,7 +68,7 @@ type Client interface {
// Returns: // Returns:
// string : the scan report of the given artifact // string : the scan report of the given artifact
// error : non nil error if any errors occurred // 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 // basicClient is default implementation of the Client interface
@ -97,7 +97,7 @@ func NewClient(url, authType, accessCredential string, skipCertVerify bool) (Cli
httpClient: &http.Client{ httpClient: &http.Client{
Timeout: time.Second * 5, Timeout: time.Second * 5,
Transport: transport, Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
}, },
}, },
@ -167,7 +167,7 @@ func (c *basicClient) SubmitScan(req *ScanRequest) (*ScanResponse, error) {
} }
// GetScanReport ... // 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 { if len(scanRequestID) == 0 {
return "", errors.New("empty scan request ID") 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) def := c.spec.GetScanReport(scanRequestID, reportMIMEType)
reportURL := def.URL
req, err := http.NewRequest(http.MethodGet, def.URL, nil) if len(urlParameter) > 0 {
reportURL = fmt.Sprintf("%s?%s", def.URL, urlParameter)
}
req, err := http.NewRequest(http.MethodGet, reportURL, nil)
if err != nil { if err != nil {
return "", errors.Wrap(err, "v1 client: get scan report") return "", errors.Wrap(err, "v1 client: get scan report")
} }

View File

@ -72,7 +72,7 @@ func (suite *ClientTestSuite) TestClientSubmitScan() {
// TestClientGetScanReportError tests getting report failed // TestClientGetScanReportError tests getting report failed
func (suite *ClientTestSuite) TestClientGetScanReportError() { func (suite *ClientTestSuite) TestClientGetScanReportError() {
_, err := suite.client.GetScanReport("id1", MimeTypeNativeReport) _, err := suite.client.GetScanReport("id1", MimeTypeNativeReport, "")
require.Error(suite.T(), err) require.Error(suite.T(), err)
assert.Condition(suite.T(), func() (success bool) { assert.Condition(suite.T(), func() (success bool) {
success = strings.Index(err.Error(), "error") != -1 success = strings.Index(err.Error(), "error") != -1
@ -82,14 +82,14 @@ func (suite *ClientTestSuite) TestClientGetScanReportError() {
// TestClientGetScanReport tests getting report // TestClientGetScanReport tests getting report
func (suite *ClientTestSuite) TestClientGetScanReport() { func (suite *ClientTestSuite) TestClientGetScanReport() {
res, err := suite.client.GetScanReport("id2", MimeTypeNativeReport) res, err := suite.client.GetScanReport("id2", MimeTypeNativeReport, "")
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
require.NotEmpty(suite.T(), res) require.NotEmpty(suite.T(), res)
} }
// TestClientGetScanReportNotReady tests the case that the report is not ready // TestClientGetScanReportNotReady tests the case that the report is not ready
func (suite *ClientTestSuite) TestClientGetScanReportNotReady() { func (suite *ClientTestSuite) TestClientGetScanReportNotReady() {
_, err := suite.client.GetScanReport("id3", MimeTypeNativeReport) _, err := suite.client.GetScanReport("id3", MimeTypeNativeReport, "")
require.Error(suite.T(), err) require.Error(suite.T(), err)
require.Condition(suite.T(), func() (success bool) { require.Condition(suite.T(), func() (success bool) {
_, success = err.(*ReportNotReadyError) _, success = err.(*ReportNotReadyError)

View File

@ -21,6 +21,11 @@ import (
"github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/errors"
) )
const (
supportVulnerability = "support_vulnerability"
supportSBOM = "support_sbom"
)
var supportedMimeTypes = []string{ var supportedMimeTypes = []string{
MimeTypeNativeReport, MimeTypeNativeReport,
MimeTypeGenericVulnerabilityReport, MimeTypeGenericVulnerabilityReport,
@ -153,6 +158,20 @@ func (md *ScannerAdapterMetadata) GetCapability(mimeType string) *ScannerCapabil
return nil 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. // Artifact represents an artifact stored in Registry.
type Artifact struct { type Artifact struct {
// ID of the namespace (project). It will not be sent to scanner adapter. // ID of the namespace (project). It will not be sent to scanner adapter.

View File

@ -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 "", ""
}

162
src/pkg/scan/sbom/sbom.go Normal file
View File

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

View File

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

View File

@ -35,6 +35,16 @@ func init() {
type ScanHandler struct { 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 // RequiredPermissions defines the permission used by the scan robot account
func (v *ScanHandler) RequiredPermissions() []*types.Policy { func (v *ScanHandler) RequiredPermissions() []*types.Policy {
return []*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 ... // PostScan ...
func (v *ScanHandler) PostScan(ctx job.Context, _ *v1.ScanRequest, origRp *scan.Report, rawReport string, _ time.Time, _ *model.Robot) (string, error) { 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 // use a new ormer here to use the short db connection

View File

@ -1,6 +1,7 @@
package vulnerability package vulnerability
import ( import (
"fmt"
"testing" "testing"
"time" "time"
@ -50,3 +51,66 @@ func TestPostScan(t *testing.T) {
assert.Equal(t, "", refreshedReport, "PostScan should return the refreshed report") assert.Equal(t, "", refreshedReport, "PostScan should return the refreshed report")
assert.Nil(t, err, "PostScan should not return an error") 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()")
})
}
}

View File

@ -52,6 +52,7 @@ func (s *ScannerRegistration) ToSwagger(_ context.Context) *models.ScannerRegist
Vendor: s.Vendor, Vendor: s.Vendor,
Version: s.Version, Version: s.Version,
Health: s.Health, Health: s.Health,
Capabilities: s.Capabilities,
} }
} }

View File

@ -594,7 +594,13 @@ func (a *projectAPI) GetScannerOfProject(ctx context.Context, params operation.G
if err != nil { if err != nil {
return a.SendError(ctx, err) 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)) return operation.NewGetScannerOfProjectOK().WithPayload(model.NewScannerRegistration(scanner).ToSwagger(ctx))
} }

View File

@ -22,6 +22,7 @@ import (
"github.com/goharbor/harbor/src/pkg/project/models" "github.com/goharbor/harbor/src/pkg/project/models"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner" "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" "github.com/goharbor/harbor/src/server/v2.0/restapi"
projecttesting "github.com/goharbor/harbor/src/testing/controller/project" projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
scannertesting "github.com/goharbor/harbor/src/testing/controller/scanner" scannertesting "github.com/goharbor/harbor/src/testing/controller/scanner"
@ -36,6 +37,7 @@ type ProjectTestSuite struct {
scannerCtl *scannertesting.Controller scannerCtl *scannertesting.Controller
project *models.Project project *models.Project
reg *scanner.Registration reg *scanner.Registration
metadata *v1.ScannerAdapterMetadata
} }
func (suite *ProjectTestSuite) SetupSuite() { func (suite *ProjectTestSuite) SetupSuite() {
@ -59,7 +61,12 @@ func (suite *ProjectTestSuite) SetupSuite() {
scannerCtl: suite.scannerCtl, 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() suite.Suite.SetupSuite()
} }
@ -81,7 +88,7 @@ func (suite *ProjectTestSuite) TestGetScannerOfProject() {
// scanner not found // scanner not found
mock.OnAnything(suite.projectCtl, "Get").Return(suite.project, nil).Once() mock.OnAnything(suite.projectCtl, "Get").Return(suite.project, nil).Once()
mock.OnAnything(suite.scannerCtl, "GetRegistrationByProject").Return(nil, 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") res, err := suite.Get("/projects/1/scanner")
suite.NoError(err) suite.NoError(err)
suite.Equal(200, res.StatusCode) 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.projectCtl, "Get").Return(suite.project, nil).Once()
mock.OnAnything(suite.scannerCtl, "GetRegistrationByProject").Return(suite.reg, 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 var scanner scanner.Registration
res, err := suite.GetJSON("/projects/1/scanner", &scanner) res, err := suite.GetJSON("/projects/1/scanner", &scanner)
suite.NoError(err) suite.NoError(err)
@ -101,6 +108,7 @@ func (suite *ProjectTestSuite) TestGetScannerOfProject() {
{ {
mock.OnAnything(projectCtlMock, "GetByName").Return(suite.project, nil).Once() mock.OnAnything(projectCtlMock, "GetByName").Return(suite.project, nil).Once()
mock.OnAnything(suite.projectCtl, "Get").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() mock.OnAnything(suite.scannerCtl, "GetRegistrationByProject").Return(suite.reg, nil).Once()
var scanner scanner.Registration var scanner scanner.Registration

View File

@ -25,6 +25,7 @@ import (
"github.com/goharbor/harbor/src/controller/scan" "github.com/goharbor/harbor/src/controller/scan"
"github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/distribution" "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" 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 { 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) 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 { 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 { scanType := v1.ScanTypeVulnerability
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)
}
options := []scan.Option{} options := []scan.Option{}
if !distribution.IsDigest(params.Reference) { if !distribution.IsDigest(params.Reference) {
options = append(options, scan.WithTag(params.Reference)) options = append(options, scan.WithTag(params.Reference))
} }
if params.ScanRequestType != nil && validScanType(params.ScanRequestType.ScanType) { 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 { if err := s.scanCtl.Scan(ctx, artifact, options...); err != nil {

View File

@ -42,9 +42,9 @@ func (_m *Client) GetMetadata() (*v1.ScannerAdapterMetadata, error) {
return r0, r1 return r0, r1
} }
// GetScanReport provides a mock function with given fields: scanRequestID, reportMIMEType // GetScanReport provides a mock function with given fields: scanRequestID, reportMIMEType, urlParameter
func (_m *Client) GetScanReport(scanRequestID string, reportMIMEType string) (string, error) { func (_m *Client) GetScanReport(scanRequestID string, reportMIMEType string, urlParameter string) (string, error) {
ret := _m.Called(scanRequestID, reportMIMEType) ret := _m.Called(scanRequestID, reportMIMEType, urlParameter)
if len(ret) == 0 { if len(ret) == 0 {
panic("no return value specified for GetScanReport") panic("no return value specified for GetScanReport")
@ -52,17 +52,17 @@ func (_m *Client) GetScanReport(scanRequestID string, reportMIMEType string) (st
var r0 string var r0 string
var r1 error var r1 error
if rf, ok := ret.Get(0).(func(string, string) (string, error)); ok { if rf, ok := ret.Get(0).(func(string, string, string) (string, error)); ok {
return rf(scanRequestID, reportMIMEType) return rf(scanRequestID, reportMIMEType, urlParameter)
} }
if rf, ok := ret.Get(0).(func(string, string) string); ok { if rf, ok := ret.Get(0).(func(string, string, string) string); ok {
r0 = rf(scanRequestID, reportMIMEType) r0 = rf(scanRequestID, reportMIMEType, urlParameter)
} else { } else {
r0 = ret.Get(0).(string) r0 = ret.Get(0).(string)
} }
if rf, ok := ret.Get(1).(func(string, string) error); ok { if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(scanRequestID, reportMIMEType) r1 = rf(scanRequestID, reportMIMEType, urlParameter)
} else { } else {
r1 = ret.Error(1) r1 = ret.Error(1)
} }