mirror of
https://github.com/goharbor/harbor.git
synced 2024-09-28 21:37:31 +02:00
feat(scan): support to scan image index (#11001)
Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
parent
6a25e6b2c6
commit
12f16c8cec
@ -10,6 +10,7 @@ schemes:
|
|||||||
basePath: /api/v2.0
|
basePath: /api/v2.0
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
|
- text/plain
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
securityDefinitions:
|
securityDefinitions:
|
||||||
@ -333,7 +334,7 @@ paths:
|
|||||||
summary: Scan the artifact
|
summary: Scan the artifact
|
||||||
description: Scan the specified artifact
|
description: Scan the specified artifact
|
||||||
tags:
|
tags:
|
||||||
- artifact
|
- scan
|
||||||
operationId: scanArtifact
|
operationId: scanArtifact
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/parameters/requestId'
|
- $ref: '#/parameters/requestId'
|
||||||
@ -351,6 +352,38 @@ paths:
|
|||||||
$ref: '#/responses/404'
|
$ref: '#/responses/404'
|
||||||
'500':
|
'500':
|
||||||
$ref: '#/responses/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:
|
/projects/{project_name}/repositories/{repository_name}/artifacts/{reference}/tags:
|
||||||
post:
|
post:
|
||||||
summary: Create tag
|
summary: Create tag
|
||||||
@ -989,6 +1022,10 @@ definitions:
|
|||||||
format: date-time
|
format: date-time
|
||||||
description: 'The end time of the scan process that generating report'
|
description: 'The end time of the scan process that generating report'
|
||||||
example: '2006-01-02T15:04:05'
|
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:
|
VulnerabilitySummary:
|
||||||
type: object
|
type: object
|
||||||
description: |
|
description: |
|
||||||
|
@ -22,7 +22,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/pkg/q"
|
"github.com/goharbor/harbor/src/pkg/q"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,18 +35,9 @@ func HandleCheckIn(ctx context.Context, checkIn string) {
|
|||||||
batchSize := 50
|
batchSize := 50
|
||||||
for repo := range fetchRepositories(ctx, batchSize) {
|
for repo := range fetchRepositories(ctx, batchSize) {
|
||||||
for artifact := range fetchArtifacts(ctx, repo.RepositoryID, batchSize) {
|
for artifact := range fetchArtifacts(ctx, repo.RepositoryID, batchSize) {
|
||||||
for _, tag := range artifact.Tags {
|
if err := DefaultController.Scan(ctx, artifact, WithRequester(checkIn)); err != nil {
|
||||||
art := &v1.Artifact{
|
// Just logged
|
||||||
NamespaceID: artifact.ProjectID,
|
log.Error(errors.Wrap(err, "handle check in"))
|
||||||
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"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,7 +57,7 @@ func fetchArtifacts(ctx context.Context, repositoryID int64, chunkSize int) <-ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
artifacts, err := artifact.Ctl.List(ctx, query, &artifact.Option{WithTag: true})
|
artifacts, err := artifact.Ctl.List(ctx, query, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("[scan all]: list artifacts failed, error: %v", err)
|
log.Errorf("[scan all]: list artifacts failed, error: %v", err)
|
||||||
return
|
return
|
||||||
|
@ -15,9 +15,12 @@
|
|||||||
package scan
|
package scan
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
ar "github.com/goharbor/harbor/src/api/artifact"
|
||||||
sc "github.com/goharbor/harbor/src/api/scanner"
|
sc "github.com/goharbor/harbor/src/api/scanner"
|
||||||
cj "github.com/goharbor/harbor/src/common/job"
|
cj "github.com/goharbor/harbor/src/common/job"
|
||||||
jm "github.com/goharbor/harbor/src/common/job/models"
|
jm "github.com/goharbor/harbor/src/common/job/models"
|
||||||
@ -62,6 +65,8 @@ type jcGetter func() cj.Client
|
|||||||
type basicController struct {
|
type basicController struct {
|
||||||
// Manage the scan report records
|
// Manage the scan report records
|
||||||
manager report.Manager
|
manager report.Manager
|
||||||
|
// Artifact controller
|
||||||
|
ar ar.Controller
|
||||||
// Scanner controller
|
// Scanner controller
|
||||||
sc sc.Controller
|
sc sc.Controller
|
||||||
// Robot account controller
|
// Robot account controller
|
||||||
@ -79,6 +84,8 @@ func NewController() Controller {
|
|||||||
return &basicController{
|
return &basicController{
|
||||||
// New report manager
|
// New report manager
|
||||||
manager: report.NewManager(),
|
manager: report.NewManager(),
|
||||||
|
// Refer to the default artifact controller
|
||||||
|
ar: ar.Ctl,
|
||||||
// Refer to the default scanner controller
|
// Refer to the default scanner controller
|
||||||
sc: sc.DefaultController,
|
sc: sc.DefaultController,
|
||||||
// Refer to the default robot account controller
|
// 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 ...
|
// 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 {
|
if artifact == nil {
|
||||||
return errors.New("nil artifact to scan")
|
return errors.New("nil artifact to scan")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse options
|
r, err := bc.sc.GetRegistrationByProject(artifact.ProjectID)
|
||||||
ops, err := parseOptions(options...)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "scan controller: scan")
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := bc.sc.GetRegistrationByProject(artifact.NamespaceID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "scan controller: scan")
|
return errors.Wrap(err, "scan controller: scan")
|
||||||
}
|
}
|
||||||
|
|
||||||
// In case it does not exist
|
// In case it does not exist
|
||||||
if r == nil {
|
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
|
// 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))
|
return errs.WithCode(errs.PreconditionFailed, errs.Errorf("scanner %s is disabled", r.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the health of the registration by ping.
|
artifacts, scannable, err := bc.collectScanningArtifacts(ctx, r, artifact)
|
||||||
// The metadata of the scanner adapter is also returned.
|
|
||||||
meta, err := bc.sc.Ping(r)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "scan controller: scan")
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a UUID as track ID which groups the report records generated
|
if !scannable {
|
||||||
// by the specified registration for the digest with given mime type.
|
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()
|
trackID, err := bc.uuid()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "scan controller: scan")
|
return "", nil, errors.Wrap(err, "scan controller: scan")
|
||||||
}
|
}
|
||||||
|
|
||||||
producesMimes := make([]string, 0)
|
// Parse options
|
||||||
matched := false
|
ops, err := parseOptions(options...)
|
||||||
statusConflict := false
|
if err != nil {
|
||||||
for _, ca := range meta.Capabilities {
|
return "", nil, errors.Wrap(err, "scan controller: scan")
|
||||||
for _, cm := range ca.ConsumesMimeTypes {
|
}
|
||||||
if cm == artifact.MimeType {
|
|
||||||
matched = true
|
create := func(ctx context.Context, digest, registrationUUID, mimeType, trackID string, status job.Status) error {
|
||||||
break
|
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 {
|
_, e := bc.manager.Create(reportPlaceholder)
|
||||||
for _, pm := range ca.ProducesMimeTypes {
|
return e
|
||||||
// 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)
|
if HasCapability(r, art) {
|
||||||
if e != nil {
|
var producesMimes []string
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recorded by error wrap and logged at the same time.
|
for _, pm := range r.GetProducesMimeTypes(art.ManifestMediaType) {
|
||||||
if err == nil {
|
if err = create(ctx, art.Digest, r.UUID, pm, trackID, job.PendingStatus); err != nil {
|
||||||
err = e
|
return "", nil, err
|
||||||
} else {
|
|
||||||
err = errors.Wrap(e, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Error(errors.Wrap(e, "scan controller: scan"))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
producesMimes = append(producesMimes, pm)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
producesMimes = append(producesMimes, pm)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(producesMimes) > 0 {
|
||||||
|
return trackID, producesMimes, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scanner does not support scanning the given artifact.
|
err = create(ctx, art.Digest, r.UUID, v1.MimeTypeNativeReport, trackID, job.ErrorStatus)
|
||||||
if !matched {
|
return "", nil, err
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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)
|
jobID, err := bc.launchScanJob(trackID, artifact, r, producesMimes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Update the status to the concrete error
|
// Update the status to the concrete error
|
||||||
@ -243,7 +304,7 @@ func (bc *basicController) Scan(artifact *v1.Artifact, options ...Option) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetReport ...
|
// 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 {
|
if artifact == nil {
|
||||||
return nil, errors.New("no way to get report for nil artifact")
|
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
|
// Get current scanner settings
|
||||||
r, err := bc.sc.GetRegistrationByProject(artifact.NamespaceID)
|
r, err := bc.sc.GetRegistrationByProject(artifact.ProjectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "scan controller: get report")
|
return nil, errors.Wrap(err, "scan controller: get report")
|
||||||
}
|
}
|
||||||
|
|
||||||
if r == nil {
|
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 ...
|
// 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 {
|
if artifact == nil {
|
||||||
return nil, errors.New("no way to get report summaries for nil artifact")
|
return nil, errors.New("no way to get report summaries for nil artifact")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get reports first
|
// Get reports first
|
||||||
rps, err := bc.GetReport(artifact, mimeTypes)
|
rps, err := bc.GetReport(ctx, artifact, mimeTypes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -287,7 +389,16 @@ func (bc *basicController) GetSummary(artifact *v1.Artifact, mimeTypes []string,
|
|||||||
return nil, err
|
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
|
return summaries, nil
|
||||||
@ -423,7 +534,7 @@ func (bc *basicController) makeRobotAccount(projectID int64, repository string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// launchScanJob launches a job to run scan
|
// 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
|
var ck string
|
||||||
if registration.UseInternalAddr {
|
if registration.UseInternalAddr {
|
||||||
ck = configCoreInternalAddr
|
ck = configCoreInternalAddr
|
||||||
@ -436,7 +547,7 @@ func (bc *basicController) launchScanJob(trackID string, artifact *v1.Artifact,
|
|||||||
return "", errors.Wrap(err, "scan controller: launch scan job")
|
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 {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "scan controller: launch scan job")
|
return "", errors.Wrap(err, "scan controller: launch scan job")
|
||||||
}
|
}
|
||||||
@ -450,7 +561,12 @@ func (bc *basicController) launchScanJob(trackID string, artifact *v1.Artifact,
|
|||||||
URL: registryAddr,
|
URL: registryAddr,
|
||||||
Authorization: authorization,
|
Authorization: authorization,
|
||||||
},
|
},
|
||||||
Artifact: artifact,
|
Artifact: &v1.Artifact{
|
||||||
|
NamespaceID: artifact.ProjectID,
|
||||||
|
Repository: artifact.RepositoryName,
|
||||||
|
Digest: artifact.Digest,
|
||||||
|
MimeType: artifact.ManifestMediaType,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
rJSON, err := registration.ToJSON()
|
rJSON, err := registration.ToJSON()
|
||||||
|
@ -15,12 +15,14 @@
|
|||||||
package scan
|
package scan
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/api/artifact"
|
||||||
"github.com/goharbor/harbor/src/common"
|
"github.com/goharbor/harbor/src/common"
|
||||||
cj "github.com/goharbor/harbor/src/common/job"
|
cj "github.com/goharbor/harbor/src/common/job"
|
||||||
cjm "github.com/goharbor/harbor/src/common/job/models"
|
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/q"
|
||||||
"github.com/goharbor/harbor/src/pkg/robot/model"
|
"github.com/goharbor/harbor/src/pkg/robot/model"
|
||||||
sca "github.com/goharbor/harbor/src/pkg/scan"
|
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/scan"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
||||||
|
artifacttesting "github.com/goharbor/harbor/src/testing/api/artifact"
|
||||||
scannertesting "github.com/goharbor/harbor/src/testing/api/scanner"
|
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/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -47,9 +51,11 @@ type ControllerTestSuite struct {
|
|||||||
suite.Suite
|
suite.Suite
|
||||||
|
|
||||||
registration *scanner.Registration
|
registration *scanner.Registration
|
||||||
artifact *v1.Artifact
|
artifact *artifact.Artifact
|
||||||
rawReport string
|
rawReport string
|
||||||
c Controller
|
|
||||||
|
ar artifact.Controller
|
||||||
|
c Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestController is the entry point of ControllerTestSuite.
|
// TestController is the entry point of ControllerTestSuite.
|
||||||
@ -59,21 +65,11 @@ func TestController(t *testing.T) {
|
|||||||
|
|
||||||
// SetupSuite ...
|
// SetupSuite ...
|
||||||
func (suite *ControllerTestSuite) SetupSuite() {
|
func (suite *ControllerTestSuite) SetupSuite() {
|
||||||
suite.registration = &scanner.Registration{
|
suite.artifact = &artifact.Artifact{}
|
||||||
ID: 1,
|
suite.artifact.ProjectID = 1
|
||||||
UUID: "uuid001",
|
suite.artifact.RepositoryName = "library/photon"
|
||||||
Name: "Test-scan-controller",
|
suite.artifact.Digest = "digest-code"
|
||||||
URL: "http://testing.com:3128",
|
suite.artifact.ManifestMediaType = v1.MimeTypeDockerArtifact
|
||||||
IsDefault: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
suite.artifact = &v1.Artifact{
|
|
||||||
NamespaceID: 1,
|
|
||||||
Repository: "scan",
|
|
||||||
Tag: "golang",
|
|
||||||
Digest: "digest-code",
|
|
||||||
MimeType: v1.MimeTypeDockerArtifact,
|
|
||||||
}
|
|
||||||
|
|
||||||
m := &v1.ScannerAdapterMetadata{
|
m := &v1.ScannerAdapterMetadata{
|
||||||
Scanner: &v1.Scanner{
|
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 := &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)
|
sc.On("Ping", suite.registration).Return(m, nil)
|
||||||
|
|
||||||
mgr := &MockReportManager{}
|
mgr := &reporttesting.Manager{}
|
||||||
mgr.On("Create", &scan.Report{
|
mgr.On("Create", &scan.Report{
|
||||||
Digest: "digest-code",
|
Digest: "digest-code",
|
||||||
RegistrationUUID: "uuid001",
|
RegistrationUUID: "uuid001",
|
||||||
@ -161,7 +166,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
|||||||
|
|
||||||
rc := &MockRobotController{}
|
rc := &MockRobotController{}
|
||||||
|
|
||||||
resource := fmt.Sprintf("/project/%d/repository", suite.artifact.NamespaceID)
|
resource := fmt.Sprintf("/project/%d/repository", suite.artifact.ProjectID)
|
||||||
access := []*rbac.Policy{{
|
access := []*rbac.Policy{{
|
||||||
Resource: rbac.Resource(resource),
|
Resource: rbac.Resource(resource),
|
||||||
Action: rbac.ActionScannerPull,
|
Action: rbac.ActionScannerPull,
|
||||||
@ -171,7 +176,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
|||||||
account := &model.RobotCreate{
|
account := &model.RobotCreate{
|
||||||
Name: rname,
|
Name: rname,
|
||||||
Description: "for scan",
|
Description: "for scan",
|
||||||
ProjectID: suite.artifact.NamespaceID,
|
ProjectID: suite.artifact.ProjectID,
|
||||||
Access: access,
|
Access: access,
|
||||||
}
|
}
|
||||||
rc.On("CreateRobotAccount", account).Return(&model.Robot{
|
rc.On("CreateRobotAccount", account).Return(&model.Robot{
|
||||||
@ -179,7 +184,7 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
|||||||
Name: common.RobotPrefix + rname,
|
Name: common.RobotPrefix + rname,
|
||||||
Token: "robot-account",
|
Token: "robot-account",
|
||||||
Description: "for scan",
|
Description: "for scan",
|
||||||
ProjectID: suite.artifact.NamespaceID,
|
ProjectID: suite.artifact.ProjectID,
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
// Set job parameters
|
// Set job parameters
|
||||||
@ -188,7 +193,12 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
|||||||
URL: "https://core.com",
|
URL: "https://core.com",
|
||||||
Authorization: "Basic " + base64.StdEncoding.EncodeToString([]byte(common.RobotPrefix+"the-uuid-123:robot-account")),
|
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()
|
rJSON, err := req.ToJSON()
|
||||||
@ -215,8 +225,15 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
|||||||
jc.On("SubmitJob", j).Return("the-job-id", nil)
|
jc.On("SubmitJob", j).Return("the-job-id", nil)
|
||||||
jc.On("GetJobLog", "the-job-id").Return([]byte("job log"), 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{
|
suite.c = &basicController{
|
||||||
manager: mgr,
|
manager: mgr,
|
||||||
|
ar: suite.ar,
|
||||||
sc: sc,
|
sc: sc,
|
||||||
jc: func() cj.Client {
|
jc: func() cj.Client {
|
||||||
return jc
|
return jc
|
||||||
@ -243,20 +260,20 @@ func (suite *ControllerTestSuite) TearDownSuite() {}
|
|||||||
|
|
||||||
// TestScanControllerScan ...
|
// TestScanControllerScan ...
|
||||||
func (suite *ControllerTestSuite) TestScanControllerScan() {
|
func (suite *ControllerTestSuite) TestScanControllerScan() {
|
||||||
err := suite.c.Scan(suite.artifact)
|
err := suite.c.Scan(context.TODO(), suite.artifact)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestScanControllerGetReport ...
|
// TestScanControllerGetReport ...
|
||||||
func (suite *ControllerTestSuite) 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)
|
require.NoError(suite.T(), err)
|
||||||
assert.Equal(suite.T(), 1, len(rep))
|
assert.Equal(suite.T(), 1, len(rep))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestScanControllerGetSummary ...
|
// TestScanControllerGetSummary ...
|
||||||
func (suite *ControllerTestSuite) 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)
|
require.NoError(suite.T(), err)
|
||||||
assert.Equal(suite.T(), 1, len(sum))
|
assert.Equal(suite.T(), 1, len(sum))
|
||||||
}
|
}
|
||||||
@ -298,73 +315,6 @@ func (suite *ControllerTestSuite) TestScanControllerHandleJobHooks() {
|
|||||||
|
|
||||||
// Mock things
|
// 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 ...
|
// MockJobServiceClient ...
|
||||||
type MockJobServiceClient struct {
|
type MockJobServiceClient struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
|
@ -19,7 +19,7 @@ import (
|
|||||||
|
|
||||||
"github.com/goharbor/harbor/src/api/artifact"
|
"github.com/goharbor/harbor/src/api/artifact"
|
||||||
"github.com/goharbor/harbor/src/api/scanner"
|
"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
|
// Checker checker which can check that the artifact is scannable
|
||||||
@ -31,24 +31,28 @@ type Checker interface {
|
|||||||
// NewChecker returns checker
|
// NewChecker returns checker
|
||||||
func NewChecker() Checker {
|
func NewChecker() Checker {
|
||||||
return &checker{
|
return &checker{
|
||||||
artifactCtl: artifact.Ctl,
|
artifactCtl: artifact.Ctl,
|
||||||
scannerCtl: scanner.DefaultController,
|
scannerCtl: scanner.DefaultController,
|
||||||
scannerMetadatas: map[int64]*v1.ScannerAdapterMetadata{},
|
registrations: map[int64]*models.Registration{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type checker struct {
|
type checker struct {
|
||||||
artifactCtl artifact.Controller
|
artifactCtl artifact.Controller
|
||||||
scannerCtl scanner.Controller
|
scannerCtl scanner.Controller
|
||||||
scannerMetadatas map[int64]*v1.ScannerAdapterMetadata
|
registrations map[int64]*models.Registration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *checker) IsScannable(ctx context.Context, art *artifact.Artifact) (bool, error) {
|
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
|
projectID := art.ProjectID
|
||||||
|
|
||||||
metadata, ok := c.scannerMetadatas[projectID]
|
r, ok := c.registrations[projectID]
|
||||||
if !ok {
|
if !ok {
|
||||||
registration, err := c.scannerCtl.GetRegistrationByProject(projectID, scanner.WithPing(false))
|
registration, err := c.scannerCtl.GetRegistrationByProject(projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -57,20 +61,15 @@ func (c *checker) IsScannable(ctx context.Context, art *artifact.Artifact) (bool
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
md, err := c.scannerCtl.Ping(registration)
|
r = registration
|
||||||
if err != nil {
|
c.registrations[projectID] = registration
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata = md
|
|
||||||
c.scannerMetadatas[projectID] = md
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var scannable bool
|
var scannable bool
|
||||||
|
|
||||||
walkFn := func(a *artifact.Artifact) error {
|
walkFn := func(a *artifact.Artifact) error {
|
||||||
scannable = metadata.HasCapability(a.ManifestMediaType)
|
if HasCapability(r, a) {
|
||||||
if scannable {
|
scannable = true
|
||||||
return artifact.ErrBreak
|
return artifact.ErrBreak
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,3 +82,13 @@ func (c *checker) IsScannable(ctx context.Context, art *artifact.Artifact) (bool
|
|||||||
|
|
||||||
return scannable, nil
|
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)
|
||||||
|
}
|
||||||
|
@ -36,9 +36,9 @@ func (suite *CheckerTestSuite) new() *checker {
|
|||||||
scannerCtl := &scannertesting.Controller{}
|
scannerCtl := &scannertesting.Controller{}
|
||||||
|
|
||||||
return &checker{
|
return &checker{
|
||||||
artifactCtl: artifactCtl,
|
artifactCtl: artifactCtl,
|
||||||
scannerCtl: scannerCtl,
|
scannerCtl: scannerCtl,
|
||||||
scannerMetadatas: map[int64]*v1.ScannerAdapterMetadata{},
|
registrations: map[int64]*scanner.Registration{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,10 +59,11 @@ func (suite *CheckerTestSuite) TestIsScannable() {
|
|||||||
|
|
||||||
supportMimeType := "support mime type"
|
supportMimeType := "support mime type"
|
||||||
|
|
||||||
mock.OnAnything(c.scannerCtl, "GetRegistrationByProject").Return(&scanner.Registration{}, nil)
|
mock.OnAnything(c.scannerCtl, "GetRegistrationByProject").Return(&scanner.Registration{
|
||||||
mock.OnAnything(c.scannerCtl, "Ping").Return(&v1.ScannerAdapterMetadata{
|
Metadata: &v1.ScannerAdapterMetadata{
|
||||||
Capabilities: []*v1.ScannerCapability{
|
Capabilities: []*v1.ScannerCapability{
|
||||||
{ConsumesMimeTypes: []string{supportMimeType}},
|
{ConsumesMimeTypes: []string{supportMimeType}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
|
@ -15,11 +15,13 @@
|
|||||||
package scan
|
package scan
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/api/artifact"
|
||||||
"github.com/goharbor/harbor/src/jobservice/job"
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/all"
|
"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/scan"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Controller provides the related operations for triggering scan.
|
// Controller provides the related operations for triggering scan.
|
||||||
@ -29,12 +31,12 @@ type Controller interface {
|
|||||||
// Scan the given artifact
|
// Scan the given artifact
|
||||||
//
|
//
|
||||||
// Arguments:
|
// Arguments:
|
||||||
// artifact *v1.Artifact : artifact to be scanned
|
// artifact *artifact.Artifact : artifact to be scanned
|
||||||
// options ...Option : options for triggering a scan
|
// options ...Option : options for triggering a scan
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// error : non nil error if any errors occurred
|
// 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
|
// GetReport gets the reports for the given artifact identified by the digest
|
||||||
//
|
//
|
||||||
@ -45,19 +47,19 @@ type Controller interface {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// []*scan.Report : scan results by different scanner vendors
|
// []*scan.Report : scan results by different scanner vendors
|
||||||
// error : non nil error if any errors occurred
|
// 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.
|
// GetSummary gets the summaries of the reports with given types.
|
||||||
//
|
//
|
||||||
// Arguments:
|
// Arguments:
|
||||||
// artifact *v1.Artifact : the scanned artifact
|
// artifact *artifact.Artifact : the scanned artifact
|
||||||
// mimeTypes []string : the mime types of the reports
|
// mimeTypes []string : the mime types of the reports
|
||||||
// options ...report.Option : optional report options, specify if needed
|
// options ...report.Option : optional report options, specify if needed
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// map[string]interface{} : report summaries indexed by mime types
|
// map[string]interface{} : report summaries indexed by mime types
|
||||||
// error : non nil error if any errors occurred
|
// error : non nil error if any errors occurred
|
||||||
GetSummary(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
|
// Get the scan log for the specified artifact with the given digest
|
||||||
//
|
//
|
||||||
|
@ -237,6 +237,8 @@ func (bc *basicController) GetRegistrationByProject(projectID int64, options ...
|
|||||||
registration.Adapter = meta.Scanner.Name
|
registration.Adapter = meta.Scanner.Name
|
||||||
registration.Vendor = meta.Scanner.Vendor
|
registration.Vendor = meta.Scanner.Vendor
|
||||||
registration.Version = meta.Scanner.Version
|
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")
|
return nil, errors.New("nil registration to ping")
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := bc.clientPool.Get(registration)
|
client, err := registration.Client(bc.clientPool)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "scanner controller: ping")
|
return nil, errors.Wrap(err, "scanner controller: ping")
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,9 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/pkg/q"
|
"github.com/goharbor/harbor/src/pkg/q"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
|
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/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -32,7 +35,7 @@ type ControllerTestSuite struct {
|
|||||||
suite.Suite
|
suite.Suite
|
||||||
|
|
||||||
c *basicController
|
c *basicController
|
||||||
mMgr *MockScannerManager
|
mMgr *scannertesting.Manager
|
||||||
mMeta *MockProMetaManager
|
mMeta *MockProMetaManager
|
||||||
|
|
||||||
sample *scanner.Registration
|
sample *scanner.Registration
|
||||||
@ -45,7 +48,7 @@ func TestController(t *testing.T) {
|
|||||||
|
|
||||||
// SetupSuite prepares env for the controller test suite
|
// SetupSuite prepares env for the controller test suite
|
||||||
func (suite *ControllerTestSuite) SetupSuite() {
|
func (suite *ControllerTestSuite) SetupSuite() {
|
||||||
suite.mMgr = new(MockScannerManager)
|
suite.mMgr = &scannertesting.Manager{}
|
||||||
suite.mMeta = new(MockProMetaManager)
|
suite.mMeta = new(MockProMetaManager)
|
||||||
|
|
||||||
m := &v1.ScannerAdapterMetadata{
|
m := &v1.ScannerAdapterMetadata{
|
||||||
@ -75,11 +78,11 @@ func (suite *ControllerTestSuite) SetupSuite() {
|
|||||||
URL: "https://sample.scanner.com",
|
URL: "https://sample.scanner.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
mc := &MockClient{}
|
mc := &v1testing.Client{}
|
||||||
mc.On("GetMetadata").Return(m, nil)
|
mc.On("GetMetadata").Return(m, nil)
|
||||||
|
|
||||||
mcp := &MockClientPool{}
|
mcp := &v1testing.ClientPool{}
|
||||||
mcp.On("Get", suite.sample).Return(mc, nil)
|
mocktesting.OnAnything(mcp, "Get").Return(mc, nil)
|
||||||
suite.c = &basicController{
|
suite.c = &basicController{
|
||||||
manager: suite.mMgr,
|
manager: suite.mMgr,
|
||||||
proMetaMgr: suite.mMeta,
|
proMetaMgr: suite.mMeta,
|
||||||
@ -242,58 +245,6 @@ func (suite *ControllerTestSuite) TestGetMetadata() {
|
|||||||
suite.Equal(1, len(meta.Capabilities))
|
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
|
// MockProMetaManager is the mock of the ProjectMetadataManager
|
||||||
type MockProMetaManager struct {
|
type MockProMetaManager struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
@ -328,50 +279,3 @@ func (m *MockProMetaManager) List(name, value string) ([]*models.ProjectMetadata
|
|||||||
args := m.Called(name, value)
|
args := m.Called(name, value)
|
||||||
return args.Get(0).([]*models.ProjectMetadata), args.Error(1)
|
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)
|
|
||||||
}
|
|
||||||
|
@ -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", proScannerAPI, "get:GetProjectScanner;put:SetProjectScanner")
|
||||||
beego.Router("/api/projects/:pid([0-9]+)/scanner/candidates", proScannerAPI, "get:GetProScannerCandidates")
|
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 {
|
if err := quota.Sync(config.GlobalProjectMgr, false); err != nil {
|
||||||
log.Fatalf("failed to sync quota from backend: %v", err)
|
log.Fatalf("failed to sync quota from backend: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -1,12 +1,16 @@
|
|||||||
package notification
|
package notification
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"time"
|
"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/api/scan"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"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/notification"
|
||||||
"github.com/goharbor/harbor/src/pkg/notifier/model"
|
"github.com/goharbor/harbor/src/pkg/notifier/model"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
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")
|
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
|
// Wait for reasonable time to make sure the report is ready
|
||||||
// Interval=500ms and total time = 5s
|
// Interval=500ms and total time = 5s
|
||||||
// If the report is still not ready in the total time, then failed at then
|
// If the report is still not ready in the total time, then failed at then
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
// First check in case it is ready
|
// 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 {
|
if len(re) > 0 && len(re[0].Report) > 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -118,7 +134,7 @@ func constructScanImagePayload(event *model.ScanImageEvent, project *models.Proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add scan overview
|
// 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 {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "construct scan payload")
|
return nil, errors.Wrap(err, "construct scan payload")
|
||||||
}
|
}
|
||||||
|
@ -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
|
package notification
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/all"
|
"github.com/goharbor/harbor/src/api/artifact"
|
||||||
|
|
||||||
sc "github.com/goharbor/harbor/src/api/scan"
|
sc "github.com/goharbor/harbor/src/api/scan"
|
||||||
"github.com/goharbor/harbor/src/common"
|
"github.com/goharbor/harbor/src/common"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"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"
|
||||||
"github.com/goharbor/harbor/src/pkg/notification/policy"
|
"github.com/goharbor/harbor/src/pkg/notification/policy"
|
||||||
"github.com/goharbor/harbor/src/pkg/notifier"
|
"github.com/goharbor/harbor/src/pkg/notifier"
|
||||||
"github.com/goharbor/harbor/src/pkg/notifier/model"
|
"github.com/goharbor/harbor/src/pkg/notifier/model"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/report"
|
"github.com/goharbor/harbor/src/pkg/scan/report"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
"github.com/stretchr/testify/mock"
|
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/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
@ -26,10 +40,11 @@ import (
|
|||||||
type ScanImagePreprocessHandlerSuite struct {
|
type ScanImagePreprocessHandlerSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
|
|
||||||
om policy.Manager
|
om policy.Manager
|
||||||
pid int64
|
pid int64
|
||||||
evt *model.ScanImageEvent
|
evt *model.ScanImageEvent
|
||||||
c sc.Controller
|
c sc.Controller
|
||||||
|
artifactCtl artifact.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestScanImagePreprocessHandler is the entry point of ScanImagePreprocessHandlerSuite.
|
// TestScanImagePreprocessHandler is the entry point of ScanImagePreprocessHandlerSuite.
|
||||||
@ -65,15 +80,29 @@ func (suite *ScanImagePreprocessHandlerSuite) SetupSuite() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suite.c = sc.DefaultController
|
suite.c = sc.DefaultController
|
||||||
mc := &MockScanAPIController{}
|
mc := &scantesting.Controller{}
|
||||||
|
|
||||||
var options []report.Option
|
var options []report.Option
|
||||||
s := make(map[string]interface{})
|
s := make(map[string]interface{})
|
||||||
mc.On("GetSummary", a, []string{v1.MimeTypeNativeReport}, options).Return(s, nil)
|
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
|
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
|
suite.om = notification.PolicyMgr
|
||||||
mp := &fakedPolicyMgr{}
|
mp := &fakedPolicyMgr{}
|
||||||
notification.PolicyMgr = mp
|
notification.PolicyMgr = mp
|
||||||
@ -88,6 +117,7 @@ func (suite *ScanImagePreprocessHandlerSuite) SetupSuite() {
|
|||||||
func (suite *ScanImagePreprocessHandlerSuite) TearDownSuite() {
|
func (suite *ScanImagePreprocessHandlerSuite) TearDownSuite() {
|
||||||
notification.PolicyMgr = suite.om
|
notification.PolicyMgr = suite.om
|
||||||
sc.DefaultController = suite.c
|
sc.DefaultController = suite.c
|
||||||
|
artifact.Ctl = suite.artifactCtl
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHandle ...
|
// TestHandle ...
|
||||||
@ -100,73 +130,6 @@ func (suite *ScanImagePreprocessHandlerSuite) TestHandle() {
|
|||||||
|
|
||||||
// Mock things
|
// 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 ...
|
// MockHTTPHandler ...
|
||||||
type MockHTTPHandler struct{}
|
type MockHTTPHandler struct{}
|
||||||
|
|
||||||
|
@ -126,17 +126,14 @@ func UpdateReportStatus(trackID string, status string, statusCode int, statusRev
|
|||||||
|
|
||||||
// qt generates sql statements:
|
// qt generates sql statements:
|
||||||
// UPDATE "scan_report" SET "end_time" = $1, "status" = $2, "status_code" = $3, "status_rev" = $4
|
// 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 )
|
// 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 )
|
// OR ( T0."status_rev" < $7 ) ) AND ( T0."track_id" = $8 )
|
||||||
cond := orm.NewCondition()
|
c1 := orm.NewCondition().And("status_rev", statusRev).And("status_code__lt", statusCode)
|
||||||
c1 := cond.And("status_rev", statusRev).And("status_code__lt", statusCode)
|
c2 := orm.NewCondition().And("status_rev__lt", statusRev)
|
||||||
c2 := cond.And("status_rev__lt", statusRev)
|
c3 := orm.NewCondition().And("track_id", trackID)
|
||||||
c := cond.AndCond(c1).OrCond(c2)
|
c := orm.NewCondition().AndCond(c1.OrCond(c2)).AndCond(c3)
|
||||||
|
|
||||||
count, err := qt.SetCond(c).
|
|
||||||
Filter("track_id", trackID).
|
|
||||||
Update(data)
|
|
||||||
|
|
||||||
|
count, err := qt.SetCond(c).Update(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/rest/auth"
|
"github.com/goharbor/harbor/src/pkg/scan/rest/auth"
|
||||||
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -57,6 +58,8 @@ type Registration struct {
|
|||||||
Vendor string `orm:"-" json:"vendor,omitempty"`
|
Vendor string `orm:"-" json:"vendor,omitempty"`
|
||||||
Version string `orm:"-" json:"version,omitempty"`
|
Version string `orm:"-" json:"version,omitempty"`
|
||||||
|
|
||||||
|
Metadata *v1.ScannerAdapterMetadata `orm:"-" json:"-"`
|
||||||
|
|
||||||
// Timestamps
|
// Timestamps
|
||||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
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
|
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
|
// Check the registration URL with url package
|
||||||
func checkURL(u string) error {
|
func checkURL(u string) error {
|
||||||
if len(strings.TrimSpace(u)) == 0 {
|
if len(strings.TrimSpace(u)) == 0 {
|
||||||
|
@ -126,7 +126,7 @@ func (j *Job) Run(ctx job.Context, params job.Parameters) error {
|
|||||||
myLogger.Infof("Report mime types: %v\n", mimes)
|
myLogger.Infof("Report mime types: %v\n", mimes)
|
||||||
|
|
||||||
// Submit scan request to the scanner adapter
|
// Submit scan request to the scanner adapter
|
||||||
client, err := v1.DefaultClientPool.Get(r)
|
client, err := r.Client(v1.DefaultClientPool)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return logAndWrapError(myLogger, err, "scan job: get client")
|
return logAndWrapError(myLogger, err, "scan job: get client")
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,8 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
"github.com/goharbor/harbor/src/pkg/scan/vuln"
|
||||||
|
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/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
@ -35,7 +37,7 @@ type JobTestSuite struct {
|
|||||||
suite.Suite
|
suite.Suite
|
||||||
|
|
||||||
defaultClientPool v1.ClientPool
|
defaultClientPool v1.ClientPool
|
||||||
mcp *MockClientPool
|
mcp *v1testing.ClientPool
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestJob is the entry of JobTestSuite.
|
// TestJob is the entry of JobTestSuite.
|
||||||
@ -45,7 +47,7 @@ func TestJob(t *testing.T) {
|
|||||||
|
|
||||||
// SetupSuite sets up test env for JobTestSuite.
|
// SetupSuite sets up test env for JobTestSuite.
|
||||||
func (suite *JobTestSuite) SetupSuite() {
|
func (suite *JobTestSuite) SetupSuite() {
|
||||||
mcp := &MockClientPool{}
|
mcp := &v1testing.ClientPool{}
|
||||||
suite.defaultClientPool = v1.DefaultClientPool
|
suite.defaultClientPool = v1.DefaultClientPool
|
||||||
v1.DefaultClientPool = mcp
|
v1.DefaultClientPool = mcp
|
||||||
|
|
||||||
@ -96,7 +98,7 @@ func (suite *JobTestSuite) TestJob() {
|
|||||||
jp[JobParameterRequest] = sData
|
jp[JobParameterRequest] = sData
|
||||||
jp[JobParameterMimes] = mimeTypes
|
jp[JobParameterMimes] = mimeTypes
|
||||||
|
|
||||||
mc := &MockClient{}
|
mc := &v1testing.Client{}
|
||||||
sre := &v1.ScanResponse{
|
sre := &v1.ScanResponse{
|
||||||
ID: "scan_id",
|
ID: "scan_id",
|
||||||
}
|
}
|
||||||
@ -127,7 +129,7 @@ func (suite *JobTestSuite) TestJob() {
|
|||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
|
|
||||||
mc.On("GetScanReport", "scan_id", v1.MimeTypeNativeReport).Return(string(jRep), nil)
|
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{
|
crp := &CheckInReport{
|
||||||
Digest: sr.Artifact.Digest,
|
Digest: sr.Artifact.Digest,
|
||||||
@ -255,52 +257,3 @@ func (mjl *MockJobLogger) Fatal(v ...interface{}) {
|
|||||||
func (mjl *MockJobLogger) Fatalf(format string, v ...interface{}) {
|
func (mjl *MockJobLogger) Fatalf(format string, v ...interface{}) {
|
||||||
logger.Fatalf(format, v...)
|
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)
|
|
||||||
}
|
|
||||||
|
@ -17,12 +17,11 @@ package report
|
|||||||
import (
|
import (
|
||||||
"time"
|
"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/jobservice/job"
|
||||||
"github.com/goharbor/harbor/src/pkg/q"
|
"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/dao/scan"
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/errs"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -81,7 +80,7 @@ func (bm *basicManager) Create(r *scan.Report) (string, error) {
|
|||||||
// Status conflict
|
// Status conflict
|
||||||
if theCopy.StartTime.Add(reportTimeout).After(time.Now()) {
|
if theCopy.StartTime.Add(reportTimeout).After(time.Now()) {
|
||||||
if theStatus.Compare(job.RunningStatus) <= 0 {
|
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
|
r.UUID = uuid.New().String()
|
||||||
UUID, err := uuid.NewUUID()
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrap(err, "create report: new UUID")
|
|
||||||
}
|
|
||||||
r.UUID = UUID.String()
|
|
||||||
|
|
||||||
// Fill in / override the related properties
|
// Fill in / override the related properties
|
||||||
r.StartTime = time.Now().UTC()
|
r.StartTime = time.Now().UTC()
|
||||||
r.Status = job.PendingStatus.String()
|
if r.Status == "" {
|
||||||
r.StatusCode = job.PendingStatus.Code()
|
r.Status = job.PendingStatus.String()
|
||||||
|
r.StatusCode = job.PendingStatus.Code()
|
||||||
|
}
|
||||||
|
|
||||||
// Insert
|
// Insert
|
||||||
if _, err = scan.CreateReport(r); err != nil {
|
if _, err = scan.CreateReport(r); err != nil {
|
||||||
|
@ -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.
|
// SupportedGenerators declares mappings between mime type and summary generator func.
|
||||||
var SupportedGenerators = map[string]SummaryGenerator{
|
var SupportedGenerators = map[string]SummaryGenerator{
|
||||||
v1.MimeTypeNativeReport: GenerateNativeSummary,
|
v1.MimeTypeNativeReport: GenerateNativeSummary,
|
||||||
@ -94,6 +127,8 @@ func GenerateNativeSummary(r *scan.Report, options ...Option) (interface{}, erro
|
|||||||
sum.ScanStatus = r.Status
|
sum.ScanStatus = r.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sum.TotalCount = 1
|
||||||
|
|
||||||
// If the status is not success/stopped, there will not be any report.
|
// If the status is not success/stopped, there will not be any report.
|
||||||
if r.Status != job.SuccessStatus.String() &&
|
if r.Status != job.SuccessStatus.String() &&
|
||||||
r.Status != job.StoppedStatus.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())
|
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
|
sum.Severity = rp.Severity
|
||||||
vsum := &vuln.VulnerabilitySummary{
|
vsum := &vuln.VulnerabilitySummary{
|
||||||
Total: len(rp.Vulnerabilities),
|
Total: len(rp.Vulnerabilities),
|
||||||
|
@ -26,7 +26,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
"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/goharbor/harbor/src/pkg/scan/rest/auth"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -81,7 +80,7 @@ type basicClient struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewClient news a basic client
|
// 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{
|
transport := &http.Transport{
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: http.ProxyFromEnvironment,
|
||||||
DialContext: (&net.Dialer{
|
DialContext: (&net.Dialer{
|
||||||
@ -93,11 +92,11 @@ func NewClient(r *scanner.Registration) (Client, error) {
|
|||||||
TLSHandshakeTimeout: 10 * time.Second,
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
TLSClientConfig: &tls.Config{
|
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 {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "new v1 client")
|
return nil, errors.Wrap(err, "new v1 client")
|
||||||
}
|
}
|
||||||
@ -109,7 +108,7 @@ func NewClient(r *scanner.Registration) (Client, error) {
|
|||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
spec: NewSpec(r.URL),
|
spec: NewSpec(url),
|
||||||
authorizer: authorizer,
|
authorizer: authorizer,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -44,7 +43,7 @@ type ClientPool interface {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// Client : v1 client
|
// Client : v1 client
|
||||||
// error : non nil error if any errors occurred
|
// 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.
|
// 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.
|
// add the following func after the first time initializing the client.
|
||||||
// pool item represents the client with a timestamp of last accessed.
|
// pool item represents the client with a timestamp of last accessed.
|
||||||
|
|
||||||
func (bcp *basicClientPool) Get(r *scanner.Registration) (Client, error) {
|
func (bcp *basicClientPool) Get(url, authType, accessCredential string, skipCertVerify bool) (Client, error) {
|
||||||
if r == nil {
|
k := fmt.Sprintf("%s:%s:%s:%v", url, authType, accessCredential, skipCertVerify)
|
||||||
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)
|
|
||||||
|
|
||||||
item, ok := bcp.pool.Load(k)
|
item, ok := bcp.pool.Load(k)
|
||||||
if !ok {
|
if !ok {
|
||||||
nc, err := NewClient(r)
|
nc, err := NewClient(url, authType, accessCredential, skipCertVerify)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "client pool: get")
|
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@ -19,7 +19,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/rest/auth"
|
"github.com/goharbor/harbor/src/pkg/scan/rest/auth"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -49,23 +48,13 @@ func (suite *ClientPoolTestSuite) SetupSuite() {
|
|||||||
|
|
||||||
// TestClientPoolGet tests the get method of client pool.
|
// TestClientPoolGet tests the get method of client pool.
|
||||||
func (suite *ClientPoolTestSuite) TestClientPoolGet() {
|
func (suite *ClientPoolTestSuite) TestClientPoolGet() {
|
||||||
r := &scanner.Registration{
|
client1, err := suite.pool.Get("http://a.b.c", auth.Basic, "u:p", false)
|
||||||
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)
|
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
require.NotNil(suite.T(), client1)
|
require.NotNil(suite.T(), client1)
|
||||||
|
|
||||||
p1 := fmt.Sprintf("%p", client1.(*basicClient))
|
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.NoError(suite.T(), err)
|
||||||
require.NotNil(suite.T(), client2)
|
require.NotNil(suite.T(), client2)
|
||||||
|
|
||||||
@ -73,7 +62,7 @@ func (suite *ClientPoolTestSuite) TestClientPoolGet() {
|
|||||||
assert.Equal(suite.T(), p1, p2)
|
assert.Equal(suite.T(), p1, p2)
|
||||||
|
|
||||||
<-time.After(400 * time.Millisecond)
|
<-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.NoError(suite.T(), err)
|
||||||
require.NotNil(suite.T(), client3)
|
require.NotNil(suite.T(), client3)
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
@ -44,15 +43,8 @@ func TestClient(t *testing.T) {
|
|||||||
// SetupSuite prepares the test suite env
|
// SetupSuite prepares the test suite env
|
||||||
func (suite *ClientTestSuite) SetupSuite() {
|
func (suite *ClientTestSuite) SetupSuite() {
|
||||||
suite.testServer = httptest.NewServer(&mockHandler{})
|
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.NoError(suite.T(), err)
|
||||||
require.NotNil(suite.T(), c)
|
require.NotNil(suite.T(), c)
|
||||||
|
|
||||||
|
@ -78,6 +78,19 @@ func (md *ScannerAdapterMetadata) HasCapability(mimeType string) bool {
|
|||||||
return false
|
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.
|
// Artifact represents an artifact stored in Registry.
|
||||||
type Artifact struct {
|
type Artifact struct {
|
||||||
// ID of the namespace (project). It will not be sent to scanner adapter.
|
// ID of the namespace (project). It will not be sent to scanner adapter.
|
||||||
|
@ -17,21 +17,76 @@ package vuln
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/jobservice/job"
|
||||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NativeReportSummary is the default supported scan report summary model.
|
// NativeReportSummary is the default supported scan report summary model.
|
||||||
// Generated based on the report with v1.MimeTypeNativeReport mime type.
|
// Generated based on the report with v1.MimeTypeNativeReport mime type.
|
||||||
type NativeReportSummary struct {
|
type NativeReportSummary struct {
|
||||||
ReportID string `json:"report_id"`
|
ReportID string `json:"report_id"`
|
||||||
ScanStatus string `json:"scan_status"`
|
ScanStatus string `json:"scan_status"`
|
||||||
Severity Severity `json:"severity"`
|
Severity Severity `json:"severity"`
|
||||||
Duration int64 `json:"duration"`
|
Duration int64 `json:"duration"`
|
||||||
Summary *VulnerabilitySummary `json:"summary"`
|
Summary *VulnerabilitySummary `json:"summary"`
|
||||||
CVEBypassed []string `json:"-"`
|
CVEBypassed []string `json:"-"`
|
||||||
StartTime time.Time `json:"start_time"`
|
StartTime time.Time `json:"start_time"`
|
||||||
EndTime time.Time `json:"end_time"`
|
EndTime time.Time `json:"end_time"`
|
||||||
Scanner *v1.Scanner `json:"scanner,omitempty"`
|
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
|
// VulnerabilitySummary contains the total number of the found vulnerabilities number
|
||||||
@ -42,5 +97,48 @@ type VulnerabilitySummary struct {
|
|||||||
Summary SeveritySummary `json:"summary"`
|
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 ...
|
// SeveritySummary ...
|
||||||
type SeveritySummary map[Severity]int
|
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
|
||||||
|
}
|
||||||
|
@ -75,6 +75,10 @@
|
|||||||
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
|
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
|
||||||
<span>{{completeTimestamp | date:'short'}}</span>
|
<span>{{completeTimestamp | date:'short'}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_PERCENT' | translate}} </span>
|
||||||
|
<span>{{completePercent}}</span>
|
||||||
|
</div>
|
||||||
</clr-tooltip-content>
|
</clr-tooltip-content>
|
||||||
</clr-tooltip>
|
</clr-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -149,7 +149,9 @@ export class ResultTipHistogramComponent implements OnInit {
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
public get completePercent(): string {
|
||||||
|
return this.vulnerabilitySummary ? `${this.vulnerabilitySummary.complete_percent}%` : '0%'
|
||||||
|
}
|
||||||
get completeTimestamp(): Date {
|
get completeTimestamp(): Date {
|
||||||
return this.vulnerabilitySummary && this.vulnerabilitySummary.end_time ? this.vulnerabilitySummary.end_time : new Date();
|
return this.vulnerabilitySummary && this.vulnerabilitySummary.end_time ? this.vulnerabilitySummary.end_time : new Date();
|
||||||
}
|
}
|
||||||
|
@ -987,6 +987,7 @@
|
|||||||
},
|
},
|
||||||
"CHART": {
|
"CHART": {
|
||||||
"SCANNING_TIME": "Scan completed time:",
|
"SCANNING_TIME": "Scan completed time:",
|
||||||
|
"SCANNING_PERCENT": "Scan completed percent:",
|
||||||
"TOOLTIPS_TITLE": "{{totalVulnerability}} of {{totalPackages}} {{package}} have known {{vulnerability}}.",
|
"TOOLTIPS_TITLE": "{{totalVulnerability}} of {{totalPackages}} {{package}} have known {{vulnerability}}.",
|
||||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} of {{totalPackages}} {{package}} has known {{vulnerability}}.",
|
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} of {{totalPackages}} {{package}} has known {{vulnerability}}.",
|
||||||
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
|
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
|
||||||
@ -1028,7 +1029,7 @@
|
|||||||
"UNTAGGED": "Untagged",
|
"UNTAGGED": "Untagged",
|
||||||
"ALL": "All",
|
"ALL": "All",
|
||||||
"PLACEHOLDER": "We couldn't find any artifactds!",
|
"PLACEHOLDER": "We couldn't find any artifactds!",
|
||||||
"SCAN_UNSUPPORTED": "Scan Unsupported"
|
"SCAN_UNSUPPORTED": "Unsupported"
|
||||||
},
|
},
|
||||||
"TAG": {
|
"TAG": {
|
||||||
"CREATION_TIME_PREFIX": "Create on",
|
"CREATION_TIME_PREFIX": "Create on",
|
||||||
|
@ -986,6 +986,7 @@
|
|||||||
},
|
},
|
||||||
"CHART": {
|
"CHART": {
|
||||||
"SCANNING_TIME": "Scan completed time:",
|
"SCANNING_TIME": "Scan completed time:",
|
||||||
|
"SCANNING_PERCENT": "Scan completed percent:",
|
||||||
"TOOLTIPS_TITLE": "{{totalVulnerability}} of {{totalPackages}} {{package}} have known {{vulnerability}}.",
|
"TOOLTIPS_TITLE": "{{totalVulnerability}} of {{totalPackages}} {{package}} have known {{vulnerability}}.",
|
||||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} of {{totalPackages}} {{package}} has known {{vulnerability}}.",
|
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} of {{totalPackages}} {{package}} has known {{vulnerability}}.",
|
||||||
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
|
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
|
||||||
@ -1027,7 +1028,7 @@
|
|||||||
"UNTAGGED": "Untagged",
|
"UNTAGGED": "Untagged",
|
||||||
"ALL": "All",
|
"ALL": "All",
|
||||||
"PLACEHOLDER": "We couldn't find any artifacts!",
|
"PLACEHOLDER": "We couldn't find any artifacts!",
|
||||||
"SCAN_UNSUPPORTED": "Scan Unsupported"
|
"SCAN_UNSUPPORTED": "Unsupported"
|
||||||
},
|
},
|
||||||
"TAG": {
|
"TAG": {
|
||||||
"CREATION_TIME_PREFIX": "Create on",
|
"CREATION_TIME_PREFIX": "Create on",
|
||||||
|
@ -959,6 +959,7 @@
|
|||||||
},
|
},
|
||||||
"CHART": {
|
"CHART": {
|
||||||
"SCANNING_TIME": "Temps d'analyse complète :",
|
"SCANNING_TIME": "Temps d'analyse complète :",
|
||||||
|
"SCANNING_PERCENT": "Scan completed percent:",
|
||||||
"TOOLTIPS_TITLE": "{{totalVulnerability}} de {{totalPackages}} {{package}} ont des {{vulnerability}} connues.",
|
"TOOLTIPS_TITLE": "{{totalVulnerability}} de {{totalPackages}} {{package}} ont des {{vulnerability}} connues.",
|
||||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} de {{totalPackages}} {{package}} a des {{vulnerability}} connues.",
|
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} de {{totalPackages}} {{package}} a des {{vulnerability}} connues.",
|
||||||
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
|
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
|
||||||
@ -1000,7 +1001,7 @@
|
|||||||
"UNTAGGED": "Untagged",
|
"UNTAGGED": "Untagged",
|
||||||
"ALL": "All",
|
"ALL": "All",
|
||||||
"PLACEHOLDER": "We couldn't find any artifacts!",
|
"PLACEHOLDER": "We couldn't find any artifacts!",
|
||||||
"SCAN_UNSUPPORTED": "Scan Unsupported"
|
"SCAN_UNSUPPORTED": "Unsupported"
|
||||||
},
|
},
|
||||||
"TAG": {
|
"TAG": {
|
||||||
"CREATION_TIME_PREFIX": "Créer le",
|
"CREATION_TIME_PREFIX": "Créer le",
|
||||||
|
@ -982,6 +982,7 @@
|
|||||||
},
|
},
|
||||||
"CHART": {
|
"CHART": {
|
||||||
"SCANNING_TIME": "Tempo de conclusão da análise:",
|
"SCANNING_TIME": "Tempo de conclusão da análise:",
|
||||||
|
"SCANNING_PERCENT": "Scan completed percent:",
|
||||||
"TOOLTIPS_TITLE": "{{totalVulnerability}} de {{totalPackages}} {{package}} possuem {{vulnerability}}.",
|
"TOOLTIPS_TITLE": "{{totalVulnerability}} de {{totalPackages}} {{package}} possuem {{vulnerability}}.",
|
||||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} de {{totalPackages}} {{package}} possuem {{vulnerability}}.",
|
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} de {{totalPackages}} {{package}} possuem {{vulnerability}}.",
|
||||||
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
|
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
|
||||||
@ -1023,7 +1024,7 @@
|
|||||||
"UNTAGGED": "Untagged",
|
"UNTAGGED": "Untagged",
|
||||||
"ALL": "All",
|
"ALL": "All",
|
||||||
"PLACEHOLDER": "We couldn't find any artifacts!",
|
"PLACEHOLDER": "We couldn't find any artifacts!",
|
||||||
"SCAN_UNSUPPORTED": "Scan Unsupported"
|
"SCAN_UNSUPPORTED": "Unsupported"
|
||||||
},
|
},
|
||||||
"TAG": {
|
"TAG": {
|
||||||
"CREATION_TIME_PREFIX": "Criado em",
|
"CREATION_TIME_PREFIX": "Criado em",
|
||||||
|
@ -986,6 +986,7 @@
|
|||||||
},
|
},
|
||||||
"CHART": {
|
"CHART": {
|
||||||
"SCANNING_TIME": "Tarama tamamlanma zamanı:",
|
"SCANNING_TIME": "Tarama tamamlanma zamanı:",
|
||||||
|
"SCANNING_PERCENT": "Scan completed percent:",
|
||||||
"TOOLTIPS_TITLE": "{{totalVulnerability}} 'nın {{totalPackages}} {{package}} bilinen {{vulnerability}}.",
|
"TOOLTIPS_TITLE": "{{totalVulnerability}} 'nın {{totalPackages}} {{package}} bilinen {{vulnerability}}.",
|
||||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} 'nın {{totalPackages}} {{package}} bilinen {{vulnerability}}.",
|
"TOOLTIPS_TITLE_SINGULAR": "{{totalVulnerability}} 'nın {{totalPackages}} {{package}} bilinen {{vulnerability}}.",
|
||||||
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
|
"TOOLTIPS_TITLE_ZERO": "No recognizable vulnerability found"
|
||||||
@ -1027,7 +1028,7 @@
|
|||||||
"UNTAGGED": "Untagged",
|
"UNTAGGED": "Untagged",
|
||||||
"ALL": "All",
|
"ALL": "All",
|
||||||
"PLACEHOLDER": "We couldn't find any artifacts!",
|
"PLACEHOLDER": "We couldn't find any artifacts!",
|
||||||
"SCAN_UNSUPPORTED": "Scan Unsupported"
|
"SCAN_UNSUPPORTED": "Unsupported"
|
||||||
},
|
},
|
||||||
"TAG": {
|
"TAG": {
|
||||||
"CREATION_TIME_PREFIX": "Oluştur",
|
"CREATION_TIME_PREFIX": "Oluştur",
|
||||||
|
@ -986,6 +986,7 @@
|
|||||||
},
|
},
|
||||||
"CHART": {
|
"CHART": {
|
||||||
"SCANNING_TIME": "扫描完成时间:",
|
"SCANNING_TIME": "扫描完成时间:",
|
||||||
|
"SCANNING_PERCENT": "扫描完成度:",
|
||||||
"TOOLTIPS_TITLE": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}。",
|
"TOOLTIPS_TITLE": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}。",
|
||||||
"TOOLTIPS_TITLE_SINGULAR": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}。",
|
"TOOLTIPS_TITLE_SINGULAR": "{{totalPackages}}个{{package}}中的{{totalVulnerability}}个含有{{vulnerability}}。",
|
||||||
"TOOLTIPS_TITLE_ZERO": "没有发现可识别的漏洞"
|
"TOOLTIPS_TITLE_ZERO": "没有发现可识别的漏洞"
|
||||||
|
@ -305,6 +305,7 @@ export interface VulnerabilitySummary {
|
|||||||
start_time?: Date;
|
start_time?: Date;
|
||||||
end_time?: Date;
|
end_time?: Date;
|
||||||
scanner?: ScannerVo;
|
scanner?: ScannerVo;
|
||||||
|
complete_percent?: number;
|
||||||
}
|
}
|
||||||
export interface ScannerVo {
|
export interface ScannerVo {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/api/artifact"
|
||||||
sc "github.com/goharbor/harbor/src/api/scan"
|
sc "github.com/goharbor/harbor/src/api/scan"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
@ -40,21 +41,15 @@ func Middleware() func(http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the vulnerability summary
|
ctx := req.Context()
|
||||||
artifact := &v1.Artifact{
|
art, err := artifact.Ctl.GetByReference(ctx, img.Repository, img.Digest, nil)
|
||||||
NamespaceID: wl.ProjectID,
|
if err != nil {
|
||||||
Repository: img.Repository,
|
// TODO: error handle
|
||||||
Tag: img.Tag,
|
return
|
||||||
Digest: img.Digest,
|
|
||||||
MimeType: v1.MimeTypeDockerArtifact,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cve := report.CVESet(wl.CVESet())
|
cve := report.CVESet(wl.CVESet())
|
||||||
summaries, err := sc.DefaultController.GetSummary(
|
summaries, err := sc.DefaultController.GetSummary(ctx, art, []string{v1.MimeTypeNativeReport}, report.WithCVEWhitelist(&cve))
|
||||||
artifact,
|
|
||||||
[]string{v1.MimeTypeNativeReport},
|
|
||||||
report.WithCVEWhitelist(&cve),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.Wrap(err, "middleware: vulnerable handler")
|
err = errors.Wrap(err, "middleware: vulnerable handler")
|
||||||
|
@ -34,7 +34,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/utils"
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||||
evt "github.com/goharbor/harbor/src/pkg/notifier/event"
|
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/assembler"
|
||||||
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
|
"github.com/goharbor/harbor/src/server/v2.0/handler/model"
|
||||||
"github.com/goharbor/harbor/src/server/v2.0/models"
|
"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)
|
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
|
// parse "repository:tag" or "repository@digest" into repository and reference parts
|
||||||
func parse(s string) (string, string, error) {
|
func parse(s string) (string, string, error) {
|
||||||
matches := reference.ReferenceRegexp.FindStringSubmatch(s)
|
matches := reference.ReferenceRegexp.FindStringSubmatch(s)
|
||||||
|
@ -72,16 +72,9 @@ func (assembler *VulAssembler) Assemble(ctx context.Context) error {
|
|||||||
artifact.SetAdditionLink(vulnerabilitiesAddition, version)
|
artifact.SetAdditionLink(vulnerabilitiesAddition, version)
|
||||||
|
|
||||||
if assembler.withScanOverview {
|
if assembler.withScanOverview {
|
||||||
art := &v1.Artifact{
|
overview, err := assembler.scanCtl.GetSummary(ctx, &artifact.Artifact, []string{v1.MimeTypeNativeReport})
|
||||||
NamespaceID: artifact.ProjectID,
|
|
||||||
Repository: artifact.RepositoryName,
|
|
||||||
Digest: artifact.Digest,
|
|
||||||
MimeType: artifact.ManifestMediaType,
|
|
||||||
}
|
|
||||||
|
|
||||||
overview, err := assembler.scanCtl.GetSummary(art, []string{v1.MimeTypeNativeReport})
|
|
||||||
if err != nil {
|
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 {
|
} else if len(overview) > 0 {
|
||||||
artifact.ScanOverview = overview
|
artifact.ScanOverview = overview
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ func New() http.Handler {
|
|||||||
ArtifactAPI: newArtifactAPI(),
|
ArtifactAPI: newArtifactAPI(),
|
||||||
RepositoryAPI: newRepositoryAPI(),
|
RepositoryAPI: newRepositoryAPI(),
|
||||||
AuditlogAPI: newAuditLogAPI(),
|
AuditlogAPI: newAuditLogAPI(),
|
||||||
|
ScanAPI: newScanAPI(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -15,8 +15,11 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
"github.com/goharbor/harbor/src/api/artifact"
|
"github.com/goharbor/harbor/src/api/artifact"
|
||||||
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/server/v2.0/models"
|
"github.com/goharbor/harbor/src/server/v2.0/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -57,5 +60,22 @@ func (a *Artifact) ToSwagger() *models.Artifact {
|
|||||||
for _, label := range a.Labels {
|
for _, label := range a.Labels {
|
||||||
art.Labels = append(art.Labels, label.ToSwagger())
|
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
|
return art
|
||||||
}
|
}
|
||||||
|
90
src/server/v2.0/handler/scan.go
Normal file
90
src/server/v2.0/handler/scan.go
Normal 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))
|
||||||
|
}
|
@ -38,14 +38,7 @@ func boolValue(v *bool) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resolveVulnerabilitiesAddition(ctx context.Context, artifact *artifact.Artifact) (*processor.Addition, error) {
|
func resolveVulnerabilitiesAddition(ctx context.Context, artifact *artifact.Artifact) (*processor.Addition, error) {
|
||||||
art := &v1.Artifact{
|
reports, err := scan.DefaultController.GetReport(ctx, artifact, []string{v1.MimeTypeNativeReport})
|
||||||
NamespaceID: artifact.ProjectID,
|
|
||||||
Repository: artifact.RepositoryName,
|
|
||||||
Digest: artifact.Digest,
|
|
||||||
MimeType: artifact.ManifestMediaType,
|
|
||||||
}
|
|
||||||
|
|
||||||
reports, err := scan.DefaultController.GetReport(art, []string{v1.MimeTypeNativeReport})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,11 @@
|
|||||||
package scan
|
package scan
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
artifact "github.com/goharbor/harbor/src/api/artifact"
|
||||||
all "github.com/goharbor/harbor/src/pkg/scan/all"
|
all "github.com/goharbor/harbor/src/pkg/scan/all"
|
||||||
|
|
||||||
|
context "context"
|
||||||
|
|
||||||
daoscan "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
daoscan "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
|
||||||
|
|
||||||
job "github.com/goharbor/harbor/src/jobservice/job"
|
job "github.com/goharbor/harbor/src/jobservice/job"
|
||||||
@ -13,8 +17,6 @@ import (
|
|||||||
report "github.com/goharbor/harbor/src/pkg/scan/report"
|
report "github.com/goharbor/harbor/src/pkg/scan/report"
|
||||||
|
|
||||||
scan "github.com/goharbor/harbor/src/api/scan"
|
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
|
// Controller is an autogenerated mock type for the Controller type
|
||||||
@ -42,13 +44,13 @@ func (_m *Controller) DeleteReports(digests ...string) error {
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetReport provides a mock function with given fields: artifact, mimeTypes
|
// GetReport provides a mock function with given fields: ctx, _a1, mimeTypes
|
||||||
func (_m *Controller) GetReport(artifact *v1.Artifact, mimeTypes []string) ([]*daoscan.Report, error) {
|
func (_m *Controller) GetReport(ctx context.Context, _a1 *artifact.Artifact, mimeTypes []string) ([]*daoscan.Report, error) {
|
||||||
ret := _m.Called(artifact, mimeTypes)
|
ret := _m.Called(ctx, _a1, mimeTypes)
|
||||||
|
|
||||||
var r0 []*daoscan.Report
|
var r0 []*daoscan.Report
|
||||||
if rf, ok := ret.Get(0).(func(*v1.Artifact, []string) []*daoscan.Report); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string) []*daoscan.Report); ok {
|
||||||
r0 = rf(artifact, mimeTypes)
|
r0 = rf(ctx, _a1, mimeTypes)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
if ret.Get(0) != nil {
|
||||||
r0 = ret.Get(0).([]*daoscan.Report)
|
r0 = ret.Get(0).([]*daoscan.Report)
|
||||||
@ -56,8 +58,8 @@ func (_m *Controller) GetReport(artifact *v1.Artifact, mimeTypes []string) ([]*d
|
|||||||
}
|
}
|
||||||
|
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(1).(func(*v1.Artifact, []string) error); ok {
|
if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, []string) error); ok {
|
||||||
r1 = rf(artifact, mimeTypes)
|
r1 = rf(ctx, _a1, mimeTypes)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
r1 = ret.Error(1)
|
||||||
}
|
}
|
||||||
@ -111,20 +113,20 @@ func (_m *Controller) GetStats(requester string) (*all.Stats, error) {
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSummary provides a mock function with given fields: artifact, mimeTypes, options
|
// GetSummary provides a mock function with given fields: ctx, _a1, mimeTypes, options
|
||||||
func (_m *Controller) GetSummary(artifact *v1.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error) {
|
func (_m *Controller) GetSummary(ctx context.Context, _a1 *artifact.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error) {
|
||||||
_va := make([]interface{}, len(options))
|
_va := make([]interface{}, len(options))
|
||||||
for _i := range options {
|
for _i := range options {
|
||||||
_va[_i] = options[_i]
|
_va[_i] = options[_i]
|
||||||
}
|
}
|
||||||
var _ca []interface{}
|
var _ca []interface{}
|
||||||
_ca = append(_ca, artifact, mimeTypes)
|
_ca = append(_ca, ctx, _a1, mimeTypes)
|
||||||
_ca = append(_ca, _va...)
|
_ca = append(_ca, _va...)
|
||||||
ret := _m.Called(_ca...)
|
ret := _m.Called(_ca...)
|
||||||
|
|
||||||
var r0 map[string]interface{}
|
var r0 map[string]interface{}
|
||||||
if rf, ok := ret.Get(0).(func(*v1.Artifact, []string, ...report.Option) map[string]interface{}); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string, ...report.Option) map[string]interface{}); ok {
|
||||||
r0 = rf(artifact, mimeTypes, options...)
|
r0 = rf(ctx, _a1, mimeTypes, options...)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
if ret.Get(0) != nil {
|
||||||
r0 = ret.Get(0).(map[string]interface{})
|
r0 = ret.Get(0).(map[string]interface{})
|
||||||
@ -132,8 +134,8 @@ func (_m *Controller) GetSummary(artifact *v1.Artifact, mimeTypes []string, opti
|
|||||||
}
|
}
|
||||||
|
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(1).(func(*v1.Artifact, []string, ...report.Option) error); ok {
|
if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, []string, ...report.Option) error); ok {
|
||||||
r1 = rf(artifact, mimeTypes, options...)
|
r1 = rf(ctx, _a1, mimeTypes, options...)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
r1 = ret.Error(1)
|
||||||
}
|
}
|
||||||
@ -155,20 +157,20 @@ func (_m *Controller) HandleJobHooks(trackID string, change *job.StatusChange) e
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan provides a mock function with given fields: artifact, options
|
// Scan provides a mock function with given fields: ctx, _a1, options
|
||||||
func (_m *Controller) Scan(artifact *v1.Artifact, options ...scan.Option) error {
|
func (_m *Controller) Scan(ctx context.Context, _a1 *artifact.Artifact, options ...scan.Option) error {
|
||||||
_va := make([]interface{}, len(options))
|
_va := make([]interface{}, len(options))
|
||||||
for _i := range options {
|
for _i := range options {
|
||||||
_va[_i] = options[_i]
|
_va[_i] = options[_i]
|
||||||
}
|
}
|
||||||
var _ca []interface{}
|
var _ca []interface{}
|
||||||
_ca = append(_ca, artifact)
|
_ca = append(_ca, ctx, _a1)
|
||||||
_ca = append(_ca, _va...)
|
_ca = append(_ca, _va...)
|
||||||
ret := _m.Called(_ca...)
|
ret := _m.Called(_ca...)
|
||||||
|
|
||||||
var r0 error
|
var r0 error
|
||||||
if rf, ok := ret.Get(0).(func(*v1.Artifact, ...scan.Option) error); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, ...scan.Option) error); ok {
|
||||||
r0 = rf(artifact, options...)
|
r0 = rf(ctx, _a1, options...)
|
||||||
} else {
|
} else {
|
||||||
r0 = ret.Error(0)
|
r0 = ret.Error(0)
|
||||||
}
|
}
|
||||||
|
@ -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/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 -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/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
|
||||||
|
167
src/testing/pkg/scan/report/manager.go
Normal file
167
src/testing/pkg/scan/report/manager.go
Normal 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
|
||||||
|
}
|
80
src/testing/pkg/scan/rest/v1/client.go
Normal file
80
src/testing/pkg/scan/rest/v1/client.go
Normal 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
|
||||||
|
}
|
36
src/testing/pkg/scan/rest/v1/client_pool.go
Normal file
36
src/testing/pkg/scan/rest/v1/client_pool.go
Normal 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
|
||||||
|
}
|
147
src/testing/pkg/scan/scanner/manager.go
Normal file
147
src/testing/pkg/scan/scanner/manager.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user