Skip to content

Commit

Permalink
Add SBOM scan feature
Browse files Browse the repository at this point in the history
  Add scan handler for sbom
  Delete previous sbom accessory before the job service

Signed-off-by: stonezdj <daojunz@vmware.com>
  • Loading branch information
stonezdj authored and stonezdj committed Apr 15, 2024
1 parent 7465a29 commit 30fc819
Show file tree
Hide file tree
Showing 25 changed files with 609 additions and 48 deletions.
15 changes: 7 additions & 8 deletions api/v2.0/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1179,7 +1179,7 @@ paths:
- name: scan_request_type
in: body
required: false
schema:
schema:
$ref: '#/definitions/ScanRequestType'
responses:
'202':
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -9986,7 +9986,6 @@ definitions:
items:
type: string
description: Links of the vulnerability

ScanType:
type: object
properties:
Expand Down
4 changes: 4 additions & 0 deletions src/common/rbac/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -182,6 +183,9 @@ var (
{Resource: ResourceScan, Action: ActionRead},
{Resource: ResourceScan, Action: ActionStop},

{Resource: ResourceSBOM, Action: ActionCreate},
{Resource: ResourceSBOM, Action: ActionRead},

{Resource: ResourceTag, Action: ActionCreate},
{Resource: ResourceTag, Action: ActionList},
{Resource: ResourceTag, Action: ActionDelete},
Expand Down
6 changes: 6 additions & 0 deletions src/common/rbac/project/rbac_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ 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.ActionRead},

{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
{Resource: rbac.ResourceScanner, Action: rbac.ActionCreate},
Expand Down Expand Up @@ -169,6 +171,7 @@ var (
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
{Resource: rbac.ResourceScan, Action: rbac.ActionStop},
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},

{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},

Expand Down Expand Up @@ -223,6 +226,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},

Expand Down Expand Up @@ -267,6 +271,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},

Expand All @@ -290,6 +295,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},

Expand Down
2 changes: 2 additions & 0 deletions src/controller/artifact/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -73,6 +74,7 @@ var (
chart.ArtifactTypeChart: icon.DigestOfIconChart,
cnab.ArtifactTypeCNAB: icon.DigestOfIconCNAB,
wasm.ArtifactTypeWASM: icon.DigestOfIconWASM,
sbom.ArtifactTypeSBOM: icon.DigestOfIconAccSBOM,
}
)

Expand Down
6 changes: 3 additions & 3 deletions src/controller/artifact/processor/sbom/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion src/controller/artifact/processor/sbom/sbom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
66 changes: 59 additions & 7 deletions src/controller/scan/base_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -108,6 +110,7 @@ type basicController struct {
rc robot.Controller
// Tag controller
tagCtl tag.Controller
artCtl artifact.Controller
// UUID generator
uuid uuidGenerator
// Configuration getter func
Expand Down Expand Up @@ -259,7 +262,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)
Expand Down Expand Up @@ -326,7 +329,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)
}
Expand Down Expand Up @@ -546,13 +549,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.ScanType)
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
Expand All @@ -574,7 +579,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,
Expand Down Expand Up @@ -991,7 +996,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
Expand Down Expand Up @@ -1032,6 +1037,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()
Expand Down Expand Up @@ -1265,3 +1275,45 @@ func parseOptions(options ...Option) (*Options, error) {

return ops, nil
}

// deleteArtifactAccessories delete the accessory in reports
func (bc *basicController) deleteArtifactAccessories(ctx context.Context, reports []*scan.Report) error {
for _, rpt := range reports {
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.SBOMAccessory()
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
}
19 changes: 19 additions & 0 deletions src/controller/scan/base_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,3 +655,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))

}
32 changes: 30 additions & 2 deletions src/controller/scanner/base_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -122,10 +126,34 @@ 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
}
capabilities := map[string]interface{}{}
for _, c := range mt.Capabilities {
if c.Type == v1.ScanTypeVulnerability {
capabilities["support_vulnerability"] = true
}
if c.Type == v1.ScanTypeSbom {
capabilities["support_sbom"] = true
}
}
r.Capabilities = capabilities
return nil
}

// RegistrationExists ...
func (bc *basicController) RegistrationExists(ctx context.Context, registrationUUID string) bool {
registration, err := bc.manager.Get(ctx, registrationUUID)
Expand Down
1 change: 1 addition & 0 deletions src/core/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
Expand Down Expand Up @@ -817,6 +818,7 @@ golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
Expand All @@ -825,6 +827,7 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
Loading

0 comments on commit 30fc819

Please sign in to comment.