[cherry-pick] Add sbom_report table to store sbom related information (#20482)

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 <stone.zhang@broadcom.com>
This commit is contained in:
stonezdj(Daojun Zhang) 2024-05-24 17:08:41 +08:00 committed by GitHub
parent a763d6b54c
commit e2826868ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1911 additions and 497 deletions

View File

@ -29,3 +29,15 @@ 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;
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)
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

@ -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,32 +51,72 @@ 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)
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) {
// PostScan defines task specific operations after the scan is complete
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
}
scanReq := v1.ScanRequest{
Registry: sr.Registry,
Artifact: sr.Artifact,
}
// the registry server url is core by default, need to replace it with real registry server url
scanReq.Registry.URL, 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 := h.GenAccessoryFunc(scanReq, sbomContent, h.annotations(), sbomMimeType, robot)
if err != nil {
myLogger.Errorf("error when create accessory from image %v", err)
return "", err
}
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 (v *scanHandler) RequiredPermissions() []*types.Policy {
func (h *scanHandler) RequiredPermissions() []*types.Policy {
return []*types.Policy{
{
Resource: rbac.ResourceRepository,
@ -85,33 +133,8 @@ func (v *scanHandler) RequiredPermissions() []*types.Policy {
}
}
// PostScan defines task specific operations after the scan is complete
func (v *scanHandler) PostScan(ctx job.Context, sr *v1.ScanRequest, _ *scanModel.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error) {
sbomContent, s, err := retrieveSBOMContent(rawReport)
if err != nil {
return "", err
}
scanReq := v1.ScanRequest{
Registry: sr.Registry,
Artifact: sr.Artifact,
}
// the registry server url is core by default, need to replace it with real registry server url
scanReq.Registry.URL, scanReq.Registry.Insecure = v.RegistryServer(ctx.SystemContext())
if len(scanReq.Registry.URL) == 0 {
return "", fmt.Errorf("empty registry server")
}
myLogger := ctx.GetLogger()
myLogger.Debugf("Pushing accessory artifact to %s/%s", scanReq.Registry.URL, scanReq.Artifact.Repository)
dgst, err := v.GenAccessoryFunc(scanReq, sbomContent, v.annotations(), sbomMimeType, robot)
if err != nil {
myLogger.Errorf("error when create accessory from image %v", err)
return "", err
}
return v.generateReport(startTime, sr.Artifact.Repository, dgst, "Success", s)
}
// 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
}

View File

@ -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 {
type SBOMTestSuite struct {
htesting.Suite
handler *scanHandler
suite.Suite
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,
}
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
},
}
func (suite *ExampleTestSuite) TearDownSuite() {
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) TestPostScan() {
func (suite *SBOMTestSuite) TearDownSuite() {
}
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 TestExampleTestSuite(t *testing.T) {
suite.Run(t, &ExampleTestSuite{})
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, &SBOMTestSuite{})
}

View File

@ -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) {
// PostScan ...
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)
return refreshedReport, err
}
// URLParameter vulnerability doesn't require any scan report parameters
func (h *scanHandler) URLParameter(_ *v1.ScanRequest) (string, error) {
return "", nil
}
// PostScan ...
func (v *ScanHandler) PostScan(ctx job.Context, _ *v1.ScanRequest, origRp *scan.Report, rawReport string, _ time.Time, _ *model.Robot) (string, error) {
// use a new ormer here to use the short db connection
_, refreshedReport, err := postprocessors.Converter.ToRelationalSchema(ctx.SystemContext(), origRp.UUID, origRp.RegistrationUUID, origRp.Digest, rawReport)
return refreshedReport, err
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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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