mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-15 23:05:57 +01: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
@ -996,7 +996,7 @@ paths:
|
||||
description: Specify whether the SBOM overview is included in returning artifacts, when this option is true, the SBOM overview will be included in the response
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
default: false
|
||||
- name: with_signature
|
||||
in: query
|
||||
description: Specify whether the signature is included inside the tags of the returning artifacts. Only works when setting "with_tag=true"
|
||||
@ -1179,7 +1179,7 @@ paths:
|
||||
- name: scan_request_type
|
||||
in: body
|
||||
required: false
|
||||
schema:
|
||||
schema:
|
||||
$ref: '#/definitions/ScanRequestType'
|
||||
responses:
|
||||
'202':
|
||||
@ -6769,7 +6769,7 @@ definitions:
|
||||
ScanRequestType:
|
||||
type: object
|
||||
properties:
|
||||
scan_type:
|
||||
scan_type:
|
||||
type: string
|
||||
description: 'The scan type for the scan request. Two options are currently supported, vulnerability and sbom'
|
||||
enum: [vulnerability, sbom]
|
||||
@ -6797,12 +6797,12 @@ definitions:
|
||||
description: 'The status of the generating SBOM task'
|
||||
sbom_digest:
|
||||
type: string
|
||||
description: 'The digest of the generated SBOM accessory'
|
||||
description: 'The digest of the generated SBOM accessory'
|
||||
report_id:
|
||||
type: string
|
||||
description: 'id of the native scan report'
|
||||
example: '5f62c830-f996-11e9-957f-0242c0a89008'
|
||||
duration:
|
||||
example: '5f62c830-f996-11e9-957f-0242c0a89008'
|
||||
duration:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 'Time in seconds required to create the report'
|
||||
@ -8437,7 +8437,7 @@ definitions:
|
||||
description: Indicates the capabilities of the scanner, e.g. support_vulnerability or support_sbom.
|
||||
additionalProperties: True
|
||||
example: {"support_vulnerability": true, "support_sbom": true}
|
||||
|
||||
|
||||
ScannerRegistrationReq:
|
||||
type: object
|
||||
required:
|
||||
@ -9986,7 +9986,6 @@ definitions:
|
||||
items:
|
||||
type: string
|
||||
description: Links of the vulnerability
|
||||
|
||||
ScanType:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -51,6 +51,7 @@ const (
|
||||
ResourceRobot = Resource("robot")
|
||||
ResourceNotificationPolicy = Resource("notification-policy")
|
||||
ResourceScan = Resource("scan")
|
||||
ResourceSBOM = Resource("sbom")
|
||||
ResourceScanner = Resource("scanner")
|
||||
ResourceArtifact = Resource("artifact")
|
||||
ResourceTag = Resource("tag")
|
||||
@ -182,6 +183,10 @@ var (
|
||||
{Resource: ResourceScan, Action: ActionRead},
|
||||
{Resource: ResourceScan, Action: ActionStop},
|
||||
|
||||
{Resource: ResourceSBOM, Action: ActionCreate},
|
||||
{Resource: ResourceSBOM, Action: ActionStop},
|
||||
{Resource: ResourceSBOM, Action: ActionRead},
|
||||
|
||||
{Resource: ResourceTag, Action: ActionCreate},
|
||||
{Resource: ResourceTag, Action: ActionList},
|
||||
{Resource: ResourceTag, Action: ActionDelete},
|
||||
|
@ -86,6 +86,9 @@ var (
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionStop},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionStop},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionCreate},
|
||||
@ -169,6 +172,9 @@ var (
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionStop},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionStop},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
||||
|
||||
@ -223,6 +229,7 @@ var (
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
||||
|
||||
@ -267,6 +274,7 @@ var (
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
||||
|
||||
@ -290,6 +298,7 @@ var (
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceScan, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceSBOM, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceScanner, Action: rbac.ActionRead},
|
||||
|
||||
|
@ -29,6 +29,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/controller/artifact/processor/chart"
|
||||
"github.com/goharbor/harbor/src/controller/artifact/processor/cnab"
|
||||
"github.com/goharbor/harbor/src/controller/artifact/processor/image"
|
||||
"github.com/goharbor/harbor/src/controller/artifact/processor/sbom"
|
||||
"github.com/goharbor/harbor/src/controller/artifact/processor/wasm"
|
||||
"github.com/goharbor/harbor/src/controller/event/metadata"
|
||||
"github.com/goharbor/harbor/src/controller/tag"
|
||||
@ -73,6 +74,7 @@ var (
|
||||
chart.ArtifactTypeChart: icon.DigestOfIconChart,
|
||||
cnab.ArtifactTypeCNAB: icon.DigestOfIconCNAB,
|
||||
wasm.ArtifactTypeWASM: icon.DigestOfIconWASM,
|
||||
sbom.ArtifactTypeSBOM: icon.DigestOfIconAccSBOM,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -29,8 +29,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// processorArtifactTypeSBOM is the artifact type for SBOM, it's scope is only used in the processor
|
||||
processorArtifactTypeSBOM = "SBOM"
|
||||
// ArtifactTypeSBOM is the artifact type for SBOM, it's scope is only used in the processor
|
||||
ArtifactTypeSBOM = "SBOM"
|
||||
// processorMediaType is the media type for SBOM, it's scope is only used to register the processor
|
||||
processorMediaType = "application/vnd.goharbor.harbor.sbom.v1"
|
||||
)
|
||||
@ -85,5 +85,5 @@ func (m *Processor) AbstractAddition(_ context.Context, art *artifact.Artifact,
|
||||
|
||||
// GetArtifactType the artifact type is used to display the artifact type in the UI
|
||||
func (m *Processor) GetArtifactType(_ context.Context, _ *artifact.Artifact) string {
|
||||
return processorArtifactTypeSBOM
|
||||
return ArtifactTypeSBOM
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -49,8 +49,10 @@ import (
|
||||
"github.com/goharbor/harbor/src/pkg/scan/postprocessors"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
sbomModel "github.com/goharbor/harbor/src/pkg/scan/sbom/model"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
||||
"github.com/goharbor/harbor/src/pkg/task"
|
||||
"github.com/goharbor/harbor/src/testing/controller/artifact"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -108,6 +110,8 @@ type basicController struct {
|
||||
rc robot.Controller
|
||||
// Tag controller
|
||||
tagCtl tag.Controller
|
||||
// Artifact controller
|
||||
artCtl artifact.Controller
|
||||
// UUID generator
|
||||
uuid uuidGenerator
|
||||
// Configuration getter func
|
||||
@ -259,7 +263,7 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti
|
||||
launchScanJobParams []*launchScanJobParam
|
||||
)
|
||||
for _, art := range artifacts {
|
||||
reports, err := bc.makeReportPlaceholder(ctx, r, art)
|
||||
reports, err := bc.makeReportPlaceholder(ctx, r, art, opts)
|
||||
if err != nil {
|
||||
if errors.IsConflictErr(err) {
|
||||
errs = append(errs, err)
|
||||
@ -326,7 +330,7 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti
|
||||
for _, launchScanJobParam := range launchScanJobParams {
|
||||
launchScanJobParam.ExecutionID = opts.ExecutionID
|
||||
|
||||
if err := bc.launchScanJob(ctx, launchScanJobParam); err != nil {
|
||||
if err := bc.launchScanJob(ctx, launchScanJobParam, opts); err != nil {
|
||||
log.G(ctx).Warningf("scan artifact %s@%s failed, error: %v", artifact.RepositoryName, artifact.Digest, err)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
@ -546,13 +550,15 @@ func (bc *basicController) startScanAll(ctx context.Context, executionID int64)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bc *basicController) makeReportPlaceholder(ctx context.Context, r *scanner.Registration, art *ar.Artifact) ([]*scan.Report, error) {
|
||||
mimeTypes := r.GetProducesMimeTypes(art.ManifestMediaType)
|
||||
|
||||
func (bc *basicController) makeReportPlaceholder(ctx context.Context, r *scanner.Registration, art *ar.Artifact, opts *Options) ([]*scan.Report, error) {
|
||||
mimeTypes := r.GetProducesMimeTypes(art.ManifestMediaType, opts.GetScanType())
|
||||
oldReports, err := bc.manager.GetBy(bc.cloneCtx(ctx), art.Digest, r.UUID, mimeTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := bc.deleteArtifactAccessories(ctx, oldReports); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := bc.assembleReports(ctx, oldReports...); err != nil {
|
||||
return nil, err
|
||||
@ -574,7 +580,7 @@ func (bc *basicController) makeReportPlaceholder(ctx context.Context, r *scanner
|
||||
|
||||
var reports []*scan.Report
|
||||
|
||||
for _, pm := range r.GetProducesMimeTypes(art.ManifestMediaType) {
|
||||
for _, pm := range r.GetProducesMimeTypes(art.ManifestMediaType, opts.GetScanType()) {
|
||||
report := &scan.Report{
|
||||
Digest: art.Digest,
|
||||
RegistrationUUID: r.UUID,
|
||||
@ -991,7 +997,7 @@ func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64
|
||||
}
|
||||
|
||||
// launchScanJob launches a job to run scan
|
||||
func (bc *basicController) launchScanJob(ctx context.Context, param *launchScanJobParam) error {
|
||||
func (bc *basicController) launchScanJob(ctx context.Context, param *launchScanJobParam, opts *Options) error {
|
||||
// don't launch scan job for the artifact which is not supported by the scanner
|
||||
if !hasCapability(param.Registration, param.Artifact) {
|
||||
return nil
|
||||
@ -1032,6 +1038,11 @@ func (bc *basicController) launchScanJob(ctx context.Context, param *launchScanJ
|
||||
MimeType: param.Artifact.ManifestMediaType,
|
||||
Size: param.Artifact.Size,
|
||||
},
|
||||
RequestType: []*v1.ScanType{
|
||||
{
|
||||
Type: opts.GetScanType(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rJSON, err := param.Registration.ToJSON()
|
||||
@ -1265,3 +1276,48 @@ func parseOptions(options ...Option) (*Options, error) {
|
||||
|
||||
return ops, nil
|
||||
}
|
||||
|
||||
// deleteArtifactAccessories delete the accessory in reports, only delete sbom accessory
|
||||
func (bc *basicController) deleteArtifactAccessories(ctx context.Context, reports []*scan.Report) error {
|
||||
for _, rpt := range reports {
|
||||
if rpt.MimeType != v1.MimeTypeSBOMReport {
|
||||
continue
|
||||
}
|
||||
if err := bc.deleteArtifactAccessory(ctx, rpt.Report); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteArtifactAccessory check if current report has accessory info, if there is, delete it
|
||||
func (bc *basicController) deleteArtifactAccessory(ctx context.Context, report string) error {
|
||||
if len(report) == 0 {
|
||||
return nil
|
||||
}
|
||||
sbomSummary := sbomModel.Summary{}
|
||||
if err := json.Unmarshal([]byte(report), &sbomSummary); err != nil {
|
||||
// it could be a non sbom report, just skip
|
||||
log.Debugf("fail to unmarshal %v, skip to delete sbom report", err)
|
||||
return nil
|
||||
}
|
||||
repo, dgst := sbomSummary.SBOMAccArt()
|
||||
if len(repo) == 0 || len(dgst) == 0 {
|
||||
return nil
|
||||
}
|
||||
art, err := bc.ar.GetByReference(ctx, repo, dgst, nil)
|
||||
if err != nil {
|
||||
if errors.IsNotFoundErr(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if art == nil {
|
||||
return nil
|
||||
}
|
||||
err = bc.ar.Delete(ctx, art.ID)
|
||||
if errors.IsNotFoundErr(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -108,6 +108,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
||||
Version: "0.1.0",
|
||||
},
|
||||
Capabilities: []*v1.ScannerCapability{{
|
||||
Type: v1.ScanTypeVulnerability,
|
||||
ConsumesMimeTypes: []string{
|
||||
v1.MimeTypeOCIArtifact,
|
||||
v1.MimeTypeDockerArtifact,
|
||||
@ -115,7 +116,17 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
||||
ProducesMimeTypes: []string{
|
||||
v1.MimeTypeNativeReport,
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
Type: v1.ScanTypeSbom,
|
||||
ConsumesMimeTypes: []string{
|
||||
v1.MimeTypeOCIArtifact,
|
||||
},
|
||||
ProducesMimeTypes: []string{
|
||||
v1.MimeTypeSBOMReport,
|
||||
},
|
||||
},
|
||||
},
|
||||
Properties: v1.ScannerProperties{
|
||||
"extra": "testing",
|
||||
},
|
||||
@ -655,3 +666,22 @@ func TestIsSBOMMimeTypes(t *testing.T) {
|
||||
// Test with an empty slice
|
||||
assert.False(t, isSBOMMimeTypes([]string{}))
|
||||
}
|
||||
|
||||
func (suite *ControllerTestSuite) TestDeleteArtifactAccessories() {
|
||||
// artifact not provided
|
||||
suite.Nil(suite.c.deleteArtifactAccessories(context.TODO(), nil))
|
||||
|
||||
// artifact is provided
|
||||
art := &artifact.Artifact{Artifact: art.Artifact{ID: 1, ProjectID: 1, RepositoryName: "library/photon"}}
|
||||
mock.OnAnything(suite.ar, "GetByReference").Return(art, nil).Once()
|
||||
mock.OnAnything(suite.ar, "Delete").Return(nil).Once()
|
||||
reportContent := `{"sbom_digest":"sha256:12345", "scan_status":"Success", "duration":3, "sbom_repository":"library/photon"}`
|
||||
emptyReportContent := ``
|
||||
reports := []*scan.Report{
|
||||
{Report: reportContent},
|
||||
{Report: emptyReportContent},
|
||||
}
|
||||
ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{})
|
||||
suite.NoError(suite.c.deleteArtifactAccessories(ctx, reports))
|
||||
|
||||
}
|
||||
|
@ -79,7 +79,11 @@ func (bc *basicController) ListRegistrations(ctx context.Context, query *q.Query
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "api controller: list registrations")
|
||||
}
|
||||
|
||||
for _, r := range l {
|
||||
if err := bc.appendCap(ctx, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
@ -122,10 +126,25 @@ func (bc *basicController) GetRegistration(ctx context.Context, registrationUUID
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "api controller: get registration")
|
||||
}
|
||||
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err := bc.appendCap(ctx, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (bc *basicController) appendCap(ctx context.Context, r *scanner.Registration) error {
|
||||
mt, err := bc.Ping(ctx, r)
|
||||
if err != nil {
|
||||
logger.Errorf("Get registration error: %s", err)
|
||||
return err
|
||||
}
|
||||
r.Capabilities = mt.ConvertCapability()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegistrationExists ...
|
||||
func (bc *basicController) RegistrationExists(ctx context.Context, registrationUUID string) bool {
|
||||
registration, err := bc.manager.Get(ctx, registrationUUID)
|
||||
|
@ -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"
|
||||
|
@ -36,6 +36,7 @@ import (
|
||||
_ "github.com/goharbor/harbor/src/pkg/accessory/model/subject"
|
||||
_ "github.com/goharbor/harbor/src/pkg/config/inmemory"
|
||||
_ "github.com/goharbor/harbor/src/pkg/config/rest"
|
||||
_ "github.com/goharbor/harbor/src/pkg/scan/sbom"
|
||||
_ "github.com/goharbor/harbor/src/pkg/scan/vulnerability"
|
||||
)
|
||||
|
||||
|
@ -66,8 +66,9 @@ type Registration struct {
|
||||
Metadata *v1.ScannerAdapterMetadata `orm:"-" json:"-"`
|
||||
|
||||
// Timestamps
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
Capabilities map[string]interface{} `orm:"-" json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
// TableName for Endpoint
|
||||
@ -151,15 +152,20 @@ func (r *Registration) HasCapability(manifestMimeType string) bool {
|
||||
}
|
||||
|
||||
// GetProducesMimeTypes returns produces mime types for the artifact
|
||||
func (r *Registration) GetProducesMimeTypes(mimeType string) []string {
|
||||
func (r *Registration) GetProducesMimeTypes(mimeType string, scanType string) []string {
|
||||
if r.Metadata == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, capability := range r.Metadata.Capabilities {
|
||||
for _, mt := range capability.ConsumesMimeTypes {
|
||||
if mt == mimeType {
|
||||
return capability.ProducesMimeTypes
|
||||
capType := capability.Type
|
||||
if len(capType) == 0 {
|
||||
capType = v1.ScanTypeVulnerability
|
||||
}
|
||||
if scanType == capType {
|
||||
for _, mt := range capability.ConsumesMimeTypes {
|
||||
if mt == mimeType {
|
||||
return capability.ProducesMimeTypes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,14 @@ func GetScanHandler(requestType string) Handler {
|
||||
|
||||
// Handler handler for scan job, it could be implement by different scan type, such as vulnerability, sbom
|
||||
type Handler interface {
|
||||
// RequestProducesMineTypes returns the produces mime types
|
||||
RequestProducesMineTypes() []string
|
||||
// RequiredPermissions defines the permission used by the scan robot account
|
||||
RequiredPermissions() []*types.Policy
|
||||
// RequestParameters defines the parameters for scan request
|
||||
RequestParameters() map[string]interface{}
|
||||
// ReportURLParameter defines the parameters for scan report
|
||||
ReportURLParameter(sr *v1.ScanRequest) (string, error)
|
||||
// PostScan defines the operation after scan
|
||||
PostScan(ctx job.Context, sr *v1.ScanRequest, rp *scan.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error)
|
||||
}
|
||||
|
@ -242,7 +242,13 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
||||
}
|
||||
|
||||
myLogger.Debugf("check scan report for mime %s at %s", m, t.Format("2006/01/02 15:04:05"))
|
||||
rawReport, err := fetchScanReportFromScanner(client, resp.ID, m)
|
||||
|
||||
reportURLParameter, err := handler.ReportURLParameter(req)
|
||||
if err != nil {
|
||||
errs[i] = errors.Wrap(err, "scan job: get report url")
|
||||
return
|
||||
}
|
||||
rawReport, err := fetchScanReportFromScanner(client, resp.ID, m, reportURLParameter)
|
||||
if err != nil {
|
||||
// Not ready yet
|
||||
if notReadyErr, ok := err.(*v1.ReportNotReadyError); ok {
|
||||
@ -332,13 +338,13 @@ func getReportPlaceholder(ctx context.Context, digest string, reportUUID string,
|
||||
return reports[0], nil
|
||||
}
|
||||
|
||||
func fetchScanReportFromScanner(client v1.Client, requestID string, m string) (rawReport string, err error) {
|
||||
rawReport, err = client.GetScanReport(requestID, m)
|
||||
func fetchScanReportFromScanner(client v1.Client, requestID string, mimType string, urlParameter string) (rawReport string, err error) {
|
||||
rawReport, err = client.GetScanReport(requestID, mimType, urlParameter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Make sure the data is aligned with the v1 spec.
|
||||
if _, err = report.ResolveData(m, []byte(rawReport)); err != nil {
|
||||
if _, err = report.ResolveData(mimType, []byte(rawReport)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return rawReport, nil
|
||||
@ -367,7 +373,20 @@ func ExtractScanReq(params job.Parameters) (*v1.ScanRequest, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reqType := v1.ScanTypeVulnerability
|
||||
// attach the request with ProducesMimeTypes and Parameters
|
||||
if len(req.RequestType) > 0 {
|
||||
// current only support requestType with one element for each request
|
||||
if len(req.RequestType[0].Type) > 0 {
|
||||
reqType = req.RequestType[0].Type
|
||||
}
|
||||
handler := GetScanHandler(reqType)
|
||||
if handler == nil {
|
||||
return nil, errors.Errorf("failed to get scan handler, request type %v", reqType)
|
||||
}
|
||||
req.RequestType[0].ProducesMimeTypes = handler.RequestProducesMineTypes()
|
||||
req.RequestType[0].Parameters = handler.RequestParameters()
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
|
@ -211,8 +211,9 @@ func (suite *JobTestSuite) TestfetchScanReportFromScanner() {
|
||||
suite.reportIDs = append(suite.reportIDs, rptID)
|
||||
require.NoError(suite.T(), err)
|
||||
client := &v1testing.Client{}
|
||||
client.On("GetScanReport", mock.Anything, v1.MimeTypeGenericVulnerabilityReport).Return(rawContent, nil)
|
||||
rawRept, err := fetchScanReportFromScanner(client, "abc", v1.MimeTypeGenericVulnerabilityReport)
|
||||
client.On("GetScanReport", mock.Anything, v1.MimeTypeGenericVulnerabilityReport, mock.Anything).Return(rawContent, nil)
|
||||
parameters := "sbom_media_type=application/spdx+json"
|
||||
rawRept, err := fetchScanReportFromScanner(client, "abc", v1.MimeTypeGenericVulnerabilityReport, parameters)
|
||||
require.NoError(suite.T(), err)
|
||||
require.Equal(suite.T(), rawContent, rawRept)
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ type Client interface {
|
||||
// Returns:
|
||||
// string : the scan report of the given artifact
|
||||
// error : non nil error if any errors occurred
|
||||
GetScanReport(scanRequestID, reportMIMEType string) (string, error)
|
||||
GetScanReport(scanRequestID, reportMIMEType string, urlParameter string) (string, error)
|
||||
}
|
||||
|
||||
// basicClient is default implementation of the Client interface
|
||||
@ -97,7 +97,7 @@ func NewClient(url, authType, accessCredential string, skipCertVerify bool) (Cli
|
||||
httpClient: &http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
Transport: transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
},
|
||||
@ -167,7 +167,7 @@ func (c *basicClient) SubmitScan(req *ScanRequest) (*ScanResponse, error) {
|
||||
}
|
||||
|
||||
// GetScanReport ...
|
||||
func (c *basicClient) GetScanReport(scanRequestID, reportMIMEType string) (string, error) {
|
||||
func (c *basicClient) GetScanReport(scanRequestID, reportMIMEType string, urlParameter string) (string, error) {
|
||||
if len(scanRequestID) == 0 {
|
||||
return "", errors.New("empty scan request ID")
|
||||
}
|
||||
@ -177,8 +177,11 @@ func (c *basicClient) GetScanReport(scanRequestID, reportMIMEType string) (strin
|
||||
}
|
||||
|
||||
def := c.spec.GetScanReport(scanRequestID, reportMIMEType)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, def.URL, nil)
|
||||
reportURL := def.URL
|
||||
if len(urlParameter) > 0 {
|
||||
reportURL = fmt.Sprintf("%s?%s", def.URL, urlParameter)
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, reportURL, nil)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "v1 client: get scan report")
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ func (suite *ClientTestSuite) TestClientSubmitScan() {
|
||||
|
||||
// TestClientGetScanReportError tests getting report failed
|
||||
func (suite *ClientTestSuite) TestClientGetScanReportError() {
|
||||
_, err := suite.client.GetScanReport("id1", MimeTypeNativeReport)
|
||||
_, err := suite.client.GetScanReport("id1", MimeTypeNativeReport, "")
|
||||
require.Error(suite.T(), err)
|
||||
assert.Condition(suite.T(), func() (success bool) {
|
||||
success = strings.Index(err.Error(), "error") != -1
|
||||
@ -82,14 +82,14 @@ func (suite *ClientTestSuite) TestClientGetScanReportError() {
|
||||
|
||||
// TestClientGetScanReport tests getting report
|
||||
func (suite *ClientTestSuite) TestClientGetScanReport() {
|
||||
res, err := suite.client.GetScanReport("id2", MimeTypeNativeReport)
|
||||
res, err := suite.client.GetScanReport("id2", MimeTypeNativeReport, "")
|
||||
require.NoError(suite.T(), err)
|
||||
require.NotEmpty(suite.T(), res)
|
||||
}
|
||||
|
||||
// TestClientGetScanReportNotReady tests the case that the report is not ready
|
||||
func (suite *ClientTestSuite) TestClientGetScanReportNotReady() {
|
||||
_, err := suite.client.GetScanReport("id3", MimeTypeNativeReport)
|
||||
_, err := suite.client.GetScanReport("id3", MimeTypeNativeReport, "")
|
||||
require.Error(suite.T(), err)
|
||||
require.Condition(suite.T(), func() (success bool) {
|
||||
_, success = err.(*ReportNotReadyError)
|
||||
|
@ -21,6 +21,11 @@ import (
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
supportVulnerability = "support_vulnerability"
|
||||
supportSBOM = "support_sbom"
|
||||
)
|
||||
|
||||
var supportedMimeTypes = []string{
|
||||
MimeTypeNativeReport,
|
||||
MimeTypeGenericVulnerabilityReport,
|
||||
@ -153,6 +158,20 @@ func (md *ScannerAdapterMetadata) GetCapability(mimeType string) *ScannerCapabil
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConvertCapability converts the capability to map, used in get scanner API
|
||||
func (md *ScannerAdapterMetadata) ConvertCapability() map[string]interface{} {
|
||||
capabilities := make(map[string]interface{})
|
||||
for _, c := range md.Capabilities {
|
||||
if c.Type == ScanTypeVulnerability {
|
||||
capabilities[supportVulnerability] = true
|
||||
}
|
||||
if c.Type == ScanTypeSbom {
|
||||
capabilities[supportSBOM] = true
|
||||
}
|
||||
}
|
||||
return capabilities
|
||||
}
|
||||
|
||||
// Artifact represents an artifact stored in Registry.
|
||||
type Artifact struct {
|
||||
// ID of the namespace (project). It will not be sent to scanner adapter.
|
||||
|
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 {
|
||||
}
|
||||
|
||||
// RequestProducesMineTypes returns the produces mime types
|
||||
func (v *ScanHandler) RequestProducesMineTypes() []string {
|
||||
return []string{v1.MimeTypeGenericVulnerabilityReport}
|
||||
}
|
||||
|
||||
// RequestParameters defines the parameters for scan request
|
||||
func (v *ScanHandler) RequestParameters() map[string]interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequiredPermissions defines the permission used by the scan robot account
|
||||
func (v *ScanHandler) RequiredPermissions() []*types.Policy {
|
||||
return []*types.Policy{
|
||||
@ -49,6 +59,11 @@ func (v *ScanHandler) RequiredPermissions() []*types.Policy {
|
||||
}
|
||||
}
|
||||
|
||||
// ReportURLParameter vulnerability doesn't require any scan report parameters
|
||||
func (v *ScanHandler) ReportURLParameter(_ *v1.ScanRequest) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// PostScan ...
|
||||
func (v *ScanHandler) PostScan(ctx job.Context, _ *v1.ScanRequest, origRp *scan.Report, rawReport string, _ time.Time, _ *model.Robot) (string, error) {
|
||||
// use a new ormer here to use the short db connection
|
||||
|
@ -1,6 +1,7 @@
|
||||
package vulnerability
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -50,3 +51,66 @@ func TestPostScan(t *testing.T) {
|
||||
assert.Equal(t, "", refreshedReport, "PostScan should return the refreshed report")
|
||||
assert.Nil(t, err, "PostScan should not return an error")
|
||||
}
|
||||
|
||||
func TestScanHandler_RequiredPermissions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want []*types.Policy
|
||||
}{
|
||||
{"normal", []*types.Policy{
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionPull,
|
||||
},
|
||||
{
|
||||
Resource: rbac.ResourceRepository,
|
||||
Action: rbac.ActionScannerPull,
|
||||
},
|
||||
}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := &ScanHandler{}
|
||||
assert.Equalf(t, tt.want, v.RequiredPermissions(), "RequiredPermissions()")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanHandler_ReportURLParameter(t *testing.T) {
|
||||
type args struct {
|
||||
in0 *v1.ScanRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{"normal", args{&v1.ScanRequest{}}, "", assert.NoError},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := &ScanHandler{}
|
||||
got, err := v.ReportURLParameter(tt.args.in0)
|
||||
if !tt.wantErr(t, err, fmt.Sprintf("ReportURLParameter(%v)", tt.args.in0)) {
|
||||
return
|
||||
}
|
||||
assert.Equalf(t, tt.want, got, "ReportURLParameter(%v)", tt.args.in0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanHandler_RequestProducesMineTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want []string
|
||||
}{
|
||||
{"normal", []string{v1.MimeTypeGenericVulnerabilityReport}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v := &ScanHandler{}
|
||||
assert.Equalf(t, tt.want, v.RequestProducesMineTypes(), "RequestProducesMineTypes()")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ func (s *ScannerRegistration) ToSwagger(_ context.Context) *models.ScannerRegist
|
||||
Vendor: s.Vendor,
|
||||
Version: s.Version,
|
||||
Health: s.Health,
|
||||
Capabilities: s.Capabilities,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -594,7 +594,13 @@ func (a *projectAPI) GetScannerOfProject(ctx context.Context, params operation.G
|
||||
if err != nil {
|
||||
return a.SendError(ctx, err)
|
||||
}
|
||||
|
||||
if scanner != nil {
|
||||
metadata, err := a.scannerCtl.GetMetadata(ctx, scanner.UUID)
|
||||
if err != nil {
|
||||
return a.SendError(ctx, err)
|
||||
}
|
||||
scanner.Capabilities = metadata.ConvertCapability()
|
||||
}
|
||||
return operation.NewGetScannerOfProjectOK().WithPayload(model.NewScannerRegistration(scanner).ToSwagger(ctx))
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/project/models"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/server/v2.0/restapi"
|
||||
projecttesting "github.com/goharbor/harbor/src/testing/controller/project"
|
||||
scannertesting "github.com/goharbor/harbor/src/testing/controller/scanner"
|
||||
@ -36,6 +37,7 @@ type ProjectTestSuite struct {
|
||||
scannerCtl *scannertesting.Controller
|
||||
project *models.Project
|
||||
reg *scanner.Registration
|
||||
metadata *v1.ScannerAdapterMetadata
|
||||
}
|
||||
|
||||
func (suite *ProjectTestSuite) SetupSuite() {
|
||||
@ -59,7 +61,12 @@ func (suite *ProjectTestSuite) SetupSuite() {
|
||||
scannerCtl: suite.scannerCtl,
|
||||
},
|
||||
}
|
||||
|
||||
suite.metadata = &v1.ScannerAdapterMetadata{
|
||||
Capabilities: []*v1.ScannerCapability{
|
||||
{Type: "vulnerability", ProducesMimeTypes: []string{v1.MimeTypeScanResponse}},
|
||||
{Type: "sbom", ProducesMimeTypes: []string{v1.MimeTypeSBOMReport}},
|
||||
},
|
||||
}
|
||||
suite.Suite.SetupSuite()
|
||||
}
|
||||
|
||||
@ -81,7 +88,7 @@ func (suite *ProjectTestSuite) TestGetScannerOfProject() {
|
||||
// scanner not found
|
||||
mock.OnAnything(suite.projectCtl, "Get").Return(suite.project, nil).Once()
|
||||
mock.OnAnything(suite.scannerCtl, "GetRegistrationByProject").Return(nil, nil).Once()
|
||||
|
||||
mock.OnAnything(suite.scannerCtl, "GetMetadata").Return(suite.metadata, nil).Once()
|
||||
res, err := suite.Get("/projects/1/scanner")
|
||||
suite.NoError(err)
|
||||
suite.Equal(200, res.StatusCode)
|
||||
@ -90,7 +97,7 @@ func (suite *ProjectTestSuite) TestGetScannerOfProject() {
|
||||
{
|
||||
mock.OnAnything(suite.projectCtl, "Get").Return(suite.project, nil).Once()
|
||||
mock.OnAnything(suite.scannerCtl, "GetRegistrationByProject").Return(suite.reg, nil).Once()
|
||||
|
||||
mock.OnAnything(suite.scannerCtl, "GetMetadata").Return(suite.metadata, nil).Once()
|
||||
var scanner scanner.Registration
|
||||
res, err := suite.GetJSON("/projects/1/scanner", &scanner)
|
||||
suite.NoError(err)
|
||||
@ -101,6 +108,7 @@ func (suite *ProjectTestSuite) TestGetScannerOfProject() {
|
||||
{
|
||||
mock.OnAnything(projectCtlMock, "GetByName").Return(suite.project, nil).Once()
|
||||
mock.OnAnything(suite.projectCtl, "Get").Return(suite.project, nil).Once()
|
||||
mock.OnAnything(suite.scannerCtl, "GetMetadata").Return(suite.metadata, nil).Once()
|
||||
mock.OnAnything(suite.scannerCtl, "GetRegistrationByProject").Return(suite.reg, nil).Once()
|
||||
|
||||
var scanner scanner.Registration
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/controller/scan"
|
||||
"github.com/goharbor/harbor/src/lib/errors"
|
||||
"github.com/goharbor/harbor/src/pkg/distribution"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/scan"
|
||||
)
|
||||
|
||||
@ -50,7 +51,15 @@ func (s *scanAPI) Prepare(ctx context.Context, _ string, params interface{}) mid
|
||||
}
|
||||
|
||||
func (s *scanAPI) StopScanArtifact(ctx context.Context, params operation.StopScanArtifactParams) middleware.Responder {
|
||||
if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionStop, rbac.ResourceScan); err != nil {
|
||||
scanType := v1.ScanTypeVulnerability
|
||||
if params.ScanType != nil && validScanType(params.ScanType.ScanType) {
|
||||
scanType = params.ScanType.ScanType
|
||||
}
|
||||
res := rbac.ResourceScan
|
||||
if scanType == v1.ScanTypeSbom {
|
||||
res = rbac.ResourceSBOM
|
||||
}
|
||||
if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionStop, res); err != nil {
|
||||
return s.SendError(ctx, err)
|
||||
}
|
||||
|
||||
@ -68,22 +77,26 @@ func (s *scanAPI) StopScanArtifact(ctx context.Context, params operation.StopSca
|
||||
}
|
||||
|
||||
func (s *scanAPI) ScanArtifact(ctx context.Context, params operation.ScanArtifactParams) middleware.Responder {
|
||||
if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionCreate, rbac.ResourceScan); err != nil {
|
||||
return s.SendError(ctx, err)
|
||||
}
|
||||
|
||||
repository := fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName)
|
||||
artifact, err := s.artCtl.GetByReference(ctx, repository, params.Reference, nil)
|
||||
if err != nil {
|
||||
return s.SendError(ctx, err)
|
||||
}
|
||||
|
||||
scanType := v1.ScanTypeVulnerability
|
||||
options := []scan.Option{}
|
||||
if !distribution.IsDigest(params.Reference) {
|
||||
options = append(options, scan.WithTag(params.Reference))
|
||||
}
|
||||
if params.ScanRequestType != nil && validScanType(params.ScanRequestType.ScanType) {
|
||||
options = append(options, scan.WithScanType(params.ScanRequestType.ScanType))
|
||||
scanType = params.ScanRequestType.ScanType
|
||||
options = append(options, scan.WithScanType(scanType))
|
||||
}
|
||||
res := rbac.ResourceScan
|
||||
if scanType == v1.ScanTypeSbom {
|
||||
res = rbac.ResourceSBOM
|
||||
}
|
||||
if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionCreate, res); err != nil {
|
||||
return s.SendError(ctx, err)
|
||||
}
|
||||
repository := fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName)
|
||||
artifact, err := s.artCtl.GetByReference(ctx, repository, params.Reference, nil)
|
||||
if err != nil {
|
||||
return s.SendError(ctx, err)
|
||||
}
|
||||
|
||||
if err := s.scanCtl.Scan(ctx, artifact, options...); err != nil {
|
||||
|
@ -42,9 +42,9 @@ func (_m *Client) GetMetadata() (*v1.ScannerAdapterMetadata, error) {
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetScanReport provides a mock function with given fields: scanRequestID, reportMIMEType
|
||||
func (_m *Client) GetScanReport(scanRequestID string, reportMIMEType string) (string, error) {
|
||||
ret := _m.Called(scanRequestID, reportMIMEType)
|
||||
// GetScanReport provides a mock function with given fields: scanRequestID, reportMIMEType, urlParameter
|
||||
func (_m *Client) GetScanReport(scanRequestID string, reportMIMEType string, urlParameter string) (string, error) {
|
||||
ret := _m.Called(scanRequestID, reportMIMEType, urlParameter)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetScanReport")
|
||||
@ -52,17 +52,17 @@ func (_m *Client) GetScanReport(scanRequestID string, reportMIMEType string) (st
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) (string, error)); ok {
|
||||
return rf(scanRequestID, reportMIMEType)
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) (string, error)); ok {
|
||||
return rf(scanRequestID, reportMIMEType, urlParameter)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, string) string); ok {
|
||||
r0 = rf(scanRequestID, reportMIMEType)
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) string); ok {
|
||||
r0 = rf(scanRequestID, reportMIMEType, urlParameter)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(scanRequestID, reportMIMEType)
|
||||
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
|
||||
r1 = rf(scanRequestID, reportMIMEType, urlParameter)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user