Add security hub summary API (#18872)

include WithCVE, WithArtifact option

Signed-off-by: stonezdj <daojunz@vmware.com>
This commit is contained in:
stonezdj(Daojun Zhang) 2023-07-12 19:18:08 +08:00 committed by GitHub
parent 90259f3c80
commit 93e428d0d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1101 additions and 1 deletions

View File

@ -6052,7 +6052,40 @@ paths:
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
/security/summary:
get:
summary: Get vulnerability system summary
description: Retrieve the vulnerability summary of the system
tags:
- securityhub
operationId: getSecuritySummary
parameters:
- $ref: '#/parameters/requestId'
- name: with_dangerous_cve
in: query
description: Specify whether the dangerous CVE is include in the security summary
type: boolean
required: false
default: false
- name: with_dangerous_artifact
in: query
description: Specify whether the dangerous artifacts is include in the security summary
type: boolean
required: false
default: false
responses:
'200':
description: Success
schema:
$ref: '#/definitions/SecuritySummary'
'401':
$ref: '#/responses/401'
'403':
$ref: '#/responses/403'
'404':
$ref: '#/responses/404'
'500':
$ref: '#/responses/500'
parameters:
query:
name: q
@ -9614,3 +9647,116 @@ definitions:
type: boolean
description: if the scheduler is paused
x-omitempty: false
SecuritySummary:
type: object
description: the security summary
properties:
critical_cnt:
type: integer
format: int64
x-omitempty: false
description: the count of critical vulnerabilities
high_cnt:
type: integer
format: int64
description: the count of high vulnerabilities
medium_cnt:
type: integer
format: int64
x-omitempty: false
description: the count of medium vulnerabilities
low_cnt:
type: integer
format: int64
x-omitempty: false
description: the count of low vulnerabilities
none_cnt:
type: integer
format: int64
description: the count of none vulnerabilities
unknown_cnt:
type: integer
format: int64
description: the count of unknown vulnerabilities
total_vuls:
type: integer
format: int64
x-omitempty: false
description: the count of total vulnerabilities
scanned_cnt:
type: integer
format: int64
x-omitempty: false
description: the count of scanned artifacts
total_artifact:
type: integer
format: int64
x-omitempty: false
description: the total count of artifacts
fixable_cnt:
type: integer
format: int64
x-omitempty: false
description: the count of fixable vulnerabilities
dangerous_cves:
type: array
x-omitempty: true
description: the list of dangerous CVEs
items:
$ref: '#/definitions/DangerousCVE'
dangerous_artifacts:
type: array
x-omitempty: true
description: the list of dangerous artifacts
items:
$ref: '#/definitions/DangerousArtifact'
DangerousCVE:
type: object
description: the dangerous CVE information
properties:
cve_id:
type: string
description: the cve id
severity:
type: string
description: the severity of the CVE
cvss_score_v3:
type: number
format: float64
description: the cvss score v3
desc:
type: string
description: the description of the CVE
package:
type: string
description: the package of the CVE
version:
type: string
description: the version of the package
DangerousArtifact:
type: object
description: the dangerous artifact information
properties:
project_id:
type: integer
format: int64
description: the project id of the artifact
repository_name:
type: string
description: the repository name of the artifact
digest:
type: string
description: the digest of the artifact
critical_cnt:
type: integer
x-omitempty: false
description: the count of critical vulnerabilities
high_cnt:
type: integer
format: int64
x-omitempty: false
description: the count of high vulnerabilities
medium_cnt:
type: integer
x-omitempty: false
description: the count of medium vulnerabilities

View File

@ -75,4 +75,5 @@ const (
ResourcePurgeAuditLog = Resource("purge-audit")
ResourceExportCVE = Resource("export-cve")
ResourceJobServiceMonitor = Resource("jobservice-monitor")
ResourceSecurityHub = Resource("security-hub")
)

View File

@ -84,5 +84,7 @@ var (
{Resource: rbac.ResourceJobServiceMonitor, Action: rbac.ActionRead},
{Resource: rbac.ResourceJobServiceMonitor, Action: rbac.ActionList},
{Resource: rbac.ResourceJobServiceMonitor, Action: rbac.ActionStop},
{Resource: rbac.ResourceSecurityHub, Action: rbac.ActionRead},
}
)

View File

