refactor: remove allowlist in GetSummary of scan controller (#14836)

Signed-off-by: He Weiwei <hweiwei@vmware.com>
This commit is contained in:
He Weiwei 2021-05-18 14:01:59 +08:00 committed by GitHub
parent 1a3335edc5
commit 0c315d8aee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 372 additions and 347 deletions

View File

@ -17,8 +17,6 @@ package preheat
import (
"context"
"fmt"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/orm"
"strings"
tk "github.com/docker/distribution/registry/auth/token"
@ -29,8 +27,10 @@ import (
"github.com/goharbor/harbor/src/controller/tag"
"github.com/goharbor/harbor/src/core/service/token"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/lib/selector"
"github.com/goharbor/harbor/src/pkg/label/model"
@ -40,8 +40,6 @@ import (
"github.com/goharbor/harbor/src/pkg/p2p/preheat/models/provider"
"github.com/goharbor/harbor/src/pkg/p2p/preheat/policy"
pr "github.com/goharbor/harbor/src/pkg/p2p/preheat/provider"
"github.com/goharbor/harbor/src/pkg/scan/report"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
"github.com/goharbor/harbor/src/pkg/task"
)
@ -483,8 +481,7 @@ func (de *defaultEnforcer) startTask(ctx context.Context, executionID int64, can
// getVulnerabilitySev gets the severity code value for the given artifact with allowlist option set
func (de *defaultEnforcer) getVulnerabilitySev(ctx context.Context, p *models.Project, art *artifact.Artifact) (uint, error) {
al := p.CVEAllowlist.CVESet()
r, err := de.scanCtl.GetSummary(ctx, art, []string{v1.MimeTypeNativeReport, v1.MimeTypeGenericVulnerabilityReport}, report.WithCVEAllowlist(&al))
vulnerable, err := de.scanCtl.GetVulnerable(ctx, art, p.CVEAllowlist.CVESet())
if err != nil {
if errors.IsNotFoundErr(err) {
// no vulnerability report
@ -494,25 +491,17 @@ func (de *defaultEnforcer) getVulnerabilitySev(ctx context.Context, p *models.Pr
return defaultSeverityCode, errors.Wrap(err, "get vulnerability severity")
}
// Severity is based on the native report format or the generic vulnerability report format.
// In case no supported report format, treat as same to the no report scenario
sum, ok := r[v1.MimeTypeNativeReport]
if !ok {
// check if a report with MimeTypeGenericVulnerabilityReport is present.
// return the default severity code only if it does not exist
sum, ok = r[v1.MimeTypeGenericVulnerabilityReport]
if !ok {
return defaultSeverityCode, nil
}
if !vulnerable.IsScanSuccess() {
// scan status may running or error
return defaultSeverityCode, nil
}
sm, ok := sum.(*vuln.NativeReportSummary)
if !ok {
return defaultSeverityCode, errors.New("malformed native summary report")
// no vulnerability found
if vulnerable.Severity == nil {
return (uint)(vuln.None.Code()), nil
}
return (uint)(sm.Severity.Code()), nil
return (uint)(vulnerable.Severity.Code()), nil
}
// toCandidates converts the artifacts to filtering candidates

View File

@ -24,6 +24,7 @@ import (
"github.com/goharbor/harbor/src/common/models"
car "github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/controller/scan"
"github.com/goharbor/harbor/src/controller/tag"
"github.com/goharbor/harbor/src/lib/selector"
models2 "github.com/goharbor/harbor/src/pkg/allowlist/models"
@ -33,12 +34,11 @@ import (
pr "github.com/goharbor/harbor/src/pkg/p2p/preheat/models/provider"
"github.com/goharbor/harbor/src/pkg/p2p/preheat/provider"
"github.com/goharbor/harbor/src/pkg/p2p/preheat/provider/auth"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
ta "github.com/goharbor/harbor/src/pkg/tag/model/tag"
"github.com/goharbor/harbor/src/testing/controller/artifact"
"github.com/goharbor/harbor/src/testing/controller/project"
"github.com/goharbor/harbor/src/testing/controller/scan"
scantesting "github.com/goharbor/harbor/src/testing/controller/scan"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/goharbor/harbor/src/testing/pkg/p2p/preheat/instance"
"github.com/goharbor/harbor/src/testing/pkg/p2p/preheat/policy"
@ -104,13 +104,13 @@ func (suite *EnforcerTestSuite) SetupSuite() {
mock.AnythingOfType("*artifact.Option"),
).Return(mockArtifacts(), nil)
fakeScanCtl := &scan.Controller{}
fakeScanCtl.On("GetSummary",
low := vuln.Low
fakeScanCtl := &scantesting.Controller{}
fakeScanCtl.On("GetVulnerable",
context.TODO(),
mock.AnythingOfType("*artifact.Artifact"),
[]string{v1.MimeTypeNativeReport, v1.MimeTypeGenericVulnerabilityReport},
mock.AnythingOfType("report.Option"),
).Return(mockVulnerabilitySummary(), nil)
mock.AnythingOfType("models.CVESet"),
).Return(&scan.Vulnerable{Severity: &low, ScanStatus: "Success"}, nil)
fakeProCtl := &project.Controller{}
fakeProCtl.On("Get",
@ -304,16 +304,3 @@ func mockArtifacts() []*car.Artifact {
},
}
}
// mock vulnerability summary
func mockVulnerabilitySummary() map[string]interface{} {
// skip all unused properties
return map[string]interface{}{
v1.MimeTypeNativeReport: &vuln.NativeReportSummary{
Severity: vuln.Low,
},
v1.MimeTypeGenericVulnerabilityReport: &vuln.NativeReportSummary{
Severity: vuln.Low,
},
}
}

View File

@ -18,7 +18,7 @@ import (
"bytes"
"context"
"fmt"
"github.com/goharbor/harbor/src/lib/config"
"reflect"
"strings"
"sync"
@ -28,10 +28,12 @@ import (
sc "github.com/goharbor/harbor/src/controller/scanner"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/config"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
allowlist "github.com/goharbor/harbor/src/pkg/allowlist/models"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/robot/model"
sca "github.com/goharbor/harbor/src/pkg/scan"
@ -533,7 +535,7 @@ func (bc *basicController) GetReport(ctx context.Context, artifact *ar.Artifact,
}
// GetSummary ...
func (bc *basicController) GetSummary(ctx context.Context, artifact *ar.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error) {
func (bc *basicController) GetSummary(ctx context.Context, artifact *ar.Artifact, mimeTypes []string) (map[string]interface{}, error) {
if artifact == nil {
return nil, errors.New("no way to get report summaries for nil artifact")
}
@ -546,7 +548,7 @@ func (bc *basicController) GetSummary(ctx context.Context, artifact *ar.Artifact
summaries := make(map[string]interface{}, len(rps))
for _, rp := range rps {
sum, err := report.GenerateSummary(rp, options...)
sum, err := report.GenerateSummary(rp)
if err != nil {
return nil, err
}
@ -699,6 +701,85 @@ func (bc *basicController) DeleteReports(ctx context.Context, digests ...string)
return nil
}
func (bc *basicController) GetVulnerable(ctx context.Context, artifact *ar.Artifact, allowlist allowlist.CVESet) (*Vulnerable, error) {
if artifact == nil {
return nil, errors.New("no way to get vulnerable for nil artifact")
}
var (
mimeType string
reports []*scan.Report
)
for _, m := range []string{v1.MimeTypeNativeReport, v1.MimeTypeGenericVulnerabilityReport} {
rps, err := bc.GetReport(ctx, artifact, []string{m})
if err != nil {
return nil, err
}
if len(rps) == 0 {
continue
}
mimeType = m
reports = rps
break
}
if len(reports) == 0 {
return nil, errors.NotFoundError(nil).WithMessage("report not found")
}
scanStatus := reports[0].Status
for _, report := range reports {
scanStatus = vuln.MergeScanStatus(scanStatus, report.Status)
}
vulnerable := &Vulnerable{
ScanStatus: scanStatus,
}
if !vulnerable.IsScanSuccess() {
return vulnerable, nil
}
raw, err := report.Reports(reports).ResolveData(mimeType)
if err != nil {
return nil, err
}
rp, ok := raw.(*vuln.Report)
if !ok {
return nil, errors.Errorf("type mismatch: expect *vuln.Report but got %s", reflect.TypeOf(raw).String())
}
if vuls := rp.GetVulnerabilityItemList().Items(); len(vuls) > 0 {
vulnerable.VulnerabilitiesCount = len(vuls)
var severity vuln.Severity
for _, v := range vuls {
if allowlist.Contains(v.ID) {
// Append the by passed CVEs specified in the allowlist
vulnerable.CVEBypassed = append(vulnerable.CVEBypassed, v.ID)
vulnerable.VulnerabilitiesCount--
continue
}
if severity == "" || v.Severity.Code() > severity.Code() {
severity = v.Severity
}
}
if severity != "" {
vulnerable.Severity = &severity
}
}
return vulnerable, nil
}
// makeRobotAccount creates a robot account based on the arguments for scanning.
func (bc *basicController) makeRobotAccount(ctx context.Context, projectID int64, repository string, registration *scanner.Registration) (*robot.Robot, error) {
// Use uuid as name to avoid duplicated entries.

View File

@ -18,14 +18,27 @@ import (
"context"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/jobservice/job"
allowlist "github.com/goharbor/harbor/src/pkg/allowlist/models"
sca "github.com/goharbor/harbor/src/pkg/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/vuln"
)
// Vulnerable ...
type Vulnerable struct {
VulnerabilitiesCount int
ScanStatus string
Severity *vuln.Severity
CVEBypassed []string
}
// IsScanSuccess returns true when the artifact scanned success
func (v *Vulnerable) IsScanSuccess() bool {
return v.ScanStatus == job.SuccessStatus.String()
}
// Controller provides the related operations for triggering scan.
// TODO: Here the artifact object is reused the v1 one which is sent to the adapter,
// it should be pointed to the general artifact object in future once it's ready.
type Controller interface {
// Scan the given artifact
//
@ -56,12 +69,11 @@ type Controller interface {
// ctx context.Context : the context for this method
// artifact *artifact.Artifact : the scanned artifact
// mimeTypes []string : the mime types of the reports
// options ...report.Option : optional report options, specify if needed
//
// Returns:
// map[string]interface{} : report summaries indexed by mime types
// error : non nil error if any errors occurred
GetSummary(ctx context.Context, artifact *artifact.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error)
GetSummary(ctx context.Context, artifact *artifact.Artifact, mimeTypes []string) (map[string]interface{}, error)
// Get the scan log for the specified artifact with the given digest
//
@ -103,4 +115,15 @@ type Controller interface {
// Returns:
// error : non nil error if any errors occurred
ScanAll(ctx context.Context, trigger string, async bool) (int64, error)
// GetVulnerable returns the vulnerable of the artifact for the allowlist
//
// Arguments:
// ctx context.Context : the context for this method
// artifact *artifact.Artifact : artifact to be scanned
//
// Returns
// *Vulnerable : the vulnerable
// error : non nil error if any errors occurred
GetVulnerable(ctx context.Context, artifact *artifact.Artifact, allowlist allowlist.CVESet) (*Vulnerable, error)
}

View File

@ -16,6 +16,7 @@ package report
import (
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
)
@ -53,3 +54,36 @@ func MergeNativeReport(r1, r2 interface{}) (interface{}, error) {
return nr1.Merge(nr2), nil
}
// Reports slice of scan.Reports pointer
type Reports []*scan.Report
// ResolveData resolve the data from the reports and merge them together
func (l Reports) ResolveData(mimeType string) (interface{}, error) {
var result interface{}
for _, rp := range l {
// Resolve scan report data only when it is ready and its mime type equal the given one
if len(rp.Report) == 0 || rp.MimeType != mimeType {
continue
}
vrp, err := ResolveData(rp.MimeType, []byte(rp.Report), WithArtifactDigest(rp.Digest))
if err != nil {
return nil, err
}
if result == nil {
result = vrp
} else {
r, err := Merge(rp.MimeType, result, vrp)
if err != nil {
return nil, err
}
result = r
}
}
return result, nil
}

View File

@ -19,7 +19,6 @@ import (
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/lib/errors"
models2 "github.com/goharbor/harbor/src/pkg/allowlist/models"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
@ -29,20 +28,11 @@ import (
type Options struct {
// If it is set, the returned report will contains artifact digest for the vulnerabilities
ArtifactDigest string
// If it is set, the returned summary will not count the CVEs in the list in.
CVEAllowlist models2.CVESet
}
// Option for getting the report w/ summary with func template way.
type Option func(options *Options)
// WithCVEAllowlist is an option of setting CVE allowlist.
func WithCVEAllowlist(set *models2.CVESet) Option {
return func(options *Options) {
options.CVEAllowlist = *set
}
}
// WithArtifactDigest is an option of setting artifact digest
func WithArtifactDigest(artifactDigest string) Option {
return func(options *Options) {
@ -107,11 +97,6 @@ type SummaryGenerator func(r *scan.Report, options ...Option) (interface{}, erro
// GenerateNativeSummary generates the report summary for the native report.
func GenerateNativeSummary(r *scan.Report, options ...Option) (interface{}, error) {
ops := &Options{}
for _, op := range options {
op(ops)
}
sum := &vuln.NativeReportSummary{}
sum.ReportID = r.UUID
sum.StartTime = r.StartTime
@ -120,9 +105,6 @@ func GenerateNativeSummary(r *scan.Report, options ...Option) (interface{}, erro
if sum.Duration < 0 {
sum.Duration = 0
}
if len(ops.CVEAllowlist) > 0 {
sum.CVEBypassed = make([]string, 0)
}
sum.ScanStatus = job.ErrorStatus.String()
if job.Status(r.Status).Code() != -1 {
@ -157,7 +139,7 @@ func GenerateNativeSummary(r *scan.Report, options ...Option) (interface{}, erro
sum.Severity = rp.Severity
sum.Scanner = rp.Scanner
sum.UpdateSeveritySummaryAndByPassed(rp.GetVulnerabilityItemList(), ops.CVEAllowlist)
sum.UpdateSeveritySummary(rp.GetVulnerabilityItemList())
return sum, nil
}

View File

@ -19,7 +19,6 @@ import (
"testing"
"time"
models2 "github.com/goharbor/harbor/src/pkg/allowlist/models"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
@ -103,23 +102,6 @@ func (suite *SummaryTestSuite) TestSummaryGenerateSummaryNoOptions() {
suite.Equal("0.1.0", nativeSummary.Scanner.Version)
}
// TestSummaryGenerateSummaryWithOptions ...
func (suite *SummaryTestSuite) TestSummaryGenerateSummaryWithOptions() {
cveSet := make(models2.CVESet)
cveSet["2019-0980-0909"] = struct{}{}
summaries, err := GenerateSummary(suite.r, WithCVEAllowlist(&cveSet))
require.NoError(suite.T(), err)
require.NotNil(suite.T(), summaries)
nativeSummary, ok := summaries.(*vuln.NativeReportSummary)
require.Equal(suite.T(), true, ok)
suite.Equal(vuln.Medium, nativeSummary.Severity)
suite.Equal(1, len(nativeSummary.CVEBypassed))
suite.Equal(1, nativeSummary.Summary.Total)
}
// TestSummaryGenerateSummaryWrongMime ...
func (suite *SummaryTestSuite) TestSummaryGenerateSummaryWrongMime() {
suite.r.MimeType = "wrong-mime"

View File

@ -18,7 +18,6 @@ import (
"encoding/json"
"fmt"
models2 "github.com/goharbor/harbor/src/pkg/allowlist/models"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
)
@ -149,27 +148,19 @@ func (l *VulnerabilityItemList) Add(items ...*VulnerabilityItem) {
}
}
// GetSeveritySummaryAndByPassed returns the Severity Summary and ByPassed by allowlist for the l
func (l *VulnerabilityItemList) GetSeveritySummaryAndByPassed(allowlist models2.CVESet) (Severity, *VulnerabilitySummary, []string) {
// GetSeveritySummary returns the severity and summary of l
func (l *VulnerabilityItemList) GetSeveritySummary() (Severity, *VulnerabilitySummary) {
if l == nil {
return Severity(""), nil
}
sum := &VulnerabilitySummary{
Total: len(l.Items()),
Summary: make(SeveritySummary),
}
var bypassed []string
severity := None
for _, v := range l.Items() {
if len(allowlist) > 0 && allowlist.Contains(v.ID) {
// If allowlist is set, then check if we need to bypass it
// Reduce the total
sum.Total--
// Append the by passed CVEs specified in the allowlist
bypassed = append(bypassed, v.ID)
continue
}
if num, ok := sum.Summary[v.Severity]; ok {
sum.Summary[v.Severity] = num + 1
} else {
@ -180,14 +171,13 @@ func (l *VulnerabilityItemList) GetSeveritySummaryAndByPassed(allowlist models2.
if v.Severity.Code() > severity.Code() {
severity = v.Severity
}
// If the CVE item has a fixable version
if len(v.FixVersion) > 0 {
sum.Fixable++
}
}
return severity, sum, bypassed
return severity, sum
}
// VulnerabilityItem represents one found vulnerability

View File

@ -19,7 +19,6 @@ import (
"reflect"
"testing"
models2 "github.com/goharbor/harbor/src/pkg/allowlist/models"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/stretchr/testify/assert"
)
@ -80,7 +79,7 @@ func TestReportMarshalJSON(t *testing.T) {
assert.Contains(string(b), "vulnerabilities")
}
func TestGetSummarySeverityAndByPassed(t *testing.T) {
func TestGetSummarySeverity(t *testing.T) {
assert := assert.New(t)
vul1 := &VulnerabilityItem{
@ -102,34 +101,14 @@ func TestGetSummarySeverityAndByPassed(t *testing.T) {
l := VulnerabilityItemList{}
l.Add(vul1, vul2, vul3)
{
s := SeveritySummary{
Low: 2,
Medium: 1,
}
severity, sum, byPassed := l.GetSeveritySummaryAndByPassed(models2.CVESet{})
assert.Equal(3, sum.Total)
assert.Equal(1, sum.Fixable)
assert.Equal(s, sum.Summary)
assert.Equal(Medium, severity)
assert.Empty(byPassed)
s := SeveritySummary{
Low: 2,
Medium: 1,
}
{
s := SeveritySummary{
Low: 2,
}
cveSet := models2.CVESet{}
cveSet.Add("cve3")
severity, sum, byPassed := l.GetSeveritySummaryAndByPassed(cveSet)
assert.Equal(2, sum.Total)
assert.Equal(1, sum.Fixable)
assert.Equal(s, sum.Summary)
assert.Equal(Low, severity)
assert.NotEmpty(byPassed)
assert.Equal([]string{"cve3"}, byPassed)
}
severity, sum := l.GetSeveritySummary()
assert.Equal(Medium, severity)
assert.Equal(3, sum.Total)
assert.Equal(1, sum.Fixable)
assert.Equal(s, sum.Summary)
}

View File

@ -18,7 +18,6 @@ import (
"time"
"github.com/goharbor/harbor/src/jobservice/job"
models2 "github.com/goharbor/harbor/src/pkg/allowlist/models"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
)
@ -39,30 +38,12 @@ type NativeReportSummary struct {
TotalCount int `json:"-"`
CompleteCount int `json:"-"`
VulnerabilityItemList *VulnerabilityItemList `json:"-"`
CVESet models2.CVESet `json:"-"`
}
// UpdateSeveritySummaryAndByPassed update the Severity, Summary and CVEBypassed of the sum from l and s
func (sum *NativeReportSummary) UpdateSeveritySummaryAndByPassed(l *VulnerabilityItemList, s models2.CVESet) {
// UpdateSeveritySummary update the Severity, Summary of the sum from l
func (sum *NativeReportSummary) UpdateSeveritySummary(l *VulnerabilityItemList) {
sum.VulnerabilityItemList = l
sum.CVESet = s
if l == nil {
return
}
var severity Severity
severity, sum.Summary, sum.CVEBypassed = l.GetSeveritySummaryAndByPassed(s)
if len(s) > 0 {
// Override the overall severity of the filtered list if needed.
sum.Severity = severity
}
}
// IsSuccessStatus returns true when the scan status is success
func (sum *NativeReportSummary) IsSuccessStatus() bool {
return sum.ScanStatus == job.SuccessStatus.String()
sum.Severity, sum.Summary = l.GetSeveritySummary()
}
// Merge ...
@ -79,17 +60,17 @@ func (sum *NativeReportSummary) Merge(another *NativeReportSummary) *NativeRepor
} else {
r.Scanner = another.Scanner
}
r.TotalCount = sum.TotalCount + another.TotalCount
r.CompleteCount = sum.CompleteCount + another.CompleteCount
r.CompletePercent = r.CompleteCount * 100 / r.TotalCount
r.ReportID = mergeReportID(sum.ReportID, another.ReportID)
r.Severity = mergeSeverity(sum.Severity, another.Severity)
r.ScanStatus = mergeScanStatus(sum.ScanStatus, another.ScanStatus)
r.ScanStatus = MergeScanStatus(sum.ScanStatus, another.ScanStatus)
r.UpdateSeveritySummaryAndByPassed(
NewVulnerabilityItemList(sum.VulnerabilityItemList, another.VulnerabilityItemList),
models2.NewCVESet(sum.CVESet, another.CVESet),
)
if r.ScanStatus != job.RunningStatus.String() {
l := NewVulnerabilityItemList(sum.VulnerabilityItemList, another.VulnerabilityItemList)
r.UpdateSeveritySummary(l)
}
return r
}

View File

@ -25,6 +25,7 @@ func TestMergeNativeReportSummary(t *testing.T) {
assert := assert.New(t)
errorStatus := job.ErrorStatus.String()
runningStatus := job.RunningStatus.String()
successStatus := job.SuccessStatus.String()
v1 := VulnerabilitySummary{
Total: 1,
@ -42,82 +43,135 @@ func TestMergeNativeReportSummary(t *testing.T) {
})
{
n1 := NativeReportSummary{
ScanStatus: runningStatus,
Severity: Low,
TotalCount: 1,
Summary: &v1,
VulnerabilityItemList: l,
}
r := n1.Merge(&NativeReportSummary{
ScanStatus: errorStatus,
Severity: Severity(""),
TotalCount: 1,
})
assert.Equal(runningStatus, r.ScanStatus)
assert.Equal(Low, r.Severity)
assert.Equal(v1, *r.Summary)
}
{
n1 := NativeReportSummary{
// running && running
n1 := &NativeReportSummary{
ScanStatus: runningStatus,
Severity: Severity(""),
TotalCount: 1,
}
r := n1.Merge(&NativeReportSummary{
ScanStatus: errorStatus,
Severity: Severity(""),
ScanStatus: runningStatus,
TotalCount: 1,
})
assert.Equal(runningStatus, r.ScanStatus)
assert.Equal(Severity(""), r.Severity)
assert.Nil(r.Summary)
}
{
n1 := &NativeReportSummary{
ScanStatus: errorStatus,
Severity: Severity(""),
// running && success
n1 := NativeReportSummary{
ScanStatus: runningStatus,
TotalCount: 1,
}
r := n1.Merge(&NativeReportSummary{
ScanStatus: runningStatus,
ScanStatus: successStatus,
Severity: Low,
TotalCount: 1,
CompleteCount: 1,
Summary: &v1,
VulnerabilityItemList: l,
})
assert.Equal(runningStatus, r.ScanStatus)
assert.Equal(Low, r.Severity)
assert.Equal(v1, *r.Summary)
assert.Nil(r.Summary)
}
{
n1 := &NativeReportSummary{
ScanStatus: runningStatus,
// running && error
n1 := NativeReportSummary{
ScanStatus: runningStatus,
TotalCount: 1,
}
r := n1.Merge(&NativeReportSummary{
ScanStatus: errorStatus,
TotalCount: 1,
})
assert.Equal(runningStatus, r.ScanStatus)
assert.Nil(r.Summary)
}
{
// success && success
n1 := NativeReportSummary{
ScanStatus: successStatus,
Severity: Low,
TotalCount: 1,
CompleteCount: 1,
Summary: &v1,
VulnerabilityItemList: l,
}
l2 := &VulnerabilityItemList{}
l2.Add(&VulnerabilityItem{
ID: "cve-id-high",
Package: "openssl-libs-high",
Version: "1:1.1.1g-11.el8",
Severity: High,
FixVersion: "1:1.1.1g-12.el8_3",
})
r := n1.Merge(&NativeReportSummary{
ScanStatus: successStatus,
Severity: High,
TotalCount: 1,
CompleteCount: 1,
Summary: &VulnerabilitySummary{
Total: 1,
Fixable: 1,
Summary: map[Severity]int{High: 1},
},
VulnerabilityItemList: l2,
})
assert.Equal(successStatus, r.ScanStatus)
assert.Equal(High, r.Severity)
assert.Equal(VulnerabilitySummary{
Total: 2,
Fixable: 2,
Summary: map[Severity]int{Low: 1, High: 1},
}, *r.Summary)
assert.Equal(100, r.CompletePercent)
}
{
// success && error
n1 := NativeReportSummary{
ScanStatus: successStatus,
Severity: Low,
TotalCount: 1,
CompleteCount: 1,
Summary: &v1,
VulnerabilityItemList: l,
}
r := n1.Merge(&NativeReportSummary{
ScanStatus: runningStatus,
Severity: Low,
TotalCount: 1,
Summary: &v1,
VulnerabilityItemList: l,
ScanStatus: errorStatus,
TotalCount: 1,
})
assert.Equal(runningStatus, r.ScanStatus)
assert.Equal(successStatus, r.ScanStatus)
assert.Equal(Low, r.Severity)
assert.Equal(v1, *r.Summary)
assert.Equal(50, r.CompletePercent)
}
{
// error && error
n1 := NativeReportSummary{
ScanStatus: errorStatus,
TotalCount: 1,
}
r := n1.Merge(&NativeReportSummary{
ScanStatus: errorStatus,
TotalCount: 1,
})
assert.Equal(errorStatus, r.ScanStatus)
assert.Nil(r.Summary)
}
}

View File

@ -79,12 +79,14 @@ func mergeSeverity(s1, s2 Severity) Severity {
return s2
}
func mergeScanStatus(s1, s2 string) string {
// MergeScanStatus ...
func MergeScanStatus(s1, s2 string) string {
j1, j2 := job.Status(s1), job.Status(s2)
if j1 == job.RunningStatus || j2 == job.RunningStatus {
return job.RunningStatus.String()
} else if j1 == job.SuccessStatus || j2 == job.SuccessStatus {
// the scan status of the image index will be treated as a success when one of its children is success
return job.SuccessStatus.String()
}

View File

@ -91,7 +91,7 @@ func Test_mergeScanStatus(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := mergeScanStatus(tt.args.s1, tt.args.s2); got != tt.want {
if got := MergeScanStatus(tt.args.s1, tt.args.s2); got != tt.want {
t.Errorf("mergeScanStatus() = %v, want %v", got, tt.want)
}
})

View File

@ -25,8 +25,6 @@ import (
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/scan/report"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
"github.com/goharbor/harbor/src/server/middleware"
"github.com/goharbor/harbor/src/server/middleware/util"
@ -92,28 +90,20 @@ func Middleware() func(http.Handler) http.Handler {
}
allowlist := proj.CVEAllowlist.CVESet()
summaries, err := scanController.GetSummary(ctx, art, []string{v1.MimeTypeNativeReport, v1.MimeTypeGenericVulnerabilityReport}, report.WithCVEAllowlist(&allowlist))
if err != nil {
logger.Errorf("get vulnerability summary of the artifact %s@%s failed, error: %v", art.RepositoryName, art.Digest, err)
return err
}
projectSeverity := vuln.ParseSeverityVersion3(proj.Severity())
rawSummary, ok := summaries[v1.MimeTypeNativeReport]
if !ok {
rawSummary, ok = summaries[v1.MimeTypeGenericVulnerabilityReport]
if !ok {
vulnerable, err := scanController.GetVulnerable(ctx, art, allowlist)
if err != nil {
if errors.IsNotFoundErr(err) {
// No report yet?
msg := fmt.Sprintf(`current image without vulnerability scanning cannot be pulled due to configured policy in 'Prevent images with vulnerability severity of "%s" or higher from running.' `+
`To continue with pull, please contact your project administrator for help.`, projectSeverity)
return errors.New(nil).WithCode(errors.PROJECTPOLICYVIOLATION).WithMessage(msg)
}
}
summary, ok := rawSummary.(*vuln.NativeReportSummary)
if !ok {
return fmt.Errorf("report summary is invalid")
logger.Errorf("get vulnerability summary of the artifact %s@%s failed, error: %v", art.RepositoryName, art.Digest, err)
return err
}
if art.IsImageIndex() {
@ -128,36 +118,27 @@ func Middleware() func(http.Handler) http.Handler {
}
}
if !summary.IsSuccessStatus() {
if !vulnerable.IsScanSuccess() {
msg := fmt.Sprintf(`current image with "%s" status of vulnerability scanning cannot be pulled due to configured policy in 'Prevent images with vulnerability severity of "%s" or higher from running.' `+
`To continue with pull, please contact your project administrator for help.`, summary.ScanStatus, projectSeverity)
`To continue with pull, please contact your project administrator for help.`, vulnerable.ScanStatus, projectSeverity)
return errors.New(nil).WithCode(errors.PROJECTPOLICYVIOLATION).WithMessage(msg)
}
if summary.Summary == nil || summary.Summary.Total == 0 {
// No vulnerabilities found in the artifact, skip the checking
// See https://github.com/goharbor/harbor/issues/11210 to get more details
logger.Debugf("no vulnerabilities found in artifact %s@%s, skip the vulnerability prevention checking", art.RepositoryName, art.Digest)
return nil
}
// Do judgement
if summary.Severity.Code() >= projectSeverity.Code() {
if vulnerable.Severity != nil && vulnerable.Severity.Code() >= projectSeverity.Code() {
thing := "vulnerability"
if summary.Summary.Total > 1 {
if vulnerable.VulnerabilitiesCount > 1 {
thing = "vulnerabilities"
}
msg := fmt.Sprintf(`current image with %d %s cannot be pulled due to configured policy in 'Prevent images with vulnerability severity of "%s" or higher from running.' `+
`To continue with pull, please contact your project administrator to exempt matched vulnerabilities through configuring the CVE allowlist.`,
summary.Summary.Total, thing, projectSeverity)
vulnerable.VulnerabilitiesCount, thing, projectSeverity)
return errors.New(nil).WithCode(errors.PROJECTPOLICYVIOLATION).WithMessage(msg)
}
// Print scannerPull CVE list
if len(summary.CVEBypassed) > 0 {
for _, cve := range summary.CVEBypassed {
logger.Infof("Vulnerable policy check: bypassed CVE %s", cve)
}
for _, cve := range vulnerable.CVEBypassed {
logger.Infof("Vulnerable policy check: bypassed CVE %s", cve)
}
return nil

View File

@ -28,7 +28,7 @@ import (
"github.com/goharbor/harbor/src/controller/project"
"github.com/goharbor/harbor/src/controller/scan"
"github.com/goharbor/harbor/src/lib"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
securitytesting "github.com/goharbor/harbor/src/testing/common/security"
artifacttesting "github.com/goharbor/harbor/src/testing/controller/artifact"
@ -222,7 +222,7 @@ func (suite *MiddlewareTestSuite) TestArtifactNotScanned() {
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
mock.OnAnything(suite.projectController, "Get").Return(suite.project, nil)
mock.OnAnything(suite.checker, "IsScannable").Return(true, nil)
mock.OnAnything(suite.scanController, "GetSummary").Return(nil, nil)
mock.OnAnything(suite.scanController, "GetVulnerable").Return(nil, errors.NotFoundError(nil))
req := suite.makeRequest()
rr := httptest.NewRecorder()
@ -235,12 +235,7 @@ func (suite *MiddlewareTestSuite) TestArtifactScanFailed() {
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
mock.OnAnything(suite.projectController, "Get").Return(suite.project, nil)
mock.OnAnything(suite.checker, "IsScannable").Return(true, nil)
mock.OnAnything(suite.scanController, "GetSummary").Return(map[string]interface{}{
v1.MimeTypeNativeReport: &vuln.NativeReportSummary{
ScanStatus: "Error",
CVEBypassed: []string{"cve-2020"},
},
}, nil)
mock.OnAnything(suite.scanController, "GetVulnerable").Return(&scan.Vulnerable{ScanStatus: "Error"}, nil)
req := suite.makeRequest()
rr := httptest.NewRecorder()
@ -249,26 +244,11 @@ func (suite *MiddlewareTestSuite) TestArtifactScanFailed() {
suite.Equal(rr.Code, http.StatusPreconditionFailed)
}
func (suite *MiddlewareTestSuite) TestGetSummaryFailed() {
func (suite *MiddlewareTestSuite) TestGetVulnerableFailed() {
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
mock.OnAnything(suite.projectController, "Get").Return(suite.project, nil)
mock.OnAnything(suite.checker, "IsScannable").Return(true, nil)
mock.OnAnything(suite.scanController, "GetSummary").Return(nil, fmt.Errorf("error"))
req := suite.makeRequest()
rr := httptest.NewRecorder()
Middleware()(suite.next).ServeHTTP(rr, req)
suite.Equal(rr.Code, http.StatusInternalServerError)
}
func (suite *MiddlewareTestSuite) TestBadSummary() {
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
mock.OnAnything(suite.projectController, "Get").Return(suite.project, nil)
mock.OnAnything(suite.checker, "IsScannable").Return(true, nil)
mock.OnAnything(suite.scanController, "GetSummary").Return(map[string]interface{}{
v1.MimeTypeNativeReport: "bad report",
}, nil)
mock.OnAnything(suite.scanController, "GetVulnerable").Return(nil, fmt.Errorf("error"))
req := suite.makeRequest()
rr := httptest.NewRecorder()
@ -281,32 +261,9 @@ func (suite *MiddlewareTestSuite) TestNoVulnerabilities() {
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
mock.OnAnything(suite.projectController, "Get").Return(suite.project, nil)
mock.OnAnything(suite.checker, "IsScannable").Return(true, nil)
mock.OnAnything(suite.scanController, "GetSummary").Return(map[string]interface{}{
v1.MimeTypeNativeReport: &vuln.NativeReportSummary{
ScanStatus: "Success",
Severity: vuln.Unknown,
CVEBypassed: []string{"cve-2020"},
},
}, nil)
req := suite.makeRequest()
rr := httptest.NewRecorder()
Middleware()(suite.next).ServeHTTP(rr, req)
suite.Equal(rr.Code, http.StatusOK)
}
func (suite *MiddlewareTestSuite) TestTotalVulnerabilitiesIsZero() {
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
mock.OnAnything(suite.projectController, "Get").Return(suite.project, nil)
mock.OnAnything(suite.checker, "IsScannable").Return(true, nil)
mock.OnAnything(suite.scanController, "GetSummary").Return(map[string]interface{}{
v1.MimeTypeNativeReport: &vuln.NativeReportSummary{
ScanStatus: "Success",
Severity: vuln.Unknown,
Summary: &vuln.VulnerabilitySummary{Total: 0},
CVEBypassed: []string{"cve-2020"},
},
mock.OnAnything(suite.scanController, "GetVulnerable").Return(&scan.Vulnerable{
ScanStatus: "Success",
CVEBypassed: []string{"cve-2020"},
}, nil)
req := suite.makeRequest()
@ -317,16 +274,15 @@ func (suite *MiddlewareTestSuite) TestTotalVulnerabilitiesIsZero() {
}
func (suite *MiddlewareTestSuite) TestAllowed() {
low := vuln.Low
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
mock.OnAnything(suite.projectController, "Get").Return(suite.project, nil)
mock.OnAnything(suite.checker, "IsScannable").Return(true, nil)
mock.OnAnything(suite.scanController, "GetSummary").Return(map[string]interface{}{
v1.MimeTypeNativeReport: &vuln.NativeReportSummary{
ScanStatus: "Success",
Severity: vuln.Low,
Summary: &vuln.VulnerabilitySummary{Total: 1},
CVEBypassed: []string{"cve-2020"},
},
mock.OnAnything(suite.scanController, "GetVulnerable").Return(&scan.Vulnerable{
ScanStatus: "Success",
Severity: &low,
VulnerabilitiesCount: 1,
CVEBypassed: []string{"cve-2020"},
}, nil)
req := suite.makeRequest()
@ -341,14 +297,14 @@ func (suite *MiddlewareTestSuite) TestPrevented() {
mock.OnAnything(suite.projectController, "Get").Return(suite.project, nil)
mock.OnAnything(suite.checker, "IsScannable").Return(true, nil)
critical := vuln.Critical
{
// only one vulnerability
mock.OnAnything(suite.scanController, "GetSummary").Return(map[string]interface{}{
v1.MimeTypeNativeReport: &vuln.NativeReportSummary{
ScanStatus: "Success",
Severity: vuln.Critical,
Summary: &vuln.VulnerabilitySummary{Total: 1},
},
mock.OnAnything(suite.scanController, "GetVulnerable").Return(&scan.Vulnerable{
ScanStatus: "Success",
Severity: &critical,
VulnerabilitiesCount: 1,
}, nil).Once()
req := suite.makeRequest()
@ -362,12 +318,10 @@ func (suite *MiddlewareTestSuite) TestPrevented() {
{
// multiple vulnerabilities
mock.OnAnything(suite.scanController, "GetSummary").Return(map[string]interface{}{
v1.MimeTypeNativeReport: &vuln.NativeReportSummary{
ScanStatus: "Success",
Severity: vuln.Critical,
Summary: &vuln.VulnerabilitySummary{Total: 2},
},
mock.OnAnything(suite.scanController, "GetVulnerable").Return(&scan.Vulnerable{
ScanStatus: "Success",
Severity: &critical,
VulnerabilitiesCount: 2,
}, nil).Once()
req := suite.makeRequest()
@ -381,14 +335,15 @@ func (suite *MiddlewareTestSuite) TestPrevented() {
}
func (suite *MiddlewareTestSuite) TestArtifactIsImageIndex() {
critical := vuln.Critical
suite.artifact.ManifestMediaType = manifestlist.MediaTypeManifestList
mock.OnAnything(suite.artifactController, "GetByReference").Return(suite.artifact, nil)
mock.OnAnything(suite.projectController, "Get").Return(suite.project, nil)
mock.OnAnything(suite.checker, "IsScannable").Return(true, nil)
mock.OnAnything(suite.scanController, "GetSummary").Return(map[string]interface{}{
v1.MimeTypeNativeReport: &vuln.NativeReportSummary{
Severity: vuln.Critical,
},
mock.OnAnything(suite.scanController, "GetVulnerable").Return(&scan.Vulnerable{
ScanStatus: "Success",
Severity: &critical,
}, nil)
req := suite.makeRequest()

View File

@ -351,28 +351,17 @@ func (a *artifactAPI) GetVulnerabilitiesAddition(ctx context.Context, params ope
return a.SendError(ctx, err)
}
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), report.WithArtifactDigest(rp.Digest))
if err != nil {
return a.SendError(ctx, err)
}
if v, ok := vulnerabilities[rp.MimeType]; ok {
r, err := report.Merge(rp.MimeType, v, vrp)
if err != nil {
return a.SendError(ctx, err)
}
vulnerabilities[rp.MimeType] = r
} else {
vulnerabilities[rp.MimeType] = vrp
}
vrp, err := report.Reports(reports).ResolveData(mimeType)
if err != nil {
return a.SendError(ctx, err)
}
if vrp == nil {
continue
}
vulnerabilities[mimeType] = vrp
if len(vulnerabilities) != 0 {
break
}

View File

@ -11,9 +11,9 @@ import (
mock "github.com/stretchr/testify/mock"
pkgscan "github.com/goharbor/harbor/src/pkg/scan"
models "github.com/goharbor/harbor/src/pkg/allowlist/models"
report "github.com/goharbor/harbor/src/pkg/scan/report"
pkgscan "github.com/goharbor/harbor/src/pkg/scan"
scan "github.com/goharbor/harbor/src/controller/scan"
)
@ -90,20 +90,13 @@ func (_m *Controller) GetScanLog(ctx context.Context, uuid string) ([]byte, erro
return r0, r1
}
// GetSummary provides a mock function with given fields: ctx, _a1, mimeTypes, options
func (_m *Controller) GetSummary(ctx context.Context, _a1 *artifact.Artifact, mimeTypes []string, options ...report.Option) (map[string]interface{}, error) {
_va := make([]interface{}, len(options))
for _i := range options {
_va[_i] = options[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, _a1, mimeTypes)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
// GetSummary provides a mock function with given fields: ctx, _a1, mimeTypes
func (_m *Controller) GetSummary(ctx context.Context, _a1 *artifact.Artifact, mimeTypes []string) (map[string]interface{}, error) {
ret := _m.Called(ctx, _a1, mimeTypes)
var r0 map[string]interface{}
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string, ...report.Option) map[string]interface{}); ok {
r0 = rf(ctx, _a1, mimeTypes, options...)
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, []string) map[string]interface{}); ok {
r0 = rf(ctx, _a1, mimeTypes)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]interface{})
@ -111,8 +104,31 @@ func (_m *Controller) GetSummary(ctx context.Context, _a1 *artifact.Artifact, mi
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, []string, ...report.Option) error); ok {
r1 = rf(ctx, _a1, mimeTypes, options...)
if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, []string) error); ok {
r1 = rf(ctx, _a1, mimeTypes)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetVulnerable provides a mock function with given fields: ctx, _a1, allowlist
func (_m *Controller) GetVulnerable(ctx context.Context, _a1 *artifact.Artifact, allowlist models.CVESet) (*scan.Vulnerable, error) {
ret := _m.Called(ctx, _a1, allowlist)
var r0 *scan.Vulnerable
if rf, ok := ret.Get(0).(func(context.Context, *artifact.Artifact, models.CVESet) *scan.Vulnerable); ok {
r0 = rf(ctx, _a1, allowlist)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*scan.Vulnerable)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *artifact.Artifact, models.CVESet) error); ok {
r1 = rf(ctx, _a1, allowlist)
} else {
r1 = ret.Error(1)
}
@ -162,13 +178,13 @@ func (_m *Controller) ScanAll(ctx context.Context, trigger string, async bool) (
return r0, r1
}
// UpdateReport provides a mock function with given fields: ctx, _a1
func (_m *Controller) UpdateReport(ctx context.Context, _a1 *pkgscan.CheckInReport) error {
ret := _m.Called(ctx, _a1)
// UpdateReport provides a mock function with given fields: ctx, report
func (_m *Controller) UpdateReport(ctx context.Context, report *pkgscan.CheckInReport) error {
ret := _m.Called(ctx, report)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *pkgscan.CheckInReport) error); ok {
r0 = rf(ctx, _a1)
r0 = rf(ctx, report)
} else {
r0 = ret.Error(0)
}