Merge pull request #9154 from steven-zou/feature/pluggable_scanner_s2

[stage2]support pluggable scanner
This commit is contained in:
Steven Zou 2019-09-23 21:12:27 +08:00 committed by GitHub
commit a73f896f23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 2940 additions and 301 deletions

View File

@ -8,9 +8,6 @@ CREATE TABLE scanner_registration
description VARCHAR(1024) NULL,
auth VARCHAR(16) NOT NULL,
access_cred VARCHAR(512) NULL,
adapter VARCHAR(128) NOT NULL,
vendor VARCHAR(128) NOT NULL,
version VARCHAR(32) NOT NULL,
disabled BOOLEAN NOT NULL DEFAULT FALSE,
is_default BOOLEAN NOT NULL DEFAULT FALSE,
skip_cert_verify BOOLEAN NOT NULL DEFAULT FALSE,
@ -18,17 +15,20 @@ CREATE TABLE scanner_registration
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
/*Table for keeping the scanner report. The report details are stored as JSONB*/
CREATE TABLE scanner_report
/*Table for keeping the scan report. The report details are stored as JSON*/
CREATE TABLE scan_report
(
id SERIAL PRIMARY KEY NOT NULL,
uuid VARCHAR(64) UNIQUE NOT NULL,
digest VARCHAR(256) NOT NULL,
registration_id VARCHAR(64) NOT NULL,
registration_uuid VARCHAR(64) NOT NULL,
mime_type VARCHAR(256) NOT NULL,
job_id VARCHAR(32),
status VARCHAR(16) NOT NULL,
status_code INTEGER DEFAULT 0,
status_rev BIGINT DEFAULT 0,
report JSON,
start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
end_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(digest, registration_id)
UNIQUE(digest, registration_uuid, mime_type)
)

View File