@ -0,0 +1,138 @@
// 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 securityhub
import (
"context"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/scan/scanner"
"github.com/goharbor/harbor/src/pkg/securityhub"
secHubModel "github.com/goharbor/harbor/src/pkg/securityhub/model"
)
// Ctl is the global controller for security hub
var Ctl = NewController()
// Options define the option to query summary info
type Options struct {
WithCVE bool
WithArtifact bool
}
// Option define the func to build options
type Option func(*Options)
func newOptions(options ...Option) *Options {
opts := &Options{}
for _, f := range options {
f(opts)
}
return opts
}
// WithCVE enable CVE info in summary
func WithCVE(enable bool) Option {
return func(o *Options) {
o.WithCVE = enable
}
}
// WithArtifact enable artifact info in summary
func WithArtifact(enable bool) Option {
return func(o *Options) {
o.WithArtifact = enable
}
}
// Controller controller of security hub
type Controller interface {
// SecuritySummary returns the security summary of the specified project.
SecuritySummary(ctx context.Context, projectID int64, options ...Option) (*secHubModel.Summary, error)
}
type controller struct {
artifactMgr artifact.Manager
scannerMgr scanner.Manager
secHubMgr securityhub.Manager
}
// NewController ...
func NewController() Controller {
return &controller{
artifactMgr: pkg.ArtifactMgr,
scannerMgr: scanner.New(),
secHubMgr: securityhub.Mgr,
}
}
func (c *controller) SecuritySummary(ctx context.Context, projectID int64, options ...Option) (*secHubModel.Summary, error) {
opts := newOptions(options...)
scannerUUID, err := c.defaultScannerUUID(ctx)
if err != nil {
return nil, err
}
sum, err := c.secHubMgr.Summary(ctx, scannerUUID, projectID, nil)
if err != nil {
return nil, err
}
sum.TotalArtifactCnt, err = c.totalArtifactCount(ctx, projectID)
if err != nil {
return nil, err
}
sum.ScannedCnt, err = c.secHubMgr.ScannedArtifactsCount(ctx, scannerUUID, projectID, nil)
if err != nil {
return nil, err
}
if opts.WithCVE {
sum.DangerousCVEs, err = c.secHubMgr.DangerousCVEs(ctx, scannerUUID, projectID, nil)
if err != nil {
return nil, err
}
}
if opts.WithArtifact {
sum.DangerousArtifacts, err = c.secHubMgr.DangerousArtifacts(ctx, scannerUUID, projectID, nil)
if err != nil {
return nil, err
}
}
return sum, nil
}
func (c *controller) scannedArtifactCount(ctx context.Context, projectID int64) (int64, error) {
scannerUUID, err := c.defaultScannerUUID(ctx)
if err != nil {
return 0, err
}
return c.secHubMgr.ScannedArtifactsCount(ctx, scannerUUID, projectID, nil)
}
func (c *controller) totalArtifactCount(ctx context.Context, projectID int64) (int64, error) {
if projectID == 0 {
return c.artifactMgr.Count(ctx, nil)
}
return c.artifactMgr.Count(ctx, q.New(q.KeyWords{"project_id": projectID}))
}
// defaultScannerUUID returns the default scanner uuid.
func (c *controller) defaultScannerUUID(ctx context.Context) (string, error) {
reg, err := c.scannerMgr.GetDefault(ctx)
if err != nil {
return "", err
}
return reg.UUID, nil
}

View File

