From 6beda617cb7845e57e6e4aac1a4513f0e1ffac2a Mon Sep 17 00:00:00 2001 From: stonezdj Date: Mon, 20 May 2024 15:25:40 +0800 Subject: [PATCH] Add sbom_report table to store sbom related information fixes #20445 Refactor scan/base_controller.go Move MakeReportPlaceholder, GetReportPlaceholder, GetSummary to vul and sbom scanHandler Signed-off-by: stonezdj --- .../postgresql/0140_2.11.0_schema.up.sql | 14 +- .../event/handler/internal/artifact.go | 17 +- .../event/handler/webhook/scan/scan.go | 4 +- src/controller/scan/base_controller.go | 213 +------------- src/controller/scan/base_controller_test.go | 135 ++++----- src/controller/scan/controller.go | 11 +- src/pkg/scan/dao/scan/report_test.go | 19 -- src/pkg/scan/handler.go | 20 +- src/pkg/scan/job.go | 22 +- src/pkg/scan/job_test.go | 20 -- src/pkg/scan/sbom/dao/dao.go | 126 ++++++++ src/pkg/scan/sbom/dao/dao_test.go | 133 +++++++++ src/pkg/scan/sbom/manager.go | 203 +++++++++++++ src/pkg/scan/sbom/model/report.go | 46 +++ src/pkg/scan/sbom/sbom.go | 263 ++++++++++++++--- src/pkg/scan/sbom/sbom_test.go | 142 +++++++++- src/pkg/scan/vulnerability/vul.go | 257 ++++++++++++++++- src/pkg/scan/vulnerability/vul_test.go | 127 ++++++++- src/pkg/task/mock_task_manager_test.go | 18 ++ src/pkg/task/task.go | 17 ++ src/pkg/task/task_test.go | 11 + src/server/v2.0/handler/assembler/report.go | 11 +- src/testing/controller/scan/controller.go | 71 ++--- src/testing/pkg/pkg.go | 2 + src/testing/pkg/scan/handler.go | 268 ++++++++++++++++++ src/testing/pkg/scan/sbom/manager.go | 216 ++++++++++++++ src/testing/pkg/task/manager.go | 18 ++ 27 files changed, 1909 insertions(+), 495 deletions(-) create mode 100644 src/pkg/scan/sbom/dao/dao.go create mode 100644 src/pkg/scan/sbom/dao/dao_test.go create mode 100644 src/pkg/scan/sbom/manager.go create mode 100644 src/pkg/scan/sbom/model/report.go create mode 100644 src/testing/pkg/scan/handler.go create mode 100644 src/testing/pkg/scan/sbom/manager.go diff --git a/make/migrations/postgresql/0140_2.11.0_schema.up.sql b/make/migrations/postgresql/0140_2.11.0_schema.up.sql index fd6fb16eda4..0552af892f8 100644 --- a/make/migrations/postgresql/0140_2.11.0_schema.up.sql +++ b/make/migrations/postgresql/0140_2.11.0_schema.up.sql @@ -28,4 +28,16 @@ then set column artifact_type as not null */ UPDATE artifact SET artifact_type = media_type WHERE artifact_type IS NULL; -ALTER TABLE artifact ALTER COLUMN artifact_type SET NOT NULL; \ No newline at end of file +ALTER TABLE artifact ALTER COLUMN artifact_type SET NOT NULL; + +CREATE TABLE IF NOT EXISTS sbom_report +( + id SERIAL PRIMARY KEY NOT NULL, + uuid VARCHAR(64) UNIQUE NOT NULL, + artifact_id INT NOT NULL, + registration_uuid VARCHAR(64) NOT NULL, + mime_type VARCHAR(256) NOT NULL, + media_type VARCHAR(256) NOT NULL, + report JSON, + UNIQUE(artifact_id, registration_uuid, mime_type, media_type) +); \ No newline at end of file diff --git a/src/controller/event/handler/internal/artifact.go b/src/controller/event/handler/internal/artifact.go index 0e9b3f5bba5..a9e380f2644 100644 --- a/src/controller/event/handler/internal/artifact.go +++ b/src/controller/event/handler/internal/artifact.go @@ -24,7 +24,7 @@ import ( "time" "github.com/goharbor/harbor/src/controller/artifact" - "github.com/goharbor/harbor/src/controller/artifact/processor/sbom" + sbomprocessor "github.com/goharbor/harbor/src/controller/artifact/processor/sbom" "github.com/goharbor/harbor/src/controller/event" "github.com/goharbor/harbor/src/controller/event/operator" "github.com/goharbor/harbor/src/controller/repository" @@ -38,6 +38,7 @@ import ( pkgArt "github.com/goharbor/harbor/src/pkg/artifact" "github.com/goharbor/harbor/src/pkg/scan/report" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" + "github.com/goharbor/harbor/src/pkg/scan/sbom" "github.com/goharbor/harbor/src/pkg/task" ) @@ -74,6 +75,8 @@ type ArtifactEventHandler struct { execMgr task.ExecutionManager // reportMgr for managing scan reports reportMgr report.Manager + // sbomReportMgr + sbomReportMgr sbom.Manager // artMgr for managing artifacts artMgr pkgArt.Manager @@ -321,9 +324,15 @@ func (a *ArtifactEventHandler) onDelete(ctx context.Context, event *event.Artifa log.Errorf("failed to delete scan reports of artifact %v, error: %v", unrefDigests, err) } - if event.Artifact.Type == sbom.ArtifactTypeSBOM && len(event.Artifact.Digest) > 0 { - if err := reportMgr.DeleteByExtraAttr(ctx, v1.MimeTypeSBOMReport, "sbom_digest", event.Artifact.Digest); err != nil { - log.Errorf("failed to delete scan reports of with sbom digest %v, error: %v", event.Artifact.Digest, err) + // delete sbom_report when the subject artifact is deleted + if err := sbom.Mgr.DeleteByArtifactID(ctx, event.Artifact.ID); err != nil { + log.Errorf("failed to delete sbom reports of artifact ID %v, error: %v", event.Artifact.ID, err) + } + + // delete sbom_report when the accessory artifact is deleted + if event.Artifact.Type == sbomprocessor.ArtifactTypeSBOM && len(event.Artifact.Digest) > 0 { + if err := sbom.Mgr.DeleteByExtraAttr(ctx, v1.MimeTypeSBOMReport, "sbom_digest", event.Artifact.Digest); err != nil { + log.Errorf("failed to delete sbom reports of with sbom digest %v, error: %v", event.Artifact.Digest, err) } } return nil diff --git a/src/controller/event/handler/webhook/scan/scan.go b/src/controller/event/handler/webhook/scan/scan.go index 04b3fffb513..9c7df3fd912 100644 --- a/src/controller/event/handler/webhook/scan/scan.go +++ b/src/controller/event/handler/webhook/scan/scan.go @@ -144,7 +144,7 @@ func constructScanImagePayload(ctx context.Context, event *event.ScanImageEvent, scanSummaries := map[string]interface{}{} if event.ScanType == v1.ScanTypeVulnerability { - scanSummaries, err = scan.DefaultController.GetSummary(ctx, art, []string{v1.MimeTypeNativeReport, v1.MimeTypeGenericVulnerabilityReport}) + scanSummaries, err = scan.DefaultController.GetSummary(ctx, art, event.ScanType, []string{v1.MimeTypeNativeReport, v1.MimeTypeGenericVulnerabilityReport}) if err != nil { return nil, errors.Wrap(err, "construct scan payload") } @@ -152,7 +152,7 @@ func constructScanImagePayload(ctx context.Context, event *event.ScanImageEvent, sbomOverview := map[string]interface{}{} if event.ScanType == v1.ScanTypeSbom { - sbomOverview, err = scan.DefaultController.GetSummary(ctx, art, []string{v1.MimeTypeSBOMReport}) + sbomOverview, err = scan.DefaultController.GetSummary(ctx, art, event.ScanType, []string{v1.MimeTypeSBOMReport}) if err != nil { return nil, errors.Wrap(err, "construct scan payload") } diff --git a/src/controller/scan/base_controller.go b/src/controller/scan/base_controller.go index 14f5a29372b..dde7c915407 100644 --- a/src/controller/scan/base_controller.go +++ b/src/controller/scan/base_controller.go @@ -17,7 +17,6 @@ package scan import ( "bytes" "context" - "encoding/json" "fmt" "reflect" "strings" @@ -49,7 +48,6 @@ 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" ) @@ -275,8 +273,9 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti errs []error launchScanJobParams []*launchScanJobParam ) + handler := sca.GetScanHandler(opts.GetScanType()) for _, art := range artifacts { - reports, err := bc.makeReportPlaceholder(ctx, r, art, opts) + reports, err := handler.MakePlaceHolder(ctx, art, r) if err != nil { if errors.IsConflictErr(err) { errs = append(errs, err) @@ -566,63 +565,6 @@ func (bc *basicController) startScanAll(ctx context.Context, executionID int64) return nil } -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 - } - - if len(oldReports) > 0 { - for _, oldReport := range oldReports { - if !job.Status(oldReport.Status).Final() { - return nil, errors.ConflictError(nil).WithMessage("a previous scan process is %s", oldReport.Status) - } - } - - for _, oldReport := range oldReports { - if err := bc.manager.Delete(ctx, oldReport.UUID); err != nil { - return nil, err - } - } - } - - var reports []*scan.Report - - for _, pm := range r.GetProducesMimeTypes(art.ManifestMediaType, opts.GetScanType()) { - report := &scan.Report{ - Digest: art.Digest, - RegistrationUUID: r.UUID, - MimeType: pm, - } - - create := func(ctx context.Context) error { - reportUUID, err := bc.manager.Create(ctx, report) - if err != nil { - return err - } - report.UUID = reportUUID - - return nil - } - - if err := orm.WithTransaction(create)(orm.SetTransactionOpNameToContext(ctx, "tx-make-report-placeholder")); err != nil { - return nil, err - } - - reports = append(reports, report) - } - - return reports, nil -} - // GetReport ... func (bc *basicController) GetReport(ctx context.Context, artifact *ar.Artifact, mimeTypes []string) ([]*scan.Report, error) { if artifact == nil { @@ -697,95 +639,10 @@ func (bc *basicController) GetReport(ctx context.Context, artifact *ar.Artifact, return reports, nil } -func isSBOMMimeTypes(mimeTypes []string) bool { - for _, mimeType := range mimeTypes { - if mimeType == v1.MimeTypeSBOMReport { - return true - } - } - return false -} - // GetSummary ... -func (bc *basicController) GetSummary(ctx context.Context, artifact *ar.Artifact, mimeTypes []string) (map[string]interface{}, error) { - if artifact == nil { - return nil, errors.New("no way to get report summaries for nil artifact") - } - if isSBOMMimeTypes(mimeTypes) { - return bc.GetSBOMSummary(ctx, artifact, mimeTypes) - } - // Get reports first - rps, err := bc.GetReport(ctx, artifact, mimeTypes) - if err != nil { - return nil, err - } - - summaries := make(map[string]interface{}, len(rps)) - for _, rp := range rps { - sum, err := report.GenerateSummary(rp) - if err != nil { - return nil, err - } - - if s, ok := summaries[rp.MimeType]; ok { - r, err := report.MergeSummary(rp.MimeType, s, sum) - if err != nil { - return nil, err - } - - summaries[rp.MimeType] = r - } else { - summaries[rp.MimeType] = sum - } - } - - return summaries, nil -} - -func (bc *basicController) GetSBOMSummary(ctx context.Context, art *ar.Artifact, mimeTypes []string) (map[string]interface{}, error) { - if art == nil { - return nil, errors.New("no way to get report summaries for nil artifact") - } - r, err := bc.sc.GetRegistrationByProject(ctx, art.ProjectID) - if err != nil { - return nil, errors.Wrap(err, "scan controller: get sbom summary") - } - reports, err := bc.manager.GetBy(ctx, art.Digest, r.UUID, mimeTypes) - if err != nil { - return nil, err - } - if len(reports) == 0 { - return map[string]interface{}{}, nil - } - reportContent := reports[0].Report - result := map[string]interface{}{} - if len(reportContent) == 0 { - status := bc.retrieveStatusFromTask(ctx, reports[0].UUID) - if len(status) > 0 { - result[sbomModel.ReportID] = reports[0].UUID - result[sbomModel.ScanStatus] = status - } - log.Debug("no content for current report") - return result, nil - } - err = json.Unmarshal([]byte(reportContent), &result) - return result, err -} - -// retrieve the status from task -func (bc *basicController) retrieveStatusFromTask(ctx context.Context, reportID string) string { - if len(reportID) == 0 { - return "" - } - tasks, err := bc.taskMgr.ListScanTasksByReportUUID(ctx, reportID) - if err != nil { - log.Warningf("can not find the task with report UUID %v, error %v", reportID, err) - return "" - } - if len(tasks) > 0 { - return tasks[0].Status - } - return "" +func (bc *basicController) GetSummary(ctx context.Context, artifact *ar.Artifact, scanType string, mimeTypes []string) (map[string]interface{}, error) { + handler := sca.GetScanHandler(scanType) + return handler.GetSummary(ctx, artifact, mimeTypes) } // GetScanLog ... @@ -821,7 +678,7 @@ func (bc *basicController) GetScanLog(ctx context.Context, artifact *ar.Artifact if !scanTaskForArtifacts(t, artifactMap) { return nil, errors.NotFoundError(nil).WithMessage("scan log with uuid: %s not found", uuid) } - for _, reportUUID := range getReportUUIDs(t.ExtraAttrs) { + for _, reportUUID := range GetReportUUIDs(t.ExtraAttrs) { reportUUIDToTasks[reportUUID] = t } } @@ -902,14 +759,6 @@ func scanTaskForArtifacts(task *task.Task, artifactMap map[int64]interface{}) bo return exist } -// DeleteReports ... -func (bc *basicController) DeleteReports(ctx context.Context, digests ...string) error { - if err := bc.manager.DeleteByDigests(ctx, digests...); err != nil { - return errors.Wrap(err, "scan controller: delete reports") - } - return nil -} - func (bc *basicController) GetVulnerable(ctx context.Context, artifact *ar.Artifact, allowlist allowlist.CVESet, allowlistIsExpired bool) (*Vulnerable, error) { if artifact == nil { return nil, errors.New("no way to get vulnerable for nil artifact") @@ -1204,7 +1053,7 @@ func (bc *basicController) assembleReports(ctx context.Context, reports ...*scan reportUUIDToTasks := map[string]*task.Task{} for _, task := range tasks { - for _, reportUUID := range getReportUUIDs(task.ExtraAttrs) { + for _, reportUUID := range GetReportUUIDs(task.ExtraAttrs) { reportUUIDToTasks[reportUUID] = task } } @@ -1275,7 +1124,8 @@ func getArtifactTag(extraAttrs map[string]interface{}) string { return tag } -func getReportUUIDs(extraAttrs map[string]interface{}) []string { +// GetReportUUIDs returns the report UUIDs from the extra attributes +func GetReportUUIDs(extraAttrs map[string]interface{}) []string { var reportUUIDs []string if extraAttrs != nil { @@ -1314,48 +1164,3 @@ func parseOptions(options ...Option) (*Options, error) { return ops, nil } - -// deleteArtifactAccessories delete the accessory in reports, only delete sbom accessory -func (bc *basicController) deleteArtifactAccessories(ctx context.Context, reports []*scan.Report) error { - for _, rpt := range reports { - if rpt.MimeType != v1.MimeTypeSBOMReport { - continue - } - if err := bc.deleteArtifactAccessory(ctx, rpt.Report); err != nil { - return err - } - } - return nil -} - -// deleteArtifactAccessory check if current report has accessory info, if there is, delete it -func (bc *basicController) deleteArtifactAccessory(ctx context.Context, report string) error { - if len(report) == 0 { - return nil - } - sbomSummary := sbomModel.Summary{} - if err := json.Unmarshal([]byte(report), &sbomSummary); err != nil { - // it could be a non sbom report, just skip - log.Debugf("fail to unmarshal %v, skip to delete sbom report", err) - return nil - } - repo, dgst := sbomSummary.SBOMAccArt() - if len(repo) == 0 || len(dgst) == 0 { - return nil - } - art, err := bc.ar.GetByReference(ctx, repo, dgst, nil) - if err != nil { - if errors.IsNotFoundErr(err) { - return nil - } - return err - } - if art == nil { - return nil - } - err = bc.ar.Delete(ctx, art.ID) - if errors.IsNotFoundErr(err) { - return nil - } - return err -} diff --git a/src/controller/scan/base_controller_test.go b/src/controller/scan/base_controller_test.go index 1cd464891d4..9283929798c 100644 --- a/src/controller/scan/base_controller_test.go +++ b/src/controller/scan/base_controller_test.go @@ -45,7 +45,6 @@ import ( "github.com/goharbor/harbor/src/pkg/scan/dao/scanner" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" "github.com/goharbor/harbor/src/pkg/scan/vuln" - _ "github.com/goharbor/harbor/src/pkg/scan/vulnerability" "github.com/goharbor/harbor/src/pkg/task" artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact" robottesting "github.com/goharbor/harbor/src/testing/controller/robot" @@ -55,6 +54,7 @@ import ( ormtesting "github.com/goharbor/harbor/src/testing/lib/orm" "github.com/goharbor/harbor/src/testing/mock" accessorytesting "github.com/goharbor/harbor/src/testing/pkg/accessory" + scanTest "github.com/goharbor/harbor/src/testing/pkg/scan" postprocessorstesting "github.com/goharbor/harbor/src/testing/pkg/scan/postprocessors" reporttesting "github.com/goharbor/harbor/src/testing/pkg/scan/report" tasktesting "github.com/goharbor/harbor/src/testing/pkg/task" @@ -64,6 +64,8 @@ import ( type ControllerTestSuite struct { suite.Suite + scanHandler *scanTest.Handler + artifactCtl *artifacttesting.Controller accessoryMgr *accessorytesting.Manager originalArtifactCtl artifact.Controller @@ -91,6 +93,8 @@ func TestController(t *testing.T) { // SetupSuite ... func (suite *ControllerTestSuite) SetupSuite() { + suite.scanHandler = &scanTest.Handler{} + sca.RegisterScanHanlder(v1.ScanTypeVulnerability, suite.scanHandler) suite.originalArtifactCtl = artifact.Ctl suite.artifactCtl = &artifacttesting.Controller{} artifact.Ctl = suite.artifactCtl @@ -212,7 +216,7 @@ func (suite *ControllerTestSuite) SetupSuite() { mgr.On("GetBy", mock.Anything, suite.artifact.Digest, suite.registration.UUID, []string{v1.MimeTypeSBOMReport}).Return(sbomReport, nil) mgr.On("GetBy", mock.Anything, suite.wrongArtifact.Digest, suite.registration.UUID, []string{v1.MimeTypeSBOMReport}).Return(emptySBOMReport, nil) mgr.On("Get", mock.Anything, "rp-uuid-001").Return(reports[0], nil) - mgr.On("UpdateReportData", "rp-uuid-001", suite.rawReport, (int64)(10000)).Return(nil) + mgr.On("Update", "rp-uuid-001", suite.rawReport, (int64)(10000)).Return(nil) mgr.On("UpdateStatus", "the-uuid-123", "Success", (int64)(10000)).Return(nil) suite.reportMgr = mgr @@ -347,6 +351,19 @@ func (suite *ControllerTestSuite) TearDownSuite() { // TestScanControllerScan ... func (suite *ControllerTestSuite) TestScanControllerScan() { + rpts := []*scan.Report{ + {UUID: "uuid"}, + } + requiredPermission := []*types.Policy{ + { + Resource: rbac.ResourceRepository, + Action: rbac.ActionPull, + }, + { + Resource: rbac.ResourceRepository, + Action: rbac.ActionScannerPull, + }, + } { // artifact not provieded suite.Require().Error(suite.c.Scan(context.TODO(), nil)) @@ -369,6 +386,8 @@ func (suite *ControllerTestSuite) TestScanControllerScan() { mock.OnAnything(suite.execMgr, "Create").Return(int64(1), nil).Once() mock.OnAnything(suite.taskMgr, "Create").Return(int64(1), nil).Once() + mock.OnAnything(suite.scanHandler, "MakePlaceHolder").Return(rpts, nil).Once() + mock.OnAnything(suite.scanHandler, "RequiredPermissions").Return(requiredPermission).Once() ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{}) @@ -388,7 +407,10 @@ func (suite *ControllerTestSuite) TestScanControllerScan() { }, nil).Once() mock.OnAnything(suite.reportMgr, "Delete").Return(fmt.Errorf("delete failed")).Once() - + mock.OnAnything(suite.scanHandler, "MakePlaceHolder").Return(rpts, nil).Once() + mock.OnAnything(suite.scanHandler, "RequiredPermissions").Return(requiredPermission).Once() + mock.OnAnything(suite.execMgr, "Create").Return(int64(1), nil).Once() + mock.OnAnything(suite.taskMgr, "Create").Return(int64(0), fmt.Errorf("failed to create task")).Once() suite.Require().Error(suite.c.Scan(context.TODO(), suite.artifact)) } @@ -403,7 +425,9 @@ func (suite *ControllerTestSuite) TestScanControllerScan() { mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return([]*task.Task{ {ExtraAttrs: suite.makeExtraAttrs(int64(1), "rp-uuid-001"), Status: "Running"}, }, nil).Once() - + mock.OnAnything(suite.scanHandler, "MakePlaceHolder").Return(rpts, nil).Once() + mock.OnAnything(suite.scanHandler, "RequiredPermissions").Return(requiredPermission).Once() + mock.OnAnything(suite.execMgr, "Create").Return(int64(0), fmt.Errorf("failed to create execution")).Once() suite.Require().Error(suite.c.Scan(context.TODO(), suite.artifact)) } } @@ -465,21 +489,6 @@ func (suite *ControllerTestSuite) TestScanControllerGetReport() { assert.Equal(suite.T(), 1, len(rep)) } -// TestScanControllerGetSummary ... -func (suite *ControllerTestSuite) TestScanControllerGetSummary() { - ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{}) - mock.OnAnything(suite.ar, "HasUnscannableLayer").Return(false, nil).Once() - mock.OnAnything(suite.accessoryMgr, "List").Return([]accessoryModel.Accessory{}, nil).Once() - mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) { - walkFn := args.Get(2).(func(*artifact.Artifact) error) - walkFn(suite.artifact) - }).Once() - mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return(nil, nil).Once() - sum, err := suite.c.GetSummary(ctx, suite.artifact, []string{v1.MimeTypeNativeReport}) - require.NoError(suite.T(), err) - assert.Equal(suite.T(), 1, len(sum)) -} - // TestScanControllerGetScanLog ... func (suite *ControllerTestSuite) TestScanControllerGetScanLog() { mock.OnAnything(suite.ar, "HasUnscannableLayer").Return(false, nil).Once() @@ -493,6 +502,13 @@ func (suite *ControllerTestSuite) TestScanControllerGetScanLog() { mock.OnAnything(suite.taskMgr, "GetLog").Return([]byte("log"), nil).Once() + mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) { + walkFn := args.Get(2).(func(*artifact.Artifact) error) + walkFn(suite.artifact) + }).Once() + + mock.OnAnything(suite.accessoryMgr, "List").Return(nil, nil) + bytes, err := suite.c.GetScanLog(ctx, &artifact.Artifact{Artifact: art.Artifact{ID: 1, ProjectID: 1}}, "rp-uuid-001") require.NoError(suite.T(), err) assert.Condition(suite.T(), func() (success bool) { @@ -566,6 +582,21 @@ func (suite *ControllerTestSuite) TestScanAll() { { // no artifacts found when scan all executionID := int64(1) + rpts := []*scan.Report{ + {UUID: "uuid"}, + } + requiredPermission := []*types.Policy{ + { + Resource: rbac.ResourceRepository, + Action: rbac.ActionPull, + }, + { + Resource: rbac.ResourceRepository, + Action: rbac.ActionScannerPull, + }, + } + mock.OnAnything(suite.scanHandler, "MakePlaceHolder").Return(rpts, nil).Once() + mock.OnAnything(suite.scanHandler, "RequiredPermissions").Return(requiredPermission).Once() mock.OnAnything(suite.ar, "HasUnscannableLayer").Return(false, nil).Once() suite.execMgr.On( "Create", mock.Anything, "SCAN_ALL", int64(0), "SCHEDULE", @@ -607,8 +638,6 @@ func (suite *ControllerTestSuite) TestScanAll() { walkFn(suite.artifact) }).Once() - mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return(nil, nil).Once() - mock.OnAnything(suite.reportMgr, "Delete").Return(nil).Once() mock.OnAnything(suite.reportMgr, "Create").Return("uuid", nil).Once() mock.OnAnything(suite.taskMgr, "Create").Return(int64(0), fmt.Errorf("failed")).Once() @@ -635,16 +664,6 @@ func (suite *ControllerTestSuite) TestStopScanAll() { suite.NoError(err) } -func (suite *ControllerTestSuite) TestDeleteReports() { - suite.reportMgr.On("DeleteByDigests", context.TODO(), "digest").Return(nil).Once() - - suite.NoError(suite.c.DeleteReports(context.TODO(), "digest")) - - suite.reportMgr.On("DeleteByDigests", context.TODO(), "digest").Return(fmt.Errorf("failed")).Once() - - suite.Error(suite.c.DeleteReports(context.TODO(), "digest")) -} - func (suite *ControllerTestSuite) makeExtraAttrs(artifactID int64, reportUUIDs ...string) map[string]interface{} { b, _ := json.Marshal(map[string]interface{}{reportUUIDsKey: reportUUIDs}) @@ -654,57 +673,3 @@ func (suite *ControllerTestSuite) makeExtraAttrs(artifactID int64, reportUUIDs . return extraAttrs } - -func (suite *ControllerTestSuite) TestGenerateSBOMSummary() { - sum, err := suite.c.GetSBOMSummary(context.TODO(), suite.artifact, []string{v1.MimeTypeSBOMReport}) - suite.Nil(err) - suite.NotNil(sum) - status := sum["scan_status"] - suite.NotNil(status) - dgst := sum["sbom_digest"] - suite.NotNil(dgst) - suite.Equal("Success", status) - suite.Equal("sha256:1234567890", dgst) - tasks := []*task.Task{{Status: "Error"}} - suite.taskMgr.On("ListScanTasksByReportUUID", mock.Anything, "rp-uuid-004").Return(tasks, nil).Once() - sum2, err := suite.c.GetSummary(context.TODO(), suite.wrongArtifact, []string{v1.MimeTypeSBOMReport}) - suite.Nil(err) - suite.NotNil(sum2) - -} - -func TestIsSBOMMimeTypes(t *testing.T) { - // Test with a slice containing the SBOM mime type - assert.True(t, isSBOMMimeTypes([]string{v1.MimeTypeSBOMReport})) - - // Test with a slice not containing the SBOM mime type - assert.False(t, isSBOMMimeTypes([]string{"application/vnd.oci.image.manifest.v1+json"})) - - // 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)) -} - -func (suite *ControllerTestSuite) TestRetrieveStatusFromTask() { - tasks := []*task.Task{{Status: "Error"}} - suite.taskMgr.On("ListScanTasksByReportUUID", mock.Anything, "rp-uuid-004").Return(tasks, nil).Once() - status := suite.c.retrieveStatusFromTask(nil, "rp-uuid-004") - suite.Equal("Error", status) -} diff --git a/src/controller/scan/controller.go b/src/controller/scan/controller.go index 625e8f86f66..3452f145430 100644 --- a/src/controller/scan/controller.go +++ b/src/controller/scan/controller.go @@ -83,7 +83,7 @@ type Controller interface { // Returns: // map[string]interface{} : report summaries indexed by mime types // error : non nil error if any errors occurred - GetSummary(ctx context.Context, artifact *artifact.Artifact, mimeTypes []string) (map[string]interface{}, error) + GetSummary(ctx context.Context, artifact *artifact.Artifact, scanType string, mimeTypes []string) (map[string]interface{}, error) // Get the scan log for the specified artifact with the given digest // @@ -96,15 +96,6 @@ type Controller interface { // error : non nil error if any errors occurred GetScanLog(ctx context.Context, art *artifact.Artifact, uuid string) ([]byte, error) - // Delete the reports related with the specified digests - // - // Arguments: - // digests ...string : specify one or more digests whose reports will be deleted - // - // Returns: - // error : non nil error if any errors occurred - DeleteReports(ctx context.Context, digests ...string) error - // Scan all the artifacts // // Arguments: diff --git a/src/pkg/scan/dao/scan/report_test.go b/src/pkg/scan/dao/scan/report_test.go index ab46622894b..6f6b81d1464 100644 --- a/src/pkg/scan/dao/scan/report_test.go +++ b/src/pkg/scan/dao/scan/report_test.go @@ -54,14 +54,6 @@ func (suite *ReportTestSuite) SetupTest() { MimeType: v1.MimeTypeNativeReport, } suite.create(r) - sbomReport := &Report{ - UUID: "uuid3", - Digest: "digest1003", - RegistrationUUID: "ruuid", - MimeType: v1.MimeTypeSBOMReport, - Report: `{"sbom_digest": "sha256:abc"}`, - } - suite.create(sbomReport) } // TearDownTest clears enf for test case. @@ -113,17 +105,6 @@ func (suite *ReportTestSuite) TestReportUpdateReportData() { suite.Require().NoError(err) } -func (suite *ReportTestSuite) TestDeleteReportBySBOMDigest() { - l, err := suite.dao.List(orm.Context(), nil) - suite.Require().NoError(err) - suite.Equal(2, len(l)) - err = suite.dao.DeleteByExtraAttr(orm.Context(), v1.MimeTypeSBOMReport, "sbom_digest", "sha256:abc") - suite.Require().NoError(err) - l2, err := suite.dao.List(orm.Context(), nil) - suite.Require().NoError(err) - suite.Equal(1, len(l2)) -} - func (suite *ReportTestSuite) create(r *Report) { id, err := suite.dao.Create(orm.Context(), r) suite.Require().NoError(err) diff --git a/src/pkg/scan/handler.go b/src/pkg/scan/handler.go index f402107c70f..46ed7fc7df8 100644 --- a/src/pkg/scan/handler.go +++ b/src/pkg/scan/handler.go @@ -15,12 +15,15 @@ package scan import ( + "context" "time" + "github.com/goharbor/harbor/src/controller/artifact" "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/dao/scan" + "github.com/goharbor/harbor/src/pkg/scan/dao/scanner" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" ) @@ -44,8 +47,21 @@ type Handler interface { 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) + ReportHandler +} + +// ReportHandler handler for scan report, it could be sbom report or vulnerability report +type ReportHandler interface { + // URLParameter defines the parameters for scan report + URLParameter(sr *v1.ScanRequest) (string, error) + // Update update the report data in the database by UUID + Update(ctx context.Context, uuid string, report string) error + // MakePlaceHolder make the report place holder, if exist, delete it and create a new one + MakePlaceHolder(ctx context.Context, art *artifact.Artifact, r *scanner.Registration) (rps []*scan.Report, err error) + // GetPlaceHolder get the the report place holder + GetPlaceHolder(ctx context.Context, artRepo string, artDigest string, scannerUUID string, mimeType string) (rp *scan.Report, err error) + // GetSummary get the summary of the report + GetSummary(ctx context.Context, ar *artifact.Artifact, mimeTypes []string) (map[string]interface{}, error) } diff --git a/src/pkg/scan/job.go b/src/pkg/scan/job.go index c386628724c..f0db850e627 100644 --- a/src/pkg/scan/job.go +++ b/src/pkg/scan/job.go @@ -16,7 +16,6 @@ package scan import ( "bytes" - "context" "encoding/base64" "encoding/json" "fmt" @@ -35,7 +34,6 @@ import ( "github.com/goharbor/harbor/src/lib/config" "github.com/goharbor/harbor/src/lib/errors" "github.com/goharbor/harbor/src/pkg/robot/model" - "github.com/goharbor/harbor/src/pkg/scan/dao/scan" "github.com/goharbor/harbor/src/pkg/scan/dao/scanner" "github.com/goharbor/harbor/src/pkg/scan/report" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" @@ -243,7 +241,7 @@ 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")) - reportURLParameter, err := handler.ReportURLParameter(req) + reportURLParameter, err := handler.URLParameter(req) if err != nil { errs[i] = errors.Wrap(err, "scan job: get report url") return @@ -298,7 +296,7 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error { } for i, mimeType := range mimeTypes { - rp, err := getReportPlaceholder(ctx.SystemContext(), req.Artifact.Digest, r.UUID, mimeType, myLogger) + rp, err := handler.GetPlaceHolder(ctx.SystemContext(), req.Artifact.Repository, req.Artifact.Digest, r.UUID, mimeType) if err != nil { return err } @@ -314,30 +312,16 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error { // this is required since the top level layers relay on the vuln.Report struct that // contains additional metadata within the report which if stored in the new columns within the scan_report table // would be redundant - if err := report.Mgr.UpdateReportData(ctx.SystemContext(), rp.UUID, reportData); err != nil { + if err := handler.Update(ctx.SystemContext(), rp.UUID, reportData); err != nil { myLogger.Errorf("Failed to update report data for report %s, error %v", rp.UUID, err) return err } - myLogger.Debugf("Converted report ID %s to the new V2 schema", rp.UUID) } return nil } -func getReportPlaceholder(ctx context.Context, digest string, reportUUID string, mimeType string, logger logger.Interface) (*scan.Report, error) { - reports, err := report.Mgr.GetBy(ctx, digest, reportUUID, []string{mimeType}) - if err != nil { - logger.Error("Failed to get report for artifact %s of mimetype %s, error %v", digest, mimeType, err) - return nil, err - } - if len(reports) == 0 { - logger.Errorf("No report found for artifact %s of mimetype %s, error %v", digest, mimeType, err) - return nil, errors.NotFoundError(nil).WithMessage("no report found to update data") - } - return reports[0], nil -} - 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 { diff --git a/src/pkg/scan/job_test.go b/src/pkg/scan/job_test.go index ff00dd20f50..31b67311fbd 100644 --- a/src/pkg/scan/job_test.go +++ b/src/pkg/scan/job_test.go @@ -161,26 +161,6 @@ func (suite *JobTestSuite) TestJob() { require.NoError(suite.T(), err) } -func (suite *JobTestSuite) TestgetReportPlaceholder() { - dgst := "sha256:mydigest" - uuid := `7f20b1b9-6117-4a2e-820b-e4cc0401f15e` - scannerUUID := `7f20b1b9-6117-4a2e-820b-e4cc0401f15f` - rpt := &scan.Report{ - UUID: uuid, - RegistrationUUID: scannerUUID, - Digest: dgst, - MimeType: v1.MimeTypeDockerArtifact, - } - ctx := suite.Context() - rptID, err := report.Mgr.Create(ctx, rpt) - suite.reportIDs = append(suite.reportIDs, rptID) - require.NoError(suite.T(), err) - jobLogger := &mockjobservice.MockJobLogger{} - report, err := getReportPlaceholder(ctx, dgst, scannerUUID, v1.MimeTypeDockerArtifact, jobLogger) - require.NoError(suite.T(), err) - require.NotNil(suite.T(), report) -} - func (suite *JobTestSuite) TestfetchScanReportFromScanner() { vulnRpt := &vuln.Report{ GeneratedAt: time.Now().UTC().String(), diff --git a/src/pkg/scan/sbom/dao/dao.go b/src/pkg/scan/sbom/dao/dao.go new file mode 100644 index 00000000000..92ed678a9ed --- /dev/null +++ b/src/pkg/scan/sbom/dao/dao.go @@ -0,0 +1,126 @@ +// 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 dao + +import ( + "context" + "fmt" + + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/scan/sbom/model" +) + +func init() { + orm.RegisterModel(new(model.Report)) +} + +// DAO is the data access object interface for sbom report +type DAO interface { + // Create creates new report + Create(ctx context.Context, r *model.Report) (int64, error) + // DeleteMany delete the reports according to the query + DeleteMany(ctx context.Context, query q.Query) (int64, error) + // List lists the reports with given query parameters. + List(ctx context.Context, query *q.Query) ([]*model.Report, error) + // UpdateReportData only updates the `report` column with conditions matched. + UpdateReportData(ctx context.Context, uuid string, report string) error + // Update update report + Update(ctx context.Context, r *model.Report, cols ...string) error + // DeleteByExtraAttr delete the scan_report by mimeType and extra attribute + DeleteByExtraAttr(ctx context.Context, mimeType, attrName, attrValue string) error +} + +// New returns an instance of the default DAO +func New() DAO { + return &dao{} +} + +type dao struct{} + +// Create creates new sbom report +func (d *dao) Create(ctx context.Context, r *model.Report) (int64, error) { + o, err := orm.FromContext(ctx) + if err != nil { + return 0, err + } + return o.Insert(r) +} + +func (d *dao) DeleteMany(ctx context.Context, query q.Query) (int64, error) { + if len(query.Keywords) == 0 { + return 0, errors.New("delete all sbom reports at once is not allowed") + } + + qs, err := orm.QuerySetter(ctx, &model.Report{}, &query) + if err != nil { + return 0, err + } + + return qs.Delete() +} + +func (d *dao) List(ctx context.Context, query *q.Query) ([]*model.Report, error) { + qs, err := orm.QuerySetter(ctx, &model.Report{}, query) + if err != nil { + return nil, err + } + + reports := []*model.Report{} + if _, err = qs.All(&reports); err != nil { + return nil, err + } + + return reports, nil +} + +// UpdateReportData only updates the `report` column with conditions matched. +func (d *dao) UpdateReportData(ctx context.Context, uuid string, report string) error { + o, err := orm.FromContext(ctx) + if err != nil { + return err + } + + qt := o.QueryTable(new(model.Report)) + + data := make(orm.Params) + data["report"] = report + + _, err = qt.Filter("uuid", uuid).Update(data) + return err +} + +func (d *dao) Update(ctx context.Context, r *model.Report, cols ...string) error { + o, err := orm.FromContext(ctx) + if err != nil { + return err + } + if _, err := o.Update(r, cols...); err != nil { + return err + } + return nil +} + +func (d *dao) DeleteByExtraAttr(ctx context.Context, mimeType, attrName, attrValue string) error { + o, err := orm.FromContext(ctx) + if err != nil { + return err + } + delReportSQL := "delete from sbom_report where mime_type = ? and report::jsonb @> ?" + dgstJSONStr := fmt.Sprintf(`{"%s":"%s"}`, attrName, attrValue) + _, err = o.Raw(delReportSQL, mimeType, dgstJSONStr).Exec() + return err +} diff --git a/src/pkg/scan/sbom/dao/dao_test.go b/src/pkg/scan/sbom/dao/dao_test.go new file mode 100644 index 00000000000..2fcb7a3b0e3 --- /dev/null +++ b/src/pkg/scan/sbom/dao/dao_test.go @@ -0,0 +1,133 @@ +package dao + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/goharbor/harbor/src/lib/orm" + "github.com/goharbor/harbor/src/lib/q" + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" + "github.com/goharbor/harbor/src/pkg/scan/sbom/model" + htesting "github.com/goharbor/harbor/src/testing" +) + +// ReportTestSuite is test suite of testing report DAO. +type ReportTestSuite struct { + htesting.Suite + dao DAO +} + +// TestReport is the entry of ReportTestSuite. +func TestReport(t *testing.T) { + suite.Run(t, &ReportTestSuite{}) +} + +// SetupSuite prepares env for test suite. +func (suite *ReportTestSuite) SetupSuite() { + suite.Suite.SetupSuite() + suite.dao = New() +} + +// SetupTest prepares env for test case. +func (suite *ReportTestSuite) SetupTest() { + sbomReport := &model.Report{ + UUID: "uuid", + ArtifactID: 111, + RegistrationUUID: "ruuid", + MimeType: v1.MimeTypeSBOMReport, + ReportSummary: `{"sbom_digest": "sha256:abc"}`, + } + suite.create(sbomReport) +} + +// TearDownTest clears enf for test case. +func (suite *ReportTestSuite) TearDownTest() { + _, err := suite.dao.DeleteMany(orm.Context(), q.Query{Keywords: q.KeyWords{"uuid": "uuid"}}) + require.NoError(suite.T(), err) +} + +func (suite *ReportTestSuite) TestDeleteReportBySBOMDigest() { + l, err := suite.dao.List(orm.Context(), nil) + suite.Require().NoError(err) + suite.Equal(1, len(l)) + err = suite.dao.DeleteByExtraAttr(orm.Context(), v1.MimeTypeSBOMReport, "sbom_digest", "sha256:abc") + suite.Require().NoError(err) + l2, err := suite.dao.List(orm.Context(), nil) + suite.Require().NoError(err) + suite.Equal(0, len(l2)) +} + +func (suite *ReportTestSuite) create(r *model.Report) { + id, err := suite.dao.Create(orm.Context(), r) + suite.Require().NoError(err) + suite.Require().Condition(func() (success bool) { + success = id > 0 + return + }) +} + +// TestReportUpdateReportData tests update the report data. +func (suite *ReportTestSuite) TestReportUpdateReportData() { + err := suite.dao.UpdateReportData(orm.Context(), "uuid", "{}") + suite.Require().NoError(err) + + l, err := suite.dao.List(orm.Context(), q.New(q.KeyWords{"uuid": "uuid"})) + suite.Require().NoError(err) + suite.Require().Equal(1, len(l)) + suite.Equal("{}", l[0].ReportSummary) + + err = suite.dao.UpdateReportData(orm.Context(), "uuid", "{\"a\": 900}") + suite.Require().NoError(err) +} + +func (suite *ReportTestSuite) TestUpdate() { + err := suite.dao.Update(orm.Context(), &model.Report{ + UUID: "uuid", + ArtifactID: 111, + RegistrationUUID: "ruuid", + MimeType: v1.MimeTypeSBOMReport, + ReportSummary: `{"sbom_digest": "sha256:abc"}`, + }, "report") + suite.Require().NoError(err) + query1 := &q.Query{ + PageSize: 1, + PageNumber: 1, + Keywords: map[string]interface{}{ + "artifact_id": 111, + "registration_uuid": "ruuid", + "mime_type": v1.MimeTypeSBOMReport, + }, + } + l, err := suite.dao.List(orm.Context(), query1) + suite.Require().Equal(1, len(l)) + suite.Equal(l[0].ReportSummary, `{"sbom_digest": "sha256:abc"}`) +} + +// TestReportList tests list reports with query parameters. +func (suite *ReportTestSuite) TestReportList() { + query1 := &q.Query{ + PageSize: 1, + PageNumber: 1, + Keywords: map[string]interface{}{ + "artifact_id": 111, + "registration_uuid": "ruuid", + "mime_type": v1.MimeTypeSBOMReport, + }, + } + l, err := suite.dao.List(orm.Context(), query1) + suite.Require().NoError(err) + suite.Require().Equal(1, len(l)) + + query2 := &q.Query{ + PageSize: 1, + PageNumber: 1, + Keywords: map[string]interface{}{ + "artifact_id": 222, + }, + } + l, err = suite.dao.List(orm.Context(), query2) + suite.Require().NoError(err) + suite.Require().Equal(0, len(l)) +} diff --git a/src/pkg/scan/sbom/manager.go b/src/pkg/scan/sbom/manager.go new file mode 100644 index 00000000000..75ccad1abd1 --- /dev/null +++ b/src/pkg/scan/sbom/manager.go @@ -0,0 +1,203 @@ +// 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" + + "github.com/google/uuid" + + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/q" + "github.com/goharbor/harbor/src/pkg/scan/sbom/dao" + "github.com/goharbor/harbor/src/pkg/scan/sbom/model" +) + +var ( + // Mgr is the global sbom report manager + Mgr = NewManager() +) + +// Manager is used to manage the sbom reports. +type Manager interface { + // Create a new report record. + // + // Arguments: + // ctx context.Context : the context for this method + // r *scan.Report : report model to be created + // + // Returns: + // string : uuid of the new report + // error : non nil error if any errors occurred + // + Create(ctx context.Context, r *model.Report) (string, error) + + // Delete delete report by uuid + // + // Arguments: + // ctx context.Context : the context for this method + // uuid string : uuid of the report to delete + // + // Returns: + // error : non nil error if any errors occurred + // + Delete(ctx context.Context, uuid string) error + + // UpdateReportData update the report data (with JSON format) of the given report. + // + // Arguments: + // ctx context.Context : the context for this method + // uuid string : uuid to identify the report + // report string : report JSON data + // + // Returns: + // error : non nil error if any errors occurred + // + UpdateReportData(ctx context.Context, uuid string, report string) error + + // GetBy the reports for the given digest by other properties. + // + // Arguments: + // ctx context.Context : the context for this method + // artifact_id int64 : the artifact id + // registrationUUID string : [optional] the report generated by which registration. + // If it is empty, reports by all the registrations are retrieved. + // mimeTypes []string : [optional] mime types of the reports requiring + // If empty array is specified, reports with all the supported mimes are retrieved. + // + // Returns: + // []*Report : sbom report list + // error : non nil error if any errors occurred + GetBy(ctx context.Context, artifactID int64, registrationUUID string, mimeType string, mediaType string) ([]*model.Report, error) + // List reports according to the query + // + // Arguments: + // ctx context.Context : the context for this method + // query *q.Query : the query to list the reports + // + // Returns: + // []*scan.Report : report list + // error : non nil error if any errors occurred + List(ctx context.Context, query *q.Query) ([]*model.Report, error) + + // Update update report information + Update(ctx context.Context, r *model.Report, cols ...string) error + // DeleteByExtraAttr delete scan_report by sbom_digest + DeleteByExtraAttr(ctx context.Context, mimeType, attrName, attrValue string) error + // DeleteByArtifactID delete sbom report by artifact id + DeleteByArtifactID(ctx context.Context, artifactID int64) error +} + +// basicManager is a default implementation of report manager. +type basicManager struct { + dao dao.DAO +} + +// NewManager news basic manager. +func NewManager() Manager { + return &basicManager{ + dao: dao.New(), + } +} + +// Create ... +func (bm *basicManager) Create(ctx context.Context, r *model.Report) (string, error) { + // Validate report object + if r == nil { + return "", errors.New("nil sbom report object") + } + + if r.ArtifactID == 0 || len(r.RegistrationUUID) == 0 || len(r.MimeType) == 0 || len(r.MediaType) == 0 { + return "", errors.New("malformed sbom report object") + } + + r.UUID = uuid.New().String() + + // Insert + if _, err := bm.dao.Create(ctx, r); err != nil { + return "", err + } + + return r.UUID, nil +} + +func (bm *basicManager) Delete(ctx context.Context, uuid string) error { + query := q.Query{Keywords: q.KeyWords{"uuid": uuid}} + count, err := bm.dao.DeleteMany(ctx, query) + if err != nil { + return err + } + if count == 0 { + return errors.Errorf("no report with uuid %s deleted", uuid) + } + return nil +} + +// GetBy ... +func (bm *basicManager) GetBy(ctx context.Context, artifactID int64, registrationUUID string, + mimeType string, mediaType string) ([]*model.Report, error) { + if artifactID == 0 { + return nil, errors.New("no artifact id to get sbom report data") + } + + kws := make(map[string]interface{}) + kws["artifact_id"] = artifactID + if len(registrationUUID) > 0 { + kws["registration_uuid"] = registrationUUID + } + if len(mimeType) > 0 { + kws["mine_type"] = mimeType + } + if len(mediaType) > 0 { + kws["media_type"] = mediaType + } + // Query all + query := &q.Query{ + PageNumber: 0, + Keywords: kws, + } + + return bm.dao.List(ctx, query) +} + +// UpdateReportData ... +func (bm *basicManager) UpdateReportData(ctx context.Context, uuid string, report string) error { + if len(uuid) == 0 { + return errors.New("missing uuid") + } + + if len(report) == 0 { + return errors.New("missing report JSON data") + } + + return bm.dao.UpdateReportData(ctx, uuid, report) +} + +func (bm *basicManager) List(ctx context.Context, query *q.Query) ([]*model.Report, error) { + return bm.dao.List(ctx, query) +} + +func (bm *basicManager) Update(ctx context.Context, r *model.Report, cols ...string) error { + return bm.dao.Update(ctx, r, cols...) +} + +func (bm *basicManager) DeleteByExtraAttr(ctx context.Context, mimeType, attrName, attrValue string) error { + return bm.dao.DeleteByExtraAttr(ctx, mimeType, attrName, attrValue) +} + +func (bm *basicManager) DeleteByArtifactID(ctx context.Context, artifactID int64) error { + _, err := bm.dao.DeleteMany(ctx, *q.New(q.KeyWords{"ArtifactID": artifactID})) + return err +} diff --git a/src/pkg/scan/sbom/model/report.go b/src/pkg/scan/sbom/model/report.go new file mode 100644 index 00000000000..6d1424b1ac3 --- /dev/null +++ b/src/pkg/scan/sbom/model/report.go @@ -0,0 +1,46 @@ +// 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 + +import v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" + +// Report sbom report. +// Identified by the `artifact_id`, `registration_uuid` and `mime_type`. +type Report struct { + ID int64 `orm:"pk;auto;column(id)"` + UUID string `orm:"unique;column(uuid)"` + ArtifactID int64 `orm:"column(artifact_id)"` + RegistrationUUID string `orm:"column(registration_uuid)"` + MimeType string `orm:"column(mime_type)"` + MediaType string `orm:"column(media_type)"` + ReportSummary string `orm:"column(report);type(json)"` +} + +// TableName for sbom report +func (r *Report) TableName() string { + return "sbom_report" +} + +// RawSBOMReport the original report of the sbom report get from scanner +type RawSBOMReport struct { + // Time of generating this report + GeneratedAt string `json:"generated_at"` + // Scanner of generating this report + Scanner *v1.Scanner `json:"scanner"` + // MediaType the media type of the report, e.g. application/spdx+json + MediaType string `json:"media_type"` + // SBOM sbom content + SBOM map[string]interface{} `json:"sbom,omitempty"` +} diff --git a/src/pkg/scan/sbom/sbom.go b/src/pkg/scan/sbom/sbom.go index 9a819d1236f..876d717ce53 100644 --- a/src/pkg/scan/sbom/sbom.go +++ b/src/pkg/scan/sbom/sbom.go @@ -23,18 +23,26 @@ import ( "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/controller/artifact" + scanCtl "github.com/goharbor/harbor/src/controller/scan" "github.com/goharbor/harbor/src/jobservice/job" + "github.com/goharbor/harbor/src/jobservice/logger" + "github.com/goharbor/harbor/src/lib/config" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/lib/orm" "github.com/goharbor/harbor/src/pkg/permission/types" "github.com/goharbor/harbor/src/pkg/robot/model" "github.com/goharbor/harbor/src/pkg/scan" + scanModel "github.com/goharbor/harbor/src/pkg/scan/dao/scan" + "github.com/goharbor/harbor/src/pkg/scan/dao/scanner" + sbom "github.com/goharbor/harbor/src/pkg/scan/sbom/model" + "github.com/goharbor/harbor/src/pkg/task" + + sc "github.com/goharbor/harbor/src/controller/scanner" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" - "github.com/goharbor/harbor/src/pkg/scan/vuln" ) const ( @@ -43,50 +51,42 @@ const ( ) func init() { - scan.RegisterScanHanlder(v1.ScanTypeSbom, &scanHandler{GenAccessoryFunc: scan.GenAccessoryArt, RegistryServer: registry}) + scan.RegisterScanHanlder(v1.ScanTypeSbom, &scanHandler{ + GenAccessoryFunc: scan.GenAccessoryArt, + RegistryServer: registry, + SBOMMgrFunc: func() Manager { return Mgr }, + TaskMgrFunc: func() task.Manager { return task.Mgr }, + ArtifactControllerFunc: func() artifact.Controller { return artifact.Ctl }, + ScanControllerFunc: func() scanCtl.Controller { return scanCtl.DefaultController }, + ScannerControllerFunc: func() sc.Controller { return sc.DefaultController }, + cloneCtx: orm.Clone, + }) } -// ScanHandler defines the Handler to generate sbom +// 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, bool) + GenAccessoryFunc func(scanRep v1.ScanRequest, sbomContent []byte, labels map[string]string, mediaType string, robot *model.Robot) (string, error) + RegistryServer func(ctx context.Context) (string, bool) + SBOMMgrFunc func() Manager + TaskMgrFunc func() task.Manager + ArtifactControllerFunc func() artifact.Controller + ScanControllerFunc func() scanCtl.Controller + ScannerControllerFunc func() sc.Controller + cloneCtx func(ctx context.Context) context.Context } // RequestProducesMineTypes defines the mine types produced by the scan handler -func (v *scanHandler) RequestProducesMineTypes() []string { +func (h *scanHandler) RequestProducesMineTypes() []string { return []string{v1.MimeTypeSBOMReport} } // RequestParameters defines the parameters for scan request -func (v *scanHandler) RequestParameters() map[string]interface{} { +func (h *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) { +func (h *scanHandler) PostScan(ctx job.Context, sr *v1.ScanRequest, _ *scanModel.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error) { sbomContent, s, err := retrieveSBOMContent(rawReport) if err != nil { return "", err @@ -96,22 +96,45 @@ func (v *scanHandler) PostScan(ctx job.Context, sr *v1.ScanRequest, _ *scanModel Artifact: sr.Artifact, } // the registry server url is core by default, need to replace it with real registry server url - scanReq.Registry.URL, scanReq.Registry.Insecure = v.RegistryServer(ctx.SystemContext()) + scanReq.Registry.URL, scanReq.Registry.Insecure = h.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) + dgst, err := h.GenAccessoryFunc(scanReq, sbomContent, h.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", s) + return h.generateReport(startTime, sr.Artifact.Repository, dgst, "Success", s) +} + +// URLParameter defines the parameters for scan report url +func (h *scanHandler) URLParameter(_ *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 (h *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, + }, + } } // annotations defines the annotations for the accessory artifact -func (v *scanHandler) annotations() map[string]string { +func (h *scanHandler) annotations() map[string]string { t := time.Now().Format(time.RFC3339) return map[string]string{ "created": t, @@ -121,7 +144,7 @@ func (v *scanHandler) annotations() map[string]string { } } -func (v *scanHandler) generateReport(startTime time.Time, repository, digest, status string, scanner *v1.Scanner) (string, error) { +func (h *scanHandler) generateReport(startTime time.Time, repository, digest, status string, scanner *v1.Scanner) (string, error) { summary := sbom.Summary{} endTime := time.Now() summary[sbom.StartTime] = startTime @@ -138,6 +161,14 @@ func (v *scanHandler) generateReport(startTime time.Time, repository, digest, st return string(rep), nil } +func (h *scanHandler) Update(ctx context.Context, uuid string, report string) error { + mgr := h.SBOMMgrFunc() + if err := mgr.UpdateReportData(ctx, uuid, report); err != nil { + return err + } + return nil +} + // extract server name from config, and remove the protocol prefix func registry(ctx context.Context) (string, bool) { cfgMgr, ok := config.FromContext(ctx) @@ -153,7 +184,7 @@ func registry(ctx context.Context) (string, bool) { // retrieveSBOMContent retrieves the "sbom" field from the raw report func retrieveSBOMContent(rawReport string) ([]byte, *v1.Scanner, error) { - rpt := vuln.Report{} + rpt := sbom.RawSBOMReport{} err := json.Unmarshal([]byte(rawReport), &rpt) if err != nil { return nil, nil, err @@ -164,3 +195,153 @@ func retrieveSBOMContent(rawReport string) ([]byte, *v1.Scanner, error) { } return sbomContent, rpt.Scanner, nil } + +func (h *scanHandler) MakePlaceHolder(ctx context.Context, art *artifact.Artifact, r *scanner.Registration) (rps []*scanModel.Report, err error) { + var reports []*scanModel.Report + mgr := h.SBOMMgrFunc() + mimeTypes := r.GetProducesMimeTypes(art.ManifestMediaType, v1.ScanTypeSbom) + if len(mimeTypes) == 0 { + return nil, errors.New("no mime types to make report placeholders") + } + sbomReports, err := mgr.GetBy(h.cloneCtx(ctx), art.ID, r.UUID, mimeTypes[0], sbomMediaTypeSpdx) + if err != nil { + return nil, err + } + if err := h.deleteSBOMAccessories(ctx, sbomReports); err != nil { + return nil, err + } + for _, mt := range mimeTypes { + report := &sbom.Report{ + ArtifactID: art.ID, + RegistrationUUID: r.UUID, + MimeType: mt, + MediaType: sbomMediaTypeSpdx, + } + + create := func(ctx context.Context) error { + reportUUID, err := mgr.Create(ctx, report) + if err != nil { + return err + } + report.UUID = reportUUID + return nil + } + + if err := orm.WithTransaction(create)(orm.SetTransactionOpNameToContext(ctx, "tx-make-report-placeholder-sbom")); err != nil { + return nil, err + } + reports = append(reports, &scanModel.Report{ + RegistrationUUID: r.UUID, + MimeType: mt, + UUID: report.UUID, + }) + } + + return reports, nil +} + +// deleteSBOMAccessories delete the sbom accessory in reports +func (h *scanHandler) deleteSBOMAccessories(ctx context.Context, reports []*sbom.Report) error { + mgr := h.SBOMMgrFunc() + for _, rpt := range reports { + if rpt.MimeType != v1.MimeTypeSBOMReport { + continue + } + if err := h.deleteSBOMAccessory(ctx, rpt.ReportSummary); err != nil { + return err + } + if err := mgr.Delete(ctx, rpt.UUID); err != nil { + return err + } + } + return nil +} + +// deleteSBOMAccessory check if current report has sbom accessory info, if there is, delete it +func (h *scanHandler) deleteSBOMAccessory(ctx context.Context, report string) error { + if len(report) == 0 { + return nil + } + sbomSummary := sbom.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 + } + artifactCtl := h.ArtifactControllerFunc() + art, err := artifactCtl.GetByReference(ctx, repo, dgst, nil) + if errors.IsNotFoundErr(err) { + return nil + } + if err != nil { + return err + } + if art == nil { + return nil + } + err = artifactCtl.Delete(ctx, art.ID) + if errors.IsNotFoundErr(err) { + return nil + } + return err +} + +func (h *scanHandler) GetPlaceHolder(ctx context.Context, artRepo string, artDigest, scannerUUID string, mimeType string) (rp *scanModel.Report, err error) { + artifactCtl := h.ArtifactControllerFunc() + a, err := artifactCtl.GetByReference(ctx, artRepo, artDigest, nil) + if err != nil { + return nil, err + } + mgr := h.SBOMMgrFunc() + rpts, err := mgr.GetBy(ctx, a.ID, scannerUUID, mimeType, sbomMediaTypeSpdx) + if err != nil { + logger.Errorf("Failed to get report for artifact %s@%s of mimetype %s, error %v", artRepo, artDigest, mimeType, err) + return nil, err + } + if len(rpts) == 0 { + logger.Errorf("No report found for artifact %s@%s of mimetype %s, error %v", artRepo, artDigest, mimeType, err) + return nil, errors.NotFoundError(nil).WithMessage("no report found to update data") + } + return &scanModel.Report{ + UUID: rpts[0].UUID, + MimeType: rpts[0].MimeType, + }, nil +} + +func (h *scanHandler) GetSummary(ctx context.Context, art *artifact.Artifact, mimeTypes []string) (map[string]interface{}, error) { + if len(mimeTypes) == 0 { + return nil, errors.New("no mime types to get report summaries") + } + if art == nil { + return nil, errors.New("no way to get report summaries for nil artifact") + } + ds := h.ScannerControllerFunc() + r, err := ds.GetRegistrationByProject(ctx, art.ProjectID) + if err != nil { + return nil, errors.Wrap(err, "get sbom summary failed") + } + reports, err := h.SBOMMgrFunc().GetBy(ctx, art.ID, r.UUID, mimeTypes[0], sbomMediaTypeSpdx) + if err != nil { + return nil, err + } + if len(reports) == 0 { + return map[string]interface{}{}, nil + } + reportContent := reports[0].ReportSummary + result := map[string]interface{}{} + if len(reportContent) == 0 { + status := h.TaskMgrFunc().RetrieveStatusFromTask(ctx, reports[0].UUID) + if len(status) > 0 { + result[sbom.ReportID] = reports[0].UUID + result[sbom.ScanStatus] = status + } + log.Debug("no content for current report") + return result, nil + } + err = json.Unmarshal([]byte(reportContent), &result) + return result, err +} diff --git a/src/pkg/scan/sbom/sbom_test.go b/src/pkg/scan/sbom/sbom_test.go index c1e0cd9721c..53c5a594813 100644 --- a/src/pkg/scan/sbom/sbom_test.go +++ b/src/pkg/scan/sbom/sbom_test.go @@ -6,15 +6,42 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + sc "github.com/goharbor/harbor/src/controller/scan" + "github.com/goharbor/harbor/src/controller/scanner" + "github.com/goharbor/harbor/src/lib/orm" + art "github.com/goharbor/harbor/src/pkg/artifact" + sbomModel "github.com/goharbor/harbor/src/pkg/scan/sbom/model" + htesting "github.com/goharbor/harbor/src/testing" + artifactTest "github.com/goharbor/harbor/src/testing/controller/artifact" + ormtesting "github.com/goharbor/harbor/src/testing/lib/orm" + "github.com/goharbor/harbor/src/testing/mock" + "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/controller/artifact" "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/pkg/task" + scanTest "github.com/goharbor/harbor/src/testing/controller/scan" + scannerTest "github.com/goharbor/harbor/src/testing/controller/scanner" "github.com/goharbor/harbor/src/testing/jobservice" - - "github.com/stretchr/testify/suite" + sbomTest "github.com/goharbor/harbor/src/testing/pkg/scan/sbom" + taskTest "github.com/goharbor/harbor/src/testing/pkg/task" ) +var registeredScanner = &scanner.Registration{ + UUID: "uuid", + Metadata: &v1.ScannerAdapterMetadata{ + Capabilities: []*v1.ScannerCapability{ + {Type: v1.ScanTypeVulnerability, ConsumesMimeTypes: []string{v1.MimeTypeDockerArtifact}, ProducesMimeTypes: []string{v1.MimeTypeGenericVulnerabilityReport}}, + {Type: v1.ScanTypeSbom, ConsumesMimeTypes: []string{v1.MimeTypeDockerArtifact}, ProducesMimeTypes: []string{v1.MimeTypeSBOMReport}}, + }, + }, +} + func Test_scanHandler_ReportURLParameter(t *testing.T) { type args struct { in0 *v1.ScanRequest @@ -30,13 +57,13 @@ func Test_scanHandler_ReportURLParameter(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := &scanHandler{} - got, err := v.ReportURLParameter(tt.args.in0) + got, err := v.URLParameter(tt.args.in0) if (err != nil) != tt.wantErr { - t.Errorf("ReportURLParameter() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("URLParameter() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { - t.Errorf("ReportURLParameter() got = %v, want %v", got, tt.want) + t.Errorf("URLParameter() got = %v, want %v", got, tt.want) } }) } @@ -97,22 +124,53 @@ func mockGenAccessory(scanRep v1.ScanRequest, sbomContent []byte, labels map[str return "sha256:1234567890", nil } -type ExampleTestSuite struct { - handler *scanHandler - suite.Suite +type SBOMTestSuite struct { + htesting.Suite + handler *scanHandler + sbomManager *sbomTest.Manager + taskMgr *taskTest.Manager + artifactCtl *artifactTest.Controller + artifact *artifact.Artifact + wrongArtifact *artifact.Artifact + scanController *scanTest.Controller + scannerController *scannerTest.Controller } -func (suite *ExampleTestSuite) SetupSuite() { +func (suite *SBOMTestSuite) SetupSuite() { + suite.sbomManager = &sbomTest.Manager{} + suite.taskMgr = &taskTest.Manager{} + suite.artifactCtl = &artifactTest.Controller{} + suite.scannerController = &scannerTest.Controller{} + suite.scanController = &scanTest.Controller{} + suite.handler = &scanHandler{ - GenAccessoryFunc: mockGenAccessory, - RegistryServer: mockGetRegistry, + GenAccessoryFunc: mockGenAccessory, + RegistryServer: mockGetRegistry, + SBOMMgrFunc: func() Manager { return suite.sbomManager }, + TaskMgrFunc: func() task.Manager { return suite.taskMgr }, + ArtifactControllerFunc: func() artifact.Controller { return suite.artifactCtl }, + ScanControllerFunc: func() sc.Controller { return suite.scanController }, + ScannerControllerFunc: func() scanner.Controller { return suite.scannerController }, + cloneCtx: func(ctx context.Context) context.Context { + return ctx + }, } + + suite.artifact = &artifact.Artifact{Artifact: art.Artifact{ID: 1}} + suite.artifact.Type = "IMAGE" + suite.artifact.ProjectID = 1 + suite.artifact.RepositoryName = "library/photon" + suite.artifact.Digest = "digest-code" + suite.artifact.ManifestMediaType = v1.MimeTypeDockerArtifact + + suite.wrongArtifact = &artifact.Artifact{Artifact: art.Artifact{ID: 2, ProjectID: 1}} + suite.wrongArtifact.Digest = "digest-wrong" } -func (suite *ExampleTestSuite) TearDownSuite() { +func (suite *SBOMTestSuite) TearDownSuite() { } -func (suite *ExampleTestSuite) TestPostScan() { +func (suite *SBOMTestSuite) TestPostScan() { req := &v1.ScanRequest{ Registry: &v1.Registry{ URL: "myregistry.example.com", @@ -134,6 +192,62 @@ func (suite *ExampleTestSuite) TestPostScan() { suite.Require().NotEmpty(accessory) } +func (suite *SBOMTestSuite) TestMakeReportPlaceHolder() { + ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{}) + art := &artifact.Artifact{Artifact: art.Artifact{ID: 1, Digest: "digest", ManifestMediaType: v1.MimeTypeDockerArtifact}} + r := &scanner.Registration{ + UUID: "uuid", + Metadata: &v1.ScannerAdapterMetadata{ + Capabilities: []*v1.ScannerCapability{ + {Type: v1.ScanTypeVulnerability, ConsumesMimeTypes: []string{v1.MimeTypeDockerArtifact}, ProducesMimeTypes: []string{v1.MimeTypeGenericVulnerabilityReport}}, + {Type: v1.ScanTypeSbom, ConsumesMimeTypes: []string{v1.MimeTypeDockerArtifact}, ProducesMimeTypes: []string{v1.MimeTypeSBOMReport}}, + }, + }, + } + mock.OnAnything(suite.sbomManager, "GetBy").Return([]*sbomModel.Report{{UUID: "uuid"}}, nil).Once() + mock.OnAnything(suite.sbomManager, "Create").Return("uuid", nil).Once() + mock.OnAnything(suite.sbomManager, "Delete").Return(nil).Once() + mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return([]*task.Task{{Status: "Success"}}, nil) + rps, err := suite.handler.MakePlaceHolder(ctx, art, r) + require.NoError(suite.T(), err) + suite.Equal(1, len(rps)) +} + +func (suite *SBOMTestSuite) TestGetSBOMSummary() { + r := registeredScanner + rpts := []*sbomModel.Report{ + {UUID: "rp-uuid-004", MimeType: v1.MimeTypeSBOMReport, ReportSummary: `{"scan_status":"Success", "sbom_digest": "sha256:1234567890"}`}, + } + mock.OnAnything(suite.scannerController, "GetRegistrationByProject").Return(r, nil) + mock.OnAnything(suite.sbomManager, "GetBy").Return(rpts, nil) + sum, err := suite.handler.GetSummary(context.TODO(), suite.artifact, []string{v1.MimeTypeSBOMReport}) + suite.Nil(err) + suite.NotNil(sum) + status := sum["scan_status"] + suite.NotNil(status) + dgst := sum["sbom_digest"] + suite.NotNil(dgst) + suite.Equal("Success", status) + suite.Equal("sha256:1234567890", dgst) + tasks := []*task.Task{{Status: "Error"}} + suite.taskMgr.On("ListScanTasksByReportUUID", mock.Anything, "rp-uuid-004").Return(tasks, nil).Once() + sum2, err := suite.handler.GetSummary(context.TODO(), suite.wrongArtifact, []string{v1.MimeTypeSBOMReport}) + suite.Nil(err) + suite.NotNil(sum2) + +} + +func (suite *SBOMTestSuite) TestGetReportPlaceHolder() { + mock.OnAnything(suite.sbomManager, "GetBy").Return([]*sbomModel.Report{{UUID: "uuid"}}, nil).Once() + mock.OnAnything(suite.artifactCtl, "GetByReference").Return(suite.artifact, nil).Twice() + rp, err := suite.handler.GetPlaceHolder(nil, "repo", "digest", "scannerUUID", "mimeType") + require.NoError(suite.T(), err) + suite.Equal("uuid", rp.UUID) + mock.OnAnything(suite.sbomManager, "GetBy").Return(nil, nil).Once() + rp, err = suite.handler.GetPlaceHolder(nil, "repo", "digest", "scannerUUID", "mimeType") + require.Error(suite.T(), err) +} + func TestExampleTestSuite(t *testing.T) { - suite.Run(t, &ExampleTestSuite{}) + suite.Run(t, &SBOMTestSuite{}) } diff --git a/src/pkg/scan/vulnerability/vul.go b/src/pkg/scan/vulnerability/vul.go index 2e9194c4a07..1c4a579eb43 100644 --- a/src/pkg/scan/vulnerability/vul.go +++ b/src/pkg/scan/vulnerability/vul.go @@ -15,38 +15,227 @@ package vulnerability import ( + "context" + "sync" "time" "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/controller/artifact" + scanCtl "github.com/goharbor/harbor/src/controller/scan" "github.com/goharbor/harbor/src/jobservice/job" + "github.com/goharbor/harbor/src/jobservice/logger" + "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/log" + "github.com/goharbor/harbor/src/lib/orm" "github.com/goharbor/harbor/src/pkg/permission/types" "github.com/goharbor/harbor/src/pkg/robot/model" scanJob "github.com/goharbor/harbor/src/pkg/scan" "github.com/goharbor/harbor/src/pkg/scan/dao/scan" + "github.com/goharbor/harbor/src/pkg/scan/dao/scanner" "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" + "github.com/goharbor/harbor/src/pkg/task" ) func init() { - scanJob.RegisterScanHanlder(v1.ScanTypeVulnerability, &ScanHandler{}) + scanJob.RegisterScanHanlder(v1.ScanTypeVulnerability, &scanHandler{ + reportConverter: postprocessors.Converter, + ReportMgrFunc: func() report.Manager { return report.Mgr }, + TaskMgrFunc: func() task.Manager { return task.Mgr }, + ScanControllerFunc: func() scanCtl.Controller { return scanCtl.DefaultController }, + cloneCtx: orm.Clone, + }) } -// ScanHandler defines the handler for scan vulnerability -type ScanHandler struct { +// scanHandler defines the handler for scan vulnerability +type scanHandler struct { + reportConverter postprocessors.NativeScanReportConverter + ReportMgrFunc func() report.Manager + TaskMgrFunc func() task.Manager + ScanControllerFunc func() scanCtl.Controller + cloneCtx func(ctx context.Context) context.Context +} + +func (h *scanHandler) MakePlaceHolder(ctx context.Context, art *artifact.Artifact, + r *scanner.Registration) (rps []*scan.Report, err error) { + mimeTypes := r.GetProducesMimeTypes(art.ManifestMediaType, v1.ScanTypeVulnerability) + reportMgr := h.ReportMgrFunc() + oldReports, err := reportMgr.GetBy(h.cloneCtx(ctx), art.Digest, r.UUID, mimeTypes) + if err != nil { + return nil, err + } + + if err := h.assembleReports(ctx, oldReports...); err != nil { + return nil, err + } + + if len(oldReports) > 0 { + for _, oldReport := range oldReports { + if !job.Status(oldReport.Status).Final() { + return nil, errors.ConflictError(nil).WithMessage("a previous scan process is %s", oldReport.Status) + } + } + + for _, oldReport := range oldReports { + if err := reportMgr.Delete(ctx, oldReport.UUID); err != nil { + return nil, err + } + } + } + + var reports []*scan.Report + + for _, pm := range r.GetProducesMimeTypes(art.ManifestMediaType, v1.ScanTypeVulnerability) { + rpt := &scan.Report{ + Digest: art.Digest, + RegistrationUUID: r.UUID, + MimeType: pm, + } + + create := func(ctx context.Context) error { + reportUUID, err := reportMgr.Create(ctx, rpt) + if err != nil { + return err + } + rpt.UUID = reportUUID + + return nil + } + + if err := orm.WithTransaction(create)(orm.SetTransactionOpNameToContext(ctx, "tx-make-report-placeholder")); err != nil { + return nil, err + } + + reports = append(reports, rpt) + } + + return reports, nil +} + +func (h *scanHandler) assembleReports(ctx context.Context, reports ...*scan.Report) error { + reportUUIDs := make([]string, len(reports)) + for i, report := range reports { + reportUUIDs[i] = report.UUID + } + + tasks, err := h.listScanTasks(ctx, reportUUIDs) + if err != nil { + return err + } + + reportUUIDToTasks := map[string]*task.Task{} + for _, task := range tasks { + for _, reportUUID := range scanCtl.GetReportUUIDs(task.ExtraAttrs) { + reportUUIDToTasks[reportUUID] = task + } + } + + for _, report := range reports { + if task, ok := reportUUIDToTasks[report.UUID]; ok { + report.Status = task.Status + report.StartTime = task.StartTime + report.EndTime = task.EndTime + } else { + report.Status = job.ErrorStatus.String() + } + + completeReport, err := h.reportConverter.FromRelationalSchema(ctx, report.UUID, report.Digest, report.Report) + if err != nil { + return err + } + report.Report = completeReport + } + + return nil +} + +// listScanTasks returns the tasks of the reports +func (h *scanHandler) listScanTasks(ctx context.Context, reportUUIDs []string) ([]*task.Task, error) { + if len(reportUUIDs) == 0 { + return nil, nil + } + + tasks := make([]*task.Task, len(reportUUIDs)) + errs := make([]error, len(reportUUIDs)) + + var wg sync.WaitGroup + for i, reportUUID := range reportUUIDs { + wg.Add(1) + + go func(ix int, reportUUID string) { + defer wg.Done() + + task, err := h.getScanTask(h.cloneCtx(ctx), reportUUID) + if err == nil { + tasks[ix] = task + } else if !errors.IsNotFoundErr(err) { + errs[ix] = err + } else { + log.G(ctx).Warningf("task for the scan report %s not found", reportUUID) + } + }(i, reportUUID) + } + wg.Wait() + + for _, err := range errs { + if err != nil { + return nil, err + } + } + + var results []*task.Task + for _, task := range tasks { + if task != nil { + results = append(results, task) + } + } + + return results, nil +} + +func (h *scanHandler) getScanTask(ctx context.Context, reportUUID string) (*task.Task, error) { + // NOTE: the method uses the postgres' unique operations and should consider here if support other database in the future. + taskMgr := h.TaskMgrFunc() + tasks, err := taskMgr.ListScanTasksByReportUUID(ctx, reportUUID) + if err != nil { + return nil, err + } + + if len(tasks) == 0 { + return nil, errors.NotFoundError(nil).WithMessage("task for report %s not found", reportUUID) + } + + return tasks[0], nil +} + +func (h *scanHandler) GetPlaceHolder(ctx context.Context, _ string, artDigest, scannerUUID string, + mimeType string) (rp *scan.Report, err error) { + reportMgr := h.ReportMgrFunc() + reports, err := reportMgr.GetBy(ctx, artDigest, scannerUUID, []string{mimeType}) + if err != nil { + logger.Errorf("failed to get report for artifact %s of mimetype %s, error %v", artDigest, mimeType, err) + return nil, err + } + if len(reports) == 0 { + logger.Errorf("no report found for artifact %s of mimetype %s, error %v", artDigest, mimeType, err) + return nil, errors.NotFoundError(nil).WithMessage("no report found to update data") + } + return reports[0], nil } // RequestProducesMineTypes returns the produces mime types -func (v *ScanHandler) RequestProducesMineTypes() []string { +func (h *scanHandler) RequestProducesMineTypes() []string { return []string{v1.MimeTypeGenericVulnerabilityReport} } // RequestParameters defines the parameters for scan request -func (v *ScanHandler) RequestParameters() map[string]interface{} { +func (h *scanHandler) RequestParameters() map[string]interface{} { return nil } // RequiredPermissions defines the permission used by the scan robot account -func (v *ScanHandler) RequiredPermissions() []*types.Policy { +func (h *scanHandler) RequiredPermissions() []*types.Policy { return []*types.Policy{ { Resource: rbac.ResourceRepository, @@ -59,14 +248,56 @@ 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) { +func (h *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 - _, refreshedReport, err := postprocessors.Converter.ToRelationalSchema(ctx.SystemContext(), origRp.UUID, origRp.RegistrationUUID, origRp.Digest, rawReport) + _, refreshedReport, err := postprocessors.Converter.ToRelationalSchema(ctx.SystemContext(), origRp.UUID, + origRp.RegistrationUUID, origRp.Digest, rawReport) return refreshedReport, err } + +// URLParameter vulnerability doesn't require any scan report parameters +func (h *scanHandler) URLParameter(_ *v1.ScanRequest) (string, error) { + return "", nil +} + +func (h *scanHandler) Update(ctx context.Context, uuid string, rpt string) error { + reportMgr := h.ReportMgrFunc() + if err := reportMgr.UpdateReportData(ctx, uuid, rpt); err != nil { + return err + } + return nil +} + +func (h *scanHandler) GetSummary(ctx context.Context, ar *artifact.Artifact, mimeTypes []string) (map[string]interface{}, error) { + bc := h.ScanControllerFunc() + if ar == nil { + return nil, errors.New("no way to get report summaries for nil artifact") + } + // Get reports first + rps, err := bc.GetReport(ctx, ar, mimeTypes) + if err != nil { + return nil, err + } + summaries := make(map[string]interface{}, len(rps)) + for _, rp := range rps { + sum, err := report.GenerateSummary(rp) + if err != nil { + return nil, err + } + + if s, ok := summaries[rp.MimeType]; ok { + r, err := report.MergeSummary(rp.MimeType, s, sum) + if err != nil { + return nil, err + } + + summaries[rp.MimeType] = r + } else { + summaries[rp.MimeType] = sum + } + } + + return summaries, nil +} diff --git a/src/pkg/scan/vulnerability/vul_test.go b/src/pkg/scan/vulnerability/vul_test.go index 003e15a0da5..2d0dcbe983e 100644 --- a/src/pkg/scan/vulnerability/vul_test.go +++ b/src/pkg/scan/vulnerability/vul_test.go @@ -1,25 +1,44 @@ package vulnerability import ( + "context" "fmt" "testing" "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/goharbor/harbor/src/controller/artifact" + scanCtl "github.com/goharbor/harbor/src/controller/scan" + art "github.com/goharbor/harbor/src/pkg/artifact" + "github.com/goharbor/harbor/src/pkg/scan/dao/scanner" + "github.com/goharbor/harbor/src/pkg/scan/report" + "github.com/goharbor/harbor/src/pkg/task" + htesting "github.com/goharbor/harbor/src/testing" + artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact" + scanCtlTest "github.com/goharbor/harbor/src/testing/controller/scan" + "github.com/goharbor/harbor/src/testing/mock" + accessorytesting "github.com/goharbor/harbor/src/testing/pkg/accessory" + reporttesting "github.com/goharbor/harbor/src/testing/pkg/scan/report" + tasktesting "github.com/goharbor/harbor/src/testing/pkg/task" "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/lib/orm" + accessoryModel "github.com/goharbor/harbor/src/pkg/accessory/model" "github.com/goharbor/harbor/src/pkg/permission/types" "github.com/goharbor/harbor/src/pkg/robot/model" "github.com/goharbor/harbor/src/pkg/scan/dao/scan" "github.com/goharbor/harbor/src/pkg/scan/postprocessors" v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" "github.com/goharbor/harbor/src/testing/jobservice" + ormtesting "github.com/goharbor/harbor/src/testing/lib/orm" postprocessorstesting "github.com/goharbor/harbor/src/testing/pkg/scan/postprocessors" ) func TestRequiredPermissions(t *testing.T) { - v := &ScanHandler{} + v := &scanHandler{} expected := []*types.Policy{ { Resource: rbac.ResourceRepository, @@ -37,7 +56,7 @@ func TestRequiredPermissions(t *testing.T) { } func TestPostScan(t *testing.T) { - v := &ScanHandler{} + v := &scanHandler{} ctx := &jobservice.MockJobContext{} artifact := &v1.Artifact{} origRp := &scan.Report{} @@ -70,7 +89,7 @@ func TestScanHandler_RequiredPermissions(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - v := &ScanHandler{} + v := &scanHandler{} assert.Equalf(t, tt.want, v.RequiredPermissions(), "RequiredPermissions()") }) } @@ -90,12 +109,12 @@ func TestScanHandler_ReportURLParameter(t *testing.T) { } 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)) { + v := &scanHandler{} + got, err := v.URLParameter(tt.args.in0) + if !tt.wantErr(t, err, fmt.Sprintf("URLParameter(%v)", tt.args.in0)) { return } - assert.Equalf(t, tt.want, got, "ReportURLParameter(%v)", tt.args.in0) + assert.Equalf(t, tt.want, got, "URLParameter(%v)", tt.args.in0) }) } } @@ -109,8 +128,98 @@ func TestScanHandler_RequestProducesMineTypes(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - v := &ScanHandler{} + v := &scanHandler{} assert.Equalf(t, tt.want, v.RequestProducesMineTypes(), "RequestProducesMineTypes()") }) } } + +type VulHandlerTestSuite struct { + htesting.Suite + ar *artifacttesting.Controller + accessoryMgr *accessorytesting.Manager + artifact *artifact.Artifact + taskMgr *tasktesting.Manager + reportMgr *reporttesting.Manager + scanController *scanCtlTest.Controller + handler *scanHandler +} + +func (suite *VulHandlerTestSuite) SetupSuite() { + suite.ar = &artifacttesting.Controller{} + suite.accessoryMgr = &accessorytesting.Manager{} + suite.taskMgr = &tasktesting.Manager{} + suite.scanController = &scanCtlTest.Controller{} + suite.reportMgr = &reporttesting.Manager{} + suite.artifact = &artifact.Artifact{Artifact: art.Artifact{ID: 1}} + suite.artifact.Type = "IMAGE" + suite.artifact.ProjectID = 1 + suite.artifact.RepositoryName = "library/photon" + suite.artifact.Digest = "digest-code" + suite.artifact.ManifestMediaType = v1.MimeTypeDockerArtifact + suite.handler = &scanHandler{ + reportConverter: postprocessors.Converter, + ReportMgrFunc: func() report.Manager { return suite.reportMgr }, + TaskMgrFunc: func() task.Manager { return suite.taskMgr }, + ScanControllerFunc: func() scanCtl.Controller { return suite.scanController }, + cloneCtx: func(ctx context.Context) context.Context { return ctx }, + } + +} + +func (suite *VulHandlerTestSuite) TearDownSuite() { +} + +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, &VulHandlerTestSuite{}) +} + +// TestScanControllerGetSummary ... +func (suite *VulHandlerTestSuite) TestScanControllerGetSummary() { + rpts := []*scan.Report{ + {UUID: "uuid", MimeType: v1.MimeTypeGenericVulnerabilityReport}, + } + ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{}) + mock.OnAnything(suite.ar, "HasUnscannableLayer").Return(false, nil).Once() + mock.OnAnything(suite.accessoryMgr, "List").Return([]accessoryModel.Accessory{}, nil).Once() + mock.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) { + walkFn := args.Get(2).(func(*artifact.Artifact) error) + walkFn(suite.artifact) + }).Once() + mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return(nil, nil).Once() + mock.OnAnything(suite.scanController, "GetReport").Return(rpts, nil).Once() + sum, err := suite.handler.GetSummary(ctx, suite.artifact, []string{v1.MimeTypeGenericVulnerabilityReport}) + require.NoError(suite.T(), err) + assert.Equal(suite.T(), 1, len(sum)) +} + +func (suite *VulHandlerTestSuite) TestMakeReportPlaceHolder() { + ctx := orm.NewContext(nil, &ormtesting.FakeOrmer{}) + art := &artifact.Artifact{Artifact: art.Artifact{ID: 1, Digest: "digest", ManifestMediaType: v1.MimeTypeDockerArtifact}} + r := &scanner.Registration{ + UUID: "uuid", + Metadata: &v1.ScannerAdapterMetadata{ + Capabilities: []*v1.ScannerCapability{ + {Type: v1.ScanTypeVulnerability, ConsumesMimeTypes: []string{v1.MimeTypeDockerArtifact}, ProducesMimeTypes: []string{v1.MimeTypeGenericVulnerabilityReport}}, + }, + }, + } + // mimeTypes := []string{v1.MimeTypeGenericVulnerabilityReport} + mock.OnAnything(suite.reportMgr, "GetBy").Return([]*scan.Report{{UUID: "uuid"}}, nil).Once() + mock.OnAnything(suite.reportMgr, "Create").Return("uuid", nil).Once() + mock.OnAnything(suite.reportMgr, "Delete").Return(nil).Once() + mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return([]*task.Task{{Status: "Success"}}, nil) + rps, err := suite.handler.MakePlaceHolder(ctx, art, r) + require.NoError(suite.T(), err) + assert.Equal(suite.T(), 1, len(rps)) +} + +func (suite *VulHandlerTestSuite) TestGetReportPlaceHolder() { + mock.OnAnything(suite.reportMgr, "GetBy").Return([]*scan.Report{{UUID: "uuid"}}, nil).Once() + rp, err := suite.handler.GetPlaceHolder(nil, "repo", "digest", "scannerUUID", "mimeType") + require.NoError(suite.T(), err) + assert.Equal(suite.T(), "uuid", rp.UUID) + mock.OnAnything(suite.reportMgr, "GetBy").Return(nil, fmt.Errorf("not found")).Once() + rp, err = suite.handler.GetPlaceHolder(nil, "repo", "digest", "scannerUUID", "mimeType") + require.Error(suite.T(), err) +} diff --git a/src/pkg/task/mock_task_manager_test.go b/src/pkg/task/mock_task_manager_test.go index bca9c411a08..271833460e3 100644 --- a/src/pkg/task/mock_task_manager_test.go +++ b/src/pkg/task/mock_task_manager_test.go @@ -257,6 +257,24 @@ func (_m *mockTaskManager) ListScanTasksByReportUUID(ctx context.Context, uuid s return r0, r1 } +// RetrieveStatusFromTask provides a mock function with given fields: ctx, reportID +func (_m *mockTaskManager) RetrieveStatusFromTask(ctx context.Context, reportID string) string { + ret := _m.Called(ctx, reportID) + + if len(ret) == 0 { + panic("no return value specified for RetrieveStatusFromTask") + } + + var r0 string + if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { + r0 = rf(ctx, reportID) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // Stop provides a mock function with given fields: ctx, id func (_m *mockTaskManager) Stop(ctx context.Context, id int64) error { ret := _m.Called(ctx, id) diff --git a/src/pkg/task/task.go b/src/pkg/task/task.go index a039d7689d0..859953909a1 100644 --- a/src/pkg/task/task.go +++ b/src/pkg/task/task.go @@ -67,6 +67,8 @@ type Manager interface { // ListScanTasksByReportUUID lists scan tasks by report uuid, although it's a specific case but it will be // more suitable to support multi database in the future. ListScanTasksByReportUUID(ctx context.Context, uuid string) (tasks []*Task, err error) + // RetrieveStatusFromTask retrieve status from task + RetrieveStatusFromTask(ctx context.Context, reportID string) string } // NewManager creates an instance of the default task manager @@ -282,3 +284,18 @@ func (m *manager) ExecutionIDsByVendorAndStatus(ctx context.Context, vendorType, func (m *manager) GetLogByJobID(_ context.Context, jobID string) (log []byte, err error) { return m.jsClient.GetJobLog(jobID) } + +func (m *manager) RetrieveStatusFromTask(ctx context.Context, reportID string) string { + if len(reportID) == 0 { + return "" + } + tasks, err := m.dao.ListScanTasksByReportUUID(ctx, reportID) + if err != nil { + log.Warningf("can not find the task with report UUID %v, error %v", reportID, err) + return "" + } + if len(tasks) > 0 { + return tasks[0].Status + } + return "" +} diff --git a/src/pkg/task/task_test.go b/src/pkg/task/task_test.go index 7b00500bb6d..6cf243364f9 100644 --- a/src/pkg/task/task_test.go +++ b/src/pkg/task/task_test.go @@ -160,6 +160,17 @@ func (t *taskManagerTestSuite) TestListScanTasksByReportUUID() { t.dao.AssertExpectations(t.T()) } +func (t *taskManagerTestSuite) TestRetrieveStatusFromTask() { + t.dao.On("ListScanTasksByReportUUID", mock.Anything, mock.Anything).Return([]*dao.Task{ + { + ID: 1, + Status: "Success", + }, + }, nil) + status := t.mgr.RetrieveStatusFromTask(nil, "uuid") + t.Equal("Success", status) +} + func TestTaskManagerTestSuite(t *testing.T) { suite.Run(t, &taskManagerTestSuite{}) } diff --git a/src/server/v2.0/handler/assembler/report.go b/src/server/v2.0/handler/assembler/report.go index 7a97ec99262..54645d85a5c 100644 --- a/src/server/v2.0/handler/assembler/report.go +++ b/src/server/v2.0/handler/assembler/report.go @@ -29,7 +29,6 @@ import ( const ( vulnerabilitiesAddition = "vulnerabilities" - sbomAddition = "sbom" ) // NewScanReportAssembler returns vul assembler @@ -80,7 +79,7 @@ func (assembler *ScanReportAssembler) Assemble(ctx context.Context) error { if assembler.overviewOption.WithVuln { for _, mimeType := range assembler.mimeTypes { - overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, []string{mimeType}) + overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, v1.ScanTypeVulnerability, []string{mimeType}) if err != nil { log.Warningf("get scan summary of artifact %s@%s for %s failed, error:%v", artifact.RepositoryName, artifact.Digest, mimeType, err) } else if len(overview) > 0 { @@ -93,13 +92,17 @@ func (assembler *ScanReportAssembler) Assemble(ctx context.Context) error { // set sbom additional link if it is supported, use the empty digest artifact.SetSBOMAdditionLink("", version) if assembler.overviewOption.WithSBOM { - overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, []string{v1.MimeTypeSBOMReport}) + overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, v1.ScanTypeSbom, []string{v1.MimeTypeSBOMReport}) if err != nil { log.Warningf("get scan summary of artifact %s@%s for %s failed, error:%v", artifact.RepositoryName, artifact.Digest, v1.MimeTypeSBOMReport, err) } if len(overview) == 0 { log.Warningf("overview is empty, retrieve sbom status from execution") - query := q.New(q.KeyWords{"extra_attrs.artifact.digest": artifact.Digest, "extra_attrs.enabled_capabilities.type": "sbom"}) + // Get latest execution with digest, repository, and scan type is sbom, the status is the scan status + query := q.New( + q.KeyWords{"extra_attrs.artifact.digest": artifact.Digest, + "extra_attrs.artifact.repository_name": artifact.RepositoryName, + "extra_attrs.enabled_capabilities.type": "sbom"}) // sort by ID desc to get the latest execution query.Sorts = []*q.Sort{q.NewSort("ID", true)} execs, err := assembler.executionMgr.List(ctx, query) diff --git a/src/testing/controller/scan/controller.go b/src/testing/controller/scan/controller.go index 5ec8091a49c..bc24b917412 100644 --- a/src/testing/controller/scan/controller.go +++ b/src/testing/controller/scan/controller.go @@ -7,13 +7,13 @@ import ( artifact "github.com/goharbor/harbor/src/controller/artifact" - daoscan "github.com/goharbor/harbor/src/pkg/scan/dao/scan" + controllerscan "github.com/goharbor/harbor/src/controller/scan" mock "github.com/stretchr/testify/mock" models "github.com/goharbor/harbor/src/pkg/allowlist/models" - scan "github.com/goharbor/harbor/src/controller/scan" + scan "github.com/goharbor/harbor/src/pkg/scan/dao/scan" ) // Controller is an autogenerated mock type for the Controller type @@ -21,49 +21,24 @@ type Controller struct { mock.Mock } -// DeleteReports provides a mock function with given fields: ctx, digests -func (_m *Controller) DeleteReports(ctx context.Context, digests ...string) error { - _va := make([]interface{}, len(digests)) - for _i := range digests { - _va[_i] = digests[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for DeleteReports") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, ...string) error); ok { - r0 = rf(ctx, digests...) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // GetReport provides a mock function with given fields: ctx, _a1, mimeTypes -func (_m *Controller) GetReport(ctx context.Context, _a1 *artifact.Artifact, mimeTypes []string) ([]*daoscan.Report, error) { +func (_m *Controller) GetReport(ctx context.Context, _a1 *artifact.Artifact, mimeTypes []string) ([]*scan.Report, error) { ret := _m.Called(ctx, _a1, mimeTypes) if len(ret) == 0 { panic("no return value specified for GetReport") } - var r0 []*daoscan.Report + var r0 []*scan.Report var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string) ([]*daoscan.Report, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string) ([]*scan.Report, error)); ok { return rf(ctx, _a1, mimeTypes) } - if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string) []*daoscan.Report); ok { + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string) []*scan.Report); ok { r0 = rf(ctx, _a1, mimeTypes) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*daoscan.Report) + r0 = ret.Get(0).([]*scan.Report) } } @@ -106,9 +81,9 @@ func (_m *Controller) GetScanLog(ctx context.Context, art *artifact.Artifact, uu return r0, r1 } -// GetSummary provides a mock function with given fields: ctx, _a1, mimeTypes -func (_m *Controller) GetSummary(ctx context.Context, _a1 *artifact.Artifact, mimeTypes []string) (map[string]interface{}, error) { - ret := _m.Called(ctx, _a1, mimeTypes) +// GetSummary provides a mock function with given fields: ctx, _a1, scanType, mimeTypes +func (_m *Controller) GetSummary(ctx context.Context, _a1 *artifact.Artifact, scanType string, mimeTypes []string) (map[string]interface{}, error) { + ret := _m.Called(ctx, _a1, scanType, mimeTypes) if len(ret) == 0 { panic("no return value specified for GetSummary") @@ -116,19 +91,19 @@ func (_m *Controller) GetSummary(ctx context.Context, _a1 *artifact.Artifact, mi var r0 map[string]interface{} var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string) (map[string]interface{}, error)); ok { - return rf(ctx, _a1, mimeTypes) + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, string, []string) (map[string]interface{}, error)); ok { + return rf(ctx, _a1, scanType, mimeTypes) } - if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string) map[string]interface{}); ok { - r0 = rf(ctx, _a1, mimeTypes) + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, string, []string) map[string]interface{}); ok { + r0 = rf(ctx, _a1, scanType, mimeTypes) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(map[string]interface{}) } } - if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, []string) error); ok { - r1 = rf(ctx, _a1, mimeTypes) + if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, string, []string) error); ok { + r1 = rf(ctx, _a1, scanType, mimeTypes) } else { r1 = ret.Error(1) } @@ -137,23 +112,23 @@ func (_m *Controller) GetSummary(ctx context.Context, _a1 *artifact.Artifact, mi } // GetVulnerable provides a mock function with given fields: ctx, _a1, allowlist, allowlistIsExpired -func (_m *Controller) GetVulnerable(ctx context.Context, _a1 *artifact.Artifact, allowlist models.CVESet, allowlistIsExpired bool) (*scan.Vulnerable, error) { +func (_m *Controller) GetVulnerable(ctx context.Context, _a1 *artifact.Artifact, allowlist models.CVESet, allowlistIsExpired bool) (*controllerscan.Vulnerable, error) { ret := _m.Called(ctx, _a1, allowlist, allowlistIsExpired) if len(ret) == 0 { panic("no return value specified for GetVulnerable") } - var r0 *scan.Vulnerable + var r0 *controllerscan.Vulnerable var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, models.CVESet, bool) (*scan.Vulnerable, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, models.CVESet, bool) (*controllerscan.Vulnerable, error)); ok { return rf(ctx, _a1, allowlist, allowlistIsExpired) } - if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, models.CVESet, bool) *scan.Vulnerable); ok { + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, models.CVESet, bool) *controllerscan.Vulnerable); ok { r0 = rf(ctx, _a1, allowlist, allowlistIsExpired) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*scan.Vulnerable) + r0 = ret.Get(0).(*controllerscan.Vulnerable) } } @@ -167,7 +142,7 @@ func (_m *Controller) GetVulnerable(ctx context.Context, _a1 *artifact.Artifact, } // Scan provides a mock function with given fields: ctx, _a1, options -func (_m *Controller) Scan(ctx context.Context, _a1 *artifact.Artifact, options ...scan.Option) error { +func (_m *Controller) Scan(ctx context.Context, _a1 *artifact.Artifact, options ...controllerscan.Option) error { _va := make([]interface{}, len(options)) for _i := range options { _va[_i] = options[_i] @@ -182,7 +157,7 @@ func (_m *Controller) Scan(ctx context.Context, _a1 *artifact.Artifact, options } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, ...scan.Option) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, ...controllerscan.Option) error); ok { r0 = rf(ctx, _a1, options...) } else { r0 = ret.Error(0) diff --git a/src/testing/pkg/pkg.go b/src/testing/pkg/pkg.go index 516bec59d2f..9ded95ee643 100644 --- a/src/testing/pkg/pkg.go +++ b/src/testing/pkg/pkg.go @@ -74,3 +74,5 @@ package pkg //go:generate mockery --case snake --dir ../../pkg/jobmonitor --name RedisClient --output ./jobmonitor --outpkg jobmonitor //go:generate mockery --case snake --dir ../../pkg/queuestatus --name Manager --output ./queuestatus --outpkg queuestatus //go:generate mockery --case snake --dir ../../pkg/securityhub --name Manager --output ./securityhub --outpkg securityhub +//go:generate mockery --case snake --dir ../../pkg/scan/sbom --name Manager --output ./scan/sbom --outpkg sbom +//go:generate mockery --case snake --dir ../../pkg/scan --name Handler --output ./scan --outpkg scan diff --git a/src/testing/pkg/scan/handler.go b/src/testing/pkg/scan/handler.go new file mode 100644 index 00000000000..3b904949cee --- /dev/null +++ b/src/testing/pkg/scan/handler.go @@ -0,0 +1,268 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + +package scan + +import ( + context "context" + + artifact "github.com/goharbor/harbor/src/controller/artifact" + + job "github.com/goharbor/harbor/src/jobservice/job" + + mock "github.com/stretchr/testify/mock" + + model "github.com/goharbor/harbor/src/pkg/robot/model" + + scan "github.com/goharbor/harbor/src/pkg/scan/dao/scan" + + scanner "github.com/goharbor/harbor/src/pkg/scan/dao/scanner" + + time "time" + + types "github.com/goharbor/harbor/src/pkg/permission/types" + + v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1" +) + +// Handler is an autogenerated mock type for the Handler type +type Handler struct { + mock.Mock +} + +// GetPlaceHolder provides a mock function with given fields: ctx, artRepo, artDigest, scannerUUID, mimeType +func (_m *Handler) GetPlaceHolder(ctx context.Context, artRepo string, artDigest string, scannerUUID string, mimeType string) (*scan.Report, error) { + ret := _m.Called(ctx, artRepo, artDigest, scannerUUID, mimeType) + + if len(ret) == 0 { + panic("no return value specified for GetPlaceHolder") + } + + var r0 *scan.Report + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) (*scan.Report, error)); ok { + return rf(ctx, artRepo, artDigest, scannerUUID, mimeType) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) *scan.Report); ok { + r0 = rf(ctx, artRepo, artDigest, scannerUUID, mimeType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*scan.Report) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) error); ok { + r1 = rf(ctx, artRepo, artDigest, scannerUUID, mimeType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSummary provides a mock function with given fields: ctx, ar, mimeTypes +func (_m *Handler) GetSummary(ctx context.Context, ar *artifact.Artifact, mimeTypes []string) (map[string]interface{}, error) { + ret := _m.Called(ctx, ar, mimeTypes) + + if len(ret) == 0 { + panic("no return value specified for GetSummary") + } + + var r0 map[string]interface{} + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string) (map[string]interface{}, error)); ok { + return rf(ctx, ar, mimeTypes) + } + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string) map[string]interface{}); ok { + r0 = rf(ctx, ar, mimeTypes) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]interface{}) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, []string) error); ok { + r1 = rf(ctx, ar, mimeTypes) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MakePlaceHolder provides a mock function with given fields: ctx, art, r +func (_m *Handler) MakePlaceHolder(ctx context.Context, art *artifact.Artifact, r *scanner.Registration) ([]*scan.Report, error) { + ret := _m.Called(ctx, art, r) + + if len(ret) == 0 { + panic("no return value specified for MakePlaceHolder") + } + + var r0 []*scan.Report + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, *scanner.Registration) ([]*scan.Report, error)); ok { + return rf(ctx, art, r) + } + if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, *scanner.Registration) []*scan.Report); ok { + r0 = rf(ctx, art, r) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*scan.Report) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, *scanner.Registration) error); ok { + r1 = rf(ctx, art, r) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// PostScan provides a mock function with given fields: ctx, sr, rp, rawReport, startTime, robot +func (_m *Handler) PostScan(ctx job.Context, sr *v1.ScanRequest, rp *scan.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error) { + ret := _m.Called(ctx, sr, rp, rawReport, startTime, robot) + + if len(ret) == 0 { + panic("no return value specified for PostScan") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(job.Context, *v1.ScanRequest, *scan.Report, string, time.Time, *model.Robot) (string, error)); ok { + return rf(ctx, sr, rp, rawReport, startTime, robot) + } + if rf, ok := ret.Get(0).(func(job.Context, *v1.ScanRequest, *scan.Report, string, time.Time, *model.Robot) string); ok { + r0 = rf(ctx, sr, rp, rawReport, startTime, robot) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(job.Context, *v1.ScanRequest, *scan.Report, string, time.Time, *model.Robot) error); ok { + r1 = rf(ctx, sr, rp, rawReport, startTime, robot) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RequestParameters provides a mock function with given fields: +func (_m *Handler) RequestParameters() map[string]interface{} { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RequestParameters") + } + + var r0 map[string]interface{} + if rf, ok := ret.Get(0).(func() map[string]interface{}); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]interface{}) + } + } + + return r0 +} + +// RequestProducesMineTypes provides a mock function with given fields: +func (_m *Handler) RequestProducesMineTypes() []string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RequestProducesMineTypes") + } + + var r0 []string + if rf, ok := ret.Get(0).(func() []string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + return r0 +} + +// RequiredPermissions provides a mock function with given fields: +func (_m *Handler) RequiredPermissions() []*types.Policy { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RequiredPermissions") + } + + var r0 []*types.Policy + if rf, ok := ret.Get(0).(func() []*types.Policy); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*types.Policy) + } + } + + return r0 +} + +// URLParameter provides a mock function with given fields: sr +func (_m *Handler) URLParameter(sr *v1.ScanRequest) (string, error) { + ret := _m.Called(sr) + + if len(ret) == 0 { + panic("no return value specified for URLParameter") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(*v1.ScanRequest) (string, error)); ok { + return rf(sr) + } + if rf, ok := ret.Get(0).(func(*v1.ScanRequest) string); ok { + r0 = rf(sr) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(*v1.ScanRequest) error); ok { + r1 = rf(sr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Update provides a mock function with given fields: ctx, uuid, report +func (_m *Handler) Update(ctx context.Context, uuid string, report string) error { + ret := _m.Called(ctx, uuid, report) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, uuid, report) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewHandler creates a new instance of Handler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewHandler(t interface { + mock.TestingT + Cleanup(func()) +}) *Handler { + mock := &Handler{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/src/testing/pkg/scan/sbom/manager.go b/src/testing/pkg/scan/sbom/manager.go new file mode 100644 index 00000000000..464e821ef92 --- /dev/null +++ b/src/testing/pkg/scan/sbom/manager.go @@ -0,0 +1,216 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + +package sbom + +import ( + context "context" + + model "github.com/goharbor/harbor/src/pkg/scan/sbom/model" + mock "github.com/stretchr/testify/mock" + + q "github.com/goharbor/harbor/src/lib/q" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// Create provides a mock function with given fields: ctx, r +func (_m *Manager) Create(ctx context.Context, r *model.Report) (string, error) { + ret := _m.Called(ctx, r) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *model.Report) (string, error)); ok { + return rf(ctx, r) + } + if rf, ok := ret.Get(0).(func(context.Context, *model.Report) string); ok { + r0 = rf(ctx, r) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, *model.Report) error); ok { + r1 = rf(ctx, r) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, uuid +func (_m *Manager) Delete(ctx context.Context, uuid string) error { + ret := _m.Called(ctx, uuid) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, uuid) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteByArtifactID provides a mock function with given fields: ctx, artifactID +func (_m *Manager) DeleteByArtifactID(ctx context.Context, artifactID int64) error { + ret := _m.Called(ctx, artifactID) + + if len(ret) == 0 { + panic("no return value specified for DeleteByArtifactID") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, artifactID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteByExtraAttr provides a mock function with given fields: ctx, mimeType, attrName, attrValue +func (_m *Manager) DeleteByExtraAttr(ctx context.Context, mimeType string, attrName string, attrValue string) error { + ret := _m.Called(ctx, mimeType, attrName, attrValue) + + if len(ret) == 0 { + panic("no return value specified for DeleteByExtraAttr") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok { + r0 = rf(ctx, mimeType, attrName, attrValue) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetBy provides a mock function with given fields: ctx, artifactID, registrationUUID, mimeType, mediaType +func (_m *Manager) GetBy(ctx context.Context, artifactID int64, registrationUUID string, mimeType string, mediaType string) ([]*model.Report, error) { + ret := _m.Called(ctx, artifactID, registrationUUID, mimeType, mediaType) + + if len(ret) == 0 { + panic("no return value specified for GetBy") + } + + var r0 []*model.Report + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64, string, string, string) ([]*model.Report, error)); ok { + return rf(ctx, artifactID, registrationUUID, mimeType, mediaType) + } + if rf, ok := ret.Get(0).(func(context.Context, int64, string, string, string) []*model.Report); ok { + r0 = rf(ctx, artifactID, registrationUUID, mimeType, mediaType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Report) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64, string, string, string) error); ok { + r1 = rf(ctx, artifactID, registrationUUID, mimeType, mediaType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, query +func (_m *Manager) List(ctx context.Context, query *q.Query) ([]*model.Report, error) { + ret := _m.Called(ctx, query) + + if len(ret) == 0 { + panic("no return value specified for List") + } + + var r0 []*model.Report + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) ([]*model.Report, error)); ok { + return rf(ctx, query) + } + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*model.Report); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Report) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Update provides a mock function with given fields: ctx, r, cols +func (_m *Manager) Update(ctx context.Context, r *model.Report, cols ...string) error { + _va := make([]interface{}, len(cols)) + for _i := range cols { + _va[_i] = cols[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, r) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *model.Report, ...string) error); ok { + r0 = rf(ctx, r, cols...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateReportData provides a mock function with given fields: ctx, uuid, report +func (_m *Manager) UpdateReportData(ctx context.Context, uuid string, report string) error { + ret := _m.Called(ctx, uuid, report) + + if len(ret) == 0 { + panic("no return value specified for UpdateReportData") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, uuid, report) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewManager(t interface { + mock.TestingT + Cleanup(func()) +}) *Manager { + mock := &Manager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/src/testing/pkg/task/manager.go b/src/testing/pkg/task/manager.go index d1172aa693c..1359afd009e 100644 --- a/src/testing/pkg/task/manager.go +++ b/src/testing/pkg/task/manager.go @@ -259,6 +259,24 @@ func (_m *Manager) ListScanTasksByReportUUID(ctx context.Context, uuid string) ( return r0, r1 } +// RetrieveStatusFromTask provides a mock function with given fields: ctx, reportID +func (_m *Manager) RetrieveStatusFromTask(ctx context.Context, reportID string) string { + ret := _m.Called(ctx, reportID) + + if len(ret) == 0 { + panic("no return value specified for RetrieveStatusFromTask") + } + + var r0 string + if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { + r0 = rf(ctx, reportID) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + // Stop provides a mock function with given fields: ctx, id func (_m *Manager) Stop(ctx context.Context, id int64) error { ret := _m.Called(ctx, id)