@ -209,7 +209,7 @@ func init() {
// Add routes for plugin scanner management
scannerAPI := &ScannerAPI{}
beego.Router("/api/scanners", scannerAPI, "post:Create;get:List")
beego.Router("/api/scanners/:uid", scannerAPI, "get:Get;delete:Delete;put:Update;patch:SetAsDefault")
beego.Router("/api/scanners/:uuid", scannerAPI, "get:Get;delete:Delete;put:Update;patch:SetAsDefault")
// Add routes for project level scanner
beego.Router("/api/projects/:pid([0-9]+)/scanner", scannerAPI, "get:GetProjectScanner;put:SetProjectScanner")

View File

@ -19,8 +19,8 @@ import (
"net/http"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/api"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
s "github.com/goharbor/harbor/src/pkg/scan/api/scanner"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/pkg/errors"
)
@ -30,7 +30,7 @@ type ScannerAPI struct {
BaseController
// Controller for the plug scanners
c api.Controller
c s.Controller
}
// Prepare sth. for the subsequent actions
@ -50,7 +50,7 @@ func (sa *ScannerAPI) Prepare() {
}
// Use the default controller
sa.c = api.DefaultController
sa.c = s.DefaultController
}
// Get the specified scanner
@ -76,7 +76,7 @@ func (sa *ScannerAPI) List() {
}
// Get query key words
kws := make(map[string]string)
kws := make(map[string]interface{})
properties := []string{"name", "description", "url"}
for _, k := range properties {
kw := sa.GetString(k)
@ -192,7 +192,7 @@ func (sa *ScannerAPI) Update() {
// Delete the scanner
func (sa *ScannerAPI) Delete() {
uid := sa.GetStringFromPath(":uid")
uid := sa.GetStringFromPath(":uuid")
if len(uid) == 0 {
sa.SendBadRequestError(errors.New("missing uid"))
return
@ -216,7 +216,7 @@ func (sa *ScannerAPI) Delete() {
// SetAsDefault sets the given registration as default one
func (sa *ScannerAPI) SetAsDefault() {
uid := sa.GetStringFromPath(":uid")
uid := sa.GetStringFromPath(":uuid")
if len(uid) == 0 {
sa.SendBadRequestError(errors.New("missing uid"))
return
@ -293,7 +293,7 @@ func (sa *ScannerAPI) SetProjectScanner() {
// get the specified scanner
func (sa *ScannerAPI) get() *scanner.Registration {
uid := sa.GetStringFromPath(":uid")
uid := sa.GetStringFromPath(":uuid")
if len(uid) == 0 {
sa.SendBadRequestError(errors.New("missing uid"))
return nil
@ -316,7 +316,7 @@ func (sa *ScannerAPI) get() *scanner.Registration {
func (sa *ScannerAPI) checkDuplicated(property, value string) bool {
// Explicitly check if conflict
kw := make(map[string]string)
kw := make(map[string]interface{})
kw[property] = value
query := &q.Query{

View File

@ -20,10 +20,8 @@ import (
"testing"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/api"
dscan "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
sc "github.com/goharbor/harbor/src/pkg/scan/api/scanner"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -38,7 +36,7 @@ const (
type ScannerAPITestSuite struct {
suite.Suite
originC api.Controller
originC sc.Controller
mockC *MockScannerAPIController
}
@ -49,9 +47,9 @@ func TestScannerAPI(t *testing.T) {
// SetupSuite prepares testing env
func (suite *ScannerAPITestSuite) SetupTest() {
suite.originC = api.DefaultController
suite.originC = sc.DefaultController
m := &MockScannerAPIController{}
api.DefaultController = m
sc.DefaultController = m
suite.mockC = m
}
@ -59,7 +57,7 @@ func (suite *ScannerAPITestSuite) SetupTest() {
// TearDownTest clears test case env
func (suite *ScannerAPITestSuite) TearDownTest() {
// Restore
api.DefaultController = suite.originC
sc.DefaultController = suite.originC
}
// TestScannerAPICreate tests the post request to create new one
@ -108,9 +106,6 @@ func (suite *ScannerAPITestSuite) TestScannerAPIGet() {
Name: "TestScannerAPIGet",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockC.On("GetRegistration", "uuid").Return(res, nil)
@ -133,9 +128,6 @@ func (suite *ScannerAPITestSuite) TestScannerAPICreate() {
Name: "TestScannerAPICreate",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockQuery(r)
@ -170,9 +162,6 @@ func (suite *ScannerAPITestSuite) TestScannerAPIList() {
Name: "TestScannerAPIList",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}}
suite.mockC.On("ListRegistrations", query).Return(ll, nil)
@ -198,9 +187,6 @@ func (suite *ScannerAPITestSuite) TestScannerAPIUpdate() {
Name: "TestScannerAPIUpdate_before",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
updated := &scanner.Registration{
@ -209,9 +195,6 @@ func (suite *ScannerAPITestSuite) TestScannerAPIUpdate() {
Name: "TestScannerAPIUpdate",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockQuery(updated)
@ -240,9 +223,6 @@ func (suite *ScannerAPITestSuite) TestScannerAPIDelete() {
Name: "TestScannerAPIDelete",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockC.On("DeleteRegistration", "uuid").Return(r, nil)
@ -299,9 +279,6 @@ func (suite *ScannerAPITestSuite) TestScannerAPIProjectScanner() {
Name: "TestScannerAPIProjectScanner",
Description: "JUST FOR TEST",
URL: "https://a.b.c",
Adapter: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
}
suite.mockC.On("GetRegistrationByProject", int64(1)).Return(r, nil)
@ -319,7 +296,7 @@ func (suite *ScannerAPITestSuite) TestScannerAPIProjectScanner() {
}
func (suite *ScannerAPITestSuite) mockQuery(r *scanner.Registration) {
kw := make(map[string]string, 1)
kw := make(map[string]interface{}, 1)
kw["name"] = r.Name
query := &q.Query{
Keywords: kw,
@ -327,7 +304,7 @@ func (suite *ScannerAPITestSuite) mockQuery(r *scanner.Registration) {
emptyL := make([]*scanner.Registration, 0)
suite.mockC.On("ListRegistrations", query).Return(emptyL, nil)
kw2 := make(map[string]string, 1)
kw2 := make(map[string]interface{}, 1)
kw2["url"] = r.URL
query2 := &q.Query{
Keywords: kw2,
@ -408,37 +385,3 @@ func (m *MockScannerAPIController) GetRegistrationByProject(projectID int64) (*s
return s.(*scanner.Registration), args.Error(1)
}
// Ping ...
func (m *MockScannerAPIController) Ping(registration *scanner.Registration) error {
args := m.Called(registration)
return args.Error(0)
}
// Scan ...
func (m *MockScannerAPIController) Scan(artifact *scan.Artifact) error {
args := m.Called(artifact)
return args.Error(0)
}
// GetReport ...
func (m *MockScannerAPIController) GetReport(artifact *scan.Artifact) ([]*dscan.Report, error) {
args := m.Called(artifact)
r := args.Get(0)
if r == nil {
return nil, args.Error(1)
}
return r.([]*dscan.Report), args.Error(1)
}
// GetScanLog ...
func (m *MockScannerAPIController) GetScanLog(digest string) ([]byte, error) {
args := m.Called(digest)
l := args.Get(0)
if l == nil {
return nil, args.Error(1)
}
return l.([]byte), args.Error(1)
}

View File

@ -195,7 +195,7 @@ func initRouters() {
// Add routes for plugin scanner management
scannerAPI := &api.ScannerAPI{}
beego.Router("/api/scanners", scannerAPI, "post:Create;get:List")
beego.Router("/api/scanners/:uid", scannerAPI, "get:Get;delete:Delete;put:Update;patch:SetAsDefault")
beego.Router("/api/scanners/:uuid", scannerAPI, "get:Get;delete:Delete;put:Update;patch:SetAsDefault")
// Add routes for project level scanner
beego.Router("/api/projects/:pid([0-9]+)/scanner", scannerAPI, "get:GetProjectScanner;put:SetProjectScanner")

View File

@ -21,5 +21,5 @@ type Query struct {
// Page size
PageSize int64
// List of key words
Keywords map[string]string
Keywords map[string]interface{}
}

View File

@ -0,0 +1,58 @@
// 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 scan
import (
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
)
// basicController is default implementation of api.Controller interface
type basicController struct {
// Client for talking to scanner adapter
client v1.Client
}
// NewController news a scan API controller
func NewController() Controller {
return &basicController{}
}
// Scan ...
func (bc *basicController) Scan(artifact *v1.Artifact) error {
return nil
}
// GetReport ...
func (bc *basicController) GetReport(artifact *v1.Artifact) ([]*scan.Report, error) {
return nil, nil
}
// GetScanLog ...
func (bc *basicController) GetScanLog(digest string) ([]byte, error) {
return nil, nil
}
// Ping ...
func (bc *basicController) Ping(registration *scanner.Registration) error {
return nil
}
// HandleJobHooks ...
func (bc *basicController) HandleJobHooks(trackID int64, change *job.StatusChange) error {
return nil
}

View File

@ -0,0 +1,78 @@
// 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 scan
import (
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
)
// 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 {
// Ping pings Scanner Adapter to test EndpointURL and Authorization settings.
// The implementation is supposed to call the GetMetadata method on scanner.Client.
// Returns `nil` if connection succeeded, a non `nil` error otherwise.
//
// Arguments:
// registration *scanner.Registration : scanner registration to ping
//
// Returns:
// error : non nil error if any errors occurred
Ping(registration *scanner.Registration) error
// Scan the given artifact
//
// Arguments:
// artifact *v1.Artifact : artifact to be scanned
//
// Returns:
// error : non nil error if any errors occurred
Scan(artifact *v1.Artifact) error
// GetReport gets the reports for the given artifact identified by the digest
//
// Arguments:
// artifact *v1.Artifact : the scanned artifact
//
// Returns:
// []*scan.Report : scan results by different scanner vendors
// error : non nil error if any errors occurred
GetReport(artifact *v1.Artifact) ([]*scan.Report, error)
// Get the scan log for the specified artifact with the given digest
//
// Arguments:
// digest string : the digest of the artifact
//
// Returns:
// []byte : the log text stream
// error : non nil error if any errors occurred
GetScanLog(digest string) ([]byte, error)
// HandleJobHooks handle the hook events from the job service
// e.g : status change of the scan job or scan result
//
// Arguments:
// trackID int64 : ID for the result record
// change *job.StatusChange : change event from the job service
//
// Returns:
// error : non nil error if any errors occurred
HandleJobHooks(trackID int64, change *job.StatusChange) error
}

View File

@ -12,15 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package api
package scanner
import (
"github.com/goharbor/harbor/src/core/promgr/metamgr"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
rscanner "github.com/goharbor/harbor/src/pkg/scan/scanner"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
"github.com/pkg/errors"
)
@ -45,9 +44,6 @@ type basicController struct {
manager rscanner.Manager
// for operating the project level configured scanner
proMetaMgr metamgr.ProjectMetadataManager
// controller for scan actions
c scan.Controller
// Client
}
// ListRegistrations ...
@ -58,9 +54,13 @@ func (bc *basicController) ListRegistrations(query *q.Query) ([]*scanner.Registr
// CreateRegistration ...
func (bc *basicController) CreateRegistration(registration *scanner.Registration) (string, error) {
// TODO: Get metadata from the adapter service first
l, err := bc.manager.List(nil)
// Check if there are any registrations already existing.
l, err := bc.manager.List(&q.Query{
PageSize: 1,
PageNumber: 1,
})
if err != nil {
return "", err
return "", errors.Wrap(err, "api controller: create registration")
}
if len(l) == 0 && !registration.IsDefault {
@ -102,7 +102,7 @@ func (bc *basicController) DeleteRegistration(registrationUUID string) (*scanner
}
if err := bc.manager.Delete(registrationUUID); err != nil {
return nil, errors.Wrap(err, "delete registration")
return nil, errors.Wrap(err, "api controller: delete registration")
}
return registration, nil
@ -127,7 +127,7 @@ func (bc *basicController) SetRegistrationByProject(projectID int64, registratio
// Scanner metadata existing?
m, err := bc.proMetaMgr.Get(projectID, proScannerMetaKey)
if err != nil {
return errors.Wrap(err, "set project scanner")
return errors.Wrap(err, "api controller: set project scanner")
}
// Update if exists
@ -136,14 +136,14 @@ func (bc *basicController) SetRegistrationByProject(projectID int64, registratio
if registrationID != m[proScannerMetaKey] {
m[proScannerMetaKey] = registrationID
if err := bc.proMetaMgr.Update(projectID, m); err != nil {
return errors.Wrap(err, "set project scanner")
return errors.Wrap(err, "api controller: set project scanner")
}
}
} else {
meta := make(map[string]string, 1)
meta[proScannerMetaKey] = registrationID
if err := bc.proMetaMgr.Add(projectID, meta); err != nil {
return errors.Wrap(err, "set project scanner")
return errors.Wrap(err, "api controller: set project scanner")
}
}
@ -159,21 +159,21 @@ func (bc *basicController) GetRegistrationByProject(projectID int64) (*scanner.R
// First, get it from the project metadata
m, err := bc.proMetaMgr.Get(projectID, proScannerMetaKey)
if err != nil {
return nil, errors.Wrap(err, "get project scanner")
return nil, errors.Wrap(err, "api controller: get project scanner")
}
if len(m) > 0 {
if registrationID, ok := m[proScannerMetaKey]; ok && len(registrationID) > 0 {
registration, err := bc.manager.Get(registrationID)
if err != nil {
return nil, errors.Wrap(err, "get project scanner")
return nil, errors.Wrap(err, "api controller: get project scanner")
}
if registration == nil {
// Not found
// Might be deleted by the admin, the project scanner ID reference should be cleared
if err := bc.proMetaMgr.Delete(projectID, proScannerMetaKey); err != nil {
return nil, errors.Wrap(err, "get project scanner")
return nil, errors.Wrap(err, "api controller: get project scanner")
}
} else {
return registration, nil
@ -187,8 +187,3 @@ func (bc *basicController) GetRegistrationByProject(projectID int64) (*scanner.R
// TODO: Check status by the client later
return registration, err
}
// Ping ...
func (bc *basicController) Ping(registration *scanner.Registration) error {
return nil
}

View File

@ -12,13 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package api
package scanner
import (
"github.com/goharbor/harbor/src/pkg/q"
dscan "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
)
// Controller provides the related operations of scanner for the upper API.
@ -114,44 +112,4 @@ type Controller interface {
// *scanner.Registration : the default scanner registration
// error : non nil error if any errors occurred
GetRegistrationByProject(projectID int64) (*scanner.Registration, error)
// Ping pings Scanner Adapter to test EndpointURL and Authorization settings.
// The implementation is supposed to call the GetMetadata method on scanner.Client.
// Returns `nil` if connection succeeded, a non `nil` error otherwise.
//
// Arguments:
// registration *scanner.Registration : scanner registration to ping
//
// Returns:
// error : non nil error if any errors occurred
Ping(registration *scanner.Registration) error
// Scan the given artifact
//
// Arguments:
// artifact *res.Artifact : artifact to be scanned
//
// Returns:
// error : non nil error if any errors occurred
Scan(artifact *scan.Artifact) error
// GetReport gets the reports for the given artifact identified by the digest
//
// Arguments:
// artifact *res.Artifact : the scanned artifact
//
// Returns:
// []*scan.Report : scan results by different scanner vendors
// error : non nil error if any errors occurred
GetReport(artifact *scan.Artifact) ([]*dscan.Report, error)
// Get the scan log for the specified artifact with the given digest
//
// Arguments:
// digest string : the digest of the artifact
//
// Returns:
// []byte : the log text stream
// error : non nil error if any errors occurred
GetScanLog(digest string) ([]byte, error)
}

View File

@ -12,14 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package api
package scanner
import (
"testing"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -56,9 +56,6 @@ func (suite *ControllerTestSuite) SetupSuite() {
Name: "forUT",
Description: "sample registration",
URL: "https://sample.scanner.com",
Adapter: "Clair",
Version: "0.1.0",
Vendor: "Harbor",
}
}

View File

@ -16,15 +16,18 @@ package scan
import "time"
// Report of the scan
// Identified by the `digest` and `endpoint_id`
// Report of the scan.
// Identified by the `digest`, `registration_uuid` and `mime_type`.
type Report struct {
ID int64 `orm:"pk;auto;column(id)"`
UUID string `orm:"unique;column(uuid)"`
Digest string `orm:"column(digest)"`
ReregistrationID string `orm:"column(registration_id)"`
RegistrationUUID string `orm:"column(registration_uuid)"`
MimeType string `orm:"column(mime_type)"`
JobID string `orm:"column(job_id)"`
Status string `orm:"column(status)"`
StatusCode int `orm:"column(status_code)"`
StatusRevision int64 `orm:"column(status_rev)"`
Report string `orm:"column(report);type(json)"`
StartTime time.Time `orm:"column(start_time);auto_now_add;type(datetime)"`
EndTime time.Time `orm:"column(end_time);type(datetime)"`
@ -32,12 +35,13 @@ type Report struct {
// TableName for Report
func (r *Report) TableName() string {
return "scanner_report"
return "scan_report"
}
// TableUnique for Report
func (r *Report) TableUnique() [][]string {
return [][]string{
{"digest", "registration_id"},
{"uuid"},
{"digest", "registration_uuid", "mime_type"},
}
}

View File

@ -0,0 +1,140 @@
// 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 scan
import (
"fmt"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/pkg/errors"
)
func init() {
orm.RegisterModel(new(Report))
}
// CreateReport creates new report
func CreateReport(r *Report) (int64, error) {
o := dao.GetOrmer()
return o.Insert(r)
}
// DeleteReport deletes the given report
func DeleteReport(uuid string) error {
o := dao.GetOrmer()
qt := o.QueryTable(new(Report))
// Delete report with query way
count, err := qt.Filter("uuid", uuid).Delete()
if err != nil {
return err
}
if count == 0 {
return errors.Errorf("no report with uuid %s deleted", uuid)
}
return nil
}
// ListReports lists the reports with given query parameters.
// Keywords in query here will be enforced with `exact` way.
func ListReports(query *q.Query) ([]*Report, error) {
o := dao.GetOrmer()
qt := o.QueryTable(new(Report))
if query != nil {
if len(query.Keywords) > 0 {
for k, v := range query.Keywords {
if vv, ok := v.([]interface{}); ok {
qt = qt.Filter(fmt.Sprintf("%s__in", k), vv...)
}
qt = qt.Filter(k, v)
}
}
if query.PageNumber > 0 && query.PageSize > 0 {
qt = qt.Limit(query.PageSize, (query.PageNumber-1)*query.PageSize)
}
}
l := make([]*Report, 0)
_, err := qt.All(&l)
return l, err
}
// UpdateReportData only updates the `report` column with conditions matched.
func UpdateReportData(uuid string, report string, statusRev int64) error {
o := dao.GetOrmer()
qt := o.QueryTable(new(Report))
data := make(orm.Params)
data["report"] = report
data["status_rev"] = statusRev
count, err := qt.Filter("uuid", uuid).
Filter("status_rev__lte", statusRev).Update(data)
if err != nil {
return err
}
if count == 0 {
return errors.Errorf("no report with uuid %s updated", uuid)
}
return nil
}
// UpdateReportStatus updates the report `status` with conditions matched.
func UpdateReportStatus(uuid string, status string, statusCode int, statusRev int64) error {
o := dao.GetOrmer()
qt := o.QueryTable(new(Report))
data := make(orm.Params)
data["status"] = status
data["status_code"] = statusCode
data["status_rev"] = statusRev
count, err := qt.Filter("uuid", uuid).
Filter("status_rev__lte", statusRev).
Filter("status_code__lte", statusCode).Update(data)
if err != nil {
return err
}
if count == 0 {
return errors.Errorf("no report with uuid %s updated", uuid)
}
return nil
}
// UpdateJobID updates the report `job_id` column
func UpdateJobID(uuid string, jobID string) error {
o := dao.GetOrmer()
qt := o.QueryTable(new(Report))
params := make(orm.Params, 1)
params["job_id"] = jobID
_, err := qt.Filter("uuid", uuid).Update(params)
return err
}

View File

@ -0,0 +1,131 @@
// 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 scan
import (
"testing"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/q"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// ReportTestSuite is test suite of testing report DAO.
type ReportTestSuite struct {
suite.Suite
}
// TestReport is the entry of ReportTestSuite.
func TestReport(t *testing.T) {
suite.Run(t, &ReportTestSuite{})
}
// SetupSuite prepares env for test suite.
func (suite *ReportTestSuite) SetupSuite() {
dao.PrepareTestForPostgresSQL()
}
// SetupTest prepares env for test case.
func (suite *ReportTestSuite) SetupTest() {
r := &Report{
UUID: "uuid",
Digest: "digest1001",
RegistrationUUID: "ruuid",
MimeType: v1.MimeTypeNativeReport,
Status: job.PendingStatus.String(),
StatusCode: job.PendingStatus.Code(),
}
id, err := CreateReport(r)
require.NoError(suite.T(), err)
require.Condition(suite.T(), func() (success bool) {
success = id > 0
return
})
}
// TearDownTest clears enf for test case.
func (suite *ReportTestSuite) TearDownTest() {
err := DeleteReport("uuid")
require.NoError(suite.T(), err)
}
// TestReportList tests list reports with query parameters.
func (suite *ReportTestSuite) TestReportList() {
query1 := &q.Query{
PageSize: 1,
PageNumber: 1,
Keywords: map[string]interface{}{
"digest": "digest1001",
"registration_uuid": "ruuid",
"mime_type": v1.MimeTypeNativeReport,
},
}
l, err := ListReports(query1)
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
query2 := &q.Query{
PageSize: 1,
PageNumber: 1,
Keywords: map[string]interface{}{
"digest": "digest1002",
},
}
l, err = ListReports(query2)
require.NoError(suite.T(), err)
require.Equal(suite.T(), 0, len(l))
}
// TestReportUpdateJobID tests update job ID of the report.
func (suite *ReportTestSuite) TestReportUpdateJobID() {
err := UpdateJobID("uuid", "jobid001")
require.NoError(suite.T(), err)
l, err := ListReports(nil)
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
assert.Equal(suite.T(), "jobid001", l[0].JobID)
}
// TestReportUpdateReportData tests update the report data.
func (suite *ReportTestSuite) TestReportUpdateReportData() {
err := UpdateReportData("uuid", "{}", 1000)
require.NoError(suite.T(), err)
l, err := ListReports(nil)
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
assert.Equal(suite.T(), "{}", l[0].Report)
err = UpdateReportData("uuid", "{\"a\": 900}", 900)
require.Error(suite.T(), err)
}
// TestReportUpdateStatus tests update the report status.
func (suite *ReportTestSuite) TestReportUpdateStatus() {
err := UpdateReportStatus("uuid", job.RunningStatus.String(), job.RunningStatus.Code(), 1000)
require.NoError(suite.T(), err)
err = UpdateReportStatus("uuid", job.RunningStatus.String(), job.RunningStatus.Code(), 900)
require.Error(suite.T(), err)
err = UpdateReportStatus("uuid", job.PendingStatus.String(), job.PendingStatus.Code(), 1000)
require.Error(suite.T(), err)
}

View File

@ -45,11 +45,6 @@ type Registration struct {
// Http connection settings
SkipCertVerify bool `orm:"column(skip_cert_verify);default(false)" json:"skip_certVerify"`
// Adapter settings
Adapter string `orm:"column(adapter);size(128)" json:"adapter"`
Vendor string `orm:"column(vendor);size(128)" json:"vendor"`
Version string `orm:"column(version);size(32)" json:"version"`
// Timestamps
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
@ -60,7 +55,7 @@ func (r *Registration) TableName() string {
return "scanner_registration"
}
// FromJSON parses json data
// FromJSON parses registration from json data
func (r *Registration) FromJSON(jsonData string) error {
if len(jsonData) == 0 {
return errors.New("empty json data to parse")
@ -69,7 +64,7 @@ func (r *Registration) FromJSON(jsonData string) error {
return json.Unmarshal([]byte(jsonData), r)
}
// ToJSON marshals endpoint to JSON data
// ToJSON marshals registration to JSON data
func (r *Registration) ToJSON() (string, error) {
data, err := json.Marshal(r)
if err != nil {
@ -79,7 +74,7 @@ func (r *Registration) ToJSON() (string, error) {
return string(data), nil
}
// Validate endpoint
// Validate registration
func (r *Registration) Validate(checkUUID bool) error {
if checkUUID && len(r.UUID) == 0 {
return errors.New("malformed endpoint")
@ -94,12 +89,6 @@ func (r *Registration) Validate(checkUUID bool) error {
return errors.Wrap(err, "scanner registration validate")
}
if len(r.Adapter) == 0 ||
len(r.Vendor) == 0 ||
len(r.Version) == 0 {
return errors.Errorf("missing adapter settings in registration %s:%s", r.Name, r.URL)
}
return nil
}

View File

@ -38,9 +38,6 @@ func (suite *ModelTestSuite) TestJSON() {
Name: "forUT",
Description: "sample registration",
URL: "https://sample.scanner.com",
Adapter: "Clair",
Version: "0.1.0",
Vendor: "Harbor",
}
json, err := r.ToJSON()
@ -77,11 +74,8 @@ func (suite *ModelTestSuite) TestValidate() {
r.URL = "http://a.b.c"
err = r.Validate(true)
require.Error(suite.T(), err)
require.NoError(suite.T(), err)
r.Adapter = "Clair"
r.Vendor = "Harbor"
r.Version = "0.1.0"
err = r.Validate(true)
require.NoError(suite.T(), err)
}

View File

@ -50,9 +50,6 @@ func (suite *RegistrationDAOTestSuite) SetupTest() {
Name: "forUT",
Description: "sample registration",
URL: "https://sample.scanner.com",
Adapter: "Clair",
Version: "0.1.0",
Vendor: "Harbor",
}
_, err := AddRegistration(r)
@ -110,8 +107,8 @@ func (suite *RegistrationDAOTestSuite) TestList() {
require.Equal(suite.T(), 1, len(l))
// with query and found items
keywords := make(map[string]string)
keywords["adapter"] = "Clair"
keywords := make(map[string]interface{})
keywords["description"] = "sample"
l, err = ListRegistrations(&q.Query{
PageSize: 5,
PageNumber: 1,
@ -121,7 +118,7 @@ func (suite *RegistrationDAOTestSuite) TestList() {
require.Equal(suite.T(), 1, len(l))
// With query and not found items
keywords["adapter"] = "Micro scanner"
keywords["description"] = "not_exist"
l, err = ListRegistrations(&q.Query{
Keywords: keywords,
})

317
src/pkg/scan/job.go Normal file
View File

@ -0,0 +1,317 @@
// 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 scan
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"sync"
"time"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/report"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/pkg/errors"
)
const (
// JobParamRegistration ...
JobParamRegistration = "registration"
// JobParameterRequest ...
JobParameterRequest = "scanRequest"
// JobParameterMimes ...
JobParameterMimes = "mimeTypes"
checkTimeout = 30 * time.Minute
firstCheckInterval = 2 * time.Second
)
// CheckInReport defines model for checking in the scan report with specified mime.
type CheckInReport struct {
Digest string `json:"digest"`
RegistrationUUID string `json:"registration_uuid"`
MimeType string `json:"mime_type"`
RawReport string `json:"raw_report"`
}
// FromJSON parse json to CheckInReport
func (cir *CheckInReport) FromJSON(jsonData string) error {
if len(jsonData) == 0 {
return errors.New("empty JSON data")
}
return json.Unmarshal([]byte(jsonData), cir)
}
// ToJSON marshal CheckInReport to JSON
func (cir *CheckInReport) ToJSON() (string, error) {
jsonData, err := json.Marshal(cir)
if err != nil {
return "", errors.Wrap(err, "To JSON: CheckInReport")
}
return string(jsonData), nil
}
// Job for running scan in the job service with async way
type Job struct{}
// MaxFails for defining the number of retries
func (j *Job) MaxFails() uint {
return 3
}
// ShouldRetry indicates if the job should be retried
func (j *Job) ShouldRetry() bool {
return true
}
// Validate the parameters of this job
func (j *Job) Validate(params job.Parameters) error {
if params == nil {
// Params are required
return errors.New("missing parameter of scan job")
}
if _, err := extractRegistration(params); err != nil {
return errors.Wrap(err, "job validate")
}
if _, err := extractScanReq(params); err != nil {
return errors.Wrap(err, "job validate")
}
if _, err := extractMimeTypes(params); err != nil {
return errors.Wrap(err, "job validate")
}
return nil
}
// Run the job
func (j *Job) Run(ctx job.Context, params job.Parameters) error {
// Get logger
myLogger := ctx.GetLogger()
// Ignore errors as they have been validated already
r, _ := extractRegistration(params)
req, _ := extractScanReq(params)
mimes, _ := extractMimeTypes(params)
// Print related infos to log
printJSONParameter(JobParamRegistration, params[JobParamRegistration].(string), myLogger)
printJSONParameter(JobParameterRequest, params[JobParameterRequest].(string), myLogger)
// Submit scan request to the scanner adapter
client, err := v1.DefaultClientPool.Get(r)
if err != nil {
return errors.Wrap(err, "run scan job")
}
resp, err := client.SubmitScan(req)
if err != nil {
return errors.Wrap(err, "run scan job")
}
// For collecting errors
errs := make([]error, len(mimes))
// Concurrently retrieving report by different mime types
wg := &sync.WaitGroup{}
wg.Add(len(mimes))
for i, mt := range mimes {
go func(i int, m string) {
defer wg.Done()
// Log info
myLogger.Infof("Get report for mime type: %s", m)
// Loop check if the report is ready
tm := time.NewTimer(firstCheckInterval)
defer tm.Stop()
for {
select {
case t := <-tm.C:
myLogger.Debugf("check scan report for mime %s at %s", m, t.Format("2006/01/02 15:04:05"))
rawReport, err := client.GetScanReport(resp.ID, m)
if err != nil {
// Not ready yet
if notReadyErr, ok := err.(*v1.ReportNotReadyError); ok {
// Reset to the new check interval
tm.Reset(time.Duration(notReadyErr.RetryAfter) * time.Second)
myLogger.Infof("Report with mime type %s is not ready yet, retry after %d seconds", m, notReadyErr.RetryAfter)
continue
}
errs[i] = errors.Wrap(err, fmt.Sprintf("check scan report with mime type %s", m))
return
}
// Make sure the data is aligned with the v1 spec.
if _, err = report.ResolveData(m, []byte(rawReport)); err != nil {
errs[i] = errors.Wrap(err, "scan job: resolve report data")
return
}
// Check in
cir := &CheckInReport{
Digest: req.Artifact.Digest,
RegistrationUUID: r.UUID,
MimeType: m,
RawReport: rawReport,
}
var (
jsonData string
er error
)
if jsonData, er = cir.ToJSON(); er == nil {
if er = ctx.Checkin(jsonData); er == nil {
// Done!
myLogger.Infof("Report with mime type %s is checked in", m)
return
}
}
// Send error and exit
errs[i] = errors.Wrap(er, fmt.Sprintf("check in scan report for mime type %s", m))
return
case <-ctx.SystemContext().Done():
// Terminated by system
return
case <-time.After(checkTimeout):
errs[i] = errors.New("check scan report timeout")
return
}
}
}(i, mt)
}
// Wait for all the retrieving routines are completed
wg.Wait()
// Merge errors
for _, e := range errs {
if e != nil {
if err != nil {
err = errors.Wrap(e, err.Error())
} else {
err = e
}
}
}
// Log error to the job log
if err != nil {
myLogger.Error(err)
}
return err
}
func printJSONParameter(parameter string, v string, logger logger.Interface) {
logger.Infof("%s:\n", parameter)
printPrettyJSON([]byte(v), logger)
}
func printPrettyJSON(in []byte, logger logger.Interface) {
var out bytes.Buffer
if err := json.Indent(&out, in, "", " "); err != nil {
logger.Errorf("Print pretty JSON error: %s", err)
return
}
logger.Infof("%s\n", out.String())
}
func extractScanReq(params job.Parameters) (*v1.ScanRequest, error) {
v, ok := params[JobParameterRequest]
if !ok {
return nil, errors.Errorf("missing job parameter '%s'", JobParameterRequest)
}
jsonData, ok := v.(string)
if !ok {
return nil, errors.Errorf(
"malformed job parameter '%s', expecting string but got %s",
JobParameterRequest,
reflect.TypeOf(v).String(),
)
}
req := &v1.ScanRequest{}
if err := req.FromJSON(jsonData); err != nil {
return nil, err
}
if err := req.Validate(); err != nil {
return nil, err
}
return req, nil
}
func extractRegistration(params job.Parameters) (*scanner.Registration, error) {
v, ok := params[JobParamRegistration]
if !ok {
return nil, errors.Errorf("missing job parameter '%s'", JobParamRegistration)
}
jsonData, ok := v.(string)
if !ok {
return nil, errors.Errorf(
"malformed job parameter '%s', expecting string but got %s",
JobParamRegistration,
reflect.TypeOf(v).String(),
)
}
r := &scanner.Registration{}
if err := r.FromJSON(jsonData); err != nil {
return nil, err
}
if err := r.Validate(true); err != nil {
return nil, err
}
return r, nil
}
func extractMimeTypes(params job.Parameters) ([]string, error) {
v, ok := params[JobParameterMimes]
if !ok {
return nil, errors.Errorf("missing job parameter '%s'", JobParameterMimes)
}
l, ok := v.([]string)
if !ok {
return nil, errors.Errorf(
"malformed job parameter '%s', expecting string but got %s",
JobParameterMimes,
reflect.TypeOf(v).String(),
)
}
return l, nil
}

306
src/pkg/scan/job_test.go Normal file
View File

@ -0,0 +1,306 @@
// 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 scan
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// JobTestSuite is a test suite to test the scan job.
type JobTestSuite struct {
suite.Suite
defaultClientPool v1.ClientPool
mcp *MockClientPool
}
// TestJob is the entry of JobTestSuite.
func TestJob(t *testing.T) {
suite.Run(t, &JobTestSuite{})
}
// SetupSuite sets up test env for JobTestSuite.
func (suite *JobTestSuite) SetupSuite() {
mcp := &MockClientPool{}
suite.defaultClientPool = v1.DefaultClientPool
v1.DefaultClientPool = mcp
suite.mcp = mcp
}
// TeraDownSuite clears test env for TeraDownSuite.
func (suite *JobTestSuite) TeraDownSuite() {
v1.DefaultClientPool = suite.defaultClientPool
}
// TestJob tests the scan job
func (suite *JobTestSuite) TestJob() {
ctx := &MockJobContext{}
lg := &MockJobLogger{}
ctx.On("GetLogger").Return(lg)
r := &scanner.Registration{
ID: 0,
UUID: "uuid",
Name: "TestJob",
URL: "https://clair.com:8080",
}
rData, err := r.ToJSON()
require.NoError(suite.T(), err)
sr := &v1.ScanRequest{
Registry: &v1.Registry{
URL: "http://localhost:5000",
Authorization: "the_token",
},
Artifact: &v1.Artifact{
Repository: "library/test_job",
Digest: "sha256:data",
MimeType: v1.MimeTypeDockerArtifact,
},
}
sData, err := sr.ToJSON()
require.NoError(suite.T(), err)
mimeTypes := []string{v1.MimeTypeNativeReport}
jp := make(job.Parameters)
jp[JobParamRegistration] = rData
jp[JobParameterRequest] = sData
jp[JobParameterMimes] = mimeTypes
mc := &MockClient{}
sre := &v1.ScanResponse{
ID: "scan_id",
}
mc.On("SubmitScan", sr).Return(sre, nil)
rp := vuln.Report{
GeneratedAt: time.Now().UTC().String(),
Scanner: &v1.Scanner{
Name: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
},
Severity: vuln.High,
Vulnerabilities: []*vuln.VulnerabilityItem{
{
ID: "2019-0980-0909",
Package: "dpkg",
Version: "0.9.1",
FixVersion: "0.9.2",
Severity: vuln.High,
Description: "mock one",
Links: []string{"https://vuln.com"},
},
},
}
jRep, err := json.Marshal(rp)
require.NoError(suite.T(), err)
mc.On("GetScanReport", "scan_id", v1.MimeTypeNativeReport).Return(string(jRep), nil)
suite.mcp.On("Get", r).Return(mc, nil)
crp := &CheckInReport{
Digest: sr.Artifact.Digest,
RegistrationUUID: r.UUID,
MimeType: v1.MimeTypeNativeReport,
RawReport: string(jRep),
}
jsonData, err := crp.ToJSON()
require.NoError(suite.T(), err)
ctx.On("Checkin", string(jsonData)).Return(nil)
j := &Job{}
err = j.Run(ctx, jp)
require.NoError(suite.T(), err)
}
// MockJobContext mocks job context interface.
// TODO: Maybe moved to a separate `mock` pkg for sharing in future.
type MockJobContext struct {
mock.Mock
}
// Build ...
func (mjc *MockJobContext) Build(tracker job.Tracker) (job.Context, error) {
args := mjc.Called(tracker)
c := args.Get(0)
if c != nil {
return c.(job.Context), nil
}
return nil, args.Error(1)
}
// Get ...
func (mjc *MockJobContext) Get(prop string) (interface{}, bool) {
args := mjc.Called(prop)
return args.Get(0), args.Bool(1)
}
// SystemContext ...
func (mjc *MockJobContext) SystemContext() context.Context {
return context.TODO()
}
// Checkin ...
func (mjc *MockJobContext) Checkin(status string) error {
args := mjc.Called(status)
return args.Error(0)
}
// OPCommand ...
func (mjc *MockJobContext) OPCommand() (job.OPCommand, bool) {
args := mjc.Called()
return (job.OPCommand)(args.String(0)), args.Bool(1)
}
// GetLogger ...
func (mjc *MockJobContext) GetLogger() logger.Interface {
return &MockJobLogger{}
}
// Tracker ...
func (mjc *MockJobContext) Tracker() job.Tracker {
args := mjc.Called()
if t := args.Get(0); t != nil {
return t.(job.Tracker)
}
return nil
}
// MockJobLogger mocks the job logger interface.
// TODO: Maybe moved to a separate `mock` pkg for sharing in future.
type MockJobLogger struct {
mock.Mock
}
// Debug ...
func (mjl *MockJobLogger) Debug(v ...interface{}) {
logger.Debug(v...)
}
// Debugf ...
func (mjl *MockJobLogger) Debugf(format string, v ...interface{}) {
logger.Debugf(format, v...)
}
// Info ...
func (mjl *MockJobLogger) Info(v ...interface{}) {
logger.Info(v...)
}
// Infof ...
func (mjl *MockJobLogger) Infof(format string, v ...interface{}) {
logger.Infof(format, v...)
}
// Warning ...
func (mjl *MockJobLogger) Warning(v ...interface{}) {
logger.Warning(v...)
}
// Warningf ...
func (mjl *MockJobLogger) Warningf(format string, v ...interface{}) {
logger.Warningf(format, v...)
}
// Error ...
func (mjl *MockJobLogger) Error(v ...interface{}) {
logger.Error(v...)
}
// Errorf ...
func (mjl *MockJobLogger) Errorf(format string, v ...interface{}) {
logger.Errorf(format, v...)
}
// Fatal ...
func (mjl *MockJobLogger) Fatal(v ...interface{}) {
logger.Fatal(v...)
}
// Fatalf ...
func (mjl *MockJobLogger) Fatalf(format string, v ...interface{}) {
logger.Fatalf(format, v...)
}
// MockClientPool mocks the client pool
type MockClientPool struct {
mock.Mock
}
// Get v1 client
func (mcp *MockClientPool) Get(r *scanner.Registration) (v1.Client, error) {
args := mcp.Called(r)
c := args.Get(0)
if c != nil {
return c.(v1.Client), nil
}
return nil, args.Error(1)
}
// MockClient mocks the v1 client
type MockClient struct {
mock.Mock
}
// GetMetadata ...
func (mc *MockClient) GetMetadata() (*v1.ScannerAdapterMetadata, error) {
args := mc.Called()
s := args.Get(0)
if s != nil {
return s.(*v1.ScannerAdapterMetadata), nil
}
return nil, args.Error(1)
}
// SubmitScan ...
func (mc *MockClient) SubmitScan(req *v1.ScanRequest) (*v1.ScanResponse, error) {
args := mc.Called(req)
sr := args.Get(0)
if sr != nil {
return sr.(*v1.ScanResponse), nil
}
return nil, args.Error(1)
}
// GetScanReport ...
func (mc *MockClient) GetScanReport(scanRequestID, reportMIMEType string) (string, error) {
args := mc.Called(scanRequestID, reportMIMEType)
return args.String(0), args.Error(1)
}

View File

@ -0,0 +1,169 @@
// 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 report
import (
"time"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
"github.com/google/uuid"
"github.com/pkg/errors"
)
// basicManager is a default implementation of report manager.
type basicManager struct{}
// NewManager news basic manager.
func NewManager() Manager {
return &basicManager{}
}
// Create ...
func (bm *basicManager) Create(r *scan.Report) (string, error) {
// Validate report object
if r == nil {
return "", errors.New("nil scan report object")
}
if len(r.Digest) == 0 || len(r.RegistrationUUID) == 0 || len(r.MimeType) == 0 {
return "", errors.New("malformed scan report object")
}
// Check if there is existing report copy
// Limit only one scanning performed by a given provider on the specified artifact can be there
kws := make(map[string]interface{}, 3)
kws["digest"] = r.Digest
kws["registration_uuid"] = r.RegistrationUUID
kws["mime_type"] = []interface{}{r.MimeType}
existingCopies, err := scan.ListReports(&q.Query{
PageNumber: 1,
PageSize: 1,
Keywords: kws,
})
if err != nil {
return "", errors.Wrap(err, "check existence of report")
}
// Delete existing copy
if len(existingCopies) > 0 {
theCopy := existingCopies[0]
// Status conflict
theStatus := job.Status(theCopy.Status)
if theStatus.Compare(job.RunningStatus) <= 0 {
return "", errors.Errorf("conflict: a previous scanning is %s", theCopy.Status)
}
// Otherwise it will be a completed report
// Clear it before insert this new one
if err := scan.DeleteReport(theCopy.UUID); err != nil {
return "", errors.Wrap(err, "clear old scan report")
}
}
// Assign uuid
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
r.StartTime = time.Now().UTC()
r.Status = job.PendingStatus.String()
r.StatusCode = job.PendingStatus.Code()
// Insert
if _, err = scan.CreateReport(r); err != nil {
return "", errors.Wrap(err, "create report")
}
return r.UUID, nil
}
// GetBy ...
func (bm *basicManager) GetBy(digest string, registrationUUID string, mimeTypes []string) ([]*scan.Report, error) {
if len(digest) == 0 {
return nil, errors.New("empty digest to get report data")
}
kws := make(map[string]interface{})
kws["digest"] = digest
if len(registrationUUID) > 0 {
kws["registration_uuid"] = registrationUUID
}
if len(mimeTypes) > 0 {
kws["mime_type"] = mimeTypes
}
// Query all
query := &q.Query{
PageNumber: 0,
Keywords: kws,
}
return scan.ListReports(query)
}
// UpdateScanJobID ...
func (bm *basicManager) UpdateScanJobID(uuid string, jobID string) error {
if len(uuid) == 0 || len(jobID) == 0 {
return errors.New("bad arguments")
}
return scan.UpdateJobID(uuid, jobID)
}
// UpdateStatus ...
func (bm *basicManager) UpdateStatus(uuid string, status string, rev int64) error {
if len(uuid) == 0 {
return errors.New("missing uuid")
}
if rev <= 0 {
return errors.New("invalid data revision")
}
stCode := job.ErrorStatus.Code()
st := job.Status(status)
// Check if it is job valid status.
// Probably an error happened before submitting jobs.
if st.Code() != -1 {
// Assign error code
stCode = st.Code()
}
return scan.UpdateReportStatus(uuid, status, stCode, rev)
}
// UpdateReportData ...
func (bm *basicManager) UpdateReportData(uuid string, report string, rev int64) error {
if len(uuid) == 0 {
return errors.New("missing uuid")
}
if rev <= 0 {
return errors.New("invalid data revision")
}
if len(report) == 0 {
return errors.New("missing report JSON data")
}
return scan.UpdateReportData(uuid, report, rev)
}

View File

@ -0,0 +1,156 @@
// 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 report
import (
"testing"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/jobservice/job"
"github.com/goharbor/harbor/src/pkg/scan/dao/scan"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// TestManagerSuite is a test suite for the report manager.
type TestManagerSuite struct {
suite.Suite
m Manager
rpUUID string
}
// TestManager is an entry of suite TestManagerSuite.
func TestManager(t *testing.T) {
suite.Run(t, &TestManagerSuite{})
}
// SetupSuite prepares test env for suite TestManagerSuite.
func (suite *TestManagerSuite) SetupSuite() {
dao.PrepareTestForPostgresSQL()
suite.m = NewManager()
}
// SetupTest prepares env for test cases.
func (suite *TestManagerSuite) SetupTest() {
rp := &scan.Report{
Digest: "d1000",
RegistrationUUID: "ruuid",
MimeType: v1.MimeTypeNativeReport,
}
uuid, err := suite.m.Create(rp)
require.NoError(suite.T(), err)
require.NotEmpty(suite.T(), uuid)
suite.rpUUID = uuid
}
// TearDownTest clears test env for test cases.
func (suite *TestManagerSuite) TearDownTest() {
// No delete method defined in manager as no requirement,
// so, to clear env, call dao method here
err := scan.DeleteReport(suite.rpUUID)
require.NoError(suite.T(), err)
}
// TestManagerCreateWithExisting tests the case that a copy already is there when creating report.
func (suite *TestManagerSuite) TestManagerCreateWithExisting() {
err := suite.m.UpdateStatus(suite.rpUUID, job.SuccessStatus.String(), 2000)
require.NoError(suite.T(), err)
rp := &scan.Report{
Digest: "d1000",
RegistrationUUID: "ruuid",
MimeType: v1.MimeTypeNativeReport,
}
uuid, err := suite.m.Create(rp)
require.NoError(suite.T(), err)
require.NotEmpty(suite.T(), uuid)
assert.NotEqual(suite.T(), suite.rpUUID, uuid)
suite.rpUUID = uuid
}
// TestManagerGetBy tests the get by method.
func (suite *TestManagerSuite) TestManagerGetBy() {
l, err := suite.m.GetBy("d1000", "ruuid", []string{v1.MimeTypeNativeReport})
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
assert.Equal(suite.T(), suite.rpUUID, l[0].UUID)
l, err = suite.m.GetBy("d1000", "ruuid", nil)
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
assert.Equal(suite.T(), suite.rpUUID, l[0].UUID)
l, err = suite.m.GetBy("d1000", "", nil)
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
assert.Equal(suite.T(), suite.rpUUID, l[0].UUID)
}
// TestManagerUpdateJobID tests update job ID method.
func (suite *TestManagerSuite) TestManagerUpdateJobID() {
l, err := suite.m.GetBy("d1000", "ruuid", []string{v1.MimeTypeNativeReport})
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
oldJID := l[0].JobID
err = suite.m.UpdateScanJobID(suite.rpUUID, "jID1001")
require.NoError(suite.T(), err)
l, err = suite.m.GetBy("d1000", "ruuid", []string{v1.MimeTypeNativeReport})
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
assert.NotEqual(suite.T(), oldJID, l[0].JobID)
assert.Equal(suite.T(), "jID1001", l[0].JobID)
}
// TestManagerUpdateStatus tests update status method
func (suite *TestManagerSuite) TestManagerUpdateStatus() {
l, err := suite.m.GetBy("d1000", "ruuid", []string{v1.MimeTypeNativeReport})
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
oldSt := l[0].Status
err = suite.m.UpdateStatus(suite.rpUUID, job.SuccessStatus.String(), 10000)
require.NoError(suite.T(), err)
l, err = suite.m.GetBy("d1000", "ruuid", []string{v1.MimeTypeNativeReport})
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
assert.NotEqual(suite.T(), oldSt, l[0].Status)
assert.Equal(suite.T(), job.SuccessStatus.String(), l[0].Status)
}
// TestManagerUpdateReportData tests update job report data.
func (suite *TestManagerSuite) TestManagerUpdateReportData() {
err := suite.m.UpdateReportData(suite.rpUUID, "{\"a\":1000}", 1000)
require.NoError(suite.T(), err)
l, err := suite.m.GetBy("d1000", "ruuid", []string{v1.MimeTypeNativeReport})
require.NoError(suite.T(), err)
require.Equal(suite.T(), 1, len(l))
assert.Equal(suite.T(), "{\"a\":1000}", l[0].Report)
}

View File

@ -0,0 +1,80 @@
// 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 report
import "github.com/goharbor/harbor/src/pkg/scan/dao/scan"
// Manager is used to manage the scan reports.
type Manager interface {
// Create a new report record.
//
// Arguments:
// r *scan.Report : report model to be created
//
// Returns:
// string : uuid of the new report
// error : non nil error if any errors occurred
//
Create(r *scan.Report) (string, error)
// Update the scan job ID of the given report.
//
// Arguments:
// uuid string : uuid to identify the report
// jobID string: scan job ID
//
// Returns:
// error : non nil error if any errors occurred
//
UpdateScanJobID(uuid string, jobID string) error
// Update the status (mapping to the scan job status) of the given report.
//
// Arguments:
// uuid string : uuid to identify the report
// status string: status info
// rev int64 : data revision info
//
// Returns:
// error : non nil error if any errors occurred
//
UpdateStatus(uuid string, status string, rev int64) error
// Update the report data (with JSON format) of the given report.
//
// Arguments:
// uuid string : uuid to identify the report
// report string: report JSON data
// rev int64 : data revision info
//
// Returns:
// error : non nil error if any errors occurred
//
UpdateReportData(uuid string, report string, rev int64) error
// Get the reports for the given digest by other properties.
//
// Arguments:
// digest string : digest of the artifact
// registrationUUID string : [optional] the report generated by which registration.
// If it is empty, reports by all the registrations are retrieved.
// mimeTypes []string : [optional] mime types of the reports requiring
// If empty array is specified, reports with all the supported mimes are retrieved.
//
// Returns:
// []*scan.Report : report list
// error : non nil error if any errors occurred
GetBy(digest string, registrationUUID string, mimeTypes []string) ([]*scan.Report, error)
}

View File

@ -0,0 +1,78 @@
// 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 report
import (
"encoding/json"
"testing"
"time"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// SupportedMimesSuite is a suite to test SupportedMimes.
type SupportedMimesSuite struct {
suite.Suite
mockData []byte
}
// TestSupportedMimesSuite is the entry of SupportedMimesSuite.
func TestSupportedMimesSuite(t *testing.T) {
suite.Run(t, new(SupportedMimesSuite))
}
// SetupSuite prepares the test suite env.
func (suite *SupportedMimesSuite) SetupSuite() {
rp := vuln.Report{
GeneratedAt: time.Now().UTC().String(),
Scanner: &v1.Scanner{
Name: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
},
Severity: vuln.High,
Vulnerabilities: []*vuln.VulnerabilityItem{
{
ID: "2019-0980-0909",
Package: "dpkg",
Version: "0.9.1",
FixVersion: "0.9.2",
Severity: vuln.High,
Description: "mock one",
Links: []string{"https://vuln.com"},
},
},
}
jsonData, err := json.Marshal(rp)
require.NoError(suite.T(), err)
suite.mockData = jsonData
}
// TestResolveData tests the ResolveData.
func (suite *SupportedMimesSuite) TestResolveData() {
obj, err := ResolveData(v1.MimeTypeNativeReport, suite.mockData)
require.NoError(suite.T(), err)
require.Condition(suite.T(), func() (success bool) {
rp, ok := obj.(*vuln.Report)
success = ok && rp != nil && rp.Severity == vuln.High
return
})
}

View File

@ -0,0 +1,56 @@
// 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 report
import (
"encoding/json"
"reflect"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
"github.com/pkg/errors"
)
// SupportedMimes indicates what mime types are supported to render at UI end.
var SupportedMimes = map[string]interface{}{
// The native report type
v1.MimeTypeNativeReport: (*vuln.Report)(nil),
}
// ResolveData is a helper func to parse the JSON data with the given mime type.
func ResolveData(mime string, jsonData []byte) (interface{}, error) {
if len(jsonData) == 0 {
return nil, errors.New("empty JSON data")
}
t, ok := SupportedMimes[mime]
if !ok {
return nil, errors.Errorf("report with mime type %s is not supported", mime)
}
ty := reflect.TypeOf(t)
if ty.Kind() == reflect.Ptr {
ty = ty.Elem()
}
// New one
rp := reflect.New(ty).Elem().Addr().Interface()
if err := json.Unmarshal(jsonData, rp); err != nil {
return nil, err
}
return rp, nil
}

View File

@ -0,0 +1,45 @@
// 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 auth
import (
"net/http"
"github.com/pkg/errors"
)
// apiKeyAuthorizer authorize by adding a header `X-ScannerAdapter-API-Key` with value "credential"
type apiKeyAuthorizer struct {
typeID string
accessCred string
}
// Authorize the requests
func (aa *apiKeyAuthorizer) Authorize(req *http.Request) error {
if req != nil && len(aa.accessCred) > 0 {
req.Header.Add(aa.typeID, aa.accessCred)
return nil
}
return errors.Errorf("%s: %s", aa.typeID, "missing data to authorize request")
}
// NewAPIKeyAuthorizer news a apiKeyAuthorizer
func NewAPIKeyAuthorizer(accessCred string) Authorizer {
return &apiKeyAuthorizer{
typeID: APIKey,
accessCred: accessCred,
}
}

View File

@ -0,0 +1,54 @@
// 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 auth
import (
"net/http"
"strings"
"github.com/pkg/errors"
)
const (
authorization = "Authorization"
// Basic ...
Basic = "Basic"
// Bearer ...
Bearer = "Bearer"
// APIKey ...
APIKey = "X-ScannerAdapter-API-Key"
)
// Authorizer defines operation for authorizing the requests
type Authorizer interface {
Authorize(req *http.Request) error
}
// GetAuthorizer is a factory method for getting an authorizer based on the given auth type
func GetAuthorizer(auth, cred string) (Authorizer, error) {
switch strings.TrimSpace(auth) {
// No authorizer required
case "":
return NewNoAuth(), nil
case Basic:
return NewBasicAuth(cred), nil
case Bearer:
return NewBearerAuth(cred), nil
case APIKey:
return NewAPIKeyAuthorizer(cred), nil
default:
return nil, errors.Errorf("auth type %s is not supported", auth)
}
}

View File

@ -0,0 +1,48 @@
// 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 auth
import (
"encoding/base64"
"fmt"
"net/http"
"github.com/pkg/errors"
)
// basicAuthorizer authorizes the request by adding `Authorization Basic base64(credential)` header
type basicAuthorizer struct {
typeID string
accessCred string
}
// Authorize requests
func (ba *basicAuthorizer) Authorize(req *http.Request) error {
if len(ba.accessCred) == 0 {
return errors.Errorf("%s:%s", ba.typeID, "missing access credential")
}
if req != nil && len(ba.accessCred) > 0 {
data := base64.StdEncoding.EncodeToString([]byte(ba.accessCred))
req.Header.Add(authorization, fmt.Sprintf("%s %s", ba.typeID, data))
}
return errors.Errorf("%s: %s", ba.typeID, "missing data to authorize request")
}
// NewBasicAuth basic authorizer
func NewBasicAuth(accessCred string) Authorizer {
return &basicAuthorizer{Basic, accessCred}
}

View File

@ -0,0 +1,42 @@
// 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 auth
import (
"fmt"
"net/http"
"github.com/pkg/errors"
)
// bearerAuthorizer authorizes the request by adding `Authorization Bearer credential` header
type bearerAuthorizer struct {
typeID string
accessCred string
}
// Authorize requests
func (ba *bearerAuthorizer) Authorize(req *http.Request) error {
if req != nil && len(ba.accessCred) > 0 {
req.Header.Add(authorization, fmt.Sprintf("%s %s", ba.typeID, ba.accessCred))
}
return errors.Errorf("%s: %s", ba.typeID, "missing data to authorize request")
}
// NewBearerAuth create bearer authorizer
func NewBearerAuth(accessCred string) Authorizer {
return &bearerAuthorizer{Bearer, accessCred}
}

View File

@ -12,24 +12,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package api
package auth
import (
dscan "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
"github.com/goharbor/harbor/src/pkg/scan/scanner/scan"
"net/http"
)
// Scan ...
func (bc *basicController) Scan(artifact *scan.Artifact) error {
// noAuth is created to handle the no authorization case which is acceptable
type noAuth struct{}
// Authorize the incoming request
func (na *noAuth) Authorize(req *http.Request) error {
// Do nothing
return nil
}
// GetReport ...
func (bc *basicController) GetReport(artifact *scan.Artifact) ([]*dscan.Report, error) {
return nil, nil
}
// GetScanLog ...
func (bc *basicController) GetScanLog(digest string) ([]byte, error) {
return nil, nil
// NewNoAuth creates a noAuth authorizer
func NewNoAuth() Authorizer {
return &noAuth{}
}

View File

@ -0,0 +1,278 @@
// 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 v1
import (
"bytes"
"crypto/tls"
"encoding/json"
"io/ioutil"
"net"
"net/http"
"strconv"
"time"
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/rest/auth"
"github.com/pkg/errors"
)
const (
// defaultRefreshInterval is the default interval with seconds of refreshing report
defaultRefreshInterval = 5
// refreshAfterHeader provides the refresh interval value
refreshAfterHeader = "Refresh-After"
)
// Client defines the methods to access the adapter services that
// implement the REST API specs
type Client interface {
// GetMetadata gets the metadata of the given scanner
//
// Returns:
// *ScannerAdapterMetadata : metadata of the given scanner
// error : non nil error if any errors occurred
GetMetadata() (*ScannerAdapterMetadata, error)
// SubmitScan initiates a scanning of the given artifact.
// Returns `nil` if the request was accepted, a non `nil` error otherwise.
//
// Arguments:
// req *ScanRequest : request including the registry and artifact data
//
// Returns:
// *ScanResponse : response with UUID for tracking the scan results
// error : non nil error if any errors occurred
SubmitScan(req *ScanRequest) (*ScanResponse, error)
// GetScanReport gets the scan result for the corresponding ScanRequest identifier.
// Note that this is a blocking method which either returns a non `nil` scan report or error.
// A caller is supposed to cast the returned interface{} to a structure that corresponds
// to the specified MIME type.
//
// Arguments:
// scanRequestID string : the ID of the scan submitted before
// reportMIMEType string : the report mime type
// Returns:
// string : the scan report of the given artifact
// error : non nil error if any errors occurred
GetScanReport(scanRequestID, reportMIMEType string) (string, error)
}
// basicClient is default implementation of the Client interface
type basicClient struct {
httpClient *http.Client
spec *Spec
authorizer auth.Authorizer
}
// NewClient news a basic client
func NewClient(r *scanner.Registration) (Client, error) {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: r.SkipCertVerify,
},
}
authorizer, err := auth.GetAuthorizer(r.Auth, r.AccessCredential)
if err != nil {
return nil, errors.Wrap(err, "new v1 client")
}
return &basicClient{
httpClient: &http.Client{
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
},
spec: NewSpec(r.URL),
authorizer: authorizer,
}, nil
}
// GetMetadata ...
func (c *basicClient) GetMetadata() (*ScannerAdapterMetadata, error) {
def := c.spec.Metadata()
request, err := http.NewRequest(http.MethodGet, def.URL, nil)
if err != nil {
return nil, errors.Wrap(err, "v1 client: get metadata")
}
// Resolve header
def.Resolver(request)
// Send request
respData, err := c.send(request, generalResponseHandler(http.StatusOK))
if err != nil {
return nil, errors.Wrap(err, "v1 client: get metadata")
}
// Unmarshal data
meta := &ScannerAdapterMetadata{}
if err := json.Unmarshal(respData, meta); err != nil {
return nil, errors.Wrap(err, "v1 client: get metadata")
}
return meta, nil
}
// SubmitScan ...
func (c *basicClient) SubmitScan(req *ScanRequest) (*ScanResponse, error) {
if req == nil {
return nil, errors.New("nil request")
}
data, err := json.Marshal(req)
if err != nil {
return nil, errors.Wrap(err, "v1 client: submit scan")
}
def := c.spec.SubmitScan()
request, err := http.NewRequest(http.MethodPost, def.URL, bytes.NewReader(data))
if err != nil {
return nil, errors.Wrap(err, "v1 client: submit scan")
}
respData, err := c.send(request, generalResponseHandler(http.StatusCreated))
if err != nil {
return nil, errors.Wrap(err, "v1 client: submit scan")
}
resp := &ScanResponse{}
if err := json.Unmarshal(respData, resp); err != nil {
return nil, errors.Wrap(err, "v1 client: submit scan")
}
return resp, nil
}
// GetScanReport ...
func (c *basicClient) GetScanReport(scanRequestID, reportMIMEType string) (string, error) {
if len(scanRequestID) == 0 {
return "", errors.New("empty scan request ID")
}
if len(reportMIMEType) == 0 {
return "", errors.New("missing report mime type")
}
def := c.spec.GetScanReport(scanRequestID, reportMIMEType)
req, err := http.NewRequest(http.MethodGet, def.URL, nil)
if err != nil {
return "", errors.Wrap(err, "v1 client: get scan report")
}
respData, err := c.send(req, reportResponseHandler())
if err != nil {
// This error should not be wrapped
return "", err
}
return string(respData), nil
}
func (c *basicClient) send(req *http.Request, h responseHandler) ([]byte, error) {
if c.authorizer != nil {
if err := c.authorizer.Authorize(req); err != nil {
return nil, errors.Wrap(err, "authorization")
}
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer func() {
if err := resp.Body.Close(); err != nil {
// Just logged
logger.Errorf("close response body error: %s", err)
}
}()
return h(resp.StatusCode, resp)
}
// responseHandlerFunc is a handler func template for handling the http response data,
// especially the error part.
type responseHandler func(code int, resp *http.Response) ([]byte, error)
// generalResponseHandler create a general response handler to cover the common cases.
func generalResponseHandler(expectedCode int) responseHandler {
return func(code int, resp *http.Response) ([]byte, error) {
return generalRespHandlerFunc(expectedCode, code, resp)
}
}
// reportResponseHandler creates response handler for get report special case.
func reportResponseHandler() responseHandler {
return func(code int, resp *http.Response) ([]byte, error) {
if code == http.StatusFound {
// Set default
retryAfter := defaultRefreshInterval // seconds
// Read `retry after` info from header
v := resp.Header.Get(refreshAfterHeader)
if len(v) > 0 {
if i, err := strconv.ParseInt(v, 10, 8); err == nil {
retryAfter = int(i)
} else {
// log error
logger.Errorf("Parse `%s` error: %s", refreshAfterHeader, err)
}
}
return nil, &ReportNotReadyError{RetryAfter: retryAfter}
}
return generalRespHandlerFunc(http.StatusOK, code, resp)
}
}
// generalRespHandlerFunc is a handler to cover the general cases
func generalRespHandlerFunc(expectedCode, code int, resp *http.Response) ([]byte, error) {
buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if code != expectedCode {
if len(buf) > 0 {
// Try to read error response
eResp := &ErrorResponse{
Err: &Error{},
}
if err := json.Unmarshal(buf, eResp); err == nil {
return nil, eResp
}
}
return nil, errors.Errorf("unexpected status code: %d, response: %s", code, string(buf))
}
return buf, nil
}

View File

@ -0,0 +1,169 @@
// 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 v1
import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/pkg/errors"
)
const (
defaultDeadCheckInterval = 1 * time.Minute
defaultExpireTime = 5 * time.Minute
)
// DefaultClientPool is a default client pool.
var DefaultClientPool = NewClientPool(nil)
// ClientPool defines operations for the client pool which provides v1 client cache.
type ClientPool interface {
// Get a v1 client interface for the specified registration.
//
// Arguments:
// r *scanner.Registration : registration for client connecting to
//
// Returns:
// Client : v1 client
// error : non nil error if any errors occurred
Get(r *scanner.Registration) (Client, error)
}
// PoolConfig provides configurations for the client pool.
type PoolConfig struct {
// Interval for checking dead instance.
DeadCheckInterval time.Duration
// Expire time for the instance to be marked as dead.
ExpireTime time.Duration
}
// poolItem append timestamp for the caching client instance.
type poolItem struct {
c Client
timestamp time.Time
}
// basicClientPool is default implementation of client pool interface.
type basicClientPool struct {
pool *sync.Map
config *PoolConfig
}
// NewClientPool news a basic client pool.
func NewClientPool(config *PoolConfig) ClientPool {
bcp := &basicClientPool{
pool: &sync.Map{},
config: config,
}
// Set config
if bcp.config == nil {
bcp.config = &PoolConfig{}
}
if bcp.config.DeadCheckInterval == 0 {
bcp.config.DeadCheckInterval = defaultDeadCheckInterval
}
if bcp.config.ExpireTime == 0 {
bcp.config.ExpireTime = defaultExpireTime
}
return bcp
}
// Get client for the specified registration.
// So far, there will not be too many scanner registrations. An then
// no need to do client instance clear work.
// If one day, we have to clear unactivated client instances in the pool,
// add the following func after the first time initializing the client.
// pool item represents the client with a timestamp of last accessed.
func (bcp *basicClientPool) Get(r *scanner.Registration) (Client, error) {
if r == nil {
return nil, errors.New("nil scanner registration")
}
if err := r.Validate(true); err != nil {
return nil, errors.Wrap(err, "client pool: get")
}
k := key(r)
item, ok := bcp.pool.Load(k)
if !ok {
nc, err := NewClient(r)
if err != nil {
return nil, errors.Wrap(err, "client pool: get")
}
// Cache it
npi := &poolItem{
c: nc,
timestamp: time.Now().UTC(),
}
bcp.pool.Store(k, npi)
item = npi
// dead check
bcp.deadCheck(k, npi)
}
return item.(*poolItem).c, nil
}
func (bcp *basicClientPool) deadCheck(key string, item *poolItem) {
// Run in a separate goroutine
go func() {
// As we do not have a global context, let's watch the system signal to
// exit the goroutine correctly.
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM, os.Kill)
tk := time.NewTicker(bcp.config.DeadCheckInterval)
defer tk.Stop()
for {
select {
case t := <-tk.C:
if item.timestamp.Add(bcp.config.ExpireTime).Before(t.UTC()) {
// Expired
bcp.pool.Delete(key)
return
}
case <-sig:
// Terminated by system
return
}
}
}()
}
func key(r *scanner.Registration) string {
return fmt.Sprintf("%s:%s:%s:%s:%v",
r.UUID,
r.URL,
r.Auth,
r.AccessCredential,
r.SkipCertVerify,
)
}

View File

@ -0,0 +1,82 @@
// 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 v1
import (
"fmt"
"testing"
"time"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/rest/auth"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// ClientPoolTestSuite is a test suite to test the client pool.
type ClientPoolTestSuite struct {
suite.Suite
pool ClientPool
}
// TestClientPool is the entry of ClientPoolTestSuite.
func TestClientPool(t *testing.T) {
suite.Run(t, &ClientPoolTestSuite{})
}
// SetupSuite sets up test suite env.
func (suite *ClientPoolTestSuite) SetupSuite() {
cfg := &PoolConfig{
DeadCheckInterval: 100 * time.Millisecond,
ExpireTime: 300 * time.Millisecond,
}
suite.pool = NewClientPool(cfg)
}
// TestClientPoolGet tests the get method of client pool.
func (suite *ClientPoolTestSuite) TestClientPoolGet() {
r := &scanner.Registration{
ID: 1,
Name: "TestClientPoolGet",
UUID: "uuid",
URL: "http://a.b.c",
Auth: auth.Basic,
AccessCredential: "u:p",
SkipCertVerify: false,
}
client1, err := suite.pool.Get(r)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), client1)
p1 := fmt.Sprintf("%p", client1.(*basicClient))
client2, err := suite.pool.Get(r)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), client2)
p2 := fmt.Sprintf("%p", client2.(*basicClient))
assert.Equal(suite.T(), p1, p2)
<-time.After(400 * time.Millisecond)
client3, err := suite.pool.Get(r)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), client3)
p3 := fmt.Sprintf("%p", client3.(*basicClient))
assert.NotEqual(suite.T(), p2, p3)
}

View File

@ -0,0 +1,197 @@
// 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 v1
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// ClientTestSuite tests the v1 client
type ClientTestSuite struct {
suite.Suite
testServer *httptest.Server
client Client
}
// TestClient is the entry of ClientTestSuite
func TestClient(t *testing.T) {
suite.Run(t, new(ClientTestSuite))
}
// SetupSuite prepares the test suite env
func (suite *ClientTestSuite) SetupSuite() {
suite.testServer = httptest.NewServer(&mockHandler{})
r := &scanner.Registration{
ID: 1000,
UUID: "uuid",
Name: "TestClient",
URL: suite.testServer.URL,
SkipCertVerify: true,
}
c, err := NewClient(r)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), c)
suite.client = c
}
// TestClientMetadata tests the metadata of the client
func (suite *ClientTestSuite) TestClientMetadata() {
m, err := suite.client.GetMetadata()
require.NoError(suite.T(), err)
require.NotNil(suite.T(), m)
assert.Equal(suite.T(), m.Scanner.Name, "Clair")
}
// TestClientSubmitScan tests the scan submission of client
func (suite *ClientTestSuite) TestClientSubmitScan() {
res, err := suite.client.SubmitScan(&ScanRequest{})
require.NoError(suite.T(), err)
require.NotNil(suite.T(), res)
assert.Equal(suite.T(), res.ID, "123456789")
}
// TestClientGetScanReportError tests getting report failed
func (suite *ClientTestSuite) TestClientGetScanReportError() {
_, err := suite.client.GetScanReport("id1", MimeTypeNativeReport)
require.Error(suite.T(), err)
assert.Condition(suite.T(), func() (success bool) {
success = strings.Index(err.Error(), "error") != -1
return
})
}
// TestClientGetScanReport tests getting report
func (suite *ClientTestSuite) TestClientGetScanReport() {
res, err := suite.client.GetScanReport("id2", MimeTypeNativeReport)
require.NoError(suite.T(), err)
require.NotEmpty(suite.T(), res)
}
// TestClientGetScanReportNotReady tests the case that the report is not ready
func (suite *ClientTestSuite) TestClientGetScanReportNotReady() {
_, err := suite.client.GetScanReport("id3", MimeTypeNativeReport)
require.Error(suite.T(), err)
require.Condition(suite.T(), func() (success bool) {
_, success = err.(*ReportNotReadyError)
return
})
assert.Equal(suite.T(), 10, err.(*ReportNotReadyError).RetryAfter)
}
// TearDownSuite clears the test suite env
func (suite *ClientTestSuite) TearDownSuite() {
suite.testServer.Close()
}
type mockHandler struct{}
// ServeHTTP ...
func (mh *mockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.RequestURI {
case "/metadata":
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusForbidden)
return
}
m := &ScannerAdapterMetadata{
Scanner: &Scanner{
Name: "Clair",
Vendor: "Harbor",
Version: "0.1.0",
},
Capabilities: &ScannerCapability{
ConsumesMimeTypes: []string{
MimeTypeOCIArtifact,
MimeTypeDockerArtifact,
},
ProducesMimeTypes: []string{
MimeTypeNativeReport,
MimeTypeRawReport,
},
},
Properties: ScannerProperties{
"extra": "testing",
},
}
data, _ := json.Marshal(m)
w.WriteHeader(http.StatusOK)
_, _ = w.Write(data)
break
case "/scan":
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusForbidden)
return
}
res := &ScanResponse{}
res.ID = "123456789"
data, _ := json.Marshal(res)
w.WriteHeader(http.StatusCreated)
_, _ = w.Write(data)
break
case "/scan/id1/report":
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusForbidden)
return
}
e := &ErrorResponse{
&Error{
Message: "error",
},
}
data, _ := json.Marshal(e)
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write(data)
break
case "/scan/id2/report":
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusForbidden)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("{}"))
break
case "/scan/id3/report":
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusForbidden)
return
}
w.Header().Add(refreshAfterHeader, fmt.Sprintf("%d", 10))
w.Header().Add("Location", "/scan/id3/report")
w.WriteHeader(http.StatusFound)
break
}
}

View File

@ -0,0 +1,173 @@
// 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 v1
import (
"encoding/json"
"fmt"
"github.com/pkg/errors"
)
// Scanner represents metadata of a Scanner Adapter which allow Harbor to lookup a scanner capable of
// scanning a given Artifact stored in its registry and making sure that it can interpret a
// returned result.
type Scanner struct {
// The name of the scanner.
Name string `json:"name"`
// The name of the scanner's provider.
Vendor string `json:"vendor"`
// The version of the scanner.
Version string `json:"version"`
}
// ScannerCapability consists of the set of recognized artifact MIME types and the set of scanner
// report MIME types. For example, a scanner capable of analyzing Docker images and producing
// a vulnerabilities report recognizable by Harbor web console might be represented with the
// following capability:
// - consumes MIME types:
// -- application/vnd.oci.image.manifest.v1+json
// -- application/vnd.docker.distribution.manifest.v2+json
// - produces MIME types
// -- application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0
// -- application/vnd.scanner.adapter.vuln.report.raw
type ScannerCapability struct {
// The set of MIME types of the artifacts supported by the scanner to produce the reports
// specified in the "produces_mime_types". A given mime type should only be present in one
// capability item.
ConsumesMimeTypes []string `json:"consumes_mime_types"`
// The set of MIME types of reports generated by the scanner for the consumes_mime_types of
// the same capability record.
ProducesMimeTypes []string `json:"produces_mime_types"`
}
// ScannerProperties is a set of custom properties that can further describe capabilities of a given scanner.
type ScannerProperties map[string]string
// ScannerAdapterMetadata represents metadata of a Scanner Adapter which allows Harbor to lookup
// a scanner capable of scanning a given Artifact stored in its registry and making sure that it
// can interpret a returned result.
type ScannerAdapterMetadata struct {
Scanner *Scanner `json:"scanner"`
Capabilities *ScannerCapability `json:"capabilities"`
Properties ScannerProperties `json:"properties"`
}
// Artifact represents an artifact stored in Registry.
type Artifact struct {
// The full name of a Harbor repository containing the artifact, including the namespace.
// For example, `library/oracle/nosql`.
Repository string `json:"repository"`
// The artifact's digest, consisting of an algorithm and hex portion.
// For example, `sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b`,
// represents sha256 based digest.
Digest string `json:"digest"`
// The mime type of the scanned artifact
MimeType string `json:"mime_type"`
}
// Registry represents Registry connection settings.
type Registry struct {
// A base URL of the Docker Registry v2 API exposed by Harbor.
URL string `json:"url"`
// An optional value of the HTTP Authorization header sent with each request to the Docker Registry v2 API.
// For example, `Bearer: JWTTOKENGOESHERE`.
Authorization string `json:"authorization"`
}
// ScanRequest represents a structure that is sent to a Scanner Adapter to initiate artifact scanning.
// Conducts all the details required to pull the artifact from a Harbor registry.
type ScanRequest struct {
// Connection settings for the Docker Registry v2 API exposed by Harbor.
Registry *Registry `json:"registry"`
// Artifact to be scanned.
Artifact *Artifact `json:"artifact"`
}
// FromJSON parses ScanRequest from json data
func (s *ScanRequest) FromJSON(jsonData string) error {
if len(jsonData) == 0 {
return errors.New("empty json data to parse")
}
return json.Unmarshal([]byte(jsonData), s)
}
// ToJSON marshals ScanRequest to JSON data
func (s *ScanRequest) ToJSON() (string, error) {
data, err := json.Marshal(s)
if err != nil {
return "", err
}
return string(data), nil
}
// Validate ScanRequest
func (s *ScanRequest) Validate() error {
if s.Registry == nil ||
len(s.Registry.URL) == 0 ||
len(s.Registry.Authorization) == 0 {
return errors.New("scan request: invalid registry")
}
if s.Artifact == nil ||
len(s.Artifact.Digest) == 0 ||
len(s.Artifact.Repository) == 0 ||
len(s.Artifact.MimeType) == 0 {
return errors.New("scan request: invalid artifact")
}
return nil
}
// ScanResponse represents the response returned by the scanner adapter after scan request successfully
// submitted.
type ScanResponse struct {
// e.g: 3fa85f64-5717-4562-b3fc-2c963f66afa6
ID string `json:"id"`
}
// ErrorResponse contains error message when requests are not correctly handled.
type ErrorResponse struct {
// Error object
Err *Error `json:"error"`
}
// Error message
type Error struct {
// Message of the error
Message string `json:"message"`
}
// Error for ErrorResponse
func (er *ErrorResponse) Error() string {
if er.Err != nil {
return er.Err.Message
}
return "nil error"
}
// ReportNotReadyError is an error to indicate the scan report is not ready
type ReportNotReadyError struct {
// Seconds for next retry with seconds
RetryAfter int
}
// Error for ReportNotReadyError
func (rnr *ReportNotReadyError) Error() string {
return fmt.Sprintf("report is not ready yet, retry after %d", rnr.RetryAfter)
}

View File

@ -0,0 +1,107 @@
// 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 v1
import (
"fmt"
"net/http"
"strings"
)
const (
// HTTPAcceptHeader represents the HTTP accept header
HTTPAcceptHeader = "Accept"
// HTTPContentType represents the HTTP content-type header
HTTPContentType = "Content-Type"
// MimeTypeOCIArtifact defines the mime type for OCI artifact
MimeTypeOCIArtifact = "application/vnd.oci.image.manifest.v1+json"
// MimeTypeDockerArtifact defines the mime type for docker artifact
MimeTypeDockerArtifact = "application/vnd.docker.distribution.manifest.v2+json"
// MimeTypeNativeReport defines the mime type for native report
MimeTypeNativeReport = "application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0"
// MimeTypeRawReport defines the mime type for raw report
MimeTypeRawReport = "application/vnd.scanner.adapter.vuln.report.raw"
// MimeTypeAdapterMeta defines the mime type for adapter metadata
MimeTypeAdapterMeta = "application/vnd.scanner.adapter.metadata+json; version=1.0"
// MimeTypeScanRequest defines the mime type for scan request
MimeTypeScanRequest = "application/vnd.scanner.adapter.scan.request+json; version=1.0"
// MimeTypeScanResponse defines the mime type for scan response
MimeTypeScanResponse = "application/vnd.scanner.adapter.scan.response+json; version=1.0"
)
// RequestResolver is a function template to modify the API request, e.g: add headers
type RequestResolver func(req *http.Request)
// Definition for API
type Definition struct {
// URL of the API
URL string
// Resolver fro the request
Resolver RequestResolver
}
// Spec of the API
// Contains URL and possible headers.
type Spec struct {
baseRoute string
}
// NewSpec news V1 spec
func NewSpec(base string) *Spec {
s := &Spec{}
if len(base) > 0 {
if strings.HasSuffix(base, "/") {
s.baseRoute = base[:len(base)-1]
} else {
s.baseRoute = base
}
}
return s
}
// Metadata API
func (s *Spec) Metadata() Definition {
return Definition{
URL: fmt.Sprintf("%s%s", s.baseRoute, "/metadata"),
Resolver: func(req *http.Request) {
req.Header.Add(HTTPAcceptHeader, MimeTypeAdapterMeta)
},
}
}
// SubmitScan API
func (s *Spec) SubmitScan() Definition {
return Definition{
URL: fmt.Sprintf("%s%s", s.baseRoute, "/scan"),
Resolver: func(req *http.Request) {
req.Header.Add(HTTPContentType, MimeTypeScanRequest)
req.Header.Add(HTTPAcceptHeader, MimeTypeScanResponse)
},
}
}
// GetScanReport API
func (s *Spec) GetScanReport(scanReqID string, mimeType string) Definition {
path := fmt.Sprintf("/scan/%s/report", scanReqID)
return Definition{
URL: fmt.Sprintf("%s%s", s.baseRoute, path),
Resolver: func(req *http.Request) {
req.Header.Add(HTTPAcceptHeader, mimeType)
},
}
}

View File

@ -16,7 +16,7 @@ package scanner
import (
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/google/uuid"
"github.com/pkg/errors"
)
@ -59,7 +59,7 @@ func New() Manager {
// Create ...
func (bm *basicManager) Create(registration *scanner.Registration) (string, error) {
if registration == nil {
return "", errors.New("nil endpoint to create")
return "", errors.New("nil registration to create")
}
// Inject new UUID
@ -92,11 +92,11 @@ func (bm *basicManager) Get(registrationUUID string) (*scanner.Registration, err
// Update ...
func (bm *basicManager) Update(registration *scanner.Registration) error {
if registration == nil {
return errors.New("nil endpoint to update")
return errors.New("nil registration to update")
}
if err := registration.Validate(true); err != nil {
return errors.Wrap(err, "update endpoint")
return errors.Wrap(err, "update registration")
}
return scanner.UpdateRegistration(registration)

View File

@ -19,7 +19,7 @@ import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/q"
"github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner"
"github.com/goharbor/harbor/src/pkg/scan/dao/scanner"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -48,9 +48,6 @@ func (suite *BasicManagerTestSuite) SetupSuite() {
Name: "forUT",
Description: "sample registration",
URL: "https://sample.scanner.com",
Adapter: "Clair",
Version: "0.1.0",
Vendor: "Harbor",
}
uid, err := suite.mgr.Create(r)
@ -66,7 +63,7 @@ func (suite *BasicManagerTestSuite) TearDownSuite() {
// TestList tests list registrations
func (suite *BasicManagerTestSuite) TestList() {
m := make(map[string]string, 1)
m := make(map[string]interface{}, 1)
m["name"] = "forUT"
l, err := suite.mgr.List(&q.Query{

View File

@ -1,48 +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 scan
import "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan"
// Options object for the scan action
type Options struct{}
// Option for scan action
type Option interface {
// Apply option to the passing in options
Apply(options *Options) error
}
// Controller defines operations for scan controlling
type Controller interface {
// Scan the given artifact
//
// Arguments:
// artifact *res.Artifact : artifact to be scanned
//
// Returns:
// error : non nil error if any errors occurred
Scan(artifact *Artifact, options ...Option) error
// GetReport gets the reports for the given artifact identified by the digest
//
// Arguments:
// artifact *res.Artifact : the scanned artifact
//
// Returns:
// []*scan.Report : scan results by different scanner vendors
// error : non nil error if any errors occurred
GetReport(artifact *Artifact) ([]*scan.Report, error)
}

View File

@ -1,46 +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 scan
// Artifact represents an artifact stored in Registry.
type Artifact struct {
// The full name of a Harbor repository containing the artifact, including the namespace.
// For example, `library/oracle/nosql`.
Repository string
// The artifact's digest, consisting of an algorithm and hex portion.
// For example, `sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b`,
// represents sha256 based digest.
Digest string
// The mime type of the scanned artifact
MimeType string
}
// Registry represents Registry connection settings.
type Registry struct {
// A base URL of the Docker Registry v2 API exposed by Harbor.
URL string
// An optional value of the HTTP Authorization header sent with each request to the Docker Registry v2 API.
// For example, `Bearer: JWTTOKENGOESHERE`.
Authorization string
}
// Request represents a structure that is sent to a Scanner Adapter to initiate artifact scanning.
// Conducts all the details required to pull the artifact from a Harbor registry.
type Request struct {
// Connection settings for the Docker Registry v2 API exposed by Harbor.
Registry *Registry
// Artifact to be scanned.
Artifact *Artifact
}

View File

@ -0,0 +1,58 @@
// 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 vuln
import (
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
)
// Report model for vulnerability scan
type Report struct {
// Time of generating this report
GeneratedAt string `json:"generated_at"`
// Scanner of generating this report
Scanner *v1.Scanner `json:"scanner"`
// A standard scale for measuring the severity of a vulnerability.
Severity Severity `json:"severity"`
// Vulnerability list
Vulnerabilities []*VulnerabilityItem `json:"vulnerabilities"`
}
// VulnerabilityItem represents one found vulnerability
type VulnerabilityItem struct {
// The unique identifier of the vulnerability.
// e.g: CVE-2017-8283
ID string `json:"id"`
// An operating system or software dependency package containing the vulnerability.
// e.g: dpkg
Package string `json:"package"`
// The version of the package containing the vulnerability.
// e.g: 1.17.27
Version string `json:"version"`
// The version of the package containing the fix if available.
// e.g: 1.18.0
FixVersion string `json:"fix_version"`
// A standard scale for measuring the severity of a vulnerability.
Severity Severity `json:"severity"`
// example: dpkg-source in dpkg 1.3.0 through 1.18.23 is able to use a non-GNU patch program
// and does not offer a protection mechanism for blank-indented diff hunks, which allows remote
// attackers to conduct directory traversal attacks via a crafted Debian source package, as
// demonstrated by using of dpkg-source on NetBSD.
Description string `json:"description"`
// The list of link to the upstream database with the full description of the vulnerability.
// Format: URI
// e.g: List [ "https://security-tracker.debian.org/tracker/CVE-2017-8283" ]
Links []string
}

View File

@ -0,0 +1,39 @@
// 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 vuln
const (
// Unknown - either a security problem that has not been assigned to a priority yet or
// a priority that the scanner did not recognize.
Unknown Severity = "Unknown"
// Low - a security problem, but is hard to exploit due to environment, requires a
// user-assisted attack, a small install base, or does very little damage.
Low Severity = "Low"
// Negligible - technically a security problem, but is only theoretical in nature, requires
// a very special situation, has almost no install base, or does no real damage.
Negligible Severity = "Negligible"
// Medium - a real security problem, and is exploitable for many people. Includes network
// daemon denial of service attacks, cross-site scripting, and gaining user privileges.
Medium Severity = "Medium"
// High - a real problem, exploitable for many people in a default installation. Includes
// serious remote denial of service, local root privilege escalations, or data loss.
High Severity = "High"
// Critical - a world-burning problem, exploitable for nearly all people in a default installation.
// Includes remote root privilege escalations, or massive data loss.
Critical Severity = "Critical"
)
// Severity is a standard scale for measuring the severity of a vulnerability.
type Severity string