diff --git a/make/migrations/postgresql/0011_1.10.0_schema.up.sql b/make/migrations/postgresql/0011_1.10.0_schema.up.sql new file mode 100644 index 000000000..e03f84b09 --- /dev/null +++ b/make/migrations/postgresql/0011_1.10.0_schema.up.sql @@ -0,0 +1,34 @@ +/*Table for keeping the plug scanner registration*/ +CREATE TABLE scanner_registration +( + id SERIAL PRIMARY KEY NOT NULL, + uuid VARCHAR(64) UNIQUE NOT NULL, + url VARCHAR(256) UNIQUE NOT NULL, + name VARCHAR(128) UNIQUE NOT NULL, + 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, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +/*Table for keeping the scanner report. The report details are stored as JSONB*/ +CREATE TABLE scanner_report +( + id SERIAL PRIMARY KEY NOT NULL, + digest VARCHAR(256) NOT NULL, + registration_id VARCHAR(64) NOT NULL, + job_id VARCHAR(32), + status VARCHAR(16) NOT NULL, + status_code INTEGER DEFAULT 0, + report JSON, + start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + end_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(digest, registration_id) +) diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index b6ed840b2..b13fb2f30 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -206,6 +206,13 @@ func init() { beego.Router("/api/internal/switchquota", &InternalAPI{}, "put:SwitchQuota") beego.Router("/api/internal/syncquota", &InternalAPI{}, "post:SyncQuota") + // 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") + // Add routes for project level scanner + beego.Router("/api/projects/:pid([0-9]+)/scanner", scannerAPI, "get:GetProjectScanner;put:SetProjectScanner") + // syncRegistry if err := SyncRegistry(config.GlobalProjectMgr); err != nil { log.Fatalf("failed to sync repositories from registry: %v", err) diff --git a/src/core/api/plug_scanners.go b/src/core/api/plug_scanners.go new file mode 100644 index 000000000..d14424675 --- /dev/null +++ b/src/core/api/plug_scanners.go @@ -0,0 +1,348 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "fmt" + "net/http" + + "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" + "github.com/pkg/errors" +) + +// ScannerAPI provides the API for managing the plugin scanners +type ScannerAPI struct { + // The base controller to provide common utilities + BaseController + + // Controller for the plug scanners + c api.Controller +} + +// Prepare sth. for the subsequent actions +func (sa *ScannerAPI) Prepare() { + // Call super prepare method + sa.BaseController.Prepare() + + // Check access permissions + if !sa.SecurityCtx.IsAuthenticated() { + sa.SendUnAuthorizedError(errors.New("UnAuthorized")) + return + } + + if !sa.SecurityCtx.IsSysAdmin() { + sa.SendForbiddenError(errors.New(sa.SecurityCtx.GetUsername())) + return + } + + // Use the default controller + sa.c = api.DefaultController +} + +// Get the specified scanner +func (sa *ScannerAPI) Get() { + if r := sa.get(); r != nil { + // Response to the client + sa.Data["json"] = r + sa.ServeJSON() + } +} + +// List all the scanners +func (sa *ScannerAPI) List() { + p, pz, err := sa.GetPaginationParams() + if err != nil { + sa.SendBadRequestError(errors.Wrap(err, "scanner API: list all")) + return + } + + query := &q.Query{ + PageSize: pz, + PageNumber: p, + } + + // Get query key words + kws := make(map[string]string) + properties := []string{"name", "description", "url"} + for _, k := range properties { + kw := sa.GetString(k) + if len(kw) > 0 { + kws[k] = kw + } + } + + if len(kws) > 0 { + query.Keywords = kws + } + + all, err := sa.c.ListRegistrations(query) + if err != nil { + sa.SendInternalServerError(errors.Wrap(err, "scanner API: list all")) + return + } + + // Response to the client + sa.Data["json"] = all + sa.ServeJSON() +} + +// Create a new scanner +func (sa *ScannerAPI) Create() { + r := &scanner.Registration{} + + if err := sa.DecodeJSONReq(r); err != nil { + sa.SendBadRequestError(errors.Wrap(err, "scanner API: create")) + return + } + + if err := r.Validate(false); err != nil { + sa.SendBadRequestError(errors.Wrap(err, "scanner API: create")) + return + } + + // Explicitly check if conflict + if !sa.checkDuplicated("name", r.Name) || + !sa.checkDuplicated("url", r.URL) { + return + } + + // All newly created should be non default one except the 1st one + r.IsDefault = false + + uuid, err := sa.c.CreateRegistration(r) + if err != nil { + sa.SendInternalServerError(errors.Wrap(err, "scanner API: create")) + return + } + + location := fmt.Sprintf("%s/%s", sa.Ctx.Request.RequestURI, uuid) + sa.Ctx.ResponseWriter.Header().Add("Location", location) + + resp := make(map[string]string, 1) + resp["uuid"] = uuid + + // Response to the client + sa.Ctx.ResponseWriter.WriteHeader(http.StatusCreated) + sa.Data["json"] = resp + sa.ServeJSON() +} + +// Update a scanner +func (sa *ScannerAPI) Update() { + r := sa.get() + if r == nil { + // meet error + return + } + + // full dose updated + rr := &scanner.Registration{} + if err := sa.DecodeJSONReq(rr); err != nil { + sa.SendBadRequestError(errors.Wrap(err, "scanner API: update")) + return + } + + if err := r.Validate(true); err != nil { + sa.SendBadRequestError(errors.Wrap(err, "scanner API: update")) + return + } + + // Name changed? + if r.Name != rr.Name { + if !sa.checkDuplicated("name", rr.Name) { + return + } + } + + // URL changed? + if r.URL != rr.URL { + if !sa.checkDuplicated("url", rr.URL) { + return + } + } + + getChanges(r, rr) + + if err := sa.c.UpdateRegistration(r); err != nil { + sa.SendInternalServerError(errors.Wrap(err, "scanner API: update")) + return + } + + location := fmt.Sprintf("%s/%s", sa.Ctx.Request.RequestURI, r.UUID) + sa.Ctx.ResponseWriter.Header().Add("Location", location) + + // Response to the client + sa.Data["json"] = r + sa.ServeJSON() +} + +// Delete the scanner +func (sa *ScannerAPI) Delete() { + uid := sa.GetStringFromPath(":uid") + if len(uid) == 0 { + sa.SendBadRequestError(errors.New("missing uid")) + return + } + + deleted, err := sa.c.DeleteRegistration(uid) + if err != nil { + sa.SendInternalServerError(errors.Wrap(err, "scanner API: delete")) + return + } + + if deleted == nil { + // Not found + sa.SendNotFoundError(errors.Errorf("scanner registration: %s", uid)) + return + } + + sa.Data["json"] = deleted + sa.ServeJSON() +} + +// SetAsDefault sets the given registration as default one +func (sa *ScannerAPI) SetAsDefault() { + uid := sa.GetStringFromPath(":uid") + if len(uid) == 0 { + sa.SendBadRequestError(errors.New("missing uid")) + return + } + + m := make(map[string]interface{}) + if err := sa.DecodeJSONReq(&m); err != nil { + sa.SendBadRequestError(errors.Wrap(err, "scanner API: set as default")) + return + } + + if v, ok := m["is_default"]; ok { + if isDefault, y := v.(bool); y && isDefault { + if err := sa.c.SetDefaultRegistration(uid); err != nil { + sa.SendInternalServerError(errors.Wrap(err, "scanner API: set as default")) + } + + return + } + } + + // Not supported + sa.SendForbiddenError(errors.Errorf("not supported: %#v", m)) +} + +// GetProjectScanner gets the project level scanner +func (sa *ScannerAPI) GetProjectScanner() { + pid, err := sa.GetInt64FromPath(":pid") + if err != nil { + sa.SendBadRequestError(errors.Wrap(err, "scanner API: get project scanners")) + return + } + + r, err := sa.c.GetRegistrationByProject(pid) + if err != nil { + sa.SendInternalServerError(errors.Wrap(err, "scanner API: get project scanners")) + return + } + + if r != nil { + sa.Data["json"] = r + } else { + sa.Data["json"] = make(map[string]interface{}) + } + + sa.ServeJSON() +} + +// SetProjectScanner sets the project level scanner +func (sa *ScannerAPI) SetProjectScanner() { + pid, err := sa.GetInt64FromPath(":pid") + if err != nil { + sa.SendBadRequestError(errors.Wrap(err, "scanner API: set project scanners")) + return + } + + body := make(map[string]string) + if err := sa.DecodeJSONReq(&body); err != nil { + sa.SendBadRequestError(errors.Wrap(err, "scanner API: set project scanners")) + return + } + + uuid, ok := body["uuid"] + if !ok || len(uuid) == 0 { + sa.SendBadRequestError(errors.New("missing scanner uuid when setting project scanner")) + return + } + + if err := sa.c.SetRegistrationByProject(pid, uuid); err != nil { + sa.SendInternalServerError(errors.Wrap(err, "scanner API: set project scanners")) + return + } +} + +// get the specified scanner +func (sa *ScannerAPI) get() *scanner.Registration { + uid := sa.GetStringFromPath(":uid") + if len(uid) == 0 { + sa.SendBadRequestError(errors.New("missing uid")) + return nil + } + + r, err := sa.c.GetRegistration(uid) + if err != nil { + sa.SendInternalServerError(errors.Wrap(err, "scanner API: get")) + return nil + } + + if r == nil { + // NOT found + sa.SendNotFoundError(errors.Errorf("scanner: %s", uid)) + return nil + } + + return r +} + +func (sa *ScannerAPI) checkDuplicated(property, value string) bool { + // Explicitly check if conflict + kw := make(map[string]string) + kw[property] = value + + query := &q.Query{ + Keywords: kw, + } + + l, err := sa.c.ListRegistrations(query) + if err != nil { + sa.SendInternalServerError(errors.Wrap(err, "scanner API: check existence")) + return false + } + + if len(l) > 0 { + sa.SendConflictError(errors.Errorf("duplicated entries: %s:%s", property, value)) + return false + } + + return true +} + +func getChanges(e *scanner.Registration, eChange *scanner.Registration) { + e.Name = eChange.Name + e.Description = eChange.Description + e.URL = eChange.URL + e.Auth = eChange.Auth + e.AccessCredential = eChange.AccessCredential + e.Disabled = eChange.Disabled + e.SkipCertVerify = eChange.SkipCertVerify +} diff --git a/src/core/api/plug_scanners_test.go b/src/core/api/plug_scanners_test.go new file mode 100644 index 000000000..f6beb9530 --- /dev/null +++ b/src/core/api/plug_scanners_test.go @@ -0,0 +1,444 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "fmt" + "net/http" + "testing" + + "github.com/goharbor/harbor/src/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" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +const ( + rootRoute = "/api/scanners" +) + +// ScannerAPITestSuite is test suite for testing the scanner API +type ScannerAPITestSuite struct { + suite.Suite + + originC api.Controller + mockC *MockScannerAPIController +} + +// TestScannerAPI is the entry of ScannerAPITestSuite +func TestScannerAPI(t *testing.T) { + suite.Run(t, new(ScannerAPITestSuite)) +} + +// SetupSuite prepares testing env +func (suite *ScannerAPITestSuite) SetupTest() { + suite.originC = api.DefaultController + m := &MockScannerAPIController{} + api.DefaultController = m + + suite.mockC = m +} + +// TearDownTest clears test case env +func (suite *ScannerAPITestSuite) TearDownTest() { + // Restore + api.DefaultController = suite.originC +} + +// TestScannerAPICreate tests the post request to create new one +func (suite *ScannerAPITestSuite) TestScannerAPIBase() { + // Including general cases + cases := []*codeCheckingCase{ + // 401 + { + request: &testingRequest{ + url: rootRoute, + method: http.MethodPost, + }, + code: http.StatusUnauthorized, + }, + // 403 + { + request: &testingRequest{ + url: rootRoute, + method: http.MethodPost, + credential: nonSysAdmin, + }, + code: http.StatusForbidden, + }, + // 400 + { + request: &testingRequest{ + url: rootRoute, + method: http.MethodPost, + credential: sysAdmin, + bodyJSON: &scanner.Registration{ + URL: "http://a.b.c", + }, + }, + code: http.StatusBadRequest, + }, + } + + runCodeCheckingCases(suite.T(), cases...) +} + +// TestScannerAPIGet tests api get +func (suite *ScannerAPITestSuite) TestScannerAPIGet() { + res := &scanner.Registration{ + ID: 1000, + UUID: "uuid", + 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) + + // Get + rr := &scanner.Registration{} + err := handleAndParse(&testingRequest{ + url: fmt.Sprintf("%s/%s", rootRoute, "uuid"), + method: http.MethodGet, + credential: sysAdmin, + }, rr) + require.NoError(suite.T(), err) + require.NotNil(suite.T(), rr) + assert.Equal(suite.T(), res.Name, rr.Name) + assert.Equal(suite.T(), res.UUID, rr.UUID) +} + +// TestScannerAPICreate tests create. +func (suite *ScannerAPITestSuite) TestScannerAPICreate() { + r := &scanner.Registration{ + Name: "TestScannerAPICreate", + Description: "JUST FOR TEST", + URL: "https://a.b.c", + Adapter: "Clair", + Vendor: "Harbor", + Version: "0.1.0", + } + + suite.mockQuery(r) + suite.mockC.On("CreateRegistration", r).Return("uuid", nil) + + // Create + res := make(map[string]string, 1) + err := handleAndParse( + &testingRequest{ + url: rootRoute, + method: http.MethodPost, + credential: sysAdmin, + bodyJSON: r, + }, &res) + require.NoError(suite.T(), err) + require.Condition(suite.T(), func() (success bool) { + success = res["uuid"] == "uuid" + return + }) +} + +// TestScannerAPIList tests list +func (suite *ScannerAPITestSuite) TestScannerAPIList() { + query := &q.Query{ + PageNumber: 1, + PageSize: 500, + } + ll := []*scanner.Registration{ + { + ID: 1001, + UUID: "uuid", + 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) + + // List + l := make([]*scanner.Registration, 0) + err := handleAndParse(&testingRequest{ + url: rootRoute, + method: http.MethodGet, + credential: sysAdmin, + }, &l) + require.NoError(suite.T(), err) + assert.Condition(suite.T(), func() (success bool) { + success = len(l) > 0 && l[0].Name == ll[0].Name + return + }) +} + +// TestScannerAPIUpdate tests the update API +func (suite *ScannerAPITestSuite) TestScannerAPIUpdate() { + before := &scanner.Registration{ + ID: 1002, + UUID: "uuid", + Name: "TestScannerAPIUpdate_before", + Description: "JUST FOR TEST", + URL: "https://a.b.c", + Adapter: "Clair", + Vendor: "Harbor", + Version: "0.1.0", + } + + updated := &scanner.Registration{ + ID: 1002, + UUID: "uuid", + Name: "TestScannerAPIUpdate", + Description: "JUST FOR TEST", + URL: "https://a.b.c", + Adapter: "Clair", + Vendor: "Harbor", + Version: "0.1.0", + } + + suite.mockQuery(updated) + suite.mockC.On("UpdateRegistration", updated).Return(nil) + suite.mockC.On("GetRegistration", "uuid").Return(before, nil) + + rr := &scanner.Registration{} + err := handleAndParse(&testingRequest{ + url: fmt.Sprintf("%s/%s", rootRoute, "uuid"), + method: http.MethodPut, + credential: sysAdmin, + bodyJSON: updated, + }, rr) + require.NoError(suite.T(), err) + require.NotNil(suite.T(), rr) + + assert.Equal(suite.T(), updated.Name, rr.Name) + assert.Equal(suite.T(), updated.UUID, rr.UUID) +} + +// +func (suite *ScannerAPITestSuite) TestScannerAPIDelete() { + r := &scanner.Registration{ + ID: 1003, + UUID: "uuid", + 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) + + deleted := &scanner.Registration{} + err := handleAndParse(&testingRequest{ + url: fmt.Sprintf("%s/%s", rootRoute, "uuid"), + method: http.MethodDelete, + credential: sysAdmin, + }, deleted) + + require.NoError(suite.T(), err) + assert.Equal(suite.T(), r.UUID, deleted.UUID) + assert.Equal(suite.T(), r.Name, deleted.Name) +} + +// TestScannerAPISetDefault tests the set default +func (suite *ScannerAPITestSuite) TestScannerAPISetDefault() { + suite.mockC.On("SetDefaultRegistration", "uuid").Return(nil) + + body := make(map[string]interface{}, 1) + body["is_default"] = true + runCodeCheckingCases(suite.T(), &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("%s/%s", rootRoute, "uuid"), + method: http.MethodPatch, + credential: sysAdmin, + bodyJSON: body, + }, + code: http.StatusOK, + }) +} + +// TestScannerAPIProjectScanner tests the API of getting/setting project level scanner +func (suite *ScannerAPITestSuite) TestScannerAPIProjectScanner() { + suite.mockC.On("SetRegistrationByProject", int64(1), "uuid").Return(nil) + + // Set + body := make(map[string]interface{}, 1) + body["uuid"] = "uuid" + runCodeCheckingCases(suite.T(), &codeCheckingCase{ + request: &testingRequest{ + url: fmt.Sprintf("/api/projects/%d/scanner", 1), + method: http.MethodPut, + credential: sysAdmin, + bodyJSON: body, + }, + code: http.StatusOK, + }) + + r := &scanner.Registration{ + ID: 1004, + UUID: "uuid", + 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) + + // Get + rr := &scanner.Registration{} + err := handleAndParse(&testingRequest{ + url: fmt.Sprintf("/api/projects/%d/scanner", 1), + method: http.MethodGet, + credential: sysAdmin, + }, rr) + require.NoError(suite.T(), err) + + assert.Equal(suite.T(), r.Name, rr.Name) + assert.Equal(suite.T(), r.UUID, rr.UUID) +} + +func (suite *ScannerAPITestSuite) mockQuery(r *scanner.Registration) { + kw := make(map[string]string, 1) + kw["name"] = r.Name + query := &q.Query{ + Keywords: kw, + } + emptyL := make([]*scanner.Registration, 0) + suite.mockC.On("ListRegistrations", query).Return(emptyL, nil) + + kw2 := make(map[string]string, 1) + kw2["url"] = r.URL + query2 := &q.Query{ + Keywords: kw2, + } + suite.mockC.On("ListRegistrations", query2).Return(emptyL, nil) +} + +// MockScannerAPIController is mock of scanner API controller +type MockScannerAPIController struct { + mock.Mock +} + +// ListRegistrations ... +func (m *MockScannerAPIController) ListRegistrations(query *q.Query) ([]*scanner.Registration, error) { + args := m.Called(query) + return args.Get(0).([]*scanner.Registration), args.Error(1) +} + +// CreateRegistration ... +func (m *MockScannerAPIController) CreateRegistration(registration *scanner.Registration) (string, error) { + args := m.Called(registration) + return args.String(0), args.Error(1) +} + +// GetRegistration ... +func (m *MockScannerAPIController) GetRegistration(registrationUUID string) (*scanner.Registration, error) { + args := m.Called(registrationUUID) + s := args.Get(0) + if s == nil { + return nil, args.Error(1) + } + + return s.(*scanner.Registration), args.Error(1) +} + +// RegistrationExists ... +func (m *MockScannerAPIController) RegistrationExists(registrationUUID string) bool { + args := m.Called(registrationUUID) + return args.Bool(0) +} + +// UpdateRegistration ... +func (m *MockScannerAPIController) UpdateRegistration(registration *scanner.Registration) error { + args := m.Called(registration) + return args.Error(0) +} + +// DeleteRegistration ... +func (m *MockScannerAPIController) DeleteRegistration(registrationUUID string) (*scanner.Registration, error) { + args := m.Called(registrationUUID) + s := args.Get(0) + if s == nil { + return nil, args.Error(1) + } + + return s.(*scanner.Registration), args.Error(1) +} + +// SetDefaultRegistration ... +func (m *MockScannerAPIController) SetDefaultRegistration(registrationUUID string) error { + args := m.Called(registrationUUID) + return args.Error(0) +} + +// SetRegistrationByProject ... +func (m *MockScannerAPIController) SetRegistrationByProject(projectID int64, scannerID string) error { + args := m.Called(projectID, scannerID) + return args.Error(0) +} + +// GetRegistrationByProject ... +func (m *MockScannerAPIController) GetRegistrationByProject(projectID int64) (*scanner.Registration, error) { + args := m.Called(projectID) + s := args.Get(0) + if s == nil { + return nil, args.Error(1) + } + + 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) +} diff --git a/src/core/router.go b/src/core/router.go index 7e01b934e..deb862c6a 100755 --- a/src/core/router.go +++ b/src/core/router.go @@ -192,6 +192,13 @@ func initRouters() { beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel") } + // 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") + // Add routes for project level scanner + beego.Router("/api/projects/:pid([0-9]+)/scanner", scannerAPI, "get:GetProjectScanner;put:SetProjectScanner") + // Error pages beego.ErrorController(&controllers.ErrorController{}) diff --git a/src/go.mod b/src/go.mod index fdc8554c8..11b8d578a 100644 --- a/src/go.mod +++ b/src/go.mod @@ -45,6 +45,7 @@ require ( github.com/google/certificate-transparency-go v1.0.21 // indirect github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect + github.com/google/uuid v1.1.1 github.com/gorilla/handlers v1.3.0 github.com/gorilla/mux v1.6.2 github.com/graph-gophers/dataloader v5.0.0+incompatible diff --git a/src/go.sum b/src/go.sum index 5ac4284ff..aa24c1d75 100644 --- a/src/go.sum +++ b/src/go.sum @@ -140,6 +140,8 @@ github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeq github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= diff --git a/src/pkg/q/query.go b/src/pkg/q/query.go new file mode 100644 index 000000000..048a25298 --- /dev/null +++ b/src/pkg/q/query.go @@ -0,0 +1,25 @@ +// 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 q + +// Query parameters +type Query struct { + // Page number + PageNumber int64 + // Page size + PageSize int64 + // List of key words + Keywords map[string]string +} diff --git a/src/pkg/scan/scanner/api/controller.go b/src/pkg/scan/scanner/api/controller.go new file mode 100644 index 000000000..090dcde5f --- /dev/null +++ b/src/pkg/scan/scanner/api/controller.go @@ -0,0 +1,157 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +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" +) + +// Controller provides the related operations of scanner for the upper API. +// All the capabilities of the scanner are defined here. +type Controller interface { + // ListRegistrations returns a list of currently configured scanner registrations. + // Query parameters are optional + // + // Arguments: + // query *q.Query : query parameters + // + // Returns: + // []*scanner.Registration : scanner list of all the matched ones + // error : non nil error if any errors occurred + ListRegistrations(query *q.Query) ([]*scanner.Registration, error) + + // CreateRegistration creates a new scanner registration with the given data. + // Returns the scanner registration identifier. + // + // Arguments: + // registration *scanner.Registration : scanner registration to create + // + // Returns: + // string : the generated UUID of the new scanner + // error : non nil error if any errors occurred + CreateRegistration(registration *scanner.Registration) (string, error) + + // GetRegistration returns the details of the specified scanner registration. + // + // Arguments: + // registrationUUID string : the UUID of the given scanner + // + // Returns: + // *scanner.Registration : the required scanner + // error : non nil error if any errors occurred + GetRegistration(registrationUUID string) (*scanner.Registration, error) + + // RegistrationExists checks if the provided registration is there. + // + // Arguments: + // registrationUUID string : the UUID of the given scanner + // + // Returns: + // true for existing or false for not existing + RegistrationExists(registrationUUID string) bool + + // UpdateRegistration updates the specified scanner registration. + // + // Arguments: + // registration *scanner.Registration : scanner registration to update + // + // Returns: + // error : non nil error if any errors occurred + UpdateRegistration(registration *scanner.Registration) error + + // DeleteRegistration deletes the specified scanner registration. + // + // Arguments: + // registrationUUID string : the UUID of the given scanner which is going to be deleted + // + // Returns: + // *scanner.Registration : the deleted scanner + // error : non nil error if any errors occurred + DeleteRegistration(registrationUUID string) (*scanner.Registration, error) + + // SetDefaultRegistration marks the specified scanner registration as default. + // The implementation is supposed to unset any registration previously set as default. + // + // Arguments: + // registrationUUID string : the UUID of the given scanner which is marked as default + // + // Returns: + // error : non nil error if any errors occurred + SetDefaultRegistration(registrationUUID string) error + + // SetRegistrationByProject sets scanner for the given project. + // + // Arguments: + // projectID int64 : the ID of the given project + // scannerID string : the UUID of the the scanner + // + // Returns: + // error : non nil error if any errors occurred + SetRegistrationByProject(projectID int64, scannerID string) error + + // GetRegistrationByProject returns the configured scanner registration of the given project or + // the system default registration if exists or `nil` if no system registrations set. + // + // Arguments: + // projectID int64 : the ID of the given project + // + // Returns: + // *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) +} diff --git a/src/pkg/scan/scanner/api/controller_test.go b/src/pkg/scan/scanner/api/controller_test.go new file mode 100644 index 000000000..0ea4c5ad2 --- /dev/null +++ b/src/pkg/scan/scanner/api/controller_test.go @@ -0,0 +1,287 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "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/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +// ControllerTestSuite is test suite to test the basic api controller. +type ControllerTestSuite struct { + suite.Suite + + c *basicController + mMgr *MockScannerManager + mMeta *MockProMetaManager + + sample *scanner.Registration +} + +// TestController is the entry of controller test suite +func TestController(t *testing.T) { + suite.Run(t, new(ControllerTestSuite)) +} + +// SetupSuite prepares env for the controller test suite +func (suite *ControllerTestSuite) SetupSuite() { + suite.mMgr = new(MockScannerManager) + suite.mMeta = new(MockProMetaManager) + + suite.c = &basicController{ + manager: suite.mMgr, + proMetaMgr: suite.mMeta, + } + + suite.sample = &scanner.Registration{ + Name: "forUT", + Description: "sample registration", + URL: "https://sample.scanner.com", + Adapter: "Clair", + Version: "0.1.0", + Vendor: "Harbor", + } +} + +// Clear test case +func (suite *ControllerTestSuite) TearDownTest() { + suite.sample.UUID = "" +} + +// TestListRegistrations tests ListRegistrations +func (suite *ControllerTestSuite) TestListRegistrations() { + query := &q.Query{ + PageSize: 10, + PageNumber: 1, + } + + suite.sample.UUID = "uuid" + l := []*scanner.Registration{suite.sample} + + suite.mMgr.On("List", query).Return(l, nil) + + rl, err := suite.c.ListRegistrations(query) + require.NoError(suite.T(), err) + assert.Equal(suite.T(), 1, len(rl)) +} + +// TestCreateRegistration tests CreateRegistration +func (suite *ControllerTestSuite) TestCreateRegistration() { + suite.mMgr.On("Create", suite.sample).Return("uuid", nil) + + uid, err := suite.mMgr.Create(suite.sample) + + require.NoError(suite.T(), err) + assert.Equal(suite.T(), uid, "uuid") +} + +// TestGetRegistration tests GetRegistration +func (suite *ControllerTestSuite) TestGetRegistration() { + suite.sample.UUID = "uuid" + suite.mMgr.On("Get", "uuid").Return(suite.sample, nil) + + rr, err := suite.c.GetRegistration("uuid") + require.NoError(suite.T(), err) + assert.NotNil(suite.T(), rr) + assert.Equal(suite.T(), "forUT", rr.Name) +} + +// TestRegistrationExists tests RegistrationExists +func (suite *ControllerTestSuite) TestRegistrationExists() { + suite.sample.UUID = "uuid" + suite.mMgr.On("Get", "uuid").Return(suite.sample, nil) + + exists := suite.c.RegistrationExists("uuid") + assert.Equal(suite.T(), true, exists) + + suite.mMgr.On("Get", "uuid2").Return(nil, nil) + + exists = suite.c.RegistrationExists("uuid2") + assert.Equal(suite.T(), false, exists) +} + +// TestUpdateRegistration tests UpdateRegistration +func (suite *ControllerTestSuite) TestUpdateRegistration() { + suite.sample.UUID = "uuid" + suite.mMgr.On("Update", suite.sample).Return(nil) + + err := suite.c.UpdateRegistration(suite.sample) + require.NoError(suite.T(), err) +} + +// TestDeleteRegistration tests DeleteRegistration +func (suite *ControllerTestSuite) TestDeleteRegistration() { + suite.sample.UUID = "uuid" + suite.mMgr.On("Get", "uuid").Return(suite.sample, nil) + suite.mMgr.On("Delete", "uuid").Return(nil) + + r, err := suite.c.DeleteRegistration("uuid") + require.NoError(suite.T(), err) + require.NotNil(suite.T(), r) + assert.Equal(suite.T(), "forUT", r.Name) +} + +// TestSetDefaultRegistration tests SetDefaultRegistration +func (suite *ControllerTestSuite) TestSetDefaultRegistration() { + suite.mMgr.On("SetAsDefault", "uuid").Return(nil) + + err := suite.c.SetDefaultRegistration("uuid") + require.NoError(suite.T(), err) +} + +// TestSetRegistrationByProject tests SetRegistrationByProject +func (suite *ControllerTestSuite) TestSetRegistrationByProject() { + m := make(map[string]string, 1) + mm := make(map[string]string, 1) + mmm := make(map[string]string, 1) + mm[proScannerMetaKey] = "uuid" + mmm[proScannerMetaKey] = "uuid2" + + var pid, pid2 int64 = 1, 2 + + // not set before + suite.mMeta.On("Get", pid, []string{proScannerMetaKey}).Return(m, nil) + suite.mMeta.On("Add", pid, mm).Return(nil) + + err := suite.c.SetRegistrationByProject(pid, "uuid") + require.NoError(suite.T(), err) + + // Set before + suite.mMeta.On("Get", pid2, []string{proScannerMetaKey}).Return(mm, nil) + suite.mMeta.On("Update", pid2, mmm).Return(nil) + + err = suite.c.SetRegistrationByProject(pid2, "uuid2") + require.NoError(suite.T(), err) +} + +// TestGetRegistrationByProject tests GetRegistrationByProject +func (suite *ControllerTestSuite) TestGetRegistrationByProject() { + m := make(map[string]string, 1) + m[proScannerMetaKey] = "uuid" + + // Configured at project level + var pid int64 = 1 + suite.sample.UUID = "uuid" + + suite.mMeta.On("Get", pid, []string{proScannerMetaKey}).Return(m, nil) + suite.mMgr.On("Get", "uuid").Return(suite.sample, nil) + + r, err := suite.c.GetRegistrationByProject(pid) + require.NoError(suite.T(), err) + require.Equal(suite.T(), "forUT", r.Name) + + // Not configured at project level, return system default + suite.mMeta.On("Get", pid, []string{proScannerMetaKey}).Return(nil, nil) + suite.mMgr.On("GetDefault").Return(suite.sample, nil) + + r, err = suite.c.GetRegistrationByProject(pid) + require.NoError(suite.T(), err) + require.NotNil(suite.T(), r) + assert.Equal(suite.T(), "forUT", r.Name) +} + +// MockScannerManager is mock of the scanner manager +type MockScannerManager struct { + mock.Mock +} + +// List ... +func (m *MockScannerManager) List(query *q.Query) ([]*scanner.Registration, error) { + args := m.Called(query) + return args.Get(0).([]*scanner.Registration), args.Error(1) +} + +// Create ... +func (m *MockScannerManager) Create(registration *scanner.Registration) (string, error) { + args := m.Called(registration) + return args.String(0), args.Error(1) +} + +// Get ... +func (m *MockScannerManager) Get(registrationUUID string) (*scanner.Registration, error) { + args := m.Called(registrationUUID) + r := args.Get(0) + if r == nil { + return nil, args.Error(1) + } + + return r.(*scanner.Registration), args.Error(1) +} + +// Update ... +func (m *MockScannerManager) Update(registration *scanner.Registration) error { + args := m.Called(registration) + return args.Error(0) +} + +// Delete ... +func (m *MockScannerManager) Delete(registrationUUID string) error { + args := m.Called(registrationUUID) + return args.Error(0) +} + +// SetAsDefault ... +func (m *MockScannerManager) SetAsDefault(registrationUUID string) error { + args := m.Called(registrationUUID) + return args.Error(0) +} + +// GetDefault ... +func (m *MockScannerManager) GetDefault() (*scanner.Registration, error) { + args := m.Called() + return args.Get(0).(*scanner.Registration), args.Error(1) +} + +// MockProMetaManager is the mock of the ProjectMetadataManager +type MockProMetaManager struct { + mock.Mock +} + +// Add ... +func (m *MockProMetaManager) Add(projectID int64, meta map[string]string) error { + args := m.Called(projectID, meta) + return args.Error(0) +} + +// Delete ... +func (m *MockProMetaManager) Delete(projecdtID int64, meta ...string) error { + args := m.Called(projecdtID, meta) + return args.Error(0) +} + +// Update ... +func (m *MockProMetaManager) Update(projectID int64, meta map[string]string) error { + args := m.Called(projectID, meta) + return args.Error(0) +} + +// Get ... +func (m *MockProMetaManager) Get(projectID int64, meta ...string) (map[string]string, error) { + args := m.Called(projectID, meta) + return args.Get(0).(map[string]string), args.Error(1) +} + +// List ... +func (m *MockProMetaManager) List(name, value string) ([]*models.ProjectMetadata, error) { + args := m.Called(name, value) + return args.Get(0).([]*models.ProjectMetadata), args.Error(1) +} diff --git a/src/pkg/scan/scanner/api/registration.go b/src/pkg/scan/scanner/api/registration.go new file mode 100644 index 000000000..879498915 --- /dev/null +++ b/src/pkg/scan/scanner/api/registration.go @@ -0,0 +1,194 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "github.com/goharbor/harbor/src/core/promgr/metamgr" + "github.com/goharbor/harbor/src/jobservice/logger" + "github.com/goharbor/harbor/src/pkg/q" + 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" +) + +const ( + proScannerMetaKey = "projectScanner" +) + +// DefaultController is a singleton api controller for plug scanners +var DefaultController = New() + +// New a basic controller +func New() Controller { + return &basicController{ + manager: rscanner.New(), + proMetaMgr: metamgr.NewDefaultProjectMetadataManager(), + } +} + +// basicController is default implementation of api.Controller interface +type basicController struct { + // managers for managing the scanner registrations + manager rscanner.Manager + // for operating the project level configured scanner + proMetaMgr metamgr.ProjectMetadataManager + // controller for scan actions + c scan.Controller + // Client +} + +// ListRegistrations ... +func (bc *basicController) ListRegistrations(query *q.Query) ([]*scanner.Registration, error) { + return bc.manager.List(query) +} + +// CreateRegistration ... +func (bc *basicController) CreateRegistration(registration *scanner.Registration) (string, error) { + // TODO: Get metadata from the adapter service first + l, err := bc.manager.List(nil) + if err != nil { + return "", err + } + + if len(l) == 0 && !registration.IsDefault { + // Mark the 1st as default automatically + registration.IsDefault = true + } + + return bc.manager.Create(registration) +} + +// GetRegistration ... +func (bc *basicController) GetRegistration(registrationUUID string) (*scanner.Registration, error) { + return bc.manager.Get(registrationUUID) +} + +// RegistrationExists ... +func (bc *basicController) RegistrationExists(registrationUUID string) bool { + registration, err := bc.manager.Get(registrationUUID) + + // Just logged when an error occurred + if err != nil { + logger.Errorf("Check existence of registration error: %s", err) + } + + return !(err == nil && registration == nil) +} + +// UpdateRegistration ... +func (bc *basicController) UpdateRegistration(registration *scanner.Registration) error { + return bc.manager.Update(registration) +} + +// SetDefaultRegistration ... +func (bc *basicController) DeleteRegistration(registrationUUID string) (*scanner.Registration, error) { + registration, err := bc.manager.Get(registrationUUID) + if registration == nil && err == nil { + // Not found + return nil, nil + } + + if err := bc.manager.Delete(registrationUUID); err != nil { + return nil, errors.Wrap(err, "delete registration") + } + + return registration, nil +} + +// SetDefaultRegistration ... +func (bc *basicController) SetDefaultRegistration(registrationUUID string) error { + return bc.manager.SetAsDefault(registrationUUID) +} + +// SetRegistrationByProject ... +func (bc *basicController) SetRegistrationByProject(projectID int64, registrationID string) error { + if projectID == 0 { + return errors.New("invalid project ID") + } + + if len(registrationID) == 0 { + return errors.New("missing scanner UUID") + } + + // Only keep the UUID in the metadata of the given project + // Scanner metadata existing? + m, err := bc.proMetaMgr.Get(projectID, proScannerMetaKey) + if err != nil { + return errors.Wrap(err, "set project scanner") + } + + // Update if exists + if len(m) > 0 { + // Compare and set new + if registrationID != m[proScannerMetaKey] { + m[proScannerMetaKey] = registrationID + if err := bc.proMetaMgr.Update(projectID, m); err != nil { + return errors.Wrap(err, "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 nil +} + +// GetRegistrationByProject ... +func (bc *basicController) GetRegistrationByProject(projectID int64) (*scanner.Registration, error) { + if projectID == 0 { + return nil, errors.New("invalid project ID") + } + + // 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") + } + + 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") + } + + 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") + } + } else { + return registration, nil + } + } + } + + // Second, get the default one + registration, err := bc.manager.GetDefault() + + // TODO: Check status by the client later + return registration, err +} + +// Ping ... +func (bc *basicController) Ping(registration *scanner.Registration) error { + return nil +} diff --git a/src/pkg/scan/scanner/api/scan.go b/src/pkg/scan/scanner/api/scan.go new file mode 100644 index 000000000..a8b1a47d4 --- /dev/null +++ b/src/pkg/scan/scanner/api/scan.go @@ -0,0 +1,35 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + dscan "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scan" + "github.com/goharbor/harbor/src/pkg/scan/scanner/scan" +) + +// Scan ... +func (bc *basicController) Scan(artifact *scan.Artifact) error { + 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 +} diff --git a/src/pkg/scan/scanner/dao/scan/report.go b/src/pkg/scan/scanner/dao/scan/report.go new file mode 100644 index 000000000..13d3f6cc9 --- /dev/null +++ b/src/pkg/scan/scanner/dao/scan/report.go @@ -0,0 +1,43 @@ +// 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 "time" + +// Report of the scan +// Identified by the `digest` and `endpoint_id` +type Report struct { + ID int64 `orm:"pk;auto;column(id)"` + Digest string `orm:"column(digest)"` + ReregistrationID string `orm:"column(registration_id)"` + JobID string `orm:"column(job_id)"` + Status string `orm:"column(status)"` + StatusCode int `orm:"column(status_code)"` + 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)"` +} + +// TableName for Report +func (r *Report) TableName() string { + return "scanner_report" +} + +// TableUnique for Report +func (r *Report) TableUnique() [][]string { + return [][]string{ + {"digest", "registration_id"}, + } +} diff --git a/src/pkg/scan/scanner/dao/scanner/model.go b/src/pkg/scan/scanner/dao/scanner/model.go new file mode 100644 index 000000000..fa28b87df --- /dev/null +++ b/src/pkg/scan/scanner/dao/scanner/model.go @@ -0,0 +1,120 @@ +// 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 scanner + +import ( + "encoding/json" + "net/url" + "strings" + "time" + + "github.com/pkg/errors" +) + +// Registration represents a named configuration for invoking a scanner via its adapter. +// UUID will be used to track the scanner.Endpoint as unique ID +type Registration struct { + // Basic information + // int64 ID is kept for being aligned with previous DB schema + ID int64 `orm:"pk;auto;column(id)" json:"-"` + UUID string `orm:"unique;column(uuid)" json:"uuid"` + Name string `orm:"unique;column(name);size(128)" json:"name"` + Description string `orm:"column(description);null;size(1024)" json:"description"` + URL string `orm:"column(url);unique;size(512)" json:"url"` + Disabled bool `orm:"column(disabled);default(true)" json:"disabled"` + IsDefault bool `orm:"column(is_default);default(false)" json:"is_default"` + Health bool `orm:"-" json:"health"` + + // Authentication settings + // "None","Basic" and "Bearer" can be supported + Auth string `orm:"column(auth);size(16)" json:"auth"` + AccessCredential string `orm:"column(access_cred);null;size(512)" json:"access_credential,omitempty"` + + // 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"` +} + +// TableName for Endpoint +func (r *Registration) TableName() string { + return "scanner_registration" +} + +// FromJSON parses json data +func (r *Registration) FromJSON(jsonData string) error { + if len(jsonData) == 0 { + return errors.New("empty json data to parse") + } + + return json.Unmarshal([]byte(jsonData), r) +} + +// ToJSON marshals endpoint to JSON data +func (r *Registration) ToJSON() (string, error) { + data, err := json.Marshal(r) + if err != nil { + return "", err + } + + return string(data), nil +} + +// Validate endpoint +func (r *Registration) Validate(checkUUID bool) error { + if checkUUID && len(r.UUID) == 0 { + return errors.New("malformed endpoint") + } + + if len(r.Name) == 0 { + return errors.New("missing registration name") + } + + err := checkURL(r.URL) + if err != nil { + 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 +} + +// Check the registration URL with url package +func checkURL(u string) error { + if len(strings.TrimSpace(u)) == 0 { + return errors.New("empty url") + } + + uri, err := url.Parse(u) + if err == nil { + if uri.Scheme != "http" && uri.Scheme != "https" { + err = errors.New("invalid scheme") + } + } + + return err +} diff --git a/src/pkg/scan/scanner/dao/scanner/model_test.go b/src/pkg/scan/scanner/dao/scanner/model_test.go new file mode 100644 index 000000000..25d837f92 --- /dev/null +++ b/src/pkg/scan/scanner/dao/scanner/model_test.go @@ -0,0 +1,87 @@ +// 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 scanner + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +// ModelTestSuite tests the utility functions of the model +type ModelTestSuite struct { + suite.Suite +} + +// TestModel is the entry of the model test suite +func TestModel(t *testing.T) { + suite.Run(t, new(ModelTestSuite)) +} + +// TestJSON tests the marshal and unmarshal functions +func (suite *ModelTestSuite) TestJSON() { + r := &Registration{ + Name: "forUT", + Description: "sample registration", + URL: "https://sample.scanner.com", + Adapter: "Clair", + Version: "0.1.0", + Vendor: "Harbor", + } + + json, err := r.ToJSON() + require.NoError(suite.T(), err) + assert.Condition(suite.T(), func() (success bool) { + success = len(json) > 0 + return + }) + + r2 := &Registration{} + err = r2.FromJSON(json) + require.NoError(suite.T(), err) + assert.Equal(suite.T(), "forUT", r2.Name) +} + +// TestValidate tests the validate function +func (suite *ModelTestSuite) TestValidate() { + r := &Registration{} + + err := r.Validate(true) + require.Error(suite.T(), err) + + r.UUID = "uuid" + err = r.Validate(true) + require.Error(suite.T(), err) + + r.Name = "forUT" + err = r.Validate(true) + require.Error(suite.T(), err) + + r.URL = "a.b.c" + err = r.Validate(true) + require.Error(suite.T(), err) + + r.URL = "http://a.b.c" + err = r.Validate(true) + require.Error(suite.T(), err) + + r.Adapter = "Clair" + r.Vendor = "Harbor" + r.Version = "0.1.0" + err = r.Validate(true) + require.NoError(suite.T(), err) +} diff --git a/src/pkg/scan/scanner/dao/scanner/registration.go b/src/pkg/scan/scanner/dao/scanner/registration.go new file mode 100644 index 000000000..da2912dea --- /dev/null +++ b/src/pkg/scan/scanner/dao/scanner/registration.go @@ -0,0 +1,147 @@ +// 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 scanner + +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(Registration)) +} + +// AddRegistration adds a new registration +func AddRegistration(r *Registration) (int64, error) { + o := dao.GetOrmer() + return o.Insert(r) +} + +// GetRegistration gets the specified registration +func GetRegistration(UUID string) (*Registration, error) { + e := &Registration{} + + o := dao.GetOrmer() + qs := o.QueryTable(new(Registration)) + + if err := qs.Filter("uuid", UUID).One(e); err != nil { + if err == orm.ErrNoRows { + // Not existing case + return nil, nil + } + return nil, err + } + + return e, nil +} + +// UpdateRegistration update the specified registration +func UpdateRegistration(r *Registration, cols ...string) error { + o := dao.GetOrmer() + count, err := o.Update(r, cols...) + if err != nil { + return err + } + + if count == 0 { + return errors.Errorf("no item with UUID %s is updated", r.UUID) + } + + return nil +} + +// DeleteRegistration deletes the registration with the specified UUID +func DeleteRegistration(UUID string) error { + o := dao.GetOrmer() + qt := o.QueryTable(new(Registration)) + + // delete with query way + count, err := qt.Filter("uuid", UUID).Delete() + + if err != nil { + return err + } + + if count == 0 { + return errors.Errorf("no item with UUID %s is deleted", UUID) + } + + return nil +} + +// ListRegistrations lists all the existing registrations +func ListRegistrations(query *q.Query) ([]*Registration, error) { + o := dao.GetOrmer() + qt := o.QueryTable(new(Registration)) + + if query != nil { + if len(query.Keywords) > 0 { + for k, v := range query.Keywords { + qt = qt.Filter(fmt.Sprintf("%s__icontains", k), v) + } + } + + if query.PageNumber > 0 && query.PageSize > 0 { + qt = qt.Limit(query.PageSize, (query.PageNumber-1)*query.PageSize) + } + } + + l := make([]*Registration, 0) + _, err := qt.All(&l) + + return l, err +} + +// SetDefaultRegistration sets the specified registration as default one +func SetDefaultRegistration(UUID string) error { + o := dao.GetOrmer() + qt := o.QueryTable(new(Registration)) + + _, err := qt.Filter("is_default", true).Update(orm.Params{ + "is_default": false, + }) + + if err != nil { + return err + } + + qt2 := o.QueryTable(new(Registration)) + _, err = qt2.Filter("uuid", UUID).Update(orm.Params{ + "is_default": true, + }) + + return err +} + +// GetDefaultRegistration gets the default registration +func GetDefaultRegistration() (*Registration, error) { + o := dao.GetOrmer() + qt := o.QueryTable(new(Registration)) + + e := &Registration{} + if err := qt.Filter("is_default", true).One(e); err != nil { + if err == orm.ErrNoRows { + return nil, nil + } + + return nil, err + } + + return e, nil +} diff --git a/src/pkg/scan/scanner/dao/scanner/registration_test.go b/src/pkg/scan/scanner/dao/scanner/registration_test.go new file mode 100644 index 000000000..1575bf67f --- /dev/null +++ b/src/pkg/scan/scanner/dao/scanner/registration_test.go @@ -0,0 +1,144 @@ +// 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 scanner + +import ( + "testing" + + "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/pkg/q" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +// RegistrationDAOTestSuite is test suite of testing registration DAO +type RegistrationDAOTestSuite struct { + suite.Suite + + registrationID string +} + +// TestRegistrationDAO is entry of test cases +func TestRegistrationDAO(t *testing.T) { + suite.Run(t, new(RegistrationDAOTestSuite)) +} + +// SetupSuite prepare testing env for the suite +func (suite *RegistrationDAOTestSuite) SetupSuite() { + dao.PrepareTestForPostgresSQL() +} + +// SetupTest prepare stuff for test cases +func (suite *RegistrationDAOTestSuite) SetupTest() { + suite.registrationID = uuid.New().String() + r := &Registration{ + UUID: suite.registrationID, + Name: "forUT", + Description: "sample registration", + URL: "https://sample.scanner.com", + Adapter: "Clair", + Version: "0.1.0", + Vendor: "Harbor", + } + + _, err := AddRegistration(r) + require.NoError(suite.T(), err, "add new registration") + +} + +// TearDownTest clears all the stuff of test cases +func (suite *RegistrationDAOTestSuite) TearDownTest() { + err := DeleteRegistration(suite.registrationID) + require.NoError(suite.T(), err, "clear registration") +} + +// TestGet tests get registration +func (suite *RegistrationDAOTestSuite) TestGet() { + // Found + r, err := GetRegistration(suite.registrationID) + require.NoError(suite.T(), err) + require.NotNil(suite.T(), r) + assert.Equal(suite.T(), r.Name, "forUT") + + // Not found + re, err := GetRegistration("not_found") + require.NoError(suite.T(), err) + require.Nil(suite.T(), re) +} + +// TestUpdate tests update registration +func (suite *RegistrationDAOTestSuite) TestUpdate() { + r, err := GetRegistration(suite.registrationID) + require.NoError(suite.T(), err) + require.NotNil(suite.T(), r) + + r.Disabled = true + r.IsDefault = true + r.URL = "http://updated.registration.com" + + err = UpdateRegistration(r) + require.NoError(suite.T(), err, "update registration") + + r, err = GetRegistration(suite.registrationID) + require.NoError(suite.T(), err) + require.NotNil(suite.T(), r) + + assert.Equal(suite.T(), true, r.Disabled) + assert.Equal(suite.T(), true, r.IsDefault) + assert.Equal(suite.T(), "http://updated.registration.com", r.URL) +} + +// TestList tests list registrations +func (suite *RegistrationDAOTestSuite) TestList() { + // no query + l, err := ListRegistrations(nil) + require.NoError(suite.T(), err) + require.Equal(suite.T(), 1, len(l)) + + // with query and found items + keywords := make(map[string]string) + keywords["adapter"] = "Clair" + l, err = ListRegistrations(&q.Query{ + PageSize: 5, + PageNumber: 1, + Keywords: keywords, + }) + require.NoError(suite.T(), err) + require.Equal(suite.T(), 1, len(l)) + + // With query and not found items + keywords["adapter"] = "Micro scanner" + l, err = ListRegistrations(&q.Query{ + Keywords: keywords, + }) + require.NoError(suite.T(), err) + require.Equal(suite.T(), 0, len(l)) +} + +// TestDefault tests set/get default +func (suite *RegistrationDAOTestSuite) TestDefault() { + dr, err := GetDefaultRegistration() + require.NoError(suite.T(), err, "not found") + require.Nil(suite.T(), dr) + + err = SetDefaultRegistration(suite.registrationID) + require.NoError(suite.T(), err) + + dr, err = GetDefaultRegistration() + require.NoError(suite.T(), err) + require.NotNil(suite.T(), dr) +} diff --git a/src/pkg/scan/scanner/manager.go b/src/pkg/scan/scanner/manager.go new file mode 100644 index 000000000..3387a8fb7 --- /dev/null +++ b/src/pkg/scan/scanner/manager.go @@ -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 scanner + +import ( + "github.com/goharbor/harbor/src/pkg/q" + "github.com/goharbor/harbor/src/pkg/scan/scanner/dao/scanner" + "github.com/google/uuid" + "github.com/pkg/errors" +) + +// Manager defines the related scanner API endpoints +type Manager interface { + // List returns a list of currently configured scanner registrations. + // Query parameters are optional + List(query *q.Query) ([]*scanner.Registration, error) + + // Create creates a new scanner registration with the given data. + // Returns the scanner registration identifier. + Create(registration *scanner.Registration) (string, error) + + // Get returns the details of the specified scanner registration. + Get(registrationUUID string) (*scanner.Registration, error) + + // Update updates the specified scanner registration. + Update(registration *scanner.Registration) error + + // Delete deletes the specified scanner registration. + Delete(registrationUUID string) error + + // SetAsDefault marks the specified scanner registration as default. + // The implementation is supposed to unset any registration previously set as default. + SetAsDefault(registrationUUID string) error + + // GetDefault returns the default scanner registration or `nil` if there are no registrations configured. + GetDefault() (*scanner.Registration, error) +} + +// basicManager is the default implementation of Manager +type basicManager struct{} + +// New a basic manager +func New() Manager { + return &basicManager{} +} + +// Create ... +func (bm *basicManager) Create(registration *scanner.Registration) (string, error) { + if registration == nil { + return "", errors.New("nil endpoint to create") + } + + // Inject new UUID + uid, err := uuid.NewUUID() + if err != nil { + return "", errors.Wrap(err, "new UUID: create registration") + } + registration.UUID = uid.String() + + if err := registration.Validate(true); err != nil { + return "", errors.Wrap(err, "create registration") + } + + if _, err := scanner.AddRegistration(registration); err != nil { + return "", errors.Wrap(err, "dao: create registration") + } + + return uid.String(), nil +} + +// Get ... +func (bm *basicManager) Get(registrationUUID string) (*scanner.Registration, error) { + if len(registrationUUID) == 0 { + return nil, errors.New("empty uuid of registration") + } + + return scanner.GetRegistration(registrationUUID) +} + +// Update ... +func (bm *basicManager) Update(registration *scanner.Registration) error { + if registration == nil { + return errors.New("nil endpoint to update") + } + + if err := registration.Validate(true); err != nil { + return errors.Wrap(err, "update endpoint") + } + + return scanner.UpdateRegistration(registration) +} + +// Delete ... +func (bm *basicManager) Delete(registrationUUID string) error { + if len(registrationUUID) == 0 { + return errors.New("empty UUID to delete") + } + + return scanner.DeleteRegistration(registrationUUID) +} + +// List ... +func (bm *basicManager) List(query *q.Query) ([]*scanner.Registration, error) { + return scanner.ListRegistrations(query) +} + +// SetAsDefault ... +func (bm *basicManager) SetAsDefault(registrationUUID string) error { + if len(registrationUUID) == 0 { + return errors.New("empty UUID to set default") + } + + return scanner.SetDefaultRegistration(registrationUUID) +} + +// GetDefault ... +func (bm *basicManager) GetDefault() (*scanner.Registration, error) { + return scanner.GetDefaultRegistration() +} diff --git a/src/pkg/scan/scanner/manager_test.go b/src/pkg/scan/scanner/manager_test.go new file mode 100644 index 000000000..0cee77117 --- /dev/null +++ b/src/pkg/scan/scanner/manager_test.go @@ -0,0 +1,115 @@ +// 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 scanner + +import ( + "testing" + + "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/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +// BasicManagerTestSuite tests the basic manager +type BasicManagerTestSuite struct { + suite.Suite + + mgr Manager + sampleUUID string +} + +// TestBasicManager is the entry of BasicManagerTestSuite +func TestBasicManager(t *testing.T) { + suite.Run(t, new(BasicManagerTestSuite)) +} + +// SetupSuite prepares env for test suite +func (suite *BasicManagerTestSuite) SetupSuite() { + dao.PrepareTestForPostgresSQL() + + suite.mgr = New() + + r := &scanner.Registration{ + Name: "forUT", + Description: "sample registration", + URL: "https://sample.scanner.com", + Adapter: "Clair", + Version: "0.1.0", + Vendor: "Harbor", + } + + uid, err := suite.mgr.Create(r) + require.NoError(suite.T(), err) + suite.sampleUUID = uid +} + +// TearDownSuite clears env for test suite +func (suite *BasicManagerTestSuite) TearDownSuite() { + err := suite.mgr.Delete(suite.sampleUUID) + require.NoError(suite.T(), err, "delete registration") +} + +// TestList tests list registrations +func (suite *BasicManagerTestSuite) TestList() { + m := make(map[string]string, 1) + m["name"] = "forUT" + + l, err := suite.mgr.List(&q.Query{ + PageNumber: 1, + PageSize: 10, + Keywords: m, + }) + + require.NoError(suite.T(), err) + require.Equal(suite.T(), 1, len(l)) +} + +// TestGet tests get registration +func (suite *BasicManagerTestSuite) TestGet() { + r, err := suite.mgr.Get(suite.sampleUUID) + require.NoError(suite.T(), err) + require.NotNil(suite.T(), r) + assert.Equal(suite.T(), "forUT", r.Name) +} + +// TestUpdate tests update registration +func (suite *BasicManagerTestSuite) TestUpdate() { + r, err := suite.mgr.Get(suite.sampleUUID) + require.NoError(suite.T(), err) + require.NotNil(suite.T(), r) + + r.URL = "https://updated.com" + err = suite.mgr.Update(r) + require.NoError(suite.T(), err) + + r, err = suite.mgr.Get(suite.sampleUUID) + require.NoError(suite.T(), err) + require.NotNil(suite.T(), r) + assert.Equal(suite.T(), "https://updated.com", r.URL) +} + +// TestDefault tests get/set default registration +func (suite *BasicManagerTestSuite) TestDefault() { + err := suite.mgr.SetAsDefault(suite.sampleUUID) + require.NoError(suite.T(), err) + + dr, err := suite.mgr.GetDefault() + require.NoError(suite.T(), err) + require.NotNil(suite.T(), dr) + assert.Equal(suite.T(), true, dr.IsDefault) +} diff --git a/src/pkg/scan/scanner/scan/controller.go b/src/pkg/scan/scanner/scan/controller.go new file mode 100644 index 000000000..e7feb622f --- /dev/null +++ b/src/pkg/scan/scanner/scan/controller.go @@ -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 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) +} diff --git a/src/pkg/scan/scanner/scan/models.go b/src/pkg/scan/scanner/scan/models.go new file mode 100644 index 000000000..7bb67b909 --- /dev/null +++ b/src/pkg/scan/scanner/scan/models.go @@ -0,0 +1,46 @@ +// 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 +} diff --git a/src/vendor/github.com/google/uuid/.travis.yml b/src/vendor/github.com/google/uuid/.travis.yml new file mode 100644 index 000000000..d8156a60b --- /dev/null +++ b/src/vendor/github.com/google/uuid/.travis.yml @@ -0,0 +1,9 @@ +language: go + +go: + - 1.4.3 + - 1.5.3 + - tip + +script: + - go test -v ./... diff --git a/src/vendor/github.com/google/uuid/CONTRIBUTING.md b/src/vendor/github.com/google/uuid/CONTRIBUTING.md new file mode 100644 index 000000000..04fdf09f1 --- /dev/null +++ b/src/vendor/github.com/google/uuid/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# How to contribute + +We definitely welcome patches and contribution to this project! + +### Legal requirements + +In order to protect both you and ourselves, you will need to sign the +[Contributor License Agreement](https://cla.developers.google.com/clas). + +You may have already signed it for other Google projects. diff --git a/src/vendor/github.com/google/uuid/CONTRIBUTORS b/src/vendor/github.com/google/uuid/CONTRIBUTORS new file mode 100644 index 000000000..b4bb97f6b --- /dev/null +++ b/src/vendor/github.com/google/uuid/CONTRIBUTORS @@ -0,0 +1,9 @@ +Paul Borman +bmatsuo +shawnps +theory +jboverfelt +dsymonds +cd1 +wallclockbuilder +dansouza diff --git a/src/vendor/github.com/google/uuid/LICENSE b/src/vendor/github.com/google/uuid/LICENSE new file mode 100644 index 000000000..5dc68268d --- /dev/null +++ b/src/vendor/github.com/google/uuid/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009,2014 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/vendor/github.com/google/uuid/README.md b/src/vendor/github.com/google/uuid/README.md new file mode 100644 index 000000000..9d92c11f1 --- /dev/null +++ b/src/vendor/github.com/google/uuid/README.md @@ -0,0 +1,19 @@ +# uuid ![build status](https://travis-ci.org/google/uuid.svg?branch=master) +The uuid package generates and inspects UUIDs based on +[RFC 4122](http://tools.ietf.org/html/rfc4122) +and DCE 1.1: Authentication and Security Services. + +This package is based on the github.com/pborman/uuid package (previously named +code.google.com/p/go-uuid). It differs from these earlier packages in that +a UUID is a 16 byte array rather than a byte slice. One loss due to this +change is the ability to represent an invalid UUID (vs a NIL UUID). + +###### Install +`go get github.com/google/uuid` + +###### Documentation +[![GoDoc](https://godoc.org/github.com/google/uuid?status.svg)](http://godoc.org/github.com/google/uuid) + +Full `go doc` style documentation for the package can be viewed online without +installing this package by using the GoDoc site here: +http://godoc.org/github.com/google/uuid diff --git a/src/vendor/github.com/google/uuid/dce.go b/src/vendor/github.com/google/uuid/dce.go new file mode 100644 index 000000000..fa820b9d3 --- /dev/null +++ b/src/vendor/github.com/google/uuid/dce.go @@ -0,0 +1,80 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "fmt" + "os" +) + +// A Domain represents a Version 2 domain +type Domain byte + +// Domain constants for DCE Security (Version 2) UUIDs. +const ( + Person = Domain(0) + Group = Domain(1) + Org = Domain(2) +) + +// NewDCESecurity returns a DCE Security (Version 2) UUID. +// +// The domain should be one of Person, Group or Org. +// On a POSIX system the id should be the users UID for the Person +// domain and the users GID for the Group. The meaning of id for +// the domain Org or on non-POSIX systems is site defined. +// +// For a given domain/id pair the same token may be returned for up to +// 7 minutes and 10 seconds. +func NewDCESecurity(domain Domain, id uint32) (UUID, error) { + uuid, err := NewUUID() + if err == nil { + uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2 + uuid[9] = byte(domain) + binary.BigEndian.PutUint32(uuid[0:], id) + } + return uuid, err +} + +// NewDCEPerson returns a DCE Security (Version 2) UUID in the person +// domain with the id returned by os.Getuid. +// +// NewDCESecurity(Person, uint32(os.Getuid())) +func NewDCEPerson() (UUID, error) { + return NewDCESecurity(Person, uint32(os.Getuid())) +} + +// NewDCEGroup returns a DCE Security (Version 2) UUID in the group +// domain with the id returned by os.Getgid. +// +// NewDCESecurity(Group, uint32(os.Getgid())) +func NewDCEGroup() (UUID, error) { + return NewDCESecurity(Group, uint32(os.Getgid())) +} + +// Domain returns the domain for a Version 2 UUID. Domains are only defined +// for Version 2 UUIDs. +func (uuid UUID) Domain() Domain { + return Domain(uuid[9]) +} + +// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2 +// UUIDs. +func (uuid UUID) ID() uint32 { + return binary.BigEndian.Uint32(uuid[0:4]) +} + +func (d Domain) String() string { + switch d { + case Person: + return "Person" + case Group: + return "Group" + case Org: + return "Org" + } + return fmt.Sprintf("Domain%d", int(d)) +} diff --git a/src/vendor/github.com/google/uuid/doc.go b/src/vendor/github.com/google/uuid/doc.go new file mode 100644 index 000000000..5b8a4b9af --- /dev/null +++ b/src/vendor/github.com/google/uuid/doc.go @@ -0,0 +1,12 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package uuid generates and inspects UUIDs. +// +// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security +// Services. +// +// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to +// maps or compared directly. +package uuid diff --git a/src/vendor/github.com/google/uuid/go.mod b/src/vendor/github.com/google/uuid/go.mod new file mode 100644 index 000000000..fc84cd79d --- /dev/null +++ b/src/vendor/github.com/google/uuid/go.mod @@ -0,0 +1 @@ +module github.com/google/uuid diff --git a/src/vendor/github.com/google/uuid/hash.go b/src/vendor/github.com/google/uuid/hash.go new file mode 100644 index 000000000..b17461631 --- /dev/null +++ b/src/vendor/github.com/google/uuid/hash.go @@ -0,0 +1,53 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "crypto/md5" + "crypto/sha1" + "hash" +) + +// Well known namespace IDs and UUIDs +var ( + NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) + Nil UUID // empty UUID, all zeros +) + +// NewHash returns a new UUID derived from the hash of space concatenated with +// data generated by h. The hash should be at least 16 byte in length. The +// first 16 bytes of the hash are used to form the UUID. The version of the +// UUID will be the lower 4 bits of version. NewHash is used to implement +// NewMD5 and NewSHA1. +func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { + h.Reset() + h.Write(space[:]) + h.Write(data) + s := h.Sum(nil) + var uuid UUID + copy(uuid[:], s) + uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) + uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant + return uuid +} + +// NewMD5 returns a new MD5 (Version 3) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(md5.New(), space, data, 3) +func NewMD5(space UUID, data []byte) UUID { + return NewHash(md5.New(), space, data, 3) +} + +// NewSHA1 returns a new SHA1 (Version 5) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(sha1.New(), space, data, 5) +func NewSHA1(space UUID, data []byte) UUID { + return NewHash(sha1.New(), space, data, 5) +} diff --git a/src/vendor/github.com/google/uuid/marshal.go b/src/vendor/github.com/google/uuid/marshal.go new file mode 100644 index 000000000..7f9e0c6c0 --- /dev/null +++ b/src/vendor/github.com/google/uuid/marshal.go @@ -0,0 +1,37 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "fmt" + +// MarshalText implements encoding.TextMarshaler. +func (uuid UUID) MarshalText() ([]byte, error) { + var js [36]byte + encodeHex(js[:], uuid) + return js[:], nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (uuid *UUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err == nil { + *uuid = id + } + return err +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (uuid UUID) MarshalBinary() ([]byte, error) { + return uuid[:], nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (uuid *UUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(uuid[:], data) + return nil +} diff --git a/src/vendor/github.com/google/uuid/node.go b/src/vendor/github.com/google/uuid/node.go new file mode 100644 index 000000000..d651a2b06 --- /dev/null +++ b/src/vendor/github.com/google/uuid/node.go @@ -0,0 +1,90 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "sync" +) + +var ( + nodeMu sync.Mutex + ifname string // name of interface being used + nodeID [6]byte // hardware for version 1 UUIDs + zeroID [6]byte // nodeID with only 0's +) + +// NodeInterface returns the name of the interface from which the NodeID was +// derived. The interface "user" is returned if the NodeID was set by +// SetNodeID. +func NodeInterface() string { + defer nodeMu.Unlock() + nodeMu.Lock() + return ifname +} + +// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs. +// If name is "" then the first usable interface found will be used or a random +// Node ID will be generated. If a named interface cannot be found then false +// is returned. +// +// SetNodeInterface never fails when name is "". +func SetNodeInterface(name string) bool { + defer nodeMu.Unlock() + nodeMu.Lock() + return setNodeInterface(name) +} + +func setNodeInterface(name string) bool { + iname, addr := getHardwareInterface(name) // null implementation for js + if iname != "" && addr != nil { + ifname = iname + copy(nodeID[:], addr) + return true + } + + // We found no interfaces with a valid hardware address. If name + // does not specify a specific interface generate a random Node ID + // (section 4.1.6) + if name == "" { + ifname = "random" + randomBits(nodeID[:]) + return true + } + return false +} + +// NodeID returns a slice of a copy of the current Node ID, setting the Node ID +// if not already set. +func NodeID() []byte { + defer nodeMu.Unlock() + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + nid := nodeID + return nid[:] +} + +// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes +// of id are used. If id is less than 6 bytes then false is returned and the +// Node ID is not set. +func SetNodeID(id []byte) bool { + if len(id) < 6 { + return false + } + defer nodeMu.Unlock() + nodeMu.Lock() + copy(nodeID[:], id) + ifname = "user" + return true +} + +// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is +// not valid. The NodeID is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) NodeID() []byte { + var node [6]byte + copy(node[:], uuid[10:]) + return node[:] +} diff --git a/src/vendor/github.com/google/uuid/node_js.go b/src/vendor/github.com/google/uuid/node_js.go new file mode 100644 index 000000000..24b78edc9 --- /dev/null +++ b/src/vendor/github.com/google/uuid/node_js.go @@ -0,0 +1,12 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build js + +package uuid + +// getHardwareInterface returns nil values for the JS version of the code. +// This remvoves the "net" dependency, because it is not used in the browser. +// Using the "net" library inflates the size of the transpiled JS code by 673k bytes. +func getHardwareInterface(name string) (string, []byte) { return "", nil } diff --git a/src/vendor/github.com/google/uuid/node_net.go b/src/vendor/github.com/google/uuid/node_net.go new file mode 100644 index 000000000..0cbbcddbd --- /dev/null +++ b/src/vendor/github.com/google/uuid/node_net.go @@ -0,0 +1,33 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !js + +package uuid + +import "net" + +var interfaces []net.Interface // cached list of interfaces + +// getHardwareInterface returns the name and hardware address of interface name. +// If name is "" then the name and hardware address of one of the system's +// interfaces is returned. If no interfaces are found (name does not exist or +// there are no interfaces) then "", nil is returned. +// +// Only addresses of at least 6 bytes are returned. +func getHardwareInterface(name string) (string, []byte) { + if interfaces == nil { + var err error + interfaces, err = net.Interfaces() + if err != nil { + return "", nil + } + } + for _, ifs := range interfaces { + if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) { + return ifs.Name, ifs.HardwareAddr + } + } + return "", nil +} diff --git a/src/vendor/github.com/google/uuid/sql.go b/src/vendor/github.com/google/uuid/sql.go new file mode 100644 index 000000000..f326b54db --- /dev/null +++ b/src/vendor/github.com/google/uuid/sql.go @@ -0,0 +1,59 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements sql.Scanner so UUIDs can be read from databases transparently +// Currently, database types that map to string and []byte are supported. Please +// consult database-specific driver documentation for matching types. +func (uuid *UUID) Scan(src interface{}) error { + switch src := src.(type) { + case nil: + return nil + + case string: + // if an empty UUID comes from a table, we return a null UUID + if src == "" { + return nil + } + + // see Parse for required string format + u, err := Parse(src) + if err != nil { + return fmt.Errorf("Scan: %v", err) + } + + *uuid = u + + case []byte: + // if an empty UUID comes from a table, we return a null UUID + if len(src) == 0 { + return nil + } + + // assumes a simple slice of bytes if 16 bytes + // otherwise attempts to parse + if len(src) != 16 { + return uuid.Scan(string(src)) + } + copy((*uuid)[:], src) + + default: + return fmt.Errorf("Scan: unable to scan type %T into UUID", src) + } + + return nil +} + +// Value implements sql.Valuer so that UUIDs can be written to databases +// transparently. Currently, UUIDs map to strings. Please consult +// database-specific driver documentation for matching types. +func (uuid UUID) Value() (driver.Value, error) { + return uuid.String(), nil +} diff --git a/src/vendor/github.com/google/uuid/time.go b/src/vendor/github.com/google/uuid/time.go new file mode 100644 index 000000000..e6ef06cdc --- /dev/null +++ b/src/vendor/github.com/google/uuid/time.go @@ -0,0 +1,123 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "sync" + "time" +) + +// A Time represents a time as the number of 100's of nanoseconds since 15 Oct +// 1582. +type Time int64 + +const ( + lillian = 2299160 // Julian day of 15 Oct 1582 + unix = 2440587 // Julian day of 1 Jan 1970 + epoch = unix - lillian // Days between epochs + g1582 = epoch * 86400 // seconds between epochs + g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs +) + +var ( + timeMu sync.Mutex + lasttime uint64 // last time we returned + clockSeq uint16 // clock sequence for this run + + timeNow = time.Now // for testing +) + +// UnixTime converts t the number of seconds and nanoseconds using the Unix +// epoch of 1 Jan 1970. +func (t Time) UnixTime() (sec, nsec int64) { + sec = int64(t - g1582ns100) + nsec = (sec % 10000000) * 100 + sec /= 10000000 + return sec, nsec +} + +// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and +// clock sequence as well as adjusting the clock sequence as needed. An error +// is returned if the current time cannot be determined. +func GetTime() (Time, uint16, error) { + defer timeMu.Unlock() + timeMu.Lock() + return getTime() +} + +func getTime() (Time, uint16, error) { + t := timeNow() + + // If we don't have a clock sequence already, set one. + if clockSeq == 0 { + setClockSequence(-1) + } + now := uint64(t.UnixNano()/100) + g1582ns100 + + // If time has gone backwards with this clock sequence then we + // increment the clock sequence + if now <= lasttime { + clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000 + } + lasttime = now + return Time(now), clockSeq, nil +} + +// ClockSequence returns the current clock sequence, generating one if not +// already set. The clock sequence is only used for Version 1 UUIDs. +// +// The uuid package does not use global static storage for the clock sequence or +// the last time a UUID was generated. Unless SetClockSequence is used, a new +// random clock sequence is generated the first time a clock sequence is +// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) +func ClockSequence() int { + defer timeMu.Unlock() + timeMu.Lock() + return clockSequence() +} + +func clockSequence() int { + if clockSeq == 0 { + setClockSequence(-1) + } + return int(clockSeq & 0x3fff) +} + +// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to +// -1 causes a new sequence to be generated. +func SetClockSequence(seq int) { + defer timeMu.Unlock() + timeMu.Lock() + setClockSequence(seq) +} + +func setClockSequence(seq int) { + if seq == -1 { + var b [2]byte + randomBits(b[:]) // clock sequence + seq = int(b[0])<<8 | int(b[1]) + } + oldSeq := clockSeq + clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant + if oldSeq != clockSeq { + lasttime = 0 + } +} + +// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in +// uuid. The time is only defined for version 1 and 2 UUIDs. +func (uuid UUID) Time() Time { + time := int64(binary.BigEndian.Uint32(uuid[0:4])) + time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32 + time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48 + return Time(time) +} + +// ClockSequence returns the clock sequence encoded in uuid. +// The clock sequence is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) ClockSequence() int { + return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff +} diff --git a/src/vendor/github.com/google/uuid/util.go b/src/vendor/github.com/google/uuid/util.go new file mode 100644 index 000000000..5ea6c7378 --- /dev/null +++ b/src/vendor/github.com/google/uuid/util.go @@ -0,0 +1,43 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "io" +) + +// randomBits completely fills slice b with random data. +func randomBits(b []byte) { + if _, err := io.ReadFull(rander, b); err != nil { + panic(err.Error()) // rand should never fail + } +} + +// xvalues returns the value of a byte as a hexadecimal digit or 255. +var xvalues = [256]byte{ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +} + +// xtob converts hex characters x1 and x2 into a byte. +func xtob(x1, x2 byte) (byte, bool) { + b1 := xvalues[x1] + b2 := xvalues[x2] + return (b1 << 4) | b2, b1 != 255 && b2 != 255 +} diff --git a/src/vendor/github.com/google/uuid/uuid.go b/src/vendor/github.com/google/uuid/uuid.go new file mode 100644 index 000000000..524404cc5 --- /dev/null +++ b/src/vendor/github.com/google/uuid/uuid.go @@ -0,0 +1,245 @@ +// Copyright 2018 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "strings" +) + +// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC +// 4122. +type UUID [16]byte + +// A Version represents a UUID's version. +type Version byte + +// A Variant represents a UUID's variant. +type Variant byte + +// Constants returned by Variant. +const ( + Invalid = Variant(iota) // Invalid UUID + RFC4122 // The variant specified in RFC4122 + Reserved // Reserved, NCS backward compatibility. + Microsoft // Reserved, Microsoft Corporation backward compatibility. + Future // Reserved for future definition. +) + +var rander = rand.Reader // random function + +// Parse decodes s into a UUID or returns an error. Both the standard UUID +// forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the +// Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex +// encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. +func Parse(s string) (UUID, error) { + var uuid UUID + switch len(s) { + // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36: + + // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: + if strings.ToLower(s[:9]) != "urn:uuid:" { + return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) + } + s = s[9:] + + // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + case 36 + 2: + s = s[1:] + + // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + case 32: + var ok bool + for i := range uuid { + uuid[i], ok = xtob(s[i*2], s[i*2+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, fmt.Errorf("invalid UUID length: %d", len(s)) + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34} { + v, ok := xtob(s[x], s[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// ParseBytes is like Parse, except it parses a byte slice instead of a string. +func ParseBytes(b []byte) (UUID, error) { + var uuid UUID + switch len(b) { + case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) { + return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) + } + b = b[9:] + case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + b = b[1:] + case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + var ok bool + for i := 0; i < 32; i += 2 { + uuid[i/2], ok = xtob(b[i], b[i+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, fmt.Errorf("invalid UUID length: %d", len(b)) + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34} { + v, ok := xtob(b[x], b[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// MustParse is like Parse but panics if the string cannot be parsed. +// It simplifies safe initialization of global variables holding compiled UUIDs. +func MustParse(s string) UUID { + uuid, err := Parse(s) + if err != nil { + panic(`uuid: Parse(` + s + `): ` + err.Error()) + } + return uuid +} + +// FromBytes creates a new UUID from a byte slice. Returns an error if the slice +// does not have a length of 16. The bytes are copied from the slice. +func FromBytes(b []byte) (uuid UUID, err error) { + err = uuid.UnmarshalBinary(b) + return uuid, err +} + +// Must returns uuid if err is nil and panics otherwise. +func Must(uuid UUID, err error) UUID { + if err != nil { + panic(err) + } + return uuid +} + +// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// , or "" if uuid is invalid. +func (uuid UUID) String() string { + var buf [36]byte + encodeHex(buf[:], uuid) + return string(buf[:]) +} + +// URN returns the RFC 2141 URN form of uuid, +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid. +func (uuid UUID) URN() string { + var buf [36 + 9]byte + copy(buf[:], "urn:uuid:") + encodeHex(buf[9:], uuid) + return string(buf[:]) +} + +func encodeHex(dst []byte, uuid UUID) { + hex.Encode(dst, uuid[:4]) + dst[8] = '-' + hex.Encode(dst[9:13], uuid[4:6]) + dst[13] = '-' + hex.Encode(dst[14:18], uuid[6:8]) + dst[18] = '-' + hex.Encode(dst[19:23], uuid[8:10]) + dst[23] = '-' + hex.Encode(dst[24:], uuid[10:]) +} + +// Variant returns the variant encoded in uuid. +func (uuid UUID) Variant() Variant { + switch { + case (uuid[8] & 0xc0) == 0x80: + return RFC4122 + case (uuid[8] & 0xe0) == 0xc0: + return Microsoft + case (uuid[8] & 0xe0) == 0xe0: + return Future + default: + return Reserved + } +} + +// Version returns the version of uuid. +func (uuid UUID) Version() Version { + return Version(uuid[6] >> 4) +} + +func (v Version) String() string { + if v > 15 { + return fmt.Sprintf("BAD_VERSION_%d", v) + } + return fmt.Sprintf("VERSION_%d", v) +} + +func (v Variant) String() string { + switch v { + case RFC4122: + return "RFC4122" + case Reserved: + return "Reserved" + case Microsoft: + return "Microsoft" + case Future: + return "Future" + case Invalid: + return "Invalid" + } + return fmt.Sprintf("BadVariant%d", int(v)) +} + +// SetRand sets the random number generator to r, which implements io.Reader. +// If r.Read returns an error when the package requests random data then +// a panic will be issued. +// +// Calling SetRand with nil sets the random number generator to the default +// generator. +func SetRand(r io.Reader) { + if r == nil { + rander = rand.Reader + return + } + rander = r +} diff --git a/src/vendor/github.com/google/uuid/version1.go b/src/vendor/github.com/google/uuid/version1.go new file mode 100644 index 000000000..199a1ac65 --- /dev/null +++ b/src/vendor/github.com/google/uuid/version1.go @@ -0,0 +1,44 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" +) + +// NewUUID returns a Version 1 UUID based on the current NodeID and clock +// sequence, and the current time. If the NodeID has not been set by SetNodeID +// or SetNodeInterface then it will be set automatically. If the NodeID cannot +// be set NewUUID returns nil. If clock sequence has not been set by +// SetClockSequence then it will be set automatically. If GetTime fails to +// return the current NewUUID returns nil and an error. +// +// In most cases, New should be used. +func NewUUID() (UUID, error) { + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + nodeMu.Unlock() + + var uuid UUID + now, seq, err := GetTime() + if err != nil { + return uuid, err + } + + timeLow := uint32(now & 0xffffffff) + timeMid := uint16((now >> 32) & 0xffff) + timeHi := uint16((now >> 48) & 0x0fff) + timeHi |= 0x1000 // Version 1 + + binary.BigEndian.PutUint32(uuid[0:], timeLow) + binary.BigEndian.PutUint16(uuid[4:], timeMid) + binary.BigEndian.PutUint16(uuid[6:], timeHi) + binary.BigEndian.PutUint16(uuid[8:], seq) + copy(uuid[10:], nodeID[:]) + + return uuid, nil +} diff --git a/src/vendor/github.com/google/uuid/version4.go b/src/vendor/github.com/google/uuid/version4.go new file mode 100644 index 000000000..84af91c9f --- /dev/null +++ b/src/vendor/github.com/google/uuid/version4.go @@ -0,0 +1,38 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "io" + +// New creates a new random UUID or panics. New is equivalent to +// the expression +// +// uuid.Must(uuid.NewRandom()) +func New() UUID { + return Must(NewRandom()) +} + +// NewRandom returns a Random (Version 4) UUID. +// +// The strength of the UUIDs is based on the strength of the crypto/rand +// package. +// +// A note about uniqueness derived from the UUID Wikipedia entry: +// +// Randomly generated UUIDs have 122 random bits. One's annual risk of being +// hit by a meteorite is estimated to be one chance in 17 billion, that +// means the probability is about 0.00000000006 (6 × 10−11), +// equivalent to the odds of creating a few tens of trillions of UUIDs in a +// year and having one duplicate. +func NewRandom() (UUID, error) { + var uuid UUID + _, err := io.ReadFull(rander, uuid[:]) + if err != nil { + return Nil, err + } + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} diff --git a/src/vendor/modules.txt b/src/vendor/modules.txt index d25277e81..91dd7b393 100644 --- a/src/vendor/modules.txt +++ b/src/vendor/modules.txt @@ -93,17 +93,17 @@ github.com/dghubble/sling # github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go # github.com/docker/distribution v2.7.1+incompatible -github.com/docker/distribution/registry/auth/token github.com/docker/distribution -github.com/docker/distribution/manifest/schema1 github.com/docker/distribution/manifest/schema2 +github.com/docker/distribution/registry/auth/token +github.com/docker/distribution/manifest/schema1 github.com/docker/distribution/reference github.com/docker/distribution/registry/client/auth/challenge github.com/docker/distribution/health github.com/docker/distribution/manifest/manifestlist +github.com/docker/distribution/manifest github.com/docker/distribution/context github.com/docker/distribution/registry/auth -github.com/docker/distribution/manifest github.com/docker/distribution/digestset github.com/docker/distribution/registry/api/errcode github.com/docker/distribution/uuid @@ -151,6 +151,8 @@ github.com/gomodule/redigo/internal github.com/google/go-querystring/query # github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf github.com/google/gofuzz +# github.com/google/uuid v1.1.1 +github.com/google/uuid # github.com/gorilla/context v1.1.1 github.com/gorilla/context # github.com/gorilla/handlers v1.3.0 @@ -206,9 +208,9 @@ github.com/spf13/pflag github.com/stretchr/objx # github.com/stretchr/testify v1.3.0 github.com/stretchr/testify/mock +github.com/stretchr/testify/suite github.com/stretchr/testify/assert github.com/stretchr/testify/require -github.com/stretchr/testify/suite # github.com/theupdateframework/notary v0.6.1 github.com/theupdateframework/notary/tuf/data github.com/theupdateframework/notary