mirror of
https://github.com/goharbor/harbor.git
synced 2024-09-27 21:12:42 +02:00
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:
parent
67c03ddc4f
commit
654aa8edcf
@ -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:
|
||||||
|
@ -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},
|
||||||
|
@ -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},
|
||||||
|
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
43
src/pkg/scan/sbom/model/summary.go
Normal file
43
src/pkg/scan/sbom/model/summary.go
Normal 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
162
src/pkg/scan/sbom/sbom.go
Normal 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
|
||||||
|
}
|
139
src/pkg/scan/sbom/sbom_test.go
Normal file
139
src/pkg/scan/sbom/sbom_test.go
Normal 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{})
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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()")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user