@ -0,0 +1,157 @@
// 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 securityhub
import (
"errors"
"testing"
"github.com/stretchr/testify/suite"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/goharbor/harbor/src/pkg/securityhub/model"
htesting "github.com/goharbor/harbor/src/testing"
"github.com/goharbor/harbor/src/testing/mock"
artifactMock "github.com/goharbor/harbor/src/testing/pkg/artifact"
scannerMock "github.com/goharbor/harbor/src/testing/pkg/scan/scanner"
securityMock "github.com/goharbor/harbor/src/testing/pkg/securityhub"
)
var sum = &model.Summary{
CriticalCnt: 50,
HighCnt: 40,
MediumCnt: 30,
LowCnt: 20,
NoneCnt: 10,
FixableCnt: 90,
}
type ControllerTestSuite struct {
htesting.Suite
c *controller
artifactMgr *artifactMock.Manager
scannerMgr *scannerMock.Manager
secHubMgr *securityMock.Manager
}
// TestController is the entry of controller test suite
func TestController(t *testing.T) {
suite.Run(t, new(ControllerTestSuite))
}
// SetupTest prepares env for the controller test suite
func (suite *ControllerTestSuite) SetupTest() {
suite.artifactMgr = &artifactMock.Manager{}
suite.secHubMgr = &securityMock.Manager{}
suite.scannerMgr = &scannerMock.Manager{}
suite.c = &controller{
artifactMgr: suite.artifactMgr,
secHubMgr: suite.secHubMgr,
scannerMgr: suite.scannerMgr,
}
}
func (suite *ControllerTestSuite) TearDownTest() {
}
// TestSecuritySummary tests the security summary
func (suite *ControllerTestSuite) TestSecuritySummary() {
ctx := suite.Context()
mock.OnAnything(suite.artifactMgr, "Count").Return(int64(1234), nil)
mock.OnAnything(suite.secHubMgr, "ScannedArtifactsCount").Return(int64(1000), nil)
mock.OnAnything(suite.secHubMgr, "Summary").Return(sum, nil).Twice()
mock.OnAnything(suite.scannerMgr, "GetDefault").Return(&scanner.Registration{UUID: "ruuid"}, nil)
summary, err := suite.c.SecuritySummary(ctx, 0, WithArtifact(false), WithCVE(false))
suite.NoError(err)
suite.NotNil(summary)
suite.Equal(int64(1234), summary.TotalArtifactCnt)
suite.Equal(int64(1000), summary.ScannedCnt)
suite.Equal(int64(50), summary.CriticalCnt)
suite.Equal(int64(40), summary.HighCnt)
suite.Equal(int64(30), summary.MediumCnt)
suite.Equal(int64(20), summary.LowCnt)
suite.Equal(int64(10), summary.NoneCnt)
suite.Equal(int64(90), summary.FixableCnt)
sum.DangerousCVEs = []*scan.VulnerabilityRecord{
{CVEID: "CVE-2020-1234", Severity: "CRITICAL"},
{CVEID: "CVE-2020-1235", Severity: "HIGH"},
{CVEID: "CVE-2020-1236", Severity: "MEDIUM"},
{CVEID: "CVE-2020-1237", Severity: "LOW"},
{CVEID: "CVE-2020-1238", Severity: "NONE"},
}
sum.DangerousArtifacts = []*model.DangerousArtifact{
{Project: 1, Repository: "library/busybox"},
{Project: 1, Repository: "library/nginx"},
{Project: 1, Repository: "library/hello-world"},
{Project: 1, Repository: "library/harbor-jobservice"},
{Project: 1, Repository: "library/harbor-core"},
}
mock.OnAnything(suite.secHubMgr, "Summary").Return(sum, nil).Once()
mock.OnAnything(suite.secHubMgr, "DangerousCVEs").Return(sum.DangerousCVEs, nil).Once()
mock.OnAnything(suite.secHubMgr, "DangerousArtifacts").Return(sum.DangerousArtifacts, nil).Once()
sum2, err := suite.c.SecuritySummary(ctx, 0, WithCVE(false), WithArtifact(false))
suite.NoError(err)
suite.NotNil(sum2)
suite.NotNil(sum2.DangerousCVEs)
suite.NotNil(sum2.DangerousArtifacts)
sum3, err := suite.c.SecuritySummary(ctx, 0, WithCVE(true), WithArtifact(true))
suite.NoError(err)
suite.NotNil(sum3)
suite.True(len(sum3.DangerousCVEs) > 0)
suite.True(len(sum3.DangerousArtifacts) > 0)
}
// TestSecuritySummaryError tests the security summary with error
func (suite *ControllerTestSuite) TestSecuritySummaryError() {
ctx := suite.Context()
mock.OnAnything(suite.scannerMgr, "GetDefault").Return(&scanner.Registration{UUID: "ruuid"}, nil)
mock.OnAnything(suite.secHubMgr, "ScannedArtifactsCount").Return(int64(1000), nil)
mock.OnAnything(suite.secHubMgr, "Summary").Return(nil, errors.New("invalid project")).Once()
summary, err := suite.c.SecuritySummary(ctx, 0, WithCVE(false), WithArtifact(false))
suite.Error(err)
suite.Nil(summary)
mock.OnAnything(suite.artifactMgr, "Count").Return(int64(0), errors.New("failed to connect db")).Once()
mock.OnAnything(suite.secHubMgr, "Summary").Return(sum, nil).Once()
summary, err = suite.c.SecuritySummary(ctx, 0, WithCVE(false), WithArtifact(false))
suite.Error(err)
suite.Nil(summary)
}
// TestGetDefaultScanner tests the get default scanner
func (suite *ControllerTestSuite) TestGetDefaultScanner() {
ctx := suite.Context()
mock.OnAnything(suite.scannerMgr, "GetDefault").Return(&scanner.Registration{UUID: ""}, nil).Once()
scanner, err := suite.c.defaultScannerUUID(ctx)
suite.NoError(err)
suite.Equal("", scanner)
mock.OnAnything(suite.scannerMgr, "GetDefault").Return(nil, errors.New("failed to get scanner")).Once()
scanner, err = suite.c.defaultScannerUUID(ctx)
suite.Error(err)
suite.Equal("", scanner)
}
func (suite *ControllerTestSuite) TestScannedArtifact() {
ctx := suite.Context()
mock.OnAnything(suite.scannerMgr, "GetDefault").Return(&scanner.Registration{UUID: "ruuid"}, nil)
mock.OnAnything(suite.secHubMgr, "ScannedArtifactsCount").Return(int64(1000), nil)
scanned, err := suite.c.scannedArtifactCount(ctx, 0)
suite.NoError(err)
suite.Equal(int64(1000), scanned)
}

