feat(scan): support to scan image index (#11001)

Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
He Weiwei 2020-03-12 19:30:12 +08:00 committed by GitHub
parent 6a25e6b2c6
commit 12f16c8cec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1256 additions and 1070 deletions

View File

@ -10,6 +10,7 @@ schemes:
basePath: /api/v2.0
produces:
- application/json
- text/plain
consumes:
- application/json
securityDefinitions:
@ -333,7 +334,7 @@ paths:
summary: Scan the artifact
description: Scan the specified artifact
tags:
- artifact
- scan
operationId: scanArtifact
parameters:
- $ref: '#/parameters/requestId'
@ -351,6 +352,38 @@ paths:
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/scan/{report_id}/log:
get:
summary: Get the log of the scan report
description: Get the log of the scan report
tags:
- scan
operationId: getReportLog
produces:
- text/plain
parameters:
- $ref: '#/parameters/requestId'
- $ref: '#/parameters/projectName'
- $ref: '#/parameters/repositoryName'
- $ref: '#/parameters/reference'
- name: report_id
type: string
in: path
required: true
description: The report id to get the log
responses:
'200':
description: Successfully get scan log file
schema:
type: string
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/tags:
post:
summary: Create tag
@ -989,6 +1022,10 @@ definitions:
format: date-time
description: 'The end time of the scan process that generating report'
example: '2006-01-02T15:04:05'
complete_percent:
type: integer
description: 'The complete percent of the scanning which value is between 0 and 100'
example: 100
VulnerabilitySummary:
type: object
description: |

View File

@ -22,7 +22,6 @@ import (
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/pkg/q"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/pkg/errors"
)
@ -36,18 +35,9 @@ func HandleCheckIn(ctx context.Context, checkIn string) {
batchSize := 50
for repo := range fetchRepositories(ctx, batchSize) {
for artifact := range fetchArtifacts(ctx, repo.RepositoryID, batchSize) {
for _, tag := range artifact.Tags {
art := &v1.Artifact{
NamespaceID: artifact.ProjectID,
Repository: repo.Name,
Tag: tag.Name,
Digest: artifact.Digest,
MimeType: artifact.ManifestMediaType,
}
if err := DefaultController.Scan(art, WithRequester(checkIn)); err != nil {
// Just logged
log.Error(errors.Wrap(err, "handle check in"))
}
if err := DefaultController.Scan(ctx, artifact, WithRequester(checkIn)); err != nil {
// Just logged
log.Error(errors.Wrap(err, "handle check in"))
}
}
}
@ -67,7 +57,7 @@ func fetchArtifacts(ctx context.Context, repositoryID int64, chunkSize int) <-ch
}
for {
artifacts, err := artifact.Ctl.List(ctx, query, &artifact.Option{WithTag: true})
artifacts, err := artifact.Ctl.List(ctx, query, nil)
if err != nil {
log.Errorf("[scan all]: list artifacts failed, error: %v", err)
return

View File

@ -15,9 +15,12 @@
package scan
import (
"context"
"encoding/base64"
"fmt"
"sync"
ar "github.com/goharbor/harbor/src/api/artifact"
sc "github.com/goharbor/harbor/src/api/scanner"
cj "github.com/goharbor/harbor/src/common/job"
jm "github.com/goharbor/harbor/src/common/job/models"
@ -62,6 +65,8 @@ type jcGetter func() cj.Client
type basicController struct {
// Manage the scan report records
manager report.Manager
// Artifact controller
ar ar.Controller
// Scanner controller
sc sc.Controller
// Robot account controller
@ -79,6 +84,8 @@ func NewController() Controller {
return &basicController{
// New report manager
manager: report.NewManager(),
// Refer to the default artifact controller
ar: ar.Ctl,
// Refer to the default scanner controller
sc: sc.DefaultController,
// Refer to the default robot account controller
@ -110,26 +117,57 @@ func NewController() Controller {
}
}
// Collect artifacts itself or its children (exclude child which is image index and not supported by the scanner) when the artifact is scannable.
// Report placeholders will be created to track when scan the artifact.
// The reports of these artifacts will make together when get the reports of the artifact.
// There are two scenarios when artifact is scannable:
// 1. The scanner has capability for the artifact directly, eg the artifact is docker image.
// 2. The artifact is image index and the scanner has capability for any artifact which is referenced by the artifact.
func (bc *basicController) collectScanningArtifacts(ctx context.Context, r *scanner.Registration, artifact *ar.Artifact) ([]*ar.Artifact, bool, error) {
var (
scannable bool
artifacts []*ar.Artifact
)
walkFn := func(a *ar.Artifact) error {
hasCapability := HasCapability(r, a)
if !hasCapability && a.HasChildren() {
// image index not supported by the scanner, so continue to walk its children
return nil
}
artifacts = append(artifacts, a)
if hasCapability {
scannable = true
return ar.ErrSkip // this artifact supported by the scanner, skip to walk its children
}
return nil
}
if err := bc.ar.Walk(ctx, artifact, walkFn, nil); err != nil {
return nil, false, err
}
return artifacts, scannable, nil
}
// Scan ...
func (bc *basicController) Scan(artifact *v1.Artifact, options ...Option) error {
func (bc *basicController) Scan(ctx context.Context, artifact *ar.Artifact, options ...Option) error {
if artifact == nil {
return errors.New("nil artifact to scan")
}
// Parse options
ops, err := parseOptions(options...)
if err != nil {
return errors.Wrap(err, "scan controller: scan")
}
r, err := bc.sc.GetRegistrationByProject(artifact.NamespaceID)
r, err := bc.sc.GetRegistrationByProject(artifact.ProjectID)
if err != nil {
return errors.Wrap(err, "scan controller: scan")
}
// In case it does not exist
if r == nil {
return errs.WithCode(errs.PreconditionFailed, errs.Errorf("no available scanner for project: %d", artifact.NamespaceID))
return errs.WithCode(errs.PreconditionFailed, errs.Errorf("no available scanner for project: %d", artifact.ProjectID))
}
// Check if it is disabled
@ -137,91 +175,114 @@ func (bc *basicController) Scan(artifact *v1.Artifact, options ...Option) error
return errs.WithCode(errs.PreconditionFailed, errs.Errorf("scanner %s is disabled", r.Name))
}
// Check the health of the registration by ping.
// The metadata of the scanner adapter is also returned.
meta, err := bc.sc.Ping(r)
artifacts, scannable, err := bc.collectScanningArtifacts(ctx, r, artifact)
if err != nil {
return errors.Wrap(err, "scan controller: scan")
return err
}
// Generate a UUID as track ID which groups the report records generated
// by the specified registration for the digest with given mime type.
if !scannable {
return errors.Errorf("the configured scanner %s does not support scanning artifact with mime type %s", r.Name, artifact.ManifestMediaType)
}
type Param struct {
Artifact *ar.Artifact
TrackID string
ProducesMimes []string
}
params := []*Param{}
var errs []error
for _, art := range artifacts {
trackID, producesMimes, err := bc.makeReportPlaceholder(ctx, r, art, options...)
if err != nil {
if ierror.IsConflictErr(err) {
errs = append(errs, err)
} else {
return err
}
}
if len(producesMimes) > 0 {
params = append(params, &Param{Artifact: art, TrackID: trackID, ProducesMimes: producesMimes})
}
}
// all report placeholder conflicted
if len(errs) == len(artifacts) {
return errs[0]
}
errs = errs[:0]
for _, param := range params {
if err := bc.scanArtifact(ctx, r, param.Artifact, param.TrackID, param.ProducesMimes); err != nil {
log.Warningf("scan artifact %s@%s failed, error: %v", artifact.RepositoryName, artifact.Digest, err)
errs = append(errs, err)
}
}
// all scanning of the artifacts failed
if len(errs) == len(params) {
return fmt.Errorf("scan artifact %s@%s failed", artifact.RepositoryName, artifact.Digest)
}
return nil
}
func (bc *basicController) makeReportPlaceholder(ctx context.Context, r *scanner.Registration, art *ar.Artifact, options ...Option) (string, []string, error) {
trackID, err := bc.uuid()
if err != nil {
return errors.Wrap(err, "scan controller: scan")
return "", nil, errors.Wrap(err, "scan controller: scan")
}
producesMimes := make([]string, 0)
matched := false
statusConflict := false
for _, ca := range meta.Capabilities {
for _, cm := range ca.ConsumesMimeTypes {
if cm == artifact.MimeType {
matched = true
break
}
// Parse options
ops, err := parseOptions(options...)
if err != nil {
return "", nil, errors.Wrap(err, "scan controller: scan")
}
create := func(ctx context.Context, digest, registrationUUID, mimeType, trackID string, status job.Status) error {
reportPlaceholder := &scan.Report{
Digest: digest,
RegistrationUUID: registrationUUID,
Status: status.String(),
StatusCode: status.Code(),
TrackID: trackID,
MimeType: mimeType,
}
// Set requester if it is specified
if len(ops.Requester) > 0 {
reportPlaceholder.Requester = ops.Requester
} else {
// Use the trackID as the requester
reportPlaceholder.Requester = trackID
}
if matched {
for _, pm := range ca.ProducesMimeTypes {
// Create report placeholder first
reportPlaceholder := &scan.Report{
Digest: artifact.Digest,
RegistrationUUID: r.UUID,
Status: job.PendingStatus.String(),
StatusCode: job.PendingStatus.Code(),
TrackID: trackID,
MimeType: pm,
}
// Set requester if it is specified
if len(ops.Requester) > 0 {
reportPlaceholder.Requester = ops.Requester
} else {
// Use the trackID as the requester
reportPlaceholder.Requester = trackID
}
_, e := bc.manager.Create(reportPlaceholder)
return e
}
_, e := bc.manager.Create(reportPlaceholder)
if e != nil {
// Check if it is a status conflict error with common error format.
// Common error returned if and only if status conflicts.
if !statusConflict {
statusConflict = errs.AsError(e, errs.Conflict)
}
if HasCapability(r, art) {
var producesMimes []string
// Recorded by error wrap and logged at the same time.
if err == nil {
err = e
} else {
err = errors.Wrap(e, err.Error())
}
logger.Error(errors.Wrap(e, "scan controller: scan"))
continue
}
producesMimes = append(producesMimes, pm)
for _, pm := range r.GetProducesMimeTypes(art.ManifestMediaType) {
if err = create(ctx, art.Digest, r.UUID, pm, trackID, job.PendingStatus); err != nil {
return "", nil, err
}
break
producesMimes = append(producesMimes, pm)
}
if len(producesMimes) > 0 {
return trackID, producesMimes, nil
}
}
// Scanner does not support scanning the given artifact.
if !matched {
return errors.Errorf("the configured scanner %s does not support scanning artifact with mime type %s", r.Name, artifact.MimeType)
}
// If all the record are created failed.
if len(producesMimes) == 0 {
// Return the last error
if statusConflict {
return errs.WithCode(errs.Conflict, errs.Wrap(err, "scan controller: scan"))
}
return errors.Wrap(err, "scan controller: scan")
}
err = create(ctx, art.Digest, r.UUID, v1.MimeTypeNativeReport, trackID, job.ErrorStatus)
return "", nil, err
}
func (bc *basicController) scanArtifact(ctx context.Context, r *scanner.Registration, artifact *ar.Artifact, trackID string, producesMimes []string) error {
jobID, err := bc.launchScanJob(trackID, artifact, r, producesMimes)
if err != nil {
// Update the status to the concrete error
@ -243,7 +304,7 @@ func (bc *basicController) Scan(artifact *v1.Artifact, options ...Option) error
}
// GetReport ...
func (bc *basicController) GetReport(artifact *v1.Artifact, mimeTypes []string) ([]*scan.Report, error) {
func (bc *basicController) GetReport(ctx context.Context, artifact *ar.Artifact, mimeTypes []string) ([]*scan.Report, error) {
if artifact == nil {
return nil, errors.New("no way to get report for nil artifact")
}
@ -256,26 +317,67 @@ func (bc *basicController) GetReport(artifact *v1.Artifact, mimeTypes []string)
}
// Get current scanner settings
r, err := bc.sc.GetRegistrationByProject(artifact.NamespaceID)
r, err := bc.sc.GetRegistrationByProject(artifact.ProjectID)
if err != nil {
return nil, errors.Wrap(err, "scan controller: get report")
}
if r == nil {
return nil, ierror.NotFoundError(nil).WithMessage("no scanner registration configured for project: %d", artifact.NamespaceID)
return nil, ierror.NotFoundError(nil).WithMessage("no scanner registration configured for project: %d", artifact.ProjectID)
}
return bc.manager.GetBy(artifact.Digest, r.UUID, mimes)
artifacts, scannable, err := bc.collectScanningArtifacts(ctx, r, artifact)
if err != nil {
return nil, err
}
if !scannable {
return nil, ierror.NotFoundError(nil).WithMessage("report not found for %s@%s", artifact.RepositoryName, artifact.Digest)
}
groupReports := make([][]*scan.Report, len(artifacts))
var wg sync.WaitGroup
for i, a := range artifacts {
wg.Add(1)
go func(i int, a *ar.Artifact) {
defer wg.Done()
reports, err := bc.manager.GetBy(a.Digest, r.UUID, mimes)
if err != nil {
log.Warningf("get reports of %s@%s failed, error: %v", a.RepositoryName, a.Digest, err)
return
}
groupReports[i] = reports
}(i, a)
}
wg.Wait()
var reports []*scan.Report
for _, group := range groupReports {
if len(group) != 0 {
reports = append(reports, group...)
} else {
// NOTE: If the artifact is OCI image, this happened when the artifact is not scanned.
// If the artifact is OCI image index, this happened when the artifact is not scanned,
// but its children artifacts may scanned so return empty report
return nil, nil
}
}
return reports, nil
}
// GetSummary ...
func (bc *basicController) GetSummary(artifact *v1.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error) {
func (bc *basicController) GetSummary(ctx context.Context, artifact *ar.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error) {
if artifact == nil {
return nil, errors.New("no way to get report summaries for nil artifact")
}
// Get reports first
rps, err := bc.GetReport(artifact, mimeTypes)
rps, err := bc.GetReport(ctx, artifact, mimeTypes)
if err != nil {
return nil, err
}
@ -287,7 +389,16 @@ func (bc *basicController) GetSummary(artifact *v1.Artifact, mimeTypes []string,
return nil, err
}
summaries[rp.MimeType] = sum
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
@ -423,7 +534,7 @@ func (bc *basicController) makeRobotAccount(projectID int64, repository string)
}
// launchScanJob launches a job to run scan
func (bc *basicController) launchScanJob(trackID string, artifact *v1.Artifact, registration *scanner.Registration, mimes []string) (jobID string, err error) {
func (bc *basicController) launchScanJob(trackID string, artifact *ar.Artifact, registration *scanner.Registration, mimes []string) (jobID string, err error) {
var ck string
if registration.UseInternalAddr {
ck = configCoreInternalAddr
@ -436,7 +547,7 @@ func (bc *basicController) launchScanJob(trackID string, artifact *v1.Artifact,
return "", errors.Wrap(err, "scan controller: launch scan job")
}
robot, err := bc.makeRobotAccount(artifact.NamespaceID, artifact.Repository)
robot, err := bc.makeRobotAccount(artifact.ProjectID, artifact.RepositoryName)
if err != nil {
return "", errors.Wrap(err, "scan controller: launch scan job")
}
@ -450,7 +561,12 @@ func (bc *basicController) launchScanJob(trackID string, artifact *v1.Artifact,
URL: registryAddr,
Authorization: authorization,
},
Artifact: artifact,
Artifact: &v1.Artifact{
NamespaceID: artifact.ProjectID,
Repository: artifact.RepositoryName,
Digest: artifact.Digest,
MimeType: artifact.ManifestMediaType,
},
}
rJSON, err := registration.ToJSON()

View File

@ -15,12 +15,14 @@
package scan
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/goharbor/harbor/src/api/artifact"
"github.com/goharbor/harbor/src/common"
cj "github.com/goharbor/harbor/src/common/job"
cjm "github.com/goharbor/harbor/src/common/job/models"
@ -30,12 +32,14 @@ import (
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/robot/model"
sca "github.com/goharbor/harbor/src/pkg/scan"
"github.com/goharbor/harbor/src/pkg/scan/all"
"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"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
artifacttesting "github.com/goharbor/harbor/src/testing/api/artifact"
scannertesting "github.com/goharbor/harbor/src/testing/api/scanner"
mocktesting "github.com/goharbor/harbor/src/testing/mock"
reporttesting "github.com/goharbor/harbor/src/testing/pkg/scan/report"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -47,9 +51,11 @@ type ControllerTestSuite struct {
suite.Suite
registration *scanner.Registration
artifact *v1.Artifact
artifact *artifact.Artifact
rawReport string
c Controller
ar artifact.Controller
c Controller
}
// TestController is the entry point of ControllerTestSuite.
@ -59,21 +65,11 @@ func TestController(t *testing.T) {
// SetupSuite ...
func (suite *ControllerTestSuite) SetupSuite() {
suite.registration = &scanner.Registration{
ID: 1,
UUID: "uuid001",
Name: "Test-scan-controller",
URL: "http://testing.com:3128",
IsDefault: true,
}
suite.artifact = &v1.Artifact{
NamespaceID: 1,
Repository: "scan",
Tag: "golang",
Digest: "digest-code",
MimeType: v1.MimeTypeDockerArtifact,
}
suite.artifact = &artifact.Artifact{}
suite.artifact.ProjectID = 1
suite.artifact.RepositoryName = "library/photon"
suite.artifact.Digest = "digest-code"
suite.artifact.ManifestMediaType = v1.MimeTypeDockerArtifact
m := &v1.ScannerAdapterMetadata{
Scanner: &v1.Scanner{
@ -95,11 +91,20 @@ func (suite *ControllerTestSuite) SetupSuite() {
},
}
suite.registration = &scanner.Registration{
ID: 1,
UUID: "uuid001",
Name: "Test-scan-controller",
URL: "http://testing.com:3128",
IsDefault: true,
Metadata: m,
}
sc := &scannertesting.Controller{}
sc.On("GetRegistrationByProject", suite.artifact.NamespaceID).Return(suite.registration, nil)
sc.On("GetRegistrationByProject", suite.artifact.ProjectID).Return(suite.registration, nil)
sc.On("Ping", suite.registration).Return(m, nil)
mgr := &MockReportManager{}
mgr := &reporttesting.Manager{}
mgr.On("Create", &scan.Report{
Digest: "digest-code",
RegistrationUUID: "uuid001",
@ -161,7 +166,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
rc := &MockRobotController{}
resource := fmt.Sprintf("/project/%d/repository", suite.artifact.NamespaceID)
resource := fmt.Sprintf("/project/%d/repository", suite.artifact.ProjectID)
access := []*rbac.Policy{{
Resource: rbac.Resource(resource),
Action: rbac.ActionScannerPull,
@ -171,7 +176,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
account := &model.RobotCreate{
Name: rname,
Description: "for scan",
ProjectID: suite.artifact.NamespaceID,
ProjectID: suite.artifact.ProjectID,
Access: access,
}
rc.On("CreateRobotAccount", account).Return(&model.Robot{
@ -179,7 +184,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
Name: common.RobotPrefix + rname,
Token: "robot-account",
Description: "for scan",
ProjectID: suite.artifact.NamespaceID,
ProjectID: suite.artifact.ProjectID,
}, nil)
// Set job parameters
@ -188,7 +193,12 @@ func (suite *ControllerTestSuite) SetupSuite() {
URL: "https://core.com",
Authorization: "Basic " + base64.StdEncoding.EncodeToString([]byte(common.RobotPrefix+"the-uuid-123:robot-account")),
},
Artifact: suite.artifact,
Artifact: &v1.Artifact{
NamespaceID: suite.artifact.ProjectID,
Digest: suite.artifact.Digest,
Repository: suite.artifact.RepositoryName,
MimeType: suite.artifact.ManifestMediaType,
},
}
rJSON, err := req.ToJSON()
@ -215,8 +225,15 @@ func (suite *ControllerTestSuite) SetupSuite() {
jc.On("SubmitJob", j).Return("the-job-id", nil)
jc.On("GetJobLog", "the-job-id").Return([]byte("job log"), nil)
suite.ar = &artifacttesting.Controller{}
mocktesting.OnAnything(suite.ar, "Walk").Return(nil).Run(func(args mock.Arguments) {
walkFn := args.Get(2).(func(*artifact.Artifact) error)
walkFn(suite.artifact)
})
suite.c = &basicController{
manager: mgr,
ar: suite.ar,
sc: sc,
jc: func() cj.Client {
return jc
@ -243,20 +260,20 @@ func (suite *ControllerTestSuite) TearDownSuite() {}
// TestScanControllerScan ...
func (suite *ControllerTestSuite) TestScanControllerScan() {
err := suite.c.Scan(suite.artifact)
err := suite.c.Scan(context.TODO(), suite.artifact)
require.NoError(suite.T(), err)
}
// TestScanControllerGetReport ...
func (suite *ControllerTestSuite) TestScanControllerGetReport() {
rep, err := suite.c.GetReport(suite.artifact, []string{v1.MimeTypeNativeReport})
rep, err := suite.c.GetReport(context.TODO(), suite.artifact, []string{v1.MimeTypeNativeReport})
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(rep))
}
// TestScanControllerGetSummary ...
func (suite *ControllerTestSuite) TestScanControllerGetSummary() {
sum, err := suite.c.GetSummary(suite.artifact, []string{v1.MimeTypeNativeReport})
sum, err := suite.c.GetSummary(context.TODO(), suite.artifact, []string{v1.MimeTypeNativeReport})
require.NoError(suite.T(), err)
assert.Equal(suite.T(), 1, len(sum))
}
@ -298,73 +315,6 @@ func (suite *ControllerTestSuite) TestScanControllerHandleJobHooks() {
// Mock things
// MockReportManager ...
type MockReportManager struct {
mock.Mock
}
// Create ...
func (mrm *MockReportManager) Create(r *scan.Report) (string, error) {
args := mrm.Called(r)
return args.String(0), args.Error(1)
}
// UpdateScanJobID ...
func (mrm *MockReportManager) UpdateScanJobID(trackID string, jobID string) error {
args := mrm.Called(trackID, jobID)
return args.Error(0)
}
func (mrm *MockReportManager) UpdateStatus(trackID string, status string, rev int64) error {
args := mrm.Called(trackID, status, rev)
return args.Error(0)
}
func (mrm *MockReportManager) UpdateReportData(uuid string, report string, rev int64) error {
args := mrm.Called(uuid, report, rev)
return args.Error(0)
}
func (mrm *MockReportManager) GetBy(digest string, registrationUUID string, mimeTypes []string) ([]*scan.Report, error) {
args := mrm.Called(digest, registrationUUID, mimeTypes)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*scan.Report), args.Error(1)
}
func (mrm *MockReportManager) Get(uuid string) (*scan.Report, error) {
args := mrm.Called(uuid)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*scan.Report), args.Error(1)
}
func (mrm *MockReportManager) DeleteByDigests(digests ...string) error {
args := mrm.Called(digests)
return args.Error(0)
}
func (mrm *MockReportManager) GetStats(requester string) (*all.Stats, error) {
args := mrm.Called(requester)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*all.Stats), args.Error(1)
}
// MockJobServiceClient ...
type MockJobServiceClient struct {
mock.Mock

View File

@ -19,7 +19,7 @@ import (
"github.com/goharbor/harbor/src/api/artifact"
"github.com/goharbor/harbor/src/api/scanner"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
models "github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
)
// Checker checker which can check that the artifact is scannable
@ -31,24 +31,28 @@ type Checker interface {
// NewChecker returns checker
func NewChecker() Checker {
return &checker{
artifactCtl: artifact.Ctl,
scannerCtl: scanner.DefaultController,
scannerMetadatas: map[int64]*v1.ScannerAdapterMetadata{},
artifactCtl: artifact.Ctl,
scannerCtl: scanner.DefaultController,
registrations: map[int64]*models.Registration{},
}
}
type checker struct {
artifactCtl artifact.Controller
scannerCtl scanner.Controller
scannerMetadatas map[int64]*v1.ScannerAdapterMetadata
artifactCtl artifact.Controller
scannerCtl scanner.Controller
registrations map[int64]*models.Registration
}
func (c *checker) IsScannable(ctx context.Context, art *artifact.Artifact) (bool, error) {
// There are two scenarios when artifact is scannable:
// 1. The scanner has capability for the artifact directly, eg the artifact is docker image.
// 2. The artifact is image index and the scanner has capability for any artifact which is referenced by the artifact.
projectID := art.ProjectID
metadata, ok := c.scannerMetadatas[projectID]
r, ok := c.registrations[projectID]
if !ok {
registration, err := c.scannerCtl.GetRegistrationByProject(projectID, scanner.WithPing(false))
registration, err := c.scannerCtl.GetRegistrationByProject(projectID)
if err != nil {
return false, err
}
@ -57,20 +61,15 @@ func (c *checker) IsScannable(ctx context.Context, art *artifact.Artifact) (bool
return false, nil
}
md, err := c.scannerCtl.Ping(registration)
if err != nil {
return false, err
}
metadata = md
c.scannerMetadatas[projectID] = md
r = registration
c.registrations[projectID] = registration
}
var scannable bool
walkFn := func(a *artifact.Artifact) error {
scannable = metadata.HasCapability(a.ManifestMediaType)
if scannable {
if HasCapability(r, a) {
scannable = true
return artifact.ErrBreak
}
@ -83,3 +82,13 @@ func (c *checker) IsScannable(ctx context.Context, art *artifact.Artifact) (bool
return scannable, nil
}
// HasCapability returns true when scanner has capability for the artifact
// See https://github.com/goharbor/pluggable-scanner-spec/issues/2 to get more info
func HasCapability(r *models.Registration, a *artifact.Artifact) bool {
if a.Type == "CHART" || a.Type == "UNKNOWN" {
return false
}
return r.HasCapability(a.ManifestMediaType)
}

View File

@ -36,9 +36,9 @@ func (suite *CheckerTestSuite) new() *checker {
scannerCtl := &scannertesting.Controller{}
return &checker{
artifactCtl: artifactCtl,
scannerCtl: scannerCtl,
scannerMetadatas: map[int64]*v1.ScannerAdapterMetadata{},
artifactCtl: artifactCtl,
scannerCtl: scannerCtl,
registrations: map[int64]*scanner.Registration{},
}
}
@ -59,10 +59,11 @@ func (suite *CheckerTestSuite) TestIsScannable() {
supportMimeType := "support mime type"
mock.OnAnything(c.scannerCtl, "GetRegistrationByProject").Return(&scanner.Registration{}, nil)
mock.OnAnything(c.scannerCtl, "Ping").Return(&v1.ScannerAdapterMetadata{
Capabilities: []*v1.ScannerCapability{
{ConsumesMimeTypes: []string{supportMimeType}},
mock.OnAnything(c.scannerCtl, "GetRegistrationByProject").Return(&scanner.Registration{
Metadata: &v1.ScannerAdapterMetadata{
Capabilities: []*v1.ScannerCapability{
{ConsumesMimeTypes: []string{supportMimeType}},
},
},
}, nil)

View File

@ -15,11 +15,13 @@
package scan
import (
"context"
"github.com/goharbor/harbor/src/api/artifact"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/scan/all"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/report"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
)
// Controller provides the related operations for triggering scan.
@ -29,12 +31,12 @@ type Controller interface {
// Scan the given artifact
//
// Arguments:
// artifact *v1.Artifact : artifact to be scanned
// artifact *artifact.Artifact : artifact to be scanned
// options ...Option : options for triggering a scan
//
// Returns:
// error : non nil error if any errors occurred
Scan(artifact *v1.Artifact, options ...Option) error
Scan(ctx context.Context, artifact *artifact.Artifact, options ...Option) error
// GetReport gets the reports for the given artifact identified by the digest
//
@ -45,19 +47,19 @@ type Controller interface {
// Returns:
// []*scan.Report : scan results by different scanner vendors
// error : non nil error if any errors occurred
GetReport(artifact *v1.Artifact, mimeTypes []string) ([]*scan.Report, error)
GetReport(ctx context.Context, artifact *artifact.Artifact, mimeTypes []string) ([]*scan.Report, error)
// GetSummary gets the summaries of the reports with given types.
//
// Arguments:
// artifact *v1.Artifact : the scanned artifact
// artifact *artifact.Artifact : the scanned artifact
// mimeTypes []string : the mime types of the reports
// options ...report.Option : optional report options, specify if needed
//
// Returns:
// map[string]interface{} : report summaries indexed by mime types
// error : non nil error if any errors occurred
GetSummary(artifact *v1.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error)
GetSummary(ctx context.Context, artifact *artifact.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error)
// Get the scan log for the specified artifact with the given digest
//

View File

@ -237,6 +237,8 @@ func (bc *basicController) GetRegistrationByProject(projectID int64, options ...
registration.Adapter = meta.Scanner.Name
registration.Vendor = meta.Scanner.Vendor
registration.Version = meta.Scanner.Version
registration.Metadata = meta
}
}
@ -249,7 +251,7 @@ func (bc *basicController) Ping(registration *scanner.Registration) (*v1.Scanner
return nil, errors.New("nil registration to ping")
}
client, err := bc.clientPool.Get(registration)
client, err := registration.Client(bc.clientPool)
if err != nil {
return nil, errors.Wrap(err, "scanner controller: ping")
}

View File

@ -21,6 +21,9 @@ import (
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
mocktesting "github.com/goharbor/harbor/src/testing/mock"
v1testing "github.com/goharbor/harbor/src/testing/pkg/scan/rest/v1"
scannertesting "github.com/goharbor/harbor/src/testing/pkg/scan/scanner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -32,7 +35,7 @@ type ControllerTestSuite struct {
suite.Suite
c *basicController
mMgr *MockScannerManager
mMgr *scannertesting.Manager
mMeta *MockProMetaManager
sample *scanner.Registration
@ -45,7 +48,7 @@ func TestController(t *testing.T) {
// SetupSuite prepares env for the controller test suite
func (suite *ControllerTestSuite) SetupSuite() {
suite.mMgr = new(MockScannerManager)
suite.mMgr = &scannertesting.Manager{}
suite.mMeta = new(MockProMetaManager)
m := &v1.ScannerAdapterMetadata{
@ -75,11 +78,11 @@ func (suite *ControllerTestSuite) SetupSuite() {
URL: "https://sample.scanner.com",
}
mc := &MockClient{}
mc := &v1testing.Client{}
mc.On("GetMetadata").Return(m, nil)
mcp := &MockClientPool{}
mcp.On("Get", suite.sample).Return(mc, nil)
mcp := &v1testing.ClientPool{}
mocktesting.OnAnything(mcp, "Get").Return(mc, nil)
suite.c = &basicController{
manager: suite.mMgr,
proMetaMgr: suite.mMeta,
@ -242,58 +245,6 @@ func (suite *ControllerTestSuite) TestGetMetadata() {
suite.Equal(1, len(meta.Capabilities))
}
// MockScannerManager is mock of the scanner manager
type MockScannerManager struct {
mock.Mock
}
// List ...
func (m *MockScannerManager) List(query *q.Query) ([]*scanner.Registration, error) {
args := m.Called(query)
return args.Get(0).([]*scanner.Registration), args.Error(1)
}
// Create ...
func (m *MockScannerManager) Create(registration *scanner.Registration) (string, error) {
args := m.Called(registration)
return args.String(0), args.Error(1)
}
// Get ...
func (m *MockScannerManager) Get(registrationUUID string) (*scanner.Registration, error) {
args := m.Called(registrationUUID)
r := args.Get(0)
if r == nil {
return nil, args.Error(1)
}
return r.(*scanner.Registration), args.Error(1)
}
// Update ...
func (m *MockScannerManager) Update(registration *scanner.Registration) error {
args := m.Called(registration)
return args.Error(0)
}
// Delete ...
func (m *MockScannerManager) Delete(registrationUUID string) error {
args := m.Called(registrationUUID)
return args.Error(0)
}
// SetAsDefault ...
func (m *MockScannerManager) SetAsDefault(registrationUUID string) error {
args := m.Called(registrationUUID)
return args.Error(0)
}
// GetDefault ...
func (m *MockScannerManager) GetDefault() (*scanner.Registration, error) {
args := m.Called()
return args.Get(0).(*scanner.Registration), args.Error(1)
}
// MockProMetaManager is the mock of the ProjectMetadataManager
type MockProMetaManager struct {
mock.Mock
@ -328,50 +279,3 @@ func (m *MockProMetaManager) List(name, value string) ([]*models.ProjectMetadata
args := m.Called(name, value)
return args.Get(0).([]*models.ProjectMetadata), args.Error(1)
}
// MockClientPool is defined and referred by other UT cases.
type MockClientPool struct {
mock.Mock
}
// Get client
func (mcp *MockClientPool) Get(r *scanner.Registration) (v1.Client, error) {
args := mcp.Called(r)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(v1.Client), args.Error(1)
}
// MockClient is defined and referred in other UT cases.
type MockClient struct {
mock.Mock
}
// GetMetadata ...
func (mc *MockClient) GetMetadata() (*v1.ScannerAdapterMetadata, error) {
args := mc.Called()
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*v1.ScannerAdapterMetadata), args.Error(1)
}
// SubmitScan ...
func (mc *MockClient) SubmitScan(req *v1.ScanRequest) (*v1.ScanResponse, error) {
args := mc.Called(req)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*v1.ScanResponse), args.Error(1)
}
// GetScanReport ...
func (mc *MockClient) GetScanReport(scanRequestID, reportMIMEType string) (string, error) {
args := mc.Called(scanRequestID, reportMIMEType)
return args.String(0), args.Error(1)
}

View File

@ -211,11 +211,6 @@ func init() {
beego.Router("/api/projects/:pid([0-9]+)/scanner", proScannerAPI, "get:GetProjectScanner;put:SetProjectScanner")
beego.Router("/api/projects/:pid([0-9]+)/scanner/candidates", proScannerAPI, "get:GetProScannerCandidates")
// Add routes for scan
scanAPI := &ScanAPI{}
beego.Router("/api/repositories/*/tags/:tag/scan", scanAPI, "post:Scan;get:Report")
beego.Router("/api/repositories/*/tags/:tag/scan/:uuid/log", scanAPI, "get:Log")
if err := quota.Sync(config.GlobalProjectMgr, false); err != nil {
log.Fatalf("failed to sync quota from backend: %v", err)
}

View File

@ -1,206 +0,0 @@
// 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 api
import (
"github.com/goharbor/harbor/src/pkg/registry"
"net/http"
"strconv"
"github.com/goharbor/harbor/src/api/scan"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/scan/errs"
"github.com/goharbor/harbor/src/pkg/scan/report"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/pkg/errors"
)
var digestFunc digestGetter = getDigest
// ScanAPI handles the scan related actions
type ScanAPI struct {
BaseController
// Target artifact
artifact *v1.Artifact
// Project reference
pro *models.Project
}
// Prepare sth. for the subsequent actions
func (sa *ScanAPI) Prepare() {
// Call super prepare method
sa.BaseController.Prepare()
// Parse parameters
repoName := sa.GetString(":splat")
tag := sa.GetString(":tag")
projectName, _ := utils.ParseRepository(repoName)
pro, err := sa.ProjectMgr.Get(projectName)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scan API: prepare"))
return
}
if pro == nil {
sa.SendNotFoundError(errors.Errorf("project %s not found", projectName))
return
}
sa.pro = pro
// Check authentication
if !sa.RequireAuthenticated() {
return
}
// Assemble artifact object
digest, err := digestFunc(repoName, tag, sa.SecurityCtx.GetUsername())
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scan API: prepare"))
return
}
sa.artifact = &v1.Artifact{
NamespaceID: pro.ProjectID,
Repository: repoName,
Tag: tag,
Digest: digest,
MimeType: v1.MimeTypeDockerArtifact,
}
logger.Debugf("Scan API receives artifact: %#v", sa.artifact)
}
// Scan artifact
func (sa *ScanAPI) Scan() {
// Check access permissions
if !sa.RequireProjectAccess(sa.pro.ProjectID, rbac.ActionCreate, rbac.ResourceScan) {
return
}
if err := scan.DefaultController.Scan(sa.artifact); err != nil {
e := errors.Wrap(err, "scan API: scan")
if errs.AsError(err, errs.PreconditionFailed) {
sa.SendPreconditionFailedError(e)
return
}
if errs.AsError(err, errs.Conflict) {
sa.SendConflictError(e)
return
}
sa.SendInternalServerError(e)
return
}
sa.Ctx.ResponseWriter.WriteHeader(http.StatusAccepted)
}
// Report returns the required reports with the given mime types.
func (sa *ScanAPI) Report() {
// Check access permissions
if !sa.RequireProjectAccess(sa.pro.ProjectID, rbac.ActionRead, rbac.ResourceScan) {
return
}
// Extract mime types
producesMimes := make([]string, 0)
if hl, ok := sa.Ctx.Request.Header[v1.HTTPAcceptHeader]; ok && len(hl) > 0 {
producesMimes = append(producesMimes, hl...)
}
// Get the reports
reports, err := scan.DefaultController.GetReport(sa.artifact, producesMimes)
if err != nil {
e := errors.Wrap(err, "scan API: get report")
if errs.AsError(err, errs.PreconditionFailed) {
sa.SendPreconditionFailedError(e)
return
}
sa.SendInternalServerError(e)
return
}
vulItems := make(map[string]interface{})
for _, rp := range reports {
// Resolve scan report data only when it is ready
if len(rp.Report) == 0 {
continue
}
vrp, err := report.ResolveData(rp.MimeType, []byte(rp.Report))
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scan API: get report"))
return
}
vulItems[rp.MimeType] = vrp
}
sa.Data["json"] = vulItems
sa.ServeJSON()
}
// Log returns the log stream
func (sa *ScanAPI) Log() {
// Check access permissions
if !sa.RequireProjectAccess(sa.pro.ProjectID, rbac.ActionRead, rbac.ResourceScan) {
return
}
uuid := sa.GetString(":uuid")
bytes, err := scan.DefaultController.GetScanLog(uuid)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scan API: log"))
return
}
if bytes == nil {
// Not found
sa.SendNotFoundError(errors.Errorf("report with uuid %s does not exist", uuid))
return
}
sa.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(len(bytes)))
sa.Ctx.ResponseWriter.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain")
_, err = sa.Ctx.ResponseWriter.Write(bytes)
if err != nil {
sa.SendInternalServerError(errors.Wrap(err, "scan API: log"))
}
}
// digestGetter is a function template for getting digest.
// TODO: This can be removed if the registry access interface is ready.
type digestGetter func(repo, tag string, username string) (string, error)
// TODO this method should be reconsidered as the tags are stored in database
// TODO rather than in registry
func getDigest(repo, tag string, username string) (string, error) {
exist, digest, err := registry.Cli.ManifestExist(repo, tag)
if err != nil {
return "", err
}
if !exist {
return "", errors.Errorf("tag %s does exist", tag)
}
return digest, nil
}

View File

@ -1,228 +0,0 @@
// 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 api
import (
"fmt"
"net/http"
"testing"
"github.com/goharbor/harbor/src/api/scan"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/scan/all"
dscan "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/report"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
var scanBaseURL = "/api/repositories/library/hello-world/tags/latest/scan"
// ScanAPITestSuite is the test suite for scan API.
type ScanAPITestSuite struct {
suite.Suite
originalC scan.Controller
c *MockScanAPIController
originalDigestGetter digestGetter
artifact *v1.Artifact
}
// TestScanAPI is the entry point of ScanAPITestSuite.
func TestScanAPI(t *testing.T) {
suite.Run(t, new(ScanAPITestSuite))
}
// SetupSuite prepares test env for suite.
func (suite *ScanAPITestSuite) SetupSuite() {
suite.artifact = &v1.Artifact{
NamespaceID: (int64)(1),
Repository: "library/hello-world",
Tag: "latest",
Digest: "digest-code-001",
MimeType: v1.MimeTypeDockerArtifact,
}
}
// SetupTest prepares test env for test cases.
func (suite *ScanAPITestSuite) SetupTest() {
suite.originalC = scan.DefaultController
suite.c = &MockScanAPIController{}
scan.DefaultController = suite.c
suite.originalDigestGetter = digestFunc
digestFunc = func(repo, tag string, username string) (s string, e error) {
return "digest-code-001", nil
}
}
// TearDownTest ...
func (suite *ScanAPITestSuite) TearDownTest() {
scan.DefaultController = suite.originalC
digestFunc = suite.originalDigestGetter
}
// TestScanAPIBase ...
func (suite *ScanAPITestSuite) TestScanAPIBase() {
suite.c.On("Scan", &v1.Artifact{}).Return(nil)
// Including general cases
cases := []*codeCheckingCase{
// 401
{
request: &testingRequest{
url: scanBaseURL,
method: http.MethodGet,
},
code: http.StatusUnauthorized,
},
// 403
{
request: &testingRequest{
url: scanBaseURL,
method: http.MethodPost,
credential: projGuest,
},
code: http.StatusForbidden,
},
}
runCodeCheckingCases(suite.T(), cases...)
}
// TestScanAPIScan ...
func (suite *ScanAPITestSuite) TestScanAPIScan() {
suite.c.On("Scan", suite.artifact).Return(nil)
// Including general cases
cases := []*codeCheckingCase{
// 202
{
request: &testingRequest{
url: scanBaseURL,
method: http.MethodPost,
credential: projAdmin,
},
code: http.StatusAccepted,
},
}
runCodeCheckingCases(suite.T(), cases...)
}
// TestScanAPIReport ...
func (suite *ScanAPITestSuite) TestScanAPIReport() {
suite.c.On("GetReport", suite.artifact, []string{v1.MimeTypeNativeReport}).Return([]*dscan.Report{}, nil)
vulItems := make(map[string]interface{})
header := make(http.Header)
header.Add("Accept", v1.MimeTypeNativeReport)
err := handleAndParse(
&testingRequest{
url: scanBaseURL,
method: http.MethodGet,
credential: projDeveloper,
header: header,
}, &vulItems)
require.NoError(suite.T(), err)
}
// TestScanAPILog ...
func (suite *ScanAPITestSuite) TestScanAPILog() {
suite.c.On("GetScanLog", "the-uuid-001").Return([]byte(`{"log": "this is my log"}`), nil)
logs := make(map[string]string)
err := handleAndParse(
&testingRequest{
url: fmt.Sprintf("%s/%s", scanBaseURL, "the-uuid-001/log"),
method: http.MethodGet,
credential: projDeveloper,
}, &logs)
require.NoError(suite.T(), err)
assert.Condition(suite.T(), func() (success bool) {
success = len(logs) > 0
return
})
}
// Mock things
// MockScanAPIController ...
type MockScanAPIController struct {
mock.Mock
}
// Scan ...
func (msc *MockScanAPIController) Scan(artifact *v1.Artifact, option ...scan.Option) error {
args := msc.Called(artifact)
return args.Error(0)
}
func (msc *MockScanAPIController) GetReport(artifact *v1.Artifact, mimeTypes []string) ([]*dscan.Report, error) {
args := msc.Called(artifact, mimeTypes)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*dscan.Report), args.Error(1)
}
func (msc *MockScanAPIController) GetSummary(artifact *v1.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error) {
args := msc.Called(artifact, mimeTypes, options)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(map[string]interface{}), args.Error(1)
}
func (msc *MockScanAPIController) GetScanLog(uuid string) ([]byte, error) {
args := msc.Called(uuid)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]byte), args.Error(1)
}
func (msc *MockScanAPIController) HandleJobHooks(trackID string, change *job.StatusChange) error {
args := msc.Called(trackID, change)
return args.Error(0)
}
func (msc *MockScanAPIController) DeleteReports(digests ...string) error {
return nil
}
func (msc *MockScanAPIController) GetStats(requester string) (*all.Stats, error) {
args := msc.Called(requester)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*all.Stats), args.Error(1)
}

View File

@ -56,7 +56,7 @@ const (
// both tagged and untagged artifacts
both = `IN (
SELECT DISTINCT art.id FROM artifact art
LEFT JOIN tag ON art.id=tag.artifact_id
LEFT JOIN tag ON art.id=tag.artifact_id
LEFT JOIN artifact_reference ref ON art.id=ref.child_id
WHERE tag.id IS NOT NULL OR ref.id IS NULL)`
// only untagged artifacts

View File

@ -1,12 +1,16 @@
package notification
import (
"context"
"time"
o "github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/api/artifact"
"github.com/goharbor/harbor/src/api/scan"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/internal/orm"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/notifier/model"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
@ -101,12 +105,24 @@ func constructScanImagePayload(event *model.ScanImageEvent, project *models.Proj
return nil, errors.Wrap(err, "construct scan payload")
}
ctx := orm.NewContext(context.TODO(), o.NewOrm())
reference := event.Artifact.Digest
if reference == "" {
reference = event.Artifact.Tag
}
art, err := artifact.Ctl.GetByReference(ctx, event.Artifact.Repository, event.Artifact.Digest, nil)
if err != nil {
return nil, err
}
// Wait for reasonable time to make sure the report is ready
// Interval=500ms and total time = 5s
// If the report is still not ready in the total time, then failed at then
for i := 0; i < 10; i++ {
// First check in case it is ready
if re, err := scan.DefaultController.GetReport(event.Artifact, []string{v1.MimeTypeNativeReport}); err == nil {
if re, err := scan.DefaultController.GetReport(ctx, art, []string{v1.MimeTypeNativeReport}); err == nil {
if len(re) > 0 && len(re[0].Report) > 0 {
break
}
@ -118,7 +134,7 @@ func constructScanImagePayload(event *model.ScanImageEvent, project *models.Proj
}
// Add scan overview
summaries, err := scan.DefaultController.GetSummary(event.Artifact, []string{v1.MimeTypeNativeReport})
summaries, err := scan.DefaultController.GetSummary(ctx, art, []string{v1.MimeTypeNativeReport})
if err != nil {
return nil, errors.Wrap(err, "construct scan payload")
}

View File

@ -1,23 +1,37 @@
// 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 notification
import (
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/scan/all"
"github.com/goharbor/harbor/src/api/artifact"
sc "github.com/goharbor/harbor/src/api/scan"
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/notification"
"github.com/goharbor/harbor/src/pkg/notification/policy"
"github.com/goharbor/harbor/src/pkg/notifier"
"github.com/goharbor/harbor/src/pkg/notifier/model"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/report"
"github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/stretchr/testify/mock"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
artifacttesting "github.com/goharbor/harbor/src/testing/api/artifact"
scantesting "github.com/goharbor/harbor/src/testing/api/scan"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -26,10 +40,11 @@ import (
type ScanImagePreprocessHandlerSuite struct {
suite.Suite
om policy.Manager
pid int64
evt *model.ScanImageEvent
c sc.Controller
om policy.Manager
pid int64
evt *model.ScanImageEvent
c sc.Controller
artifactCtl artifact.Controller
}
// TestScanImagePreprocessHandler is the entry point of ScanImagePreprocessHandlerSuite.
@ -65,15 +80,29 @@ func (suite *ScanImagePreprocessHandlerSuite) SetupSuite() {
}
suite.c = sc.DefaultController
mc := &MockScanAPIController{}
mc := &scantesting.Controller{}
var options []report.Option
s := make(map[string]interface{})
mc.On("GetSummary", a, []string{v1.MimeTypeNativeReport}, options).Return(s, nil)
mc.On("GetReport", a, []string{v1.MimeTypeNativeReport}).Return(reports, nil)
mock.OnAnything(mc, "GetSummary").Return(s, nil)
mock.OnAnything(mc, "GetReport").Return(reports, nil)
sc.DefaultController = mc
suite.artifactCtl = artifact.Ctl
artifactCtl := &artifacttesting.Controller{}
art := &artifact.Artifact{}
art.ProjectID = a.NamespaceID
art.RepositoryName = a.Repository
art.Digest = a.Digest
mock.OnAnything(artifactCtl, "GetByReference").Return(art, nil)
artifact.Ctl = artifactCtl
suite.om = notification.PolicyMgr
mp := &fakedPolicyMgr{}
notification.PolicyMgr = mp
@ -88,6 +117,7 @@ func (suite *ScanImagePreprocessHandlerSuite) SetupSuite() {
func (suite *ScanImagePreprocessHandlerSuite) TearDownSuite() {
notification.PolicyMgr = suite.om
sc.DefaultController = suite.c
artifact.Ctl = suite.artifactCtl
}
// TestHandle ...
@ -100,73 +130,6 @@ func (suite *ScanImagePreprocessHandlerSuite) TestHandle() {
// Mock things
// MockScanAPIController ...
type MockScanAPIController struct {
mock.Mock
}
// Scan ...
func (msc *MockScanAPIController) Scan(artifact *v1.Artifact, option ...sc.Option) error {
args := msc.Called(artifact)
return args.Error(0)
}
func (msc *MockScanAPIController) GetReport(artifact *v1.Artifact, mimeTypes []string) ([]*scan.Report, error) {
args := msc.Called(artifact, mimeTypes)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*scan.Report), args.Error(1)
}
func (msc *MockScanAPIController) GetSummary(artifact *v1.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error) {
args := msc.Called(artifact, mimeTypes, options)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(map[string]interface{}), args.Error(1)
}
func (msc *MockScanAPIController) GetScanLog(uuid string) ([]byte, error) {
args := msc.Called(uuid)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]byte), args.Error(1)
}
func (msc *MockScanAPIController) HandleJobHooks(trackID string, change *job.StatusChange) error {
args := msc.Called(trackID, change)
return args.Error(0)
}
func (msc *MockScanAPIController) DeleteReports(digests ...string) error {
pl := make([]interface{}, 0)
for _, d := range digests {
pl = append(pl, d)
}
args := msc.Called(pl...)
return args.Error(0)
}
func (msc *MockScanAPIController) GetStats(requester string) (*all.Stats, error) {
args := msc.Called(requester)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*all.Stats), args.Error(1)
}
// MockHTTPHandler ...
type MockHTTPHandler struct{}

View File

@ -126,17 +126,14 @@ func UpdateReportStatus(trackID string, status string, statusCode int, statusRev
// qt generates sql statements:
// UPDATE "scan_report" SET "end_time" = $1, "status" = $2, "status_code" = $3, "status_rev" = $4
// WHERE "id" IN ( SELECT T0."id" FROM "scan_report" T0 WHERE ( T0."status_rev" = $5 AND T0."status_code" < $6 )
// OR ( T0."status_rev" < $7 ) AND T0."track_id" = $8 )
cond := orm.NewCondition()
c1 := cond.And("status_rev", statusRev).And("status_code__lt", statusCode)
c2 := cond.And("status_rev__lt", statusRev)
c := cond.AndCond(c1).OrCond(c2)
count, err := qt.SetCond(c).
Filter("track_id", trackID).
Update(data)
// WHERE "id" IN ( SELECT T0."id" FROM "scan_report" T0 WHERE ( T0."status_rev" = $5 AND T0."status_code" < $6
// OR ( T0."status_rev" < $7 ) ) AND ( T0."track_id" = $8 )
c1 := orm.NewCondition().And("status_rev", statusRev).And("status_code__lt", statusCode)
c2 := orm.NewCondition().And("status_rev__lt", statusRev)
c3 := orm.NewCondition().And("track_id", trackID)
c := orm.NewCondition().AndCond(c1.OrCond(c2)).AndCond(c3)
count, err := qt.SetCond(c).Update(data)
if err != nil {
return err
}

View File

@ -21,6 +21,7 @@ import (
"time"
"github.com/goharbor/harbor/src/pkg/scan/rest/auth"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/pkg/errors"
)
@ -57,6 +58,8 @@ type Registration struct {
Vendor string `orm:"-" json:"vendor,omitempty"`
Version string `orm:"-" json:"version,omitempty"`
Metadata *v1.ScannerAdapterMetadata `orm:"-" json:"-"`
// Timestamps
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
@ -115,6 +118,66 @@ func (r *Registration) Validate(checkUUID bool) error {
return nil
}
// Client returns client of registration
func (r *Registration) Client(pool v1.ClientPool) (v1.Client, error) {
if err := r.Validate(false); err != nil {
return nil, err
}
return pool.Get(r.URL, r.Auth, r.AccessCredential, r.SkipCertVerify)
}
// HasCapability returns true when mime type of the artifact support by the scanner
func (r *Registration) HasCapability(manifestMimeType string) bool {
if r.Metadata == nil {
return false
}
for _, capability := range r.Metadata.Capabilities {
for _, mt := range capability.ConsumesMimeTypes {
if mt == manifestMimeType {
return true
}
}
}
return false
}
// GetProducesMimeTypes returns produces mime types for the artifact
func (r *Registration) GetProducesMimeTypes(mimeType string) []string {
if r.Metadata == nil {
return nil
}
for _, capability := range r.Metadata.Capabilities {
for _, mt := range capability.ConsumesMimeTypes {
if mt == mimeType {
return capability.ProducesMimeTypes
}
}
}
return nil
}
// GetCapability returns capability for the mime type
func (r *Registration) GetCapability(mimeType string) *v1.ScannerCapability {
if r.Metadata == nil {
return nil
}
for _, capability := range r.Metadata.Capabilities {
for _, mt := range capability.ConsumesMimeTypes {
if mt == mimeType {
return capability
}
}
}
return nil
}
// Check the registration URL with url package
func checkURL(u string) error {
if len(strings.TrimSpace(u)) == 0 {

View File

@ -126,7 +126,7 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
myLogger.Infof("Report mime types: %v\n", mimes)
// Submit scan request to the scanner adapter
client, err := v1.DefaultClientPool.Get(r)
client, err := r.Client(v1.DefaultClientPool)
if err != nil {
return logAndWrapError(myLogger, err, "scan job: get client")
}

View File

@ -25,6 +25,8 @@ import (
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
mocktesting "github.com/goharbor/harbor/src/testing/mock"
v1testing "github.com/goharbor/harbor/src/testing/pkg/scan/rest/v1"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -35,7 +37,7 @@ type JobTestSuite struct {
suite.Suite
defaultClientPool v1.ClientPool
mcp *MockClientPool
mcp *v1testing.ClientPool
}
// TestJob is the entry of JobTestSuite.
@ -45,7 +47,7 @@ func TestJob(t *testing.T) {
// SetupSuite sets up test env for JobTestSuite.
func (suite *JobTestSuite) SetupSuite() {
mcp := &MockClientPool{}
mcp := &v1testing.ClientPool{}
suite.defaultClientPool = v1.DefaultClientPool
v1.DefaultClientPool = mcp
@ -96,7 +98,7 @@ func (suite *JobTestSuite) TestJob() {
jp[JobParameterRequest] = sData
jp[JobParameterMimes] = mimeTypes
mc := &MockClient{}
mc := &v1testing.Client{}
sre := &v1.ScanResponse{
ID: "scan_id",
}
@ -127,7 +129,7 @@ func (suite *JobTestSuite) TestJob() {
require.NoError(suite.T(), err)
mc.On("GetScanReport", "scan_id", v1.MimeTypeNativeReport).Return(string(jRep), nil)
suite.mcp.On("Get", r).Return(mc, nil)
mocktesting.OnAnything(suite.mcp, "Get").Return(mc, nil)
crp := &CheckInReport{
Digest: sr.Artifact.Digest,
@ -255,52 +257,3 @@ func (mjl *MockJobLogger) Fatal(v ...interface{}) {
func (mjl *MockJobLogger) Fatalf(format string, v ...interface{}) {
logger.Fatalf(format, v...)
}
// MockClientPool mocks the client pool
type MockClientPool struct {
mock.Mock
}
// Get v1 client
func (mcp *MockClientPool) Get(r *scanner.Registration) (v1.Client, error) {
args := mcp.Called(r)
c := args.Get(0)
if c != nil {
return c.(v1.Client), nil
}
return nil, args.Error(1)
}
// MockClient mocks the v1 client
type MockClient struct {
mock.Mock
}
// GetMetadata ...
func (mc *MockClient) GetMetadata() (*v1.ScannerAdapterMetadata, error) {
args := mc.Called()
s := args.Get(0)
if s != nil {
return s.(*v1.ScannerAdapterMetadata), nil
}
return nil, args.Error(1)
}
// SubmitScan ...
func (mc *MockClient) SubmitScan(req *v1.ScanRequest) (*v1.ScanResponse, error) {
args := mc.Called(req)
sr := args.Get(0)
if sr != nil {
return sr.(*v1.ScanResponse), nil
}
return nil, args.Error(1)
}
// GetScanReport ...
func (mc *MockClient) GetScanReport(scanRequestID, reportMIMEType string) (string, error) {
args := mc.Called(scanRequestID, reportMIMEType)
return args.String(0), args.Error(1)
}

View File

@ -17,12 +17,11 @@ package report
import (
"time"
"github.com/goharbor/harbor/src/pkg/scan/all"
ierror "github.com/goharbor/harbor/src/internal/error"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/all"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/errs"
"github.com/google/uuid"
"github.com/pkg/errors"
)
@ -81,7 +80,7 @@ func (bm *basicManager) Create(r *scan.Report) (string, error) {
// Status conflict
if theCopy.StartTime.Add(reportTimeout).After(time.Now()) {
if theStatus.Compare(job.RunningStatus) <= 0 {
return "", errs.WithCode(errs.Conflict, errs.Errorf("a previous scan process is %s", theCopy.Status))
return "", ierror.ConflictError(nil).WithMessage("a previous scan process is %s", theCopy.Status)
}
}
@ -92,17 +91,14 @@ func (bm *basicManager) Create(r *scan.Report) (string, error) {
}
}
// Assign uuid
UUID, err := uuid.NewUUID()
if err != nil {
return "", errors.Wrap(err, "create report: new UUID")
}
r.UUID = UUID.String()
r.UUID = uuid.New().String()
// Fill in / override the related properties
r.StartTime = time.Now().UTC()
r.Status = job.PendingStatus.String()
r.StatusCode = job.PendingStatus.Code()
if r.Status == "" {
r.Status = job.PendingStatus.String()
r.StatusCode = job.PendingStatus.Code()
}
// Insert
if _, err = scan.CreateReport(r); err != nil {

View File

@ -50,6 +50,39 @@ func WithCVEWhitelist(set *CVESet) Option {
}
}
// SummaryMerger is a helper function to merge summary together
type SummaryMerger func(s1, s2 interface{}) (interface{}, error)
// SupportedMergers declares mappings between mime type and summary merger func.
var SupportedMergers = map[string]SummaryMerger{
v1.MimeTypeNativeReport: MergeNativeSummary,
}
// MergeSummary merge summary s1 and s2
func MergeSummary(mimeType string, s1, s2 interface{}) (interface{}, error) {
m, ok := SupportedMergers[mimeType]
if !ok {
return nil, errors.Errorf("no mreger bound with mime type %s", mimeType)
}
return m(s1, s2)
}
// MergeNativeSummary merge vuln.NativeReportSummary together
func MergeNativeSummary(s1, s2 interface{}) (interface{}, error) {
nrs1, ok := s1.(*vuln.NativeReportSummary)
if !ok {
return nil, errors.New("native report summary required")
}
nrs2, ok := s2.(*vuln.NativeReportSummary)
if !ok {
return nil, errors.New("native report summary required")
}
return nrs1.Merge(nrs2), nil
}
// SupportedGenerators declares mappings between mime type and summary generator func.
var SupportedGenerators = map[string]SummaryGenerator{
v1.MimeTypeNativeReport: GenerateNativeSummary,
@ -94,6 +127,8 @@ func GenerateNativeSummary(r *scan.Report, options ...Option) (interface{}, erro
sum.ScanStatus = r.Status
}
sum.TotalCount = 1
// If the status is not success/stopped, there will not be any report.
if r.Status != job.SuccessStatus.String() &&
r.Status != job.StoppedStatus.String() {
@ -115,6 +150,9 @@ func GenerateNativeSummary(r *scan.Report, options ...Option) (interface{}, erro
return nil, errors.Errorf("type mismatch: expect *vuln.Report but got %s", reflect.TypeOf(raw).String())
}
sum.CompleteCount = 1
sum.CompletePercent = 100
sum.Severity = rp.Severity
vsum := &vuln.VulnerabilitySummary{
Total: len(rp.Vulnerabilities),

View File

@ -26,7 +26,6 @@ import (
"time"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/rest/auth"
"github.com/pkg/errors"
)
@ -81,7 +80,7 @@ type basicClient struct {
}
// NewClient news a basic client
func NewClient(r *scanner.Registration) (Client, error) {
func NewClient(url, authType, accessCredential string, skipCertVerify bool) (Client, error) {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
@ -93,11 +92,11 @@ func NewClient(r *scanner.Registration) (Client, error) {
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: r.SkipCertVerify,
InsecureSkipVerify: skipCertVerify,
},
}
authorizer, err := auth.GetAuthorizer(r.Auth, r.AccessCredential)
authorizer, err := auth.GetAuthorizer(authType, accessCredential)
if err != nil {
return nil, errors.Wrap(err, "new v1 client")
}
@ -109,7 +108,7 @@ func NewClient(r *scanner.Registration) (Client, error) {
return http.ErrUseLastResponse
},
},
spec: NewSpec(r.URL),
spec: NewSpec(url),
authorizer: authorizer,
}, nil
}

View File

@ -22,7 +22,6 @@ import (
"syscall"
"time"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/pkg/errors"
)
@ -44,7 +43,7 @@ type ClientPool interface {
// Returns:
// Client : v1 client
// error : non nil error if any errors occurred
Get(r *scanner.Registration) (Client, error)
Get(url, authType, accessCredential string, skipCertVerify bool) (Client, error)
}
// PoolConfig provides configurations for the client pool.
@ -97,20 +96,12 @@ func NewClientPool(config *PoolConfig) ClientPool {
// add the following func after the first time initializing the client.
// pool item represents the client with a timestamp of last accessed.
func (bcp *basicClientPool) Get(r *scanner.Registration) (Client, error) {
if r == nil {
return nil, errors.New("nil scanner registration")
}
if err := r.Validate(false); err != nil {
return nil, errors.Wrap(err, "client pool: get")
}
k := key(r)
func (bcp *basicClientPool) Get(url, authType, accessCredential string, skipCertVerify bool) (Client, error) {
k := fmt.Sprintf("%s:%s:%s:%v", url, authType, accessCredential, skipCertVerify)
item, ok := bcp.pool.Load(k)
if !ok {
nc, err := NewClient(r)
nc, err := NewClient(url, authType, accessCredential, skipCertVerify)
if err != nil {
return nil, errors.Wrap(err, "client pool: get")
}
@ -157,12 +148,3 @@ func (bcp *basicClientPool) deadCheck(key string, item *poolItem) {
}
}()
}
func key(r *scanner.Registration) string {
return fmt.Sprintf("%s:%s:%s:%v",
r.URL,
r.Auth,
r.AccessCredential,
r.SkipCertVerify,
)
}

View File

@ -19,7 +19,6 @@ import (
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/rest/auth"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -49,23 +48,13 @@ func (suite *ClientPoolTestSuite) SetupSuite() {
// TestClientPoolGet tests the get method of client pool.
func (suite *ClientPoolTestSuite) TestClientPoolGet() {
r := &scanner.Registration{
ID: 1,
Name: "TestClientPoolGet",
UUID: "uuid",
URL: "http://a.b.c",
Auth: auth.Basic,
AccessCredential: "u:p",
SkipCertVerify: false,
}
client1, err := suite.pool.Get(r)
client1, err := suite.pool.Get("http://a.b.c", auth.Basic, "u:p", false)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), client1)
p1 := fmt.Sprintf("%p", client1.(*basicClient))
client2, err := suite.pool.Get(r)
client2, err := suite.pool.Get("http://a.b.c", auth.Basic, "u:p", false)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), client2)
@ -73,7 +62,7 @@ func (suite *ClientPoolTestSuite) TestClientPoolGet() {
assert.Equal(suite.T(), p1, p2)
<-time.After(400 * time.Millisecond)
client3, err := suite.pool.Get(r)
client3, err := suite.pool.Get("http://a.b.c", auth.Basic, "u:p", false)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), client3)

View File

@ -22,7 +22,6 @@ import (
"strings"
"testing"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -44,15 +43,8 @@ func TestClient(t *testing.T) {
// SetupSuite prepares the test suite env
func (suite *ClientTestSuite) SetupSuite() {
suite.testServer = httptest.NewServer(&mockHandler{})
r := &scanner.Registration{
ID: 1000,
UUID: "uuid",
Name: "TestClient",
URL: suite.testServer.URL,
SkipCertVerify: true,
}
c, err := NewClient(r)
c, err := NewClient(suite.testServer.URL, "", "", true)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), c)

View File

@ -78,6 +78,19 @@ func (md *ScannerAdapterMetadata) HasCapability(mimeType string) bool {
return false
}
// GetCapability returns capability for the mime type
func (md *ScannerAdapterMetadata) GetCapability(mimeType string) *ScannerCapability {
for _, capability := range md.Capabilities {
for _, mt := range capability.ConsumesMimeTypes {
if mt == mimeType {
return capability
}
}
}
return nil
}
// Artifact represents an artifact stored in Registry.
type Artifact struct {
// ID of the namespace (project). It will not be sent to scanner adapter.

View File

@ -17,21 +17,76 @@ package vuln
import (
"time"
"github.com/goharbor/harbor/src/jobservice/job"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
)
// NativeReportSummary is the default supported scan report summary model.
// Generated based on the report with v1.MimeTypeNativeReport mime type.
type NativeReportSummary struct {
ReportID string `json:"report_id"`
ScanStatus string `json:"scan_status"`
Severity Severity `json:"severity"`
Duration int64 `json:"duration"`
Summary *VulnerabilitySummary `json:"summary"`
CVEBypassed []string `json:"-"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Scanner *v1.Scanner `json:"scanner,omitempty"`
ReportID string `json:"report_id"`
ScanStatus string `json:"scan_status"`
Severity Severity `json:"severity"`
Duration int64 `json:"duration"`
Summary *VulnerabilitySummary `json:"summary"`
CVEBypassed []string `json:"-"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Scanner *v1.Scanner `json:"scanner,omitempty"`
CompletePercent int `json:"complete_percent"`
TotalCount int `json:"-"`
CompleteCount int `json:"-"`
}
// Merge ...
func (sum *NativeReportSummary) Merge(another *NativeReportSummary) *NativeReportSummary {
r := &NativeReportSummary{}
r.ReportID = sum.ReportID
r.StartTime = minTime(sum.StartTime, another.StartTime)
r.EndTime = maxTime(sum.EndTime, another.EndTime)
r.Duration = r.EndTime.Unix() - r.StartTime.Unix()
r.Scanner = sum.Scanner
r.TotalCount = sum.TotalCount + another.TotalCount
r.CompleteCount = sum.CompleteCount + another.CompleteCount
r.CompletePercent = r.CompleteCount * 100 / r.TotalCount
if sum.Severity.String() != "" && another.Severity.String() != "" {
if sum.Severity.Code() > another.Severity.Code() {
r.Severity = sum.Severity
} else {
r.Severity = another.Severity
}
} else if sum.Severity.String() != "" {
r.Severity = sum.Severity
} else {
r.Severity = another.Severity
}
if isRunningStatus(sum.ScanStatus) || isRunningStatus(another.ScanStatus) {
r.ScanStatus = job.RunningStatus.String()
} else {
diff := job.Status(sum.ScanStatus).Compare(job.Status(another.ScanStatus))
if diff < 0 {
r.ScanStatus = another.ScanStatus
} else if diff == 0 {
if job.Status(sum.ScanStatus) == job.SuccessStatus ||
job.Status(another.ScanStatus) == job.SuccessStatus {
r.ScanStatus = job.SuccessStatus.String()
}
}
}
if sum.Summary != nil && another.Summary != nil {
r.Summary = sum.Summary.Merge(another.Summary)
} else if sum.Summary != nil {
r.Summary = sum.Summary
} else {
r.Summary = another.Summary
}
return r
}
// VulnerabilitySummary contains the total number of the found vulnerabilities number
@ -42,5 +97,48 @@ type VulnerabilitySummary struct {
Summary SeveritySummary `json:"summary"`
}
// Merge ...
func (v *VulnerabilitySummary) Merge(a *VulnerabilitySummary) *VulnerabilitySummary {
r := &VulnerabilitySummary{
Total: v.Total + a.Total,
Fixable: v.Fixable + a.Fixable,
Summary: SeveritySummary{},
}
for k, v := range v.Summary {
r.Summary[k] = v
}
for k, v := range a.Summary {
if _, ok := r.Summary[k]; ok {
r.Summary[k] += v
} else {
r.Summary[k] = v
}
}
return r
}
// SeveritySummary ...
type SeveritySummary map[Severity]int
func minTime(t1, t2 time.Time) time.Time {
if t1.Before(t2) {
return t1
}
return t2
}
func maxTime(t1, t2 time.Time) time.Time {
if t1.Before(t2) {
return t2
}
return t1
}
func isRunningStatus(status string) bool {
return job.Status(status) == job.RunningStatus
}

View File

@ -55,9 +55,9 @@
<div *ngFor="let filter of mutipleFilter" >
<ul class="list-unstyled" *ngIf="filterByType ===filter.filterBy">
<li class="cursor-pointer" (click)="selectFilter(item.showItem, item.filterText)" *ngFor="let item of filter.listItem">{{item.showItem | translate}}</li>
</ul>
</ul>
</div>
</div>
<div class="label-filter-panel" *ngIf="!withAdmiral" [hidden]="!(openLabelFilterPanel&&filterByType==='label.id')">
<a class="filterClose" (click)="closeFilter()">&times;</a>

View File

@ -75,6 +75,10 @@
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
<span>{{completeTimestamp | date:'short'}}</span>
</div>
<div>
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_PERCENT' | translate}} </span>
<span>{{completePercent}}</span>
</div>
</clr-tooltip-content>
</clr-tooltip>
</div>

View File

@ -149,7 +149,9 @@ export class ResultTipHistogramComponent implements OnInit {
return 0;
}
public get completePercent(): string {
return this.vulnerabilitySummary ? `${this.vulnerabilitySummary.complete_percent}%` : '0%'
}
get completeTimestamp(): Date {
return this.vulnerabilitySummary && this.vulnerabilitySummary.end_time ? this.vulnerabilitySummary.end_time : new Date();
}

View File

@ -987,6 +987,7 @@
},
"CHART": {
"SCANNING_TIME": "Scan completed time:",
"SCANNING_PERCENT": "Scan completed percent:",
"TOOLTIPS_TITLE": "{{totalVulnerability}} of {{totalPackages}} {{package}} have known {{vulnerability}}.",
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} of {{totalPackages}} {{package}} has known {{vulnerability}}.",
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
@ -1028,7 +1029,7 @@
"UNTAGGED": "Untagged",
"ALL": "All",
"PLACEHOLDER": "We couldn't find any artifactds!",
"SCAN_UNSUPPORTED": "Scan Unsupported"
"SCAN_UNSUPPORTED": "Unsupported"
},
"TAG": {
"CREATION_TIME_PREFIX": "Create on",

View File

@ -986,6 +986,7 @@
},
"CHART": {
"SCANNING_TIME": "Scan completed time:",
"SCANNING_PERCENT": "Scan completed percent:",
"TOOLTIPS_TITLE": "{{totalVulnerability}} of {{totalPackages}} {{package}} have known {{vulnerability}}.",
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} of {{totalPackages}} {{package}} has known {{vulnerability}}.",
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
@ -1027,7 +1028,7 @@
"UNTAGGED": "Untagged",
"ALL": "All",
"PLACEHOLDER": "We couldn't find any artifacts!",
"SCAN_UNSUPPORTED": "Scan Unsupported"
"SCAN_UNSUPPORTED": "Unsupported"
},
"TAG": {
"CREATION_TIME_PREFIX": "Create on",

View File

@ -959,6 +959,7 @@
},
"CHART": {
"SCANNING_TIME": "Temps d'analyse complète :",
"SCANNING_PERCENT": "Scan completed percent:",
"TOOLTIPS_TITLE": "{{totalVulnerability}} de {{totalPackages}} {{package}} ont des {{vulnerability}} connues.",
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} de {{totalPackages}} {{package}} a des {{vulnerability}} connues.",
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
@ -1000,7 +1001,7 @@
"UNTAGGED": "Untagged",
"ALL": "All",
"PLACEHOLDER": "We couldn't find any artifacts!",
"SCAN_UNSUPPORTED": "Scan Unsupported"
"SCAN_UNSUPPORTED": "Unsupported"
},
"TAG": {
"CREATION_TIME_PREFIX": "Créer le",

View File

@ -982,6 +982,7 @@
},
"CHART": {
"SCANNING_TIME": "Tempo de conclusão da análise:",
"SCANNING_PERCENT": "Scan completed percent:",
"TOOLTIPS_TITLE": "{{totalVulnerability}} de {{totalPackages}} {{package}} possuem {{vulnerability}}.",
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} de {{totalPackages}} {{package}} possuem {{vulnerability}}.",
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
@ -1023,7 +1024,7 @@
"UNTAGGED": "Untagged",
"ALL": "All",
"PLACEHOLDER": "We couldn't find any artifacts!",
"SCAN_UNSUPPORTED": "Scan Unsupported"
"SCAN_UNSUPPORTED": "Unsupported"
},
"TAG": {
"CREATION_TIME_PREFIX": "Criado em",

View File

@ -986,6 +986,7 @@
},
"CHART": {
"SCANNING_TIME": "Tarama tamamlanma zamanı:",
"SCANNING_PERCENT": "Scan completed percent:",
"TOOLTIPS_TITLE": "{{totalVulnerability}} 'nın {{totalPackages}} {{package}} bilinen {{vulnerability}}.",
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} 'nın {{totalPackages}} {{package}} bilinen {{vulnerability}}.",
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
@ -1027,7 +1028,7 @@
"UNTAGGED": "Untagged",
"ALL": "All",
"PLACEHOLDER": "We couldn't find any artifacts!",
"SCAN_UNSUPPORTED": "Scan Unsupported"
"SCAN_UNSUPPORTED": "Unsupported"
},
"TAG": {
"CREATION_TIME_PREFIX": "Oluştur",

View File

@ -986,6 +986,7 @@
},
"CHART": {
"SCANNING_TIME": "扫描完成时间:",
"SCANNING_PERCENT": "扫描完成度:",
"TOOLTIPS_TITLE": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}。",
"TOOLTIPS_TITLE_SINGULAR": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}。",
"TOOLTIPS_TITLE_ZERO": "没有发现可识别的漏洞"

View File

@ -305,6 +305,7 @@ export interface VulnerabilitySummary {
start_time?: Date;
end_time?: Date;
scanner?: ScannerVo;
complete_percent?: number;
}
export interface ScannerVo {
name?: string;

View File

@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"github.com/goharbor/harbor/src/api/artifact"
sc "github.com/goharbor/harbor/src/api/scan"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils/log"
@ -40,21 +41,15 @@ func Middleware() func(http.Handler) http.Handler {
return
}
// Get the vulnerability summary
artifact := &v1.Artifact{
NamespaceID: wl.ProjectID,
Repository: img.Repository,
Tag: img.Tag,
Digest: img.Digest,
MimeType: v1.MimeTypeDockerArtifact,
ctx := req.Context()
art, err := artifact.Ctl.GetByReference(ctx, img.Repository, img.Digest, nil)
if err != nil {
// TODO: error handle
return
}
cve := report.CVESet(wl.CVESet())
summaries, err := sc.DefaultController.GetSummary(
artifact,
[]string{v1.MimeTypeNativeReport},
report.WithCVEWhitelist(&cve),
)
summaries, err := sc.DefaultController.GetSummary(ctx, art, []string{v1.MimeTypeNativeReport}, report.WithCVEWhitelist(&cve))
if err != nil {
err = errors.Wrap(err, "middleware: vulnerable handler")

View File

@ -34,7 +34,6 @@ import (
"github.com/goharbor/harbor/src/common/utils"
ierror "github.com/goharbor/harbor/src/internal/error"
evt "github.com/goharbor/harbor/src/pkg/notifier/event"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/server/v2.0/handler/assembler"
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
"github.com/goharbor/harbor/src/server/v2.0/models"
@ -178,30 +177,6 @@ func (a *artifactAPI) CopyArtifact(ctx context.Context, params operation.CopyArt
return operation.NewCopyArtifactCreated().WithLocation(location)
}
func (a *artifactAPI) ScanArtifact(ctx context.Context, params operation.ScanArtifactParams) middleware.Responder {
if err := a.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionCreate, rbac.ResourceScan); err != nil {
return a.SendError(ctx, err)
}
repository := fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName)
artifact, err := a.artCtl.GetByReference(ctx, repository, params.Reference, nil)
if err != nil {
return a.SendError(ctx, err)
}
art := &v1.Artifact{
NamespaceID: artifact.ProjectID,
Repository: repository,
Digest: artifact.Digest,
MimeType: artifact.ManifestMediaType,
}
if err := a.scanCtl.Scan(art); err != nil {
return a.SendError(ctx, err)
}
return operation.NewScanArtifactAccepted()
}
// parse "repository:tag" or "repository@digest" into repository and reference parts
func parse(s string) (string, string, error) {
matches := reference.ReferenceRegexp.FindStringSubmatch(s)

View File

@ -72,16 +72,9 @@ func (assembler *VulAssembler) Assemble(ctx context.Context) error {
artifact.SetAdditionLink(vulnerabilitiesAddition, version)
if assembler.withScanOverview {
art := &v1.Artifact{
NamespaceID: artifact.ProjectID,
Repository: artifact.RepositoryName,
Digest: artifact.Digest,
MimeType: artifact.ManifestMediaType,
}
overview, err := assembler.scanCtl.GetSummary(art, []string{v1.MimeTypeNativeReport})
overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, []string{v1.MimeTypeNativeReport})
if err != nil {
log.Warningf("get scan summary of artifact %s failed, error:%v", artifact.Digest, err)
log.Warningf("get scan summary of artifact %s@%s failed, error:%v", artifact.RepositoryName, artifact.Digest, err)
} else if len(overview) > 0 {
artifact.ScanOverview = overview
}

View File

@ -32,6 +32,7 @@ func New() http.Handler {
ArtifactAPI: newArtifactAPI(),
RepositoryAPI: newRepositoryAPI(),
AuditlogAPI: newAuditLogAPI(),
ScanAPI: newScanAPI(),
})
if err != nil {
log.Fatal(err)

View File

@ -15,8 +15,11 @@
package model
import (
"encoding/json"
"github.com/go-openapi/strfmt"
"github.com/goharbor/harbor/src/api/artifact"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/server/v2.0/models"
)
@ -57,5 +60,22 @@ func (a *Artifact) ToSwagger() *models.Artifact {
for _, label := range a.Labels {
art.Labels = append(art.Labels, label.ToSwagger())
}
if len(a.ScanOverview) > 0 {
art.ScanOverview = models.ScanOverview{}
for key, value := range a.ScanOverview {
js, err := json.Marshal(value)
if err != nil {
log.Warningf("convert summary of %s failed, error: %v", key, err)
continue
}
var summary models.NativeReportSummary
if err := summary.UnmarshalBinary(js); err != nil {
log.Warningf("convert summary of %s failed, error: %v", key, err)
continue
}
art.ScanOverview[key] = summary
}
}
return art
}

View File

@ -0,0 +1,90 @@
// 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 handler
import (
"context"
"fmt"
"github.com/go-openapi/runtime/middleware"
"github.com/goharbor/harbor/src/api/artifact"
"github.com/goharbor/harbor/src/api/scan"
"github.com/goharbor/harbor/src/common/rbac"
ierror "github.com/goharbor/harbor/src/internal/error"
operation "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/scan"
)
func newScanAPI() *scanAPI {
return &scanAPI{
artCtl: artifact.Ctl,
scanCtl: scan.DefaultController,
}
}
type scanAPI struct {
BaseAPI
artCtl artifact.Controller
scanCtl scan.Controller
}
func (s *scanAPI) Prepare(ctx context.Context, operation string, params interface{}) middleware.Responder {
if err := unescapePathParams(params, "RepositoryName"); err != nil {
s.SendError(ctx, err)
}
return nil
}
func (s *scanAPI) ScanArtifact(ctx context.Context, params operation.ScanArtifactParams) middleware.Responder {
if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionRead, rbac.ResourceScan); err != nil {
return s.SendError(ctx, err)
}
repository := fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName)
artifact, err := s.artCtl.GetByReference(ctx, repository, params.Reference, nil)
if err != nil {
return s.SendError(ctx, err)
}
if err := s.scanCtl.Scan(ctx, artifact); err != nil {
return s.SendError(ctx, err)
}
return operation.NewScanArtifactAccepted()
}
func (s *scanAPI) GetReportLog(ctx context.Context, params operation.GetReportLogParams) middleware.Responder {
if err := s.RequireProjectAccess(ctx, params.ProjectName, rbac.ActionRead, rbac.ResourceScan); err != nil {
return s.SendError(ctx, err)
}
repository := fmt.Sprintf("%s/%s", params.ProjectName, params.RepositoryName)
_, err := s.artCtl.GetByReference(ctx, repository, params.Reference, nil)
if err != nil {
return s.SendError(ctx, err)
}
bytes, err := s.scanCtl.GetScanLog(params.ReportID)
if err != nil {
return s.SendError(ctx, err)
}
if bytes == nil {
// Not found
return s.SendError(ctx, ierror.NotFoundError(nil).WithMessage("report with uuid %s does not exist", params.ReportID))
}
return operation.NewGetReportLogOK().WithPayload(string(bytes))
}

View File

@ -38,14 +38,7 @@ func boolValue(v *bool) bool {
}
func resolveVulnerabilitiesAddition(ctx context.Context, artifact *artifact.Artifact) (*processor.Addition, error) {
art := &v1.Artifact{
NamespaceID: artifact.ProjectID,
Repository: artifact.RepositoryName,
Digest: artifact.Digest,
MimeType: artifact.ManifestMediaType,
}
reports, err := scan.DefaultController.GetReport(art, []string{v1.MimeTypeNativeReport})
reports, err := scan.DefaultController.GetReport(ctx, artifact, []string{v1.MimeTypeNativeReport})
if err != nil {
return nil, err
}

View File

@ -3,7 +3,11 @@
package scan
import (
artifact "github.com/goharbor/harbor/src/api/artifact"
all "github.com/goharbor/harbor/src/pkg/scan/all"
context "context"
daoscan "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
job "github.com/goharbor/harbor/src/jobservice/job"
@ -13,8 +17,6 @@ import (
report "github.com/goharbor/harbor/src/pkg/scan/report"
scan "github.com/goharbor/harbor/src/api/scan"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
)
// Controller is an autogenerated mock type for the Controller type
@ -42,13 +44,13 @@ func (_m *Controller) DeleteReports(digests ...string) error {
return r0
}
// GetReport provides a mock function with given fields: artifact, mimeTypes
func (_m *Controller) GetReport(artifact *v1.Artifact, mimeTypes []string) ([]*daoscan.Report, error) {
ret := _m.Called(artifact, 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) {
ret := _m.Called(ctx, _a1, mimeTypes)
var r0 []*daoscan.Report
if rf, ok := ret.Get(0).(func(*v1.Artifact, []string) []*daoscan.Report); ok {
r0 = rf(artifact, mimeTypes)
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string) []*daoscan.Report); ok {
r0 = rf(ctx, _a1, mimeTypes)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*daoscan.Report)
@ -56,8 +58,8 @@ func (_m *Controller) GetReport(artifact *v1.Artifact, mimeTypes []string) ([]*d
}
var r1 error
if rf, ok := ret.Get(1).(func(*v1.Artifact, []string) error); ok {
r1 = rf(artifact, mimeTypes)
if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, []string) error); ok {
r1 = rf(ctx, _a1, mimeTypes)
} else {
r1 = ret.Error(1)
}
@ -111,20 +113,20 @@ func (_m *Controller) GetStats(requester string) (*all.Stats, error) {
return r0, r1
}
// GetSummary provides a mock function with given fields: artifact, mimeTypes, options
func (_m *Controller) GetSummary(artifact *v1.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error) {
// GetSummary provides a mock function with given fields: ctx, _a1, mimeTypes, options
func (_m *Controller) GetSummary(ctx context.Context, _a1 *artifact.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error) {
_va := make([]interface{}, len(options))
for _i := range options {
_va[_i] = options[_i]
}
var _ca []interface{}
_ca = append(_ca, artifact, mimeTypes)
_ca = append(_ca, ctx, _a1, mimeTypes)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 map[string]interface{}
if rf, ok := ret.Get(0).(func(*v1.Artifact, []string, ...report.Option) map[string]interface{}); ok {
r0 = rf(artifact, mimeTypes, options...)
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string, ...report.Option) map[string]interface{}); ok {
r0 = rf(ctx, _a1, mimeTypes, options...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]interface{})
@ -132,8 +134,8 @@ func (_m *Controller) GetSummary(artifact *v1.Artifact, mimeTypes []string, opti
}
var r1 error
if rf, ok := ret.Get(1).(func(*v1.Artifact, []string, ...report.Option) error); ok {
r1 = rf(artifact, mimeTypes, options...)
if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, []string, ...report.Option) error); ok {
r1 = rf(ctx, _a1, mimeTypes, options...)
} else {
r1 = ret.Error(1)
}
@ -155,20 +157,20 @@ func (_m *Controller) HandleJobHooks(trackID string, change *job.StatusChange) e
return r0
}
// Scan provides a mock function with given fields: artifact, options
func (_m *Controller) Scan(artifact *v1.Artifact, options ...scan.Option) error {
// 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 {
_va := make([]interface{}, len(options))
for _i := range options {
_va[_i] = options[_i]
}
var _ca []interface{}
_ca = append(_ca, artifact)
_ca = append(_ca, ctx, _a1)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 error
if rf, ok := ret.Get(0).(func(*v1.Artifact, ...scan.Option) error); ok {
r0 = rf(artifact, options...)
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, ...scan.Option) error); ok {
r0 = rf(ctx, _a1, options...)
} else {
r0 = ret.Error(0)
}

View File

@ -17,3 +17,6 @@ package pkg
//go:generate mockery -case snake -dir ../../pkg/blob -name Manager -output ./blob -outpkg blob
//go:generate mockery -case snake -dir ../../pkg/quota -name Manager -output ./quota -outpkg quota
//go:generate mockery -case snake -dir ../../pkg/quota/driver -name Driver -output ./quota/driver -outpkg driver
//go:generate mockery -case snake -dir ../../pkg/scan/report -name Manager -output ./scan/report -outpkg report
//go:generate mockery -case snake -dir ../../pkg/scan/rest/v1 -all -output ./scan/rest/v1 -outpkg v1
//go:generate mockery -case snake -dir ../../pkg/scan/scanner -all -output ./scan/scanner -outpkg scanner

View File

@ -0,0 +1,167 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package report
import (
all "github.com/goharbor/harbor/src/pkg/scan/all"
mock "github.com/stretchr/testify/mock"
scan "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
)
// Manager is an autogenerated mock type for the Manager type
type Manager struct {
mock.Mock
}
// Create provides a mock function with given fields: r
func (_m *Manager) Create(r *scan.Report) (string, error) {
ret := _m.Called(r)
var r0 string
if rf, ok := ret.Get(0).(func(*scan.Report) string); ok {
r0 = rf(r)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(*scan.Report) error); ok {
r1 = rf(r)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteByDigests provides a mock function with given fields: digests
func (_m *Manager) DeleteByDigests(digests ...string) error {
_va := make([]interface{}, len(digests))
for _i := range digests {
_va[_i] = digests[_i]
}
var _ca []interface{}
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 error
if rf, ok := ret.Get(0).(func(...string) error); ok {
r0 = rf(digests...)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: uuid
func (_m *Manager) Get(uuid string) (*scan.Report, error) {
ret := _m.Called(uuid)
var r0 *scan.Report
if rf, ok := ret.Get(0).(func(string) *scan.Report); ok {
r0 = rf(uuid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*scan.Report)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(uuid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetBy provides a mock function with given fields: digest, registrationUUID, mimeTypes
func (_m *Manager) GetBy(digest string, registrationUUID string, mimeTypes []string) ([]*scan.Report, error) {
ret := _m.Called(digest, registrationUUID, mimeTypes)
var r0 []*scan.Report
if rf, ok := ret.Get(0).(func(string, string, []string) []*scan.Report); ok {
r0 = rf(digest, registrationUUID, mimeTypes)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*scan.Report)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, []string) error); ok {
r1 = rf(digest, registrationUUID, mimeTypes)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetStats provides a mock function with given fields: requester
func (_m *Manager) GetStats(requester string) (*all.Stats, error) {
ret := _m.Called(requester)
var r0 *all.Stats
if rf, ok := ret.Get(0).(func(string) *all.Stats); ok {
r0 = rf(requester)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*all.Stats)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(requester)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateReportData provides a mock function with given fields: uuid, _a1, rev
func (_m *Manager) UpdateReportData(uuid string, _a1 string, rev int64) error {
ret := _m.Called(uuid, _a1, rev)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, int64) error); ok {
r0 = rf(uuid, _a1, rev)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateScanJobID provides a mock function with given fields: trackID, jobID
func (_m *Manager) UpdateScanJobID(trackID string, jobID string) error {
ret := _m.Called(trackID, jobID)
var r0 error
if rf, ok := ret.Get(0).(func(string, string) error); ok {
r0 = rf(trackID, jobID)
} else {
r0 = ret.Error(0)
}
return r0
}
// UpdateStatus provides a mock function with given fields: trackID, status, rev
func (_m *Manager) UpdateStatus(trackID string, status string, rev int64) error {
ret := _m.Called(trackID, status, rev)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, int64) error); ok {
r0 = rf(trackID, status, rev)
} else {
r0 = ret.Error(0)
}
return r0
}

View File

@ -0,0 +1,80 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package v1
import (
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
mock "github.com/stretchr/testify/mock"
)
// Client is an autogenerated mock type for the Client type
type Client struct {
mock.Mock
}
// GetMetadata provides a mock function with given fields:
func (_m *Client) GetMetadata() (*v1.ScannerAdapterMetadata, error) {
ret := _m.Called()
var r0 *v1.ScannerAdapterMetadata
if rf, ok := ret.Get(0).(func() *v1.ScannerAdapterMetadata); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1.ScannerAdapterMetadata)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetScanReport provides a mock function with given fields: scanRequestID, reportMIMEType
func (_m *Client) GetScanReport(scanRequestID string, reportMIMEType string) (string, error) {
ret := _m.Called(scanRequestID, reportMIMEType)
var r0 string
if rf, ok := ret.Get(0).(func(string, string) string); ok {
r0 = rf(scanRequestID, reportMIMEType)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(scanRequestID, reportMIMEType)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SubmitScan provides a mock function with given fields: req
func (_m *Client) SubmitScan(req *v1.ScanRequest) (*v1.ScanResponse, error) {
ret := _m.Called(req)
var r0 *v1.ScanResponse
if rf, ok := ret.Get(0).(func(*v1.ScanRequest) *v1.ScanResponse); ok {
r0 = rf(req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1.ScanResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*v1.ScanRequest) error); ok {
r1 = rf(req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,36 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package v1
import (
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
mock "github.com/stretchr/testify/mock"
)
// ClientPool is an autogenerated mock type for the ClientPool type
type ClientPool struct {
mock.Mock
}
// Get provides a mock function with given fields: url, authType, accessCredential, skipCertVerify
func (_m *ClientPool) Get(url string, authType string, accessCredential string, skipCertVerify bool) (v1.Client, error) {
ret := _m.Called(url, authType, accessCredential, skipCertVerify)
var r0 v1.Client
if rf, ok := ret.Get(0).(func(string, string, string, bool) v1.Client); ok {
r0 = rf(url, authType, accessCredential, skipCertVerify)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(v1.Client)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, bool) error); ok {
r1 = rf(url, authType, accessCredential, skipCertVerify)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,147 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package scanner
import (
q "github.com/goharbor/harbor/src/pkg/q"
mock "github.com/stretchr/testify/mock"
scanner "github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
)
// Manager is an autogenerated mock type for the Manager type
type Manager struct {
mock.Mock
}
// Create provides a mock function with given fields: registration
func (_m *Manager) Create(registration *scanner.Registration) (string, error) {
ret := _m.Called(registration)
var r0 string
if rf, ok := ret.Get(0).(func(*scanner.Registration) string); ok {
r0 = rf(registration)
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func(*scanner.Registration) error); ok {
r1 = rf(registration)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Delete provides a mock function with given fields: registrationUUID
func (_m *Manager) Delete(registrationUUID string) error {
ret := _m.Called(registrationUUID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(registrationUUID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: registrationUUID
func (_m *Manager) Get(registrationUUID string) (*scanner.Registration, error) {
ret := _m.Called(registrationUUID)
var r0 *scanner.Registration
if rf, ok := ret.Get(0).(func(string) *scanner.Registration); ok {
r0 = rf(registrationUUID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*scanner.Registration)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(registrationUUID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDefault provides a mock function with given fields:
func (_m *Manager) GetDefault() (*scanner.Registration, error) {
ret := _m.Called()
var r0 *scanner.Registration
if rf, ok := ret.Get(0).(func() *scanner.Registration); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*scanner.Registration)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// List provides a mock function with given fields: query
func (_m *Manager) List(query *q.Query) ([]*scanner.Registration, error) {
ret := _m.Called(query)
var r0 []*scanner.Registration
if rf, ok := ret.Get(0).(func(*q.Query) []*scanner.Registration); ok {
r0 = rf(query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*scanner.Registration)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*q.Query) error); ok {
r1 = rf(query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetAsDefault provides a mock function with given fields: registrationUUID
func (_m *Manager) SetAsDefault(registrationUUID string) error {
ret := _m.Called(registrationUUID)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(registrationUUID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: registration
func (_m *Manager) Update(registration *scanner.Registration) error {
ret := _m.Called(registration)
var r0 error
if rf, ok := ret.Get(0).(func(*scanner.Registration) error); ok {
r0 = rf(registration)
} else {
r0 = ret.Error(0)
}
return r0
}