mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-26 04:05:40 +01:00
[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:
parent
a763d6b54c
commit
e2826868ee
@ -28,4 +28,16 @@ then set column artifact_type as not null
|
|||||||
*/
|
*/
|
||||||
UPDATE artifact SET artifact_type = media_type WHERE artifact_type IS NULL;
|
UPDATE artifact SET artifact_type = media_type WHERE artifact_type IS NULL;
|
||||||
|
|
||||||
ALTER TABLE artifact ALTER COLUMN artifact_type SET NOT 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)
|
||||||
|
);
|
@ -24,7 +24,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/controller/artifact"
|
"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"
|
||||||
"github.com/goharbor/harbor/src/controller/event/operator"
|
"github.com/goharbor/harbor/src/controller/event/operator"
|
||||||
"github.com/goharbor/harbor/src/controller/repository"
|
"github.com/goharbor/harbor/src/controller/repository"
|
||||||
@ -38,6 +38,7 @@ import (
|
|||||||
pkgArt "github.com/goharbor/harbor/src/pkg/artifact"
|
pkgArt "github.com/goharbor/harbor/src/pkg/artifact"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/sbom"
|
||||||
"github.com/goharbor/harbor/src/pkg/task"
|
"github.com/goharbor/harbor/src/pkg/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -74,6 +75,8 @@ type ArtifactEventHandler struct {
|
|||||||
execMgr task.ExecutionManager
|
execMgr task.ExecutionManager
|
||||||
// reportMgr for managing scan reports
|
// reportMgr for managing scan reports
|
||||||
reportMgr report.Manager
|
reportMgr report.Manager
|
||||||
|
// sbomReportMgr
|
||||||
|
sbomReportMgr sbom.Manager
|
||||||
// artMgr for managing artifacts
|
// artMgr for managing artifacts
|
||||||
artMgr pkgArt.Manager
|
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)
|
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 {
|
// delete sbom_report when the subject artifact is deleted
|
||||||
if err := reportMgr.DeleteByExtraAttr(ctx, v1.MimeTypeSBOMReport, "sbom_digest", event.Artifact.Digest); err != nil {
|
if err := sbom.Mgr.DeleteByArtifactID(ctx, event.Artifact.ID); err != nil {
|
||||||
log.Errorf("failed to delete scan reports of with sbom digest %v, error: %v", event.Artifact.Digest, err)
|
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
|
return nil
|
||||||
|
@ -144,7 +144,7 @@ func constructScanImagePayload(ctx context.Context, event *event.ScanImageEvent,
|
|||||||
|
|
||||||
scanSummaries := map[string]interface{}{}
|
scanSummaries := map[string]interface{}{}
|
||||||
if event.ScanType == v1.ScanTypeVulnerability {
|
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 {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "construct scan payload")
|
return nil, errors.Wrap(err, "construct scan payload")
|
||||||
}
|
}
|
||||||
@ -152,7 +152,7 @@ func constructScanImagePayload(ctx context.Context, event *event.ScanImageEvent,
|
|||||||
|
|
||||||
sbomOverview := map[string]interface{}{}
|
sbomOverview := map[string]interface{}{}
|
||||||
if event.ScanType == v1.ScanTypeSbom {
|
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 {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "construct scan payload")
|
return nil, errors.Wrap(err, "construct scan payload")
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ package scan
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
@ -49,7 +48,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/pkg/scan/postprocessors"
|
"github.com/goharbor/harbor/src/pkg/scan/postprocessors"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
sbomModel "github.com/goharbor/harbor/src/pkg/scan/sbom/model"
|
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
||||||
"github.com/goharbor/harbor/src/pkg/task"
|
"github.com/goharbor/harbor/src/pkg/task"
|
||||||
)
|
)
|
||||||
@ -275,8 +273,9 @@ func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, opti
|
|||||||
errs []error
|
errs []error
|
||||||
launchScanJobParams []*launchScanJobParam
|
launchScanJobParams []*launchScanJobParam
|
||||||
)
|
)
|
||||||
|
handler := sca.GetScanHandler(opts.GetScanType())
|
||||||
for _, art := range artifacts {
|
for _, art := range artifacts {
|
||||||
reports, err := bc.makeReportPlaceholder(ctx, r, art, opts)
|
reports, err := handler.MakePlaceHolder(ctx, art, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsConflictErr(err) {
|
if errors.IsConflictErr(err) {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
@ -566,63 +565,6 @@ func (bc *basicController) startScanAll(ctx context.Context, executionID int64)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc *basicController) makeReportPlaceholder(ctx context.Context, r *scanner.Registration, art *ar.Artifact, 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 ...
|
// GetReport ...
|
||||||
func (bc *basicController) GetReport(ctx context.Context, artifact *ar.Artifact, mimeTypes []string) ([]*scan.Report, error) {
|
func (bc *basicController) GetReport(ctx context.Context, artifact *ar.Artifact, mimeTypes []string) ([]*scan.Report, error) {
|
||||||
if artifact == nil {
|
if artifact == nil {
|
||||||
@ -697,95 +639,10 @@ func (bc *basicController) GetReport(ctx context.Context, artifact *ar.Artifact,
|
|||||||
return reports, nil
|
return reports, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSBOMMimeTypes(mimeTypes []string) bool {
|
|
||||||
for _, mimeType := range mimeTypes {
|
|
||||||
if mimeType == v1.MimeTypeSBOMReport {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSummary ...
|
// GetSummary ...
|
||||||
func (bc *basicController) GetSummary(ctx context.Context, artifact *ar.Artifact, mimeTypes []string) (map[string]interface{}, error) {
|
func (bc *basicController) GetSummary(ctx context.Context, artifact *ar.Artifact, scanType string, mimeTypes []string) (map[string]interface{}, error) {
|
||||||
if artifact == nil {
|
handler := sca.GetScanHandler(scanType)
|
||||||
return nil, errors.New("no way to get report summaries for nil artifact")
|
return handler.GetSummary(ctx, artifact, mimeTypes)
|
||||||
}
|
|
||||||
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 ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetScanLog ...
|
// GetScanLog ...
|
||||||
@ -821,7 +678,7 @@ func (bc *basicController) GetScanLog(ctx context.Context, artifact *ar.Artifact
|
|||||||
if !scanTaskForArtifacts(t, artifactMap) {
|
if !scanTaskForArtifacts(t, artifactMap) {
|
||||||
return nil, errors.NotFoundError(nil).WithMessage("scan log with uuid: %s not found", uuid)
|
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
|
reportUUIDToTasks[reportUUID] = t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -902,14 +759,6 @@ func scanTaskForArtifacts(task *task.Task, artifactMap map[int64]interface{}) bo
|
|||||||
return exist
|
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) {
|
func (bc *basicController) GetVulnerable(ctx context.Context, artifact *ar.Artifact, allowlist allowlist.CVESet, allowlistIsExpired bool) (*Vulnerable, error) {
|
||||||
if artifact == nil {
|
if artifact == nil {
|
||||||
return nil, errors.New("no way to get vulnerable for nil artifact")
|
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{}
|
reportUUIDToTasks := map[string]*task.Task{}
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
for _, reportUUID := range getReportUUIDs(task.ExtraAttrs) {
|
for _, reportUUID := range GetReportUUIDs(task.ExtraAttrs) {
|
||||||
reportUUIDToTasks[reportUUID] = task
|
reportUUIDToTasks[reportUUID] = task
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1275,7 +1124,8 @@ func getArtifactTag(extraAttrs map[string]interface{}) string {
|
|||||||
return tag
|
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
|
var reportUUIDs []string
|
||||||
|
|
||||||
if extraAttrs != nil {
|
if extraAttrs != nil {
|
||||||
@ -1314,48 +1164,3 @@ func parseOptions(options ...Option) (*Options, error) {
|
|||||||
|
|
||||||
return ops, nil
|
return ops, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteArtifactAccessories delete the accessory in reports, only delete sbom accessory
|
|
||||||
func (bc *basicController) deleteArtifactAccessories(ctx context.Context, reports []*scan.Report) error {
|
|
||||||
for _, rpt := range reports {
|
|
||||||
if rpt.MimeType != v1.MimeTypeSBOMReport {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := bc.deleteArtifactAccessory(ctx, rpt.Report); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteArtifactAccessory check if current report has accessory info, if there is, delete it
|
|
||||||
func (bc *basicController) deleteArtifactAccessory(ctx context.Context, report string) error {
|
|
||||||
if len(report) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sbomSummary := sbomModel.Summary{}
|
|
||||||
if err := json.Unmarshal([]byte(report), &sbomSummary); err != nil {
|
|
||||||
// it could be a non sbom report, just skip
|
|
||||||
log.Debugf("fail to unmarshal %v, skip to delete sbom report", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
repo, dgst := sbomSummary.SBOMAccArt()
|
|
||||||
if len(repo) == 0 || len(dgst) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
art, err := bc.ar.GetByReference(ctx, repo, dgst, nil)
|
|
||||||
if err != nil {
|
|
||||||
if errors.IsNotFoundErr(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if art == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = bc.ar.Delete(ctx, art.ID)
|
|
||||||
if errors.IsNotFoundErr(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
@ -45,7 +45,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
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/vuln"
|
||||||
_ "github.com/goharbor/harbor/src/pkg/scan/vulnerability"
|
|
||||||
"github.com/goharbor/harbor/src/pkg/task"
|
"github.com/goharbor/harbor/src/pkg/task"
|
||||||
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact"
|
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact"
|
||||||
robottesting "github.com/goharbor/harbor/src/testing/controller/robot"
|
robottesting "github.com/goharbor/harbor/src/testing/controller/robot"
|
||||||
@ -55,6 +54,7 @@ import (
|
|||||||
ormtesting "github.com/goharbor/harbor/src/testing/lib/orm"
|
ormtesting "github.com/goharbor/harbor/src/testing/lib/orm"
|
||||||
"github.com/goharbor/harbor/src/testing/mock"
|
"github.com/goharbor/harbor/src/testing/mock"
|
||||||
accessorytesting "github.com/goharbor/harbor/src/testing/pkg/accessory"
|
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"
|
postprocessorstesting "github.com/goharbor/harbor/src/testing/pkg/scan/postprocessors"
|
||||||
reporttesting "github.com/goharbor/harbor/src/testing/pkg/scan/report"
|
reporttesting "github.com/goharbor/harbor/src/testing/pkg/scan/report"
|
||||||
tasktesting "github.com/goharbor/harbor/src/testing/pkg/task"
|
tasktesting "github.com/goharbor/harbor/src/testing/pkg/task"
|
||||||
@ -64,6 +64,8 @@ import (
|
|||||||
type ControllerTestSuite struct {
|
type ControllerTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
|
|
||||||
|
scanHandler *scanTest.Handler
|
||||||
|
|
||||||
artifactCtl *artifacttesting.Controller
|
artifactCtl *artifacttesting.Controller
|
||||||
accessoryMgr *accessorytesting.Manager
|
accessoryMgr *accessorytesting.Manager
|
||||||
originalArtifactCtl artifact.Controller
|
originalArtifactCtl artifact.Controller
|
||||||
@ -91,6 +93,8 @@ func TestController(t *testing.T) {
|
|||||||
|
|
||||||
// SetupSuite ...
|
// SetupSuite ...
|
||||||
func (suite *ControllerTestSuite) SetupSuite() {
|
func (suite *ControllerTestSuite) SetupSuite() {
|
||||||
|
suite.scanHandler = &scanTest.Handler{}
|
||||||
|
sca.RegisterScanHanlder(v1.ScanTypeVulnerability, suite.scanHandler)
|
||||||
suite.originalArtifactCtl = artifact.Ctl
|
suite.originalArtifactCtl = artifact.Ctl
|
||||||
suite.artifactCtl = &artifacttesting.Controller{}
|
suite.artifactCtl = &artifacttesting.Controller{}
|
||||||
artifact.Ctl = suite.artifactCtl
|
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.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("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("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)
|
mgr.On("UpdateStatus", "the-uuid-123", "Success", (int64)(10000)).Return(nil)
|
||||||
suite.reportMgr = mgr
|
suite.reportMgr = mgr
|
||||||
|
|
||||||
@ -347,6 +351,19 @@ func (suite *ControllerTestSuite) TearDownSuite() {
|
|||||||
|
|
||||||
// TestScanControllerScan ...
|
// TestScanControllerScan ...
|
||||||
func (suite *ControllerTestSuite) 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
|
// artifact not provieded
|
||||||
suite.Require().Error(suite.c.Scan(context.TODO(), nil))
|
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.execMgr, "Create").Return(int64(1), nil).Once()
|
||||||
mock.OnAnything(suite.taskMgr, "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{})
|
ctx := orm.NewContext(context.TODO(), &ormtesting.FakeOrmer{})
|
||||||
|
|
||||||
@ -388,7 +407,10 @@ func (suite *ControllerTestSuite) TestScanControllerScan() {
|
|||||||
}, nil).Once()
|
}, nil).Once()
|
||||||
|
|
||||||
mock.OnAnything(suite.reportMgr, "Delete").Return(fmt.Errorf("delete failed")).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))
|
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{
|
mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return([]*task.Task{
|
||||||
{ExtraAttrs: suite.makeExtraAttrs(int64(1), "rp-uuid-001"), Status: "Running"},
|
{ExtraAttrs: suite.makeExtraAttrs(int64(1), "rp-uuid-001"), Status: "Running"},
|
||||||
}, nil).Once()
|
}, 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))
|
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))
|
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 ...
|
// TestScanControllerGetScanLog ...
|
||||||
func (suite *ControllerTestSuite) TestScanControllerGetScanLog() {
|
func (suite *ControllerTestSuite) TestScanControllerGetScanLog() {
|
||||||
mock.OnAnything(suite.ar, "HasUnscannableLayer").Return(false, nil).Once()
|
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.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")
|
bytes, err := suite.c.GetScanLog(ctx, &artifact.Artifact{Artifact: art.Artifact{ID: 1, ProjectID: 1}}, "rp-uuid-001")
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
assert.Condition(suite.T(), func() (success bool) {
|
assert.Condition(suite.T(), func() (success bool) {
|
||||||
@ -566,6 +582,21 @@ func (suite *ControllerTestSuite) TestScanAll() {
|
|||||||
{
|
{
|
||||||
// no artifacts found when scan all
|
// no artifacts found when scan all
|
||||||
executionID := int64(1)
|
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()
|
mock.OnAnything(suite.ar, "HasUnscannableLayer").Return(false, nil).Once()
|
||||||
suite.execMgr.On(
|
suite.execMgr.On(
|
||||||
"Create", mock.Anything, "SCAN_ALL", int64(0), "SCHEDULE",
|
"Create", mock.Anything, "SCAN_ALL", int64(0), "SCHEDULE",
|
||||||
@ -607,8 +638,6 @@ func (suite *ControllerTestSuite) TestScanAll() {
|
|||||||
walkFn(suite.artifact)
|
walkFn(suite.artifact)
|
||||||
}).Once()
|
}).Once()
|
||||||
|
|
||||||
mock.OnAnything(suite.taskMgr, "ListScanTasksByReportUUID").Return(nil, nil).Once()
|
|
||||||
|
|
||||||
mock.OnAnything(suite.reportMgr, "Delete").Return(nil).Once()
|
mock.OnAnything(suite.reportMgr, "Delete").Return(nil).Once()
|
||||||
mock.OnAnything(suite.reportMgr, "Create").Return("uuid", nil).Once()
|
mock.OnAnything(suite.reportMgr, "Create").Return("uuid", nil).Once()
|
||||||
mock.OnAnything(suite.taskMgr, "Create").Return(int64(0), fmt.Errorf("failed")).Once()
|
mock.OnAnything(suite.taskMgr, "Create").Return(int64(0), fmt.Errorf("failed")).Once()
|
||||||
@ -635,16 +664,6 @@ func (suite *ControllerTestSuite) TestStopScanAll() {
|
|||||||
suite.NoError(err)
|
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{} {
|
func (suite *ControllerTestSuite) makeExtraAttrs(artifactID int64, reportUUIDs ...string) map[string]interface{} {
|
||||||
b, _ := json.Marshal(map[string]interface{}{reportUUIDsKey: reportUUIDs})
|
b, _ := json.Marshal(map[string]interface{}{reportUUIDsKey: reportUUIDs})
|
||||||
|
|
||||||
@ -654,57 +673,3 @@ func (suite *ControllerTestSuite) makeExtraAttrs(artifactID int64, reportUUIDs .
|
|||||||
|
|
||||||
return extraAttrs
|
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)
|
|
||||||
}
|
|
||||||
|
@ -83,7 +83,7 @@ type Controller interface {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// map[string]interface{} : report summaries indexed by mime types
|
// map[string]interface{} : report summaries indexed by mime types
|
||||||
// error : non nil error if any errors occurred
|
// 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
|
// 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
|
// error : non nil error if any errors occurred
|
||||||
GetScanLog(ctx context.Context, art *artifact.Artifact, uuid string) ([]byte, error)
|
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
|
// Scan all the artifacts
|
||||||
//
|
//
|
||||||
// Arguments:
|
// Arguments:
|
||||||
|
@ -54,14 +54,6 @@ func (suite *ReportTestSuite) SetupTest() {
|
|||||||
MimeType: v1.MimeTypeNativeReport,
|
MimeType: v1.MimeTypeNativeReport,
|
||||||
}
|
}
|
||||||
suite.create(r)
|
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.
|
// TearDownTest clears enf for test case.
|
||||||
@ -113,17 +105,6 @@ func (suite *ReportTestSuite) TestReportUpdateReportData() {
|
|||||||
suite.Require().NoError(err)
|
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) {
|
func (suite *ReportTestSuite) create(r *Report) {
|
||||||
id, err := suite.dao.Create(orm.Context(), r)
|
id, err := suite.dao.Create(orm.Context(), r)
|
||||||
suite.Require().NoError(err)
|
suite.Require().NoError(err)
|
||||||
|
@ -15,12 +15,15 @@
|
|||||||
package scan
|
package scan
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/controller/artifact"
|
||||||
"github.com/goharbor/harbor/src/jobservice/job"
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
"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/scan"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -44,8 +47,21 @@ type Handler interface {
|
|||||||
RequiredPermissions() []*types.Policy
|
RequiredPermissions() []*types.Policy
|
||||||
// RequestParameters defines the parameters for scan request
|
// RequestParameters defines the parameters for scan request
|
||||||
RequestParameters() map[string]interface{}
|
RequestParameters() map[string]interface{}
|
||||||
// ReportURLParameter defines the parameters for scan report
|
|
||||||
ReportURLParameter(sr *v1.ScanRequest) (string, error)
|
|
||||||
// PostScan defines the operation after scan
|
// PostScan defines the operation after scan
|
||||||
PostScan(ctx job.Context, sr *v1.ScanRequest, rp *scan.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error)
|
PostScan(ctx job.Context, sr *v1.ScanRequest, rp *scan.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ package scan
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -35,7 +34,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/lib/config"
|
"github.com/goharbor/harbor/src/lib/config"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
"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/dao/scanner"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
@ -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"))
|
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 {
|
if err != nil {
|
||||||
errs[i] = errors.Wrap(err, "scan job: get report url")
|
errs[i] = errors.Wrap(err, "scan job: get report url")
|
||||||
return
|
return
|
||||||
@ -298,7 +296,7 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, mimeType := range mimeTypes {
|
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 {
|
if err != nil {
|
||||||
return err
|
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
|
// 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
|
// contains additional metadata within the report which if stored in the new columns within the scan_report table
|
||||||
// would be redundant
|
// 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)
|
myLogger.Errorf("Failed to update report data for report %s, error %v", rp.UUID, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
myLogger.Debugf("Converted report ID %s to the new V2 schema", rp.UUID)
|
myLogger.Debugf("Converted report ID %s to the new V2 schema", rp.UUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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) {
|
func fetchScanReportFromScanner(client v1.Client, requestID string, mimType string, urlParameter string) (rawReport string, err error) {
|
||||||
rawReport, err = client.GetScanReport(requestID, mimType, urlParameter)
|
rawReport, err = client.GetScanReport(requestID, mimType, urlParameter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -161,26 +161,6 @@ func (suite *JobTestSuite) TestJob() {
|
|||||||
require.NoError(suite.T(), err)
|
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() {
|
func (suite *JobTestSuite) TestfetchScanReportFromScanner() {
|
||||||
vulnRpt := &vuln.Report{
|
vulnRpt := &vuln.Report{
|
||||||
GeneratedAt: time.Now().UTC().String(),
|
GeneratedAt: time.Now().UTC().String(),
|
||||||
|
126
src/pkg/scan/sbom/dao/dao.go
Normal file
126
src/pkg/scan/sbom/dao/dao.go
Normal 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
|
||||||
|
}
|
133
src/pkg/scan/sbom/dao/dao_test.go
Normal file
133
src/pkg/scan/sbom/dao/dao_test.go
Normal 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))
|
||||||
|
}
|
203
src/pkg/scan/sbom/manager.go
Normal file
203
src/pkg/scan/sbom/manager.go
Normal 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
|
||||||
|
}
|
46
src/pkg/scan/sbom/model/report.go
Normal file
46
src/pkg/scan/sbom/model/report.go
Normal 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"`
|
||||||
|
}
|
@ -23,18 +23,26 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common"
|
"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/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/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/permission/types"
|
||||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan"
|
"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"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -43,32 +51,72 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
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 {
|
type scanHandler struct {
|
||||||
GenAccessoryFunc func(scanRep v1.ScanRequest, sbomContent []byte, labels map[string]string, mediaType string, robot *model.Robot) (string, error)
|
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)
|
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
|
// RequestProducesMineTypes defines the mine types produced by the scan handler
|
||||||
func (v *scanHandler) RequestProducesMineTypes() []string {
|
func (h *scanHandler) RequestProducesMineTypes() []string {
|
||||||
return []string{v1.MimeTypeSBOMReport}
|
return []string{v1.MimeTypeSBOMReport}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestParameters defines the parameters for scan request
|
// 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}}
|
return map[string]interface{}{"sbom_media_types": []string{sbomMediaTypeSpdx}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReportURLParameter defines the parameters for scan report url
|
// PostScan defines task specific operations after the scan is complete
|
||||||
func (v *scanHandler) ReportURLParameter(_ *v1.ScanRequest) (string, error) {
|
func (h *scanHandler) PostScan(ctx job.Context, sr *v1.ScanRequest, _ *scanModel.Report, rawReport string, startTime time.Time, robot *model.Robot) (string, error) {
|
||||||
|
sbomContent, s, err := retrieveSBOMContent(rawReport)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
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
|
return fmt.Sprintf("sbom_media_type=%s", url.QueryEscape(sbomMediaTypeSpdx)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequiredPermissions defines the permission used by the scan robot account
|
// RequiredPermissions defines the permission used by the scan robot account
|
||||||
func (v *scanHandler) RequiredPermissions() []*types.Policy {
|
func (h *scanHandler) RequiredPermissions() []*types.Policy {
|
||||||
return []*types.Policy{
|
return []*types.Policy{
|
||||||
{
|
{
|
||||||
Resource: rbac.ResourceRepository,
|
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
|
// 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)
|
t := time.Now().Format(time.RFC3339)
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
"created": t,
|
"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{}
|
summary := sbom.Summary{}
|
||||||
endTime := time.Now()
|
endTime := time.Now()
|
||||||
summary[sbom.StartTime] = startTime
|
summary[sbom.StartTime] = startTime
|
||||||
@ -138,6 +161,14 @@ func (v *scanHandler) generateReport(startTime time.Time, repository, digest, st
|
|||||||
return string(rep), nil
|
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
|
// extract server name from config, and remove the protocol prefix
|
||||||
func registry(ctx context.Context) (string, bool) {
|
func registry(ctx context.Context) (string, bool) {
|
||||||
cfgMgr, ok := config.FromContext(ctx)
|
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
|
// retrieveSBOMContent retrieves the "sbom" field from the raw report
|
||||||
func retrieveSBOMContent(rawReport string) ([]byte, *v1.Scanner, error) {
|
func retrieveSBOMContent(rawReport string) ([]byte, *v1.Scanner, error) {
|
||||||
rpt := vuln.Report{}
|
rpt := sbom.RawSBOMReport{}
|
||||||
err := json.Unmarshal([]byte(rawReport), &rpt)
|
err := json.Unmarshal([]byte(rawReport), &rpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@ -164,3 +195,153 @@ func retrieveSBOMContent(rawReport string) ([]byte, *v1.Scanner, error) {
|
|||||||
}
|
}
|
||||||
return sbomContent, rpt.Scanner, nil
|
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
|
||||||
|
}
|
||||||
|
@ -6,15 +6,42 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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/common/rbac"
|
||||||
|
"github.com/goharbor/harbor/src/controller/artifact"
|
||||||
"github.com/goharbor/harbor/src/pkg/permission/types"
|
"github.com/goharbor/harbor/src/pkg/permission/types"
|
||||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
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/goharbor/harbor/src/testing/jobservice"
|
||||||
|
sbomTest "github.com/goharbor/harbor/src/testing/pkg/scan/sbom"
|
||||||
"github.com/stretchr/testify/suite"
|
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) {
|
func Test_scanHandler_ReportURLParameter(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
in0 *v1.ScanRequest
|
in0 *v1.ScanRequest
|
||||||
@ -30,13 +57,13 @@ func Test_scanHandler_ReportURLParameter(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
v := &scanHandler{}
|
v := &scanHandler{}
|
||||||
got, err := v.ReportURLParameter(tt.args.in0)
|
got, err := v.URLParameter(tt.args.in0)
|
||||||
if (err != nil) != tt.wantErr {
|
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
|
return
|
||||||
}
|
}
|
||||||
if got != tt.want {
|
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
|
return "sha256:1234567890", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExampleTestSuite struct {
|
type SBOMTestSuite struct {
|
||||||
handler *scanHandler
|
htesting.Suite
|
||||||
suite.Suite
|
handler *scanHandler
|
||||||
|
sbomManager *sbomTest.Manager
|
||||||
|
taskMgr *taskTest.Manager
|
||||||
|
artifactCtl *artifactTest.Controller
|
||||||
|
artifact *artifact.Artifact
|
||||||
|
wrongArtifact *artifact.Artifact
|
||||||
|
scanController *scanTest.Controller
|
||||||
|
scannerController *scannerTest.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ExampleTestSuite) SetupSuite() {
|
func (suite *SBOMTestSuite) SetupSuite() {
|
||||||
|
suite.sbomManager = &sbomTest.Manager{}
|
||||||
|
suite.taskMgr = &taskTest.Manager{}
|
||||||
|
suite.artifactCtl = &artifactTest.Controller{}
|
||||||
|
suite.scannerController = &scannerTest.Controller{}
|
||||||
|
suite.scanController = &scanTest.Controller{}
|
||||||
|
|
||||||
suite.handler = &scanHandler{
|
suite.handler = &scanHandler{
|
||||||
GenAccessoryFunc: mockGenAccessory,
|
GenAccessoryFunc: mockGenAccessory,
|
||||||
RegistryServer: mockGetRegistry,
|
RegistryServer: mockGetRegistry,
|
||||||
|
SBOMMgrFunc: func() Manager { return suite.sbomManager },
|
||||||
|
TaskMgrFunc: func() task.Manager { return suite.taskMgr },
|
||||||
|
ArtifactControllerFunc: func() artifact.Controller { return suite.artifactCtl },
|
||||||
|
ScanControllerFunc: func() sc.Controller { return suite.scanController },
|
||||||
|
ScannerControllerFunc: func() scanner.Controller { return suite.scannerController },
|
||||||
|
cloneCtx: func(ctx context.Context) context.Context {
|
||||||
|
return ctx
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suite.artifact = &artifact.Artifact{Artifact: art.Artifact{ID: 1}}
|
||||||
|
suite.artifact.Type = "IMAGE"
|
||||||
|
suite.artifact.ProjectID = 1
|
||||||
|
suite.artifact.RepositoryName = "library/photon"
|
||||||
|
suite.artifact.Digest = "digest-code"
|
||||||
|
suite.artifact.ManifestMediaType = v1.MimeTypeDockerArtifact
|
||||||
|
|
||||||
|
suite.wrongArtifact = &artifact.Artifact{Artifact: art.Artifact{ID: 2, ProjectID: 1}}
|
||||||
|
suite.wrongArtifact.Digest = "digest-wrong"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ExampleTestSuite) TearDownSuite() {
|
func (suite *SBOMTestSuite) TearDownSuite() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ExampleTestSuite) TestPostScan() {
|
func (suite *SBOMTestSuite) TestPostScan() {
|
||||||
req := &v1.ScanRequest{
|
req := &v1.ScanRequest{
|
||||||
Registry: &v1.Registry{
|
Registry: &v1.Registry{
|
||||||
URL: "myregistry.example.com",
|
URL: "myregistry.example.com",
|
||||||
@ -134,6 +192,62 @@ func (suite *ExampleTestSuite) TestPostScan() {
|
|||||||
suite.Require().NotEmpty(accessory)
|
suite.Require().NotEmpty(accessory)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExampleTestSuite(t *testing.T) {
|
func (suite *SBOMTestSuite) TestMakeReportPlaceHolder() {
|
||||||
suite.Run(t, &ExampleTestSuite{})
|
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{})
|
||||||
}
|
}
|
||||||
|
@ -15,38 +15,227 @@
|
|||||||
package vulnerability
|
package vulnerability
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/rbac"
|
"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/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/permission/types"
|
||||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||||
scanJob "github.com/goharbor/harbor/src/pkg/scan"
|
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/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/postprocessors"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
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
|
// scanHandler defines the handler for scan vulnerability
|
||||||
type ScanHandler struct {
|
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
|
// RequestProducesMineTypes returns the produces mime types
|
||||||
func (v *ScanHandler) RequestProducesMineTypes() []string {
|
func (h *scanHandler) RequestProducesMineTypes() []string {
|
||||||
return []string{v1.MimeTypeGenericVulnerabilityReport}
|
return []string{v1.MimeTypeGenericVulnerabilityReport}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestParameters defines the parameters for scan request
|
// RequestParameters defines the parameters for scan request
|
||||||
func (v *ScanHandler) RequestParameters() map[string]interface{} {
|
func (h *scanHandler) RequestParameters() map[string]interface{} {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequiredPermissions defines the permission used by the scan robot account
|
// RequiredPermissions defines the permission used by the scan robot account
|
||||||
func (v *ScanHandler) RequiredPermissions() []*types.Policy {
|
func (h *scanHandler) RequiredPermissions() []*types.Policy {
|
||||||
return []*types.Policy{
|
return []*types.Policy{
|
||||||
{
|
{
|
||||||
Resource: rbac.ResourceRepository,
|
Resource: rbac.ResourceRepository,
|
||||||
@ -59,14 +248,56 @@ func (v *ScanHandler) RequiredPermissions() []*types.Policy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReportURLParameter vulnerability doesn't require any scan report parameters
|
// PostScan ...
|
||||||
func (v *ScanHandler) ReportURLParameter(_ *v1.ScanRequest) (string, error) {
|
func (h *scanHandler) PostScan(ctx job.Context, _ *v1.ScanRequest, origRp *scan.Report, rawReport string,
|
||||||
|
_ time.Time, _ *model.Robot) (string, error) {
|
||||||
|
// use a new ormer here to use the short db connection
|
||||||
|
_, refreshedReport, err := postprocessors.Converter.ToRelationalSchema(ctx.SystemContext(), origRp.UUID,
|
||||||
|
origRp.RegistrationUUID, origRp.Digest, rawReport)
|
||||||
|
return refreshedReport, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// URLParameter vulnerability doesn't require any scan report parameters
|
||||||
|
func (h *scanHandler) URLParameter(_ *v1.ScanRequest) (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostScan ...
|
func (h *scanHandler) Update(ctx context.Context, uuid string, rpt string) error {
|
||||||
func (v *ScanHandler) PostScan(ctx job.Context, _ *v1.ScanRequest, origRp *scan.Report, rawReport string, _ time.Time, _ *model.Robot) (string, error) {
|
reportMgr := h.ReportMgrFunc()
|
||||||
// use a new ormer here to use the short db connection
|
if err := reportMgr.UpdateReportData(ctx, uuid, rpt); err != nil {
|
||||||
_, refreshedReport, err := postprocessors.Converter.ToRelationalSchema(ctx.SystemContext(), origRp.UUID, origRp.RegistrationUUID, origRp.Digest, rawReport)
|
return err
|
||||||
return refreshedReport, 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
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,44 @@
|
|||||||
package vulnerability
|
package vulnerability
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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/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/permission/types"
|
||||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
"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/scan"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/postprocessors"
|
"github.com/goharbor/harbor/src/pkg/scan/postprocessors"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
"github.com/goharbor/harbor/src/testing/jobservice"
|
"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"
|
postprocessorstesting "github.com/goharbor/harbor/src/testing/pkg/scan/postprocessors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRequiredPermissions(t *testing.T) {
|
func TestRequiredPermissions(t *testing.T) {
|
||||||
v := &ScanHandler{}
|
v := &scanHandler{}
|
||||||
expected := []*types.Policy{
|
expected := []*types.Policy{
|
||||||
{
|
{
|
||||||
Resource: rbac.ResourceRepository,
|
Resource: rbac.ResourceRepository,
|
||||||
@ -37,7 +56,7 @@ func TestRequiredPermissions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPostScan(t *testing.T) {
|
func TestPostScan(t *testing.T) {
|
||||||
v := &ScanHandler{}
|
v := &scanHandler{}
|
||||||
ctx := &jobservice.MockJobContext{}
|
ctx := &jobservice.MockJobContext{}
|
||||||
artifact := &v1.Artifact{}
|
artifact := &v1.Artifact{}
|
||||||
origRp := &scan.Report{}
|
origRp := &scan.Report{}
|
||||||
@ -70,7 +89,7 @@ func TestScanHandler_RequiredPermissions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
v := &ScanHandler{}
|
v := &scanHandler{}
|
||||||
assert.Equalf(t, tt.want, v.RequiredPermissions(), "RequiredPermissions()")
|
assert.Equalf(t, tt.want, v.RequiredPermissions(), "RequiredPermissions()")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -90,12 +109,12 @@ func TestScanHandler_ReportURLParameter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
v := &ScanHandler{}
|
v := &scanHandler{}
|
||||||
got, err := v.ReportURLParameter(tt.args.in0)
|
got, err := v.URLParameter(tt.args.in0)
|
||||||
if !tt.wantErr(t, err, fmt.Sprintf("ReportURLParameter(%v)", tt.args.in0)) {
|
if !tt.wantErr(t, err, fmt.Sprintf("URLParameter(%v)", tt.args.in0)) {
|
||||||
return
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
v := &ScanHandler{}
|
v := &scanHandler{}
|
||||||
assert.Equalf(t, tt.want, v.RequestProducesMineTypes(), "RequestProducesMineTypes()")
|
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)
|
||||||
|
}
|
||||||
|
@ -257,6 +257,24 @@ func (_m *mockTaskManager) ListScanTasksByReportUUID(ctx context.Context, uuid s
|
|||||||
return r0, r1
|
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
|
// Stop provides a mock function with given fields: ctx, id
|
||||||
func (_m *mockTaskManager) Stop(ctx context.Context, id int64) error {
|
func (_m *mockTaskManager) Stop(ctx context.Context, id int64) error {
|
||||||
ret := _m.Called(ctx, id)
|
ret := _m.Called(ctx, id)
|
||||||
|
@ -67,6 +67,8 @@ type Manager interface {
|
|||||||
// ListScanTasksByReportUUID lists scan tasks by report uuid, although it's a specific case but it will be
|
// 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.
|
// more suitable to support multi database in the future.
|
||||||
ListScanTasksByReportUUID(ctx context.Context, uuid string) (tasks []*Task, err error)
|
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
|
// 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) {
|
func (m *manager) GetLogByJobID(_ context.Context, jobID string) (log []byte, err error) {
|
||||||
return m.jsClient.GetJobLog(jobID)
|
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 ""
|
||||||
|
}
|
||||||
|
@ -160,6 +160,17 @@ func (t *taskManagerTestSuite) TestListScanTasksByReportUUID() {
|
|||||||
t.dao.AssertExpectations(t.T())
|
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) {
|
func TestTaskManagerTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &taskManagerTestSuite{})
|
suite.Run(t, &taskManagerTestSuite{})
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,6 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
vulnerabilitiesAddition = "vulnerabilities"
|
vulnerabilitiesAddition = "vulnerabilities"
|
||||||
sbomAddition = "sbom"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewScanReportAssembler returns vul assembler
|
// NewScanReportAssembler returns vul assembler
|
||||||
@ -80,7 +79,7 @@ func (assembler *ScanReportAssembler) Assemble(ctx context.Context) error {
|
|||||||
|
|
||||||
if assembler.overviewOption.WithVuln {
|
if assembler.overviewOption.WithVuln {
|
||||||
for _, mimeType := range assembler.mimeTypes {
|
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 {
|
if err != nil {
|
||||||
log.Warningf("get scan summary of artifact %s@%s for %s failed, error:%v", artifact.RepositoryName, artifact.Digest, mimeType, err)
|
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 {
|
} 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
|
// set sbom additional link if it is supported, use the empty digest
|
||||||
artifact.SetSBOMAdditionLink("", version)
|
artifact.SetSBOMAdditionLink("", version)
|
||||||
if assembler.overviewOption.WithSBOM {
|
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 {
|
if err != nil {
|
||||||
log.Warningf("get scan summary of artifact %s@%s for %s failed, error:%v", artifact.RepositoryName, artifact.Digest, v1.MimeTypeSBOMReport, err)
|
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 {
|
if len(overview) == 0 {
|
||||||
log.Warningf("overview is empty, retrieve sbom status from execution")
|
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
|
// sort by ID desc to get the latest execution
|
||||||
query.Sorts = []*q.Sort{q.NewSort("ID", true)}
|
query.Sorts = []*q.Sort{q.NewSort("ID", true)}
|
||||||
execs, err := assembler.executionMgr.List(ctx, query)
|
execs, err := assembler.executionMgr.List(ctx, query)
|
||||||
|
@ -7,13 +7,13 @@ import (
|
|||||||
|
|
||||||
artifact "github.com/goharbor/harbor/src/controller/artifact"
|
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"
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
models "github.com/goharbor/harbor/src/pkg/allowlist/models"
|
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
|
// Controller is an autogenerated mock type for the Controller type
|
||||||
@ -21,49 +21,24 @@ type Controller struct {
|
|||||||
mock.Mock
|
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
|
// 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)
|
ret := _m.Called(ctx, _a1, mimeTypes)
|
||||||
|
|
||||||
if len(ret) == 0 {
|
if len(ret) == 0 {
|
||||||
panic("no return value specified for GetReport")
|
panic("no return value specified for GetReport")
|
||||||
}
|
}
|
||||||
|
|
||||||
var r0 []*daoscan.Report
|
var r0 []*scan.Report
|
||||||
var r1 error
|
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)
|
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)
|
r0 = rf(ctx, _a1, mimeTypes)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
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
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSummary provides a mock function with given fields: 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, mimeTypes []string) (map[string]interface{}, error) {
|
func (_m *Controller) GetSummary(ctx context.Context, _a1 *artifact.Artifact, scanType string, mimeTypes []string) (map[string]interface{}, error) {
|
||||||
ret := _m.Called(ctx, _a1, mimeTypes)
|
ret := _m.Called(ctx, _a1, scanType, mimeTypes)
|
||||||
|
|
||||||
if len(ret) == 0 {
|
if len(ret) == 0 {
|
||||||
panic("no return value specified for GetSummary")
|
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 r0 map[string]interface{}
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string) (map[string]interface{}, error)); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, string, []string) (map[string]interface{}, error)); ok {
|
||||||
return rf(ctx, _a1, mimeTypes)
|
return rf(ctx, _a1, scanType, mimeTypes)
|
||||||
}
|
}
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string) map[string]interface{}); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, string, []string) map[string]interface{}); ok {
|
||||||
r0 = rf(ctx, _a1, mimeTypes)
|
r0 = rf(ctx, _a1, scanType, mimeTypes)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
if ret.Get(0) != nil {
|
||||||
r0 = ret.Get(0).(map[string]interface{})
|
r0 = ret.Get(0).(map[string]interface{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, []string) error); ok {
|
if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, string, []string) error); ok {
|
||||||
r1 = rf(ctx, _a1, mimeTypes)
|
r1 = rf(ctx, _a1, scanType, mimeTypes)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
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
|
// 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)
|
ret := _m.Called(ctx, _a1, allowlist, allowlistIsExpired)
|
||||||
|
|
||||||
if len(ret) == 0 {
|
if len(ret) == 0 {
|
||||||
panic("no return value specified for GetVulnerable")
|
panic("no return value specified for GetVulnerable")
|
||||||
}
|
}
|
||||||
|
|
||||||
var r0 *scan.Vulnerable
|
var r0 *controllerscan.Vulnerable
|
||||||
var r1 error
|
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)
|
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)
|
r0 = rf(ctx, _a1, allowlist, allowlistIsExpired)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
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
|
// 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))
|
_va := make([]interface{}, len(options))
|
||||||
for _i := range options {
|
for _i := range options {
|
||||||
_va[_i] = options[_i]
|
_va[_i] = options[_i]
|
||||||
@ -182,7 +157,7 @@ func (_m *Controller) Scan(ctx context.Context, _a1 *artifact.Artifact, options
|
|||||||
}
|
}
|
||||||
|
|
||||||
var r0 error
|
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...)
|
r0 = rf(ctx, _a1, options...)
|
||||||
} else {
|
} else {
|
||||||
r0 = ret.Error(0)
|
r0 = ret.Error(0)
|
||||||
|
@ -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/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/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/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
|
||||||
|
268
src/testing/pkg/scan/handler.go
Normal file
268
src/testing/pkg/scan/handler.go
Normal 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
|
||||||
|
}
|
216
src/testing/pkg/scan/sbom/manager.go
Normal file
216
src/testing/pkg/scan/sbom/manager.go
Normal 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
|
||||||
|
}
|
@ -259,6 +259,24 @@ func (_m *Manager) ListScanTasksByReportUUID(ctx context.Context, uuid string) (
|
|||||||
return r0, r1
|
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
|
// Stop provides a mock function with given fields: ctx, id
|
||||||
func (_m *Manager) Stop(ctx context.Context, id int64) error {
|
func (_m *Manager) Stop(ctx context.Context, id int64) error {
|
||||||
ret := _m.Called(ctx, id)
|
ret := _m.Called(ctx, id)
|
||||||
|
Loading…
Reference in New Issue
Block a user