View File

@ -71,6 +71,10 @@ func (c *nativeToRelationalSchemaConverter) ToRelationalSchema(ctx context.Conte
return "", "", errors.Wrap(err, "Error when converting vulnerability report")
}
if err := c.updateReport(ctx, rawReport.Vulnerabilities, reportUUID); err != nil {
return "", "", errors.Wrap(err, "Error when updating report")
}
rawReport.Vulnerabilities = nil
data, err := json.Marshal(rawReport)
if err != nil {

View File

@ -0,0 +1,133 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dao
import (
"context"
"github.com/goharbor/harbor/src/lib/orm"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
"github.com/goharbor/harbor/src/pkg/securityhub/model"
)
const (
summarySQL = `select sum(s.critical_cnt) critical_cnt,
sum(s.high_cnt) high_cnt,
sum(s.medium_cnt) medium_cnt,
sum(s.low_cnt) low_cnt,
sum(s.none_cnt) none_cnt,
sum(s.unknown_cnt) unknown_cnt,
sum(s.fixable_cnt) fixable_cnt
from artifact a
left join scan_report s on a.digest = s.digest
where s.registration_uuid = ?`
dangerousArtifactSQL = `select a.project_id project, a.repository_name repository, a.digest, s.critical_cnt, s.high_cnt, s.medium_cnt, s.low_cnt
from artifact a,
scan_report s
where a.digest = s.digest
and s.registration_uuid = ?
order by s.critical_cnt desc, s.high_cnt desc, s.medium_cnt desc, s.low_cnt desc
limit 5`
scannedArtifactCountSQL = `select count(1)
from artifact a
left join scan_report s on a.digest = s.digest
where s.registration_uuid= ? and s.uuid is not null`
dangerousCVESQL = `select vr.*
from vulnerability_record vr
where vr.cvss_score_v3 is not null
and vr.registration_uuid = ?
order by vr.cvss_score_v3 desc
limit 5`
)
// SecurityHubDao defines the interface to access security hub data.
type SecurityHubDao interface {
// Summary returns the summary of the scan cve reports.
Summary(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) (*model.Summary, error)
// DangerousCVEs get the top 5 most dangerous CVEs, return top 5 result
DangerousCVEs(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) ([]*scan.VulnerabilityRecord, error)
// DangerousArtifacts returns top 5 dangerous artifact for the given scanner. return top 5 result
DangerousArtifacts(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) ([]*model.DangerousArtifact, error)
// ScannedArtifactsCount return the count of scanned artifacts.
ScannedArtifactsCount(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) (int64, error)
}
// New creates a new SecurityHubDao instance.
func New() SecurityHubDao {
return &dao{}
}
type dao struct {
}
func (d *dao) Summary(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) (*model.Summary, error) {
if len(scannerUUID) == 0 || projectID != 0 {
return nil, nil
}
o, err := orm.FromContext(ctx)
if err != nil {
return nil, err
}
var sum model.Summary
err = o.Raw(summarySQL, scannerUUID).QueryRow(&sum.CriticalCnt,
&sum.HighCnt,
&sum.MediumCnt,
&sum.LowCnt,
&sum.NoneCnt,
&sum.UnknownCnt,
&sum.FixableCnt)
return &sum, err
}
func (d *dao) DangerousArtifacts(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) ([]*model.DangerousArtifact, error) {
if len(scannerUUID) == 0 || projectID != 0 {
return nil, nil
}
o, err := orm.FromContext(ctx)
if err != nil {
return nil, err
}
var artifacts []*model.DangerousArtifact
_, err = o.Raw(dangerousArtifactSQL, scannerUUID).QueryRows(&artifacts)
return artifacts, err
}
func (d *dao) ScannedArtifactsCount(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) (int64, error) {
if len(scannerUUID) == 0 || projectID != 0 {
return 0, nil
}
var cnt int64
o, err := orm.FromContext(ctx)
if err != nil {
return cnt, err
}
err = o.Raw(scannedArtifactCountSQL, scannerUUID).QueryRow(&cnt)
return cnt, err
}
func (d *dao) DangerousCVEs(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) ([]*scan.VulnerabilityRecord, error) {
if len(scannerUUID) == 0 || projectID != 0 {
return nil, nil
}
cves := make([]*scan.VulnerabilityRecord, 0)
o, err := orm.FromContext(ctx)
if err != nil {
return nil, err
}
_, err = o.Raw(dangerousCVESQL, scannerUUID).QueryRows(&cves)
return cves, err
}

View File

@ -0,0 +1,104 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dao
import (
"testing"
"github.com/stretchr/testify/suite"
testDao "github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/lib/orm"
htesting "github.com/goharbor/harbor/src/testing"
)
func TestDao(t *testing.T) {
suite.Run(t, &SecurityDaoTestSuite{})
}
type SecurityDaoTestSuite struct {
htesting.Suite
dao SecurityHubDao
}
// SetupSuite prepares env for test suite.
func (suite *SecurityDaoTestSuite) SetupSuite() {
suite.Suite.SetupSuite()
suite.dao = New()
}
// SetupTest prepares env for test case.
func (suite *SecurityDaoTestSuite) SetupTest() {
testDao.ExecuteBatchSQL([]string{
`insert into scan_report(uuid, digest, registration_uuid, mime_type, critical_cnt, high_cnt, medium_cnt, low_cnt, unknown_cnt, fixable_cnt) values('uuid', 'digest1001', 'ruuid', 'application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0', 50, 50, 50, 0, 0, 20)`,
`insert into artifact (project_id, repository_name, digest, type, pull_time, push_time, repository_id, media_type, manifest_media_type, size, extra_attrs, annotations, icon)
values (1, 'library/hello-world', 'digest1001', 'IMAGE', '2023-06-02 09:16:47.838778', '2023-06-02 01:45:55.050785', 1742, 'application/vnd.docker.container.image.v1+json', 'application/vnd.docker.distribution.manifest.v2+json', 4452, '{"architecture":"amd64","author":"","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/hello"]},"created":"2023-05-04T17:37:03.872958712Z","os":"linux"}', null, '');`,
`insert into scanner_registration (name, url, uuid, auth) values('trivy', 'https://www.vmware.com', 'ruuid', 'empty')`,
`insert into vulnerability_record (id, cve_id, registration_uuid, cvss_score_v3) values (1, '2023-4567-12345', 'ruuid', 9.8)`,
`insert into report_vulnerability_record (report_uuid, vuln_record_id) VALUES ('uuid', 1)`,
})
testDao.ExecuteBatchSQL([]string{
`INSERT INTO scanner_registration (name, url, uuid, auth) values('trivy2', 'https://www.trivy.com', 'uuid2', 'empty')`,
`INSERT INTO vulnerability_record(cve_id, registration_uuid, cvss_score_v3, package) VALUES ('CVE-2021-44228', 'uuid2', 10, 'org.apache.logging.log4j:log4j-core');
INSERT INTO vulnerability_record(cve_id, registration_uuid, cvss_score_v3, package) VALUES ('CVE-2021-21345', 'uuid2', 9.9, 'com.thoughtworks.xstream:xstream');
INSERT INTO vulnerability_record(cve_id, registration_uuid, cvss_score_v3, package) VALUES ('CVE-2016-1585', 'uuid2', 9.8, 'libapparmor1');
INSERT INTO vulnerability_record(cve_id, registration_uuid, cvss_score_v3, package) VALUES ('CVE-2023-0950', 'uuid2', 9.8, 'ure');
INSERT INTO vulnerability_record(cve_id, registration_uuid, cvss_score_v3, package) VALUES ('CVE-2022-47629', 'uuid2', 9.8, 'libksba8');
`,
})
}
// TearDownTest clears enf for test case.
func (suite *SecurityDaoTestSuite) TearDownTest() {
testDao.ExecuteBatchSQL([]string{
`delete from scan_report where uuid = 'uuid'`,
`delete from artifact where digest = 'digest1001'`,
`delete from scanner_registration where uuid='ruuid'`,
`delete from vulnerability_record where cve_id='2023-4567-12345'`,
`delete from report_vulnerability_record where report_uuid='ruuid'`,
`delete from vulnerability_record where registration_uuid ='uuid2'`,
})
}
func (suite *SecurityDaoTestSuite) TestGetSummary() {
s, err := suite.dao.Summary(suite.Context(), "ruuid", 0, nil)
suite.Require().NoError(err)
suite.Equal(int64(50), s.CriticalCnt)
suite.Equal(int64(50), s.HighCnt)
suite.Equal(int64(50), s.MediumCnt)
suite.Equal(int64(20), s.FixableCnt)
}
func (suite *SecurityDaoTestSuite) TestGetMostDangerousArtifact() {
aList, err := suite.dao.DangerousArtifacts(orm.Context(), "ruuid", 0, nil)
suite.Require().NoError(err)
suite.Equal(1, len(aList))
suite.Equal(int64(50), aList[0].CriticalCnt)
suite.Equal(int64(50), aList[0].HighCnt)
suite.Equal(int64(50), aList[0].MediumCnt)
suite.Equal(int64(0), aList[0].LowCnt)
}
func (suite *SecurityDaoTestSuite) TestGetScannedArtifactCount() {
count, err := suite.dao.ScannedArtifactsCount(orm.Context(), "ruuid", 0, nil)
suite.Require().NoError(err)
suite.Equal(int64(1), count)
}
func (suite *SecurityDaoTestSuite) TestGetDangerousCVEs() {
records, err := suite.dao.DangerousCVEs(suite.Context(), `uuid2`, 0, nil)
suite.NoError(err, "Error when fetching most dangerous artifact")
suite.Equal(5, len(records))
}

View File

@ -0,0 +1,69 @@
// 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 securityhub
import (
"context"
"github.com/goharbor/harbor/src/lib/q"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
"github.com/goharbor/harbor/src/pkg/securityhub/dao"
"github.com/goharbor/harbor/src/pkg/securityhub/model"
)
var (
// Mgr is the global security manager
Mgr = NewManager()
)
// Manager is used to manage the security manager.
type Manager interface {
// Summary returns the summary of the scan cve reports.
Summary(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) (*model.Summary, error)
// DangerousArtifacts returns the most dangerous artifact for the given scanner.
DangerousArtifacts(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) ([]*model.DangerousArtifact, error)
// ScannedArtifactsCount return the count of scanned artifacts.
ScannedArtifactsCount(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) (int64, error)
// DangerousCVEs returns the most dangerous CVEs for the given scanner.
DangerousCVEs(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) ([]*scan.VulnerabilityRecord, error)
}
// NewManager news security manager.
func NewManager() Manager {
return &securityManager{
dao: dao.New(),
}
}
// securityManager is a default implementation of security manager.
type securityManager struct {
dao dao.SecurityHubDao
}
func (s *securityManager) Summary(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) (*model.Summary, error) {
return s.dao.Summary(ctx, scannerUUID, projectID, query)
}
func (s *securityManager) DangerousArtifacts(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) ([]*model.DangerousArtifact, error) {
return s.dao.DangerousArtifacts(ctx, scannerUUID, projectID, query)
}
func (s *securityManager) ScannedArtifactsCount(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) (int64, error) {
return s.dao.ScannedArtifactsCount(ctx, scannerUUID, projectID, query)
}
func (s *securityManager) DangerousCVEs(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) ([]*scan.VulnerabilityRecord, error) {
return s.dao.DangerousCVEs(ctx, scannerUUID, projectID, query)
}

View File

@ -0,0 +1,44 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
// Summary is the summary of scan result
type Summary struct {
CriticalCnt int64 `json:"critical_cnt"`
HighCnt int64 `json:"high_cnt"`
MediumCnt int64 `json:"medium_cnt"`
LowCnt int64 `json:"low_cnt"`
NoneCnt int64 `json:"none_cnt"`
UnknownCnt int64 `json:"unknown_cnt"`
FixableCnt int64 `json:"fixable_cnt"`
ScannedCnt int64 `json:"scanned_cnt"`
NotScanCnt int64 `json:"not_scan_cnt"`
TotalArtifactCnt int64 `json:"total_artifact_cnt"`
DangerousCVEs []*scan.VulnerabilityRecord `json:"dangerous_cves"`
DangerousArtifacts []*DangerousArtifact `json:"dangerous_artifacts"`
}
// DangerousArtifact define the most dangerous artifact
type DangerousArtifact struct {
Project int64 `json:"project" orm:"column(project)"`
Repository string `json:"repository" orm:"column(repository)"`
Digest string `json:"digest" orm:"column(digest)"`
CriticalCnt int64 `json:"critical_cnt" orm:"column(critical_cnt)"`
HighCnt int64 `json:"high_cnt" orm:"column(high_cnt)"`
MediumCnt int64 `json:"medium_cnt" orm:"column(medium_cnt)"`
LowCnt int64 `json:"low_cnt" orm:"column(low_cnt)"`
}

View File

@ -69,6 +69,7 @@ func New() http.Handler {
ScanDataExportAPI: newScanDataExportAPI(),
JobserviceAPI: newJobServiceAPI(),
ScheduleAPI: newScheduleAPI(),
SecurityhubAPI: newSecurityAPI(),
})
if err != nil {
log.Fatal(err)

View File

@ -0,0 +1,98 @@
// 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"
"github.com/go-openapi/runtime/middleware"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/server/v2.0/models"
securityModel "github.com/goharbor/harbor/src/server/v2.0/restapi/operations/securityhub"
"github.com/goharbor/harbor/src/controller/securityhub"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
secHubModel "github.com/goharbor/harbor/src/pkg/securityhub/model"
)
func newSecurityAPI() *securityAPI {
return &securityAPI{
controller: securityhub.Ctl,
}
}
type securityAPI struct {
BaseAPI
controller securityhub.Controller
}
func (s *securityAPI) GetSecuritySummary(ctx context.Context,
params securityModel.GetSecuritySummaryParams) middleware.Responder {
if err := s.RequireSystemAccess(ctx, rbac.ActionRead, rbac.ResourceSecurityHub); err != nil {
return s.SendError(ctx, err)
}
summary, err := s.controller.SecuritySummary(ctx, 0, securityhub.WithCVE(*params.WithDangerousCVE), securityhub.WithArtifact(*params.WithDangerousArtifact))
if err != nil {
return s.SendError(ctx, err)
}
sum := toSecuritySummaryModel(summary)
return securityModel.NewGetSecuritySummaryOK().WithPayload(sum)
}
func toSecuritySummaryModel(summary *secHubModel.Summary) *models.SecuritySummary {
return &models.SecuritySummary{
CriticalCnt: summary.CriticalCnt,
HighCnt: summary.HighCnt,
MediumCnt: summary.MediumCnt,
LowCnt: summary.LowCnt,
NoneCnt: summary.NoneCnt,
UnknownCnt: summary.UnknownCnt,
FixableCnt: summary.FixableCnt,
TotalVuls: summary.CriticalCnt + summary.HighCnt + summary.MediumCnt + summary.LowCnt + summary.NoneCnt + summary.UnknownCnt,
TotalArtifact: summary.TotalArtifactCnt,
ScannedCnt: summary.ScannedCnt,
DangerousCves: toDangerousCves(summary.DangerousCVEs),
DangerousArtifacts: toDangerousArtifacts(summary.DangerousArtifacts),
}
}
func toDangerousArtifacts(artifacts []*secHubModel.DangerousArtifact) []*models.DangerousArtifact {
var result []*models.DangerousArtifact
for _, artifact := range artifacts {
result = append(result, &models.DangerousArtifact{
ProjectID: artifact.Project,
RepositoryName: artifact.Repository,
Digest: artifact.Digest,
CriticalCnt: artifact.CriticalCnt,
HighCnt: artifact.HighCnt,
MediumCnt: artifact.MediumCnt,
})
}
return result
}
func toDangerousCves(cves []*scan.VulnerabilityRecord) []*models.DangerousCVE {
var result []*models.DangerousCVE
for _, vul := range cves {
result = append(result, &models.DangerousCVE{
CVEID: vul.CVEID,
Package: vul.Package,
Version: vul.PackageVersion,
Severity: vul.Severity,
CvssScoreV3: *vul.CVE3Score,
})
}
return result
}

View File

@ -35,3 +35,4 @@ package controller
//go:generate mockery --case snake --dir ../../controller/task --name Controller --output ./task --outpkg task
//go:generate mockery --case snake --dir ../../controller/task --name ExecutionController --output ./task --outpkg task
//go:generate mockery --case snake --dir ../../controller/webhook --name Controller --output ./webhook --outpkg webhook
//go:generate mockery --case snake --dir ../../controller/securityhub --name Controller --output ./securityhub --outpkg securityhub

View File

@ -0,0 +1,65 @@
// Code generated by mockery v2.22.1. DO NOT EDIT.
package securityhub
import (
context "context"
model "github.com/goharbor/harbor/src/pkg/securityhub/model"
mock "github.com/stretchr/testify/mock"
securityhub "github.com/goharbor/harbor/src/controller/securityhub"
)
// Controller is an autogenerated mock type for the Controller type
type Controller struct {
mock.Mock
}
// SecuritySummary provides a mock function with given fields: ctx, projectID, options
func (_m *Controller) SecuritySummary(ctx context.Context, projectID int64, options ...securityhub.Option) (*model.Summary, error) {
_va := make([]interface{}, len(options))
for _i := range options {
_va[_i] = options[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, projectID)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *model.Summary
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, ...securityhub.Option) (*model.Summary, error)); ok {
return rf(ctx, projectID, options...)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, ...securityhub.Option) *model.Summary); ok {
r0 = rf(ctx, projectID, options...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Summary)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64, ...securityhub.Option) error); ok {
r1 = rf(ctx, projectID, options...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewController interface {
mock.TestingT
Cleanup(func())
}
// NewController creates a new instance of Controller. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewController(t mockConstructorTestingTNewController) *Controller {
mock := &Controller{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -73,3 +73,4 @@ package pkg
//go:generate mockery --case snake --dir ../../pkg/jobmonitor --name QueueManager --output ./jobmonitor --outpkg jobmonitor
//go:generate mockery --case snake --dir ../../pkg/jobmonitor --name RedisClient --output ./jobmonitor --outpkg jobmonitor
//go:generate mockery --case snake --dir ../../pkg/queuestatus --name Manager --output ./queuestatus --outpkg queuestatus
//go:generate mockery --case snake --dir ../../pkg/securityhub --name Manager --output ./securityhub --outpkg securityhub

View File

@ -0,0 +1,136 @@
// Code generated by mockery v2.22.1. DO NOT EDIT.
package securityhub
import (
context "context"
model "github.com/goharbor/harbor/src/pkg/securityhub/model"
mock "github.com/stretchr/testify/mock"
q "github.com/goharbor/harbor/src/lib/q"
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
}
// DangerousArtifacts provides a mock function with given fields: ctx, scannerUUID, projectID, query
func (_m *Manager) DangerousArtifacts(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) ([]*model.DangerousArtifact, error) {
ret := _m.Called(ctx, scannerUUID, projectID, query)
var r0 []*model.DangerousArtifact
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, int64, *q.Query) ([]*model.DangerousArtifact, error)); ok {
return rf(ctx, scannerUUID, projectID, query)
}
if rf, ok := ret.Get(0).(func(context.Context, string, int64, *q.Query) []*model.DangerousArtifact); ok {
r0 = rf(ctx, scannerUUID, projectID, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.DangerousArtifact)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, int64, *q.Query) error); ok {
r1 = rf(ctx, scannerUUID, projectID, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DangerousCVEs provides a mock function with given fields: ctx, scannerUUID, projectID, query
func (_m *Manager) DangerousCVEs(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) ([]*scan.VulnerabilityRecord, error) {
ret := _m.Called(ctx, scannerUUID, projectID, query)
var r0 []*scan.VulnerabilityRecord
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, int64, *q.Query) ([]*scan.VulnerabilityRecord, error)); ok {
return rf(ctx, scannerUUID, projectID, query)
}
if rf, ok := ret.Get(0).(func(context.Context, string, int64, *q.Query) []*scan.VulnerabilityRecord); ok {
r0 = rf(ctx, scannerUUID, projectID, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*scan.VulnerabilityRecord)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, int64, *q.Query) error); ok {
r1 = rf(ctx, scannerUUID, projectID, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ScannedArtifactsCount provides a mock function with given fields: ctx, scannerUUID, projectID, query
func (_m *Manager) ScannedArtifactsCount(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) (int64, error) {
ret := _m.Called(ctx, scannerUUID, projectID, query)
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, int64, *q.Query) (int64, error)); ok {
return rf(ctx, scannerUUID, projectID, query)
}
if rf, ok := ret.Get(0).(func(context.Context, string, int64, *q.Query) int64); ok {
r0 = rf(ctx, scannerUUID, projectID, query)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(context.Context, string, int64, *q.Query) error); ok {
r1 = rf(ctx, scannerUUID, projectID, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Summary provides a mock function with given fields: ctx, scannerUUID, projectID, query
func (_m *Manager) Summary(ctx context.Context, scannerUUID string, projectID int64, query *q.Query) (*model.Summary, error) {
ret := _m.Called(ctx, scannerUUID, projectID, query)
var r0 *model.Summary
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, int64, *q.Query) (*model.Summary, error)); ok {
return rf(ctx, scannerUUID, projectID, query)
}
if rf, ok := ret.Get(0).(func(context.Context, string, int64, *q.Query) *model.Summary); ok {
r0 = rf(ctx, scannerUUID, projectID, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Summary)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, int64, *q.Query) error); ok {
r1 = rf(ctx, scannerUUID, projectID, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewManager interface {
mock.TestingT
Cleanup(func())
}
// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewManager(t mockConstructorTestingTNewManager) *Manager {
mock := &Manager{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}