diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4865f3cc6..804d4a44c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2,7 +2,7 @@ swagger: '2.0' info: title: Harbor API description: These APIs provide services for manipulating Harbor project. - version: 1.8.0 + version: 1.9.0 host: localhost schemes: - http @@ -3478,6 +3478,44 @@ paths: description: The robot account is not found. '500': description: Unexpected internal errors. + '/system/CVEWhitelist': + get: + summary: Get the system level whitelist of CVE. + description: Get the system level whitelist of CVE. This API can be called by all authenticated users. + tags: + - Products + - System + responses: + '200': + description: Successfully retrieved the CVE whitelist. + schema: + $ref: "#/definitions/CVEWhitelist" + '401': + description: User is not authenticated. + '500': + description: Unexpected internal errors. + put: + summary: Update the system level whitelist of CVE. + description: This API overwrites the system level whitelist of CVE with the list in request body. Only system Admin + has permission to call this API. + tags: + - Products + - System + parameters: + - in: body + name: whitelist + description: The whitelist with new content + schema: + $ref: "#/definitions/CVEWhitelist" + responses: + '200': + description: Successfully updated the CVE whitelist. + '401': + description: User is not authenticated. + '403': + description: User does not have permission to call this API. + '500': + description: Unexpected internal errors. responses: OK: description: 'Success' @@ -5069,4 +5107,28 @@ definitions: description: The name of namespace metadata: type: object - description: The metadata of namespace \ No newline at end of file + description: The metadata of namespace + CVEWhitelist: + type: object + description: The CVE Whitelist for system or project + properties: + id: + type: integer + description: ID of the whitelist + project_id: + type: integer + description: ID of the project which the whitelist belongs to. For system level whitelist this attribute is zero. + expires_at: + type: integer + description: the time for expiration of the whitelist, in the form of seconds since epoch. This is an optional attribute, if it's not set the CVE whitelist does not expire. + items: + type: array + items: + $ref: "#/definitions/CVEWhitelistItem" + CVEWhitelistItem: + type: object + description: The item in CVE whitelist + properties: + cve_id: + type: string + description: The ID of the CVE, such as "CVE-2019-10164" diff --git a/make/migrations/postgresql/0010_1.9.0_schema.up.sql b/make/migrations/postgresql/0010_1.9.0_schema.up.sql new file mode 100644 index 000000000..11c2248a6 --- /dev/null +++ b/make/migrations/postgresql/0010_1.9.0_schema.up.sql @@ -0,0 +1,10 @@ +/* add table for CVE whitelist */ +CREATE TABLE cve_whitelist ( + id SERIAL PRIMARY KEY NOT NULL, + project_id int, + creation_time timestamp default CURRENT_TIMESTAMP, + update_time timestamp default CURRENT_TIMESTAMP, + expires_at bigint, + items text NOT NULL, + UNIQUE (project_id) +); \ No newline at end of file diff --git a/src/common/dao/cve_whitelist.go b/src/common/dao/cve_whitelist.go new file mode 100644 index 000000000..25c1c9b98 --- /dev/null +++ b/src/common/dao/cve_whitelist.go @@ -0,0 +1,74 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "encoding/json" + "fmt" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/log" +) + +// UpdateCVEWhitelist Updates the vulnerability white list to DB +func UpdateCVEWhitelist(l models.CVEWhitelist) (int64, error) { + o := GetOrmer() + itemsBytes, _ := json.Marshal(l.Items) + l.ItemsText = string(itemsBytes) + id, err := o.InsertOrUpdate(&l, "project_id") + return id, err +} + +// GetSysCVEWhitelist Gets the system level vulnerability white list from DB +func GetSysCVEWhitelist() (*models.CVEWhitelist, error) { + return GetCVEWhitelist(0) +} + +// UpdateSysCVEWhitelist updates the system level CVE whitelist +/* +func UpdateSysCVEWhitelist(l models.CVEWhitelist) error { + if l.ProjectID != 0 { + return fmt.Errorf("system level CVE whitelist cannot set project ID") + } + l.ProjectID = -1 + _, err := UpdateCVEWhitelist(l) + return err +} +*/ + +// GetCVEWhitelist Gets the CVE whitelist of the project based on the project ID in parameter +func GetCVEWhitelist(pid int64) (*models.CVEWhitelist, error) { + o := GetOrmer() + qs := o.QueryTable(&models.CVEWhitelist{}) + qs = qs.Filter("ProjectID", pid) + r := []*models.CVEWhitelist{} + _, err := qs.All(&r) + if err != nil { + return nil, fmt.Errorf("failed to get CVE whitelist for project %d, error: %v", pid, err) + } + if len(r) == 0 { + log.Infof("No CVE whitelist found for project %d, returning empty list.", pid) + return &models.CVEWhitelist{ProjectID: pid, Items: []models.CVEWhitelistItem{}}, nil + } else if len(r) > 1 { + log.Infof("Multiple CVE whitelists found for project %d, length: %d, returning first element.", pid, len(r)) + } + items := []models.CVEWhitelistItem{} + err = json.Unmarshal([]byte(r[0].ItemsText), &items) + if err != nil { + log.Errorf("Failed to decode item list, err: %v, text: %s", err, r[0].ItemsText) + return nil, err + } + r[0].Items = items + return r[0], nil +} diff --git a/src/common/dao/cve_whitelist_test.go b/src/common/dao/cve_whitelist_test.go new file mode 100644 index 000000000..35af2f294 --- /dev/null +++ b/src/common/dao/cve_whitelist_test.go @@ -0,0 +1,72 @@ +// Copyright Project Harbor Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dao + +import ( + "github.com/goharbor/harbor/src/common/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestUpdateAndGetCVEWhitelist(t *testing.T) { + require.Nil(t, ClearTable("cve_whitelist")) + l, err := GetSysCVEWhitelist() + assert.Nil(t, err) + assert.Equal(t, models.CVEWhitelist{ProjectID: 0, Items: []models.CVEWhitelistItem{}}, *l) + l2, err := GetCVEWhitelist(5) + assert.Nil(t, err) + assert.Equal(t, models.CVEWhitelist{ProjectID: 5, Items: []models.CVEWhitelistItem{}}, *l2) + + longList := []models.CVEWhitelistItem{} + for i := 0; i < 50; i++ { + longList = append(longList, models.CVEWhitelistItem{CVEID: "CVE-1999-0067"}) + } + + e := int64(1573254000) + in1 := models.CVEWhitelist{ProjectID: 3, Items: longList, ExpiresAt: &e} + _, err = UpdateCVEWhitelist(in1) + require.Nil(t, err) + // assert.Equal(t, int64(1), n) + out1, err := GetCVEWhitelist(3) + require.Nil(t, err) + assert.Equal(t, int64(3), out1.ProjectID) + assert.Equal(t, longList, out1.Items) + assert.Equal(t, e, *out1.ExpiresAt) + + in2 := models.CVEWhitelist{ProjectID: 3, Items: []models.CVEWhitelistItem{}} + _, err = UpdateCVEWhitelist(in2) + require.Nil(t, err) + // assert.Equal(t, int64(1), n2) + out2, err := GetCVEWhitelist(3) + require.Nil(t, err) + assert.Equal(t, int64(3), out2.ProjectID) + assert.Equal(t, []models.CVEWhitelistItem{}, out2.Items) + + sysCVEs := []models.CVEWhitelistItem{ + {CVEID: "CVE-2019-10164"}, + {CVEID: "CVE-2017-12345"}, + } + in3 := models.CVEWhitelist{Items: sysCVEs} + _, err = UpdateCVEWhitelist(in3) + require.Nil(t, err) + // assert.Equal(t, int64(1), n3) + sysList, err := GetSysCVEWhitelist() + require.Nil(t, err) + assert.Equal(t, int64(0), sysList.ProjectID) + assert.Equal(t, sysCVEs, sysList.Items) + + // require.Nil(t, ClearTable("cve_whitelist")) +} diff --git a/src/common/models/base.go b/src/common/models/base.go index be8877cb8..f6d666ee8 100644 --- a/src/common/models/base.go +++ b/src/common/models/base.go @@ -36,5 +36,6 @@ func init() { new(AdminJob), new(JobLog), new(Robot), - new(OIDCUser)) + new(OIDCUser), + new(CVEWhitelist)) } diff --git a/src/common/models/cve_whitelist.go b/src/common/models/cve_whitelist.go new file mode 100644 index 000000000..318bf7693 --- /dev/null +++ b/src/common/models/cve_whitelist.go @@ -0,0 +1,38 @@ +// 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 models + +import "time" + +// CVEWhitelist defines the data model for a CVE whitelist +type CVEWhitelist struct { + ID int64 `orm:"pk;auto;column(id)" json:"id"` + ProjectID int64 `orm:"column(project_id)" json:"project_id"` + ExpiresAt *int64 `orm:"column(expires_at)" json:"expires_at,omitempty"` + Items []CVEWhitelistItem `orm:"-" json:"items"` + ItemsText string `orm:"column(items)" json:"-"` + CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"` + UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"` +} + +// CVEWhitelistItem defines one item in the CVE whitelist +type CVEWhitelistItem struct { + CVEID string `json:"cve_id"` +} + +// TableName ... +func (r *CVEWhitelist) TableName() string { + return "cve_whitelist" +} diff --git a/src/core/api/harborapi_test.go b/src/core/api/harborapi_test.go index c345f7ffa..eed976dfd 100644 --- a/src/core/api/harborapi_test.go +++ b/src/core/api/harborapi_test.go @@ -144,6 +144,7 @@ func init() { beego.Router("/api/system/gc/:id([0-9]+)/log", &GCAPI{}, "get:GetLog") beego.Router("/api/system/gc/schedule", &GCAPI{}, "get:Get;put:Put;post:Post") beego.Router("/api/system/scanAll/schedule", &ScanAllAPI{}, "get:Get;put:Put;post:Post") + beego.Router("/api/system/CVEWhitelist", &SysCVEWhitelistAPI{}, "get:Get;put:Put") beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List") beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete") diff --git a/src/core/api/sys_cve_whitelist.go b/src/core/api/sys_cve_whitelist.go new file mode 100644 index 000000000..3185d8e34 --- /dev/null +++ b/src/core/api/sys_cve_whitelist.go @@ -0,0 +1,74 @@ +// 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 ( + "errors" + "fmt" + "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/log" + "net/http" +) + +// SysCVEWhitelistAPI Handles the requests to manage system level CVE whitelist +type SysCVEWhitelistAPI struct { + BaseController +} + +// Prepare validates the request initially +func (sca *SysCVEWhitelistAPI) Prepare() { + sca.BaseController.Prepare() + if !sca.SecurityCtx.IsAuthenticated() { + sca.SendUnAuthorizedError(errors.New("Unauthorized")) + return + } + if !sca.SecurityCtx.IsSysAdmin() && sca.Ctx.Request.Method != http.MethodGet { + msg := fmt.Sprintf("only system admin has permission issue %s request to this API", sca.Ctx.Request.Method) + log.Errorf(msg) + sca.SendForbiddenError(errors.New(msg)) + return + } +} + +// Get handles the GET request to retrieve the system level CVE whitelist +func (sca *SysCVEWhitelistAPI) Get() { + l, err := dao.GetSysCVEWhitelist() + if err != nil { + sca.SendInternalServerError(err) + return + } + sca.WriteJSONData(l) +} + +// Put handles the PUT request to update the system level CVE whitelist +func (sca *SysCVEWhitelistAPI) Put() { + var l models.CVEWhitelist + if err := sca.DecodeJSONReq(&l); err != nil { + log.Errorf("Failed to decode JSON array from request") + sca.SendBadRequestError(err) + return + } + if l.ProjectID != 0 { + msg := fmt.Sprintf("Non-zero project ID for system CVE whitelist: %d.", l.ProjectID) + log.Error(msg) + sca.SendBadRequestError(errors.New(msg)) + return + } + if _, err := dao.UpdateCVEWhitelist(l); err != nil { + sca.SendInternalServerError(err) + return + } +} diff --git a/src/core/api/sys_cve_whitelist_test.go b/src/core/api/sys_cve_whitelist_test.go new file mode 100644 index 000000000..0cbd648e7 --- /dev/null +++ b/src/core/api/sys_cve_whitelist_test.go @@ -0,0 +1,110 @@ +// 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/common/models" + "net/http" + "testing" +) + +func TestSysCVEWhitelistAPIGet(t *testing.T) { + url := "/api/system/CVEWhitelist" + cases := []*codeCheckingCase{ + // 401 + { + request: &testingRequest{ + method: http.MethodGet, + url: url, + }, + code: http.StatusUnauthorized, + }, + // 200 + { + request: &testingRequest{ + method: http.MethodGet, + url: url, + credential: nonSysAdmin, + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +} + +func TestSysCVEWhitelistAPIPut(t *testing.T) { + url := "/api/system/CVEWhitelist" + s := int64(1573254000) + cases := []*codeCheckingCase{ + // 401 + { + request: &testingRequest{ + method: http.MethodPut, + url: url, + }, + code: http.StatusUnauthorized, + }, + // 403 + { + request: &testingRequest{ + method: http.MethodPut, + url: url, + credential: nonSysAdmin, + }, + code: http.StatusForbidden, + }, + // 400 + { + request: &testingRequest{ + method: http.MethodPut, + url: url, + bodyJSON: []string{"CVE-1234-1234"}, + credential: sysAdmin, + }, + code: http.StatusBadRequest, + }, + // 400 + { + request: &testingRequest{ + method: http.MethodPut, + url: url, + bodyJSON: models.CVEWhitelist{ + ExpiresAt: &s, + Items: []models.CVEWhitelistItem{ + {CVEID: "CVE-2019-12310"}, + }, + ProjectID: 2, + }, + credential: sysAdmin, + }, + code: http.StatusBadRequest, + }, + // 200 + { + request: &testingRequest{ + method: http.MethodPut, + url: url, + bodyJSON: models.CVEWhitelist{ + ExpiresAt: &s, + Items: []models.CVEWhitelistItem{ + {CVEID: "CVE-2019-12310"}, + }, + }, + credential: sysAdmin, + }, + code: http.StatusOK, + }, + } + runCodeCheckingCases(t, cases...) +} diff --git a/src/core/router.go b/src/core/router.go index 1c4c31f3f..344967c1d 100644 --- a/src/core/router.go +++ b/src/core/router.go @@ -96,6 +96,7 @@ func initRouters() { beego.Router("/api/system/gc/:id([0-9]+)/log", &api.GCAPI{}, "get:GetLog") beego.Router("/api/system/gc/schedule", &api.GCAPI{}, "get:Get;put:Put;post:Post") beego.Router("/api/system/scanAll/schedule", &api.ScanAllAPI{}, "get:Get;put:Put;post:Post") + beego.Router("/api/system/CVEWhitelist", &api.SysCVEWhitelistAPI{}, "get:Get;put:Put") beego.Router("/api/logs", &api.LogAPI{}) diff --git a/src/replication/dao/dao_test.go b/src/replication/dao/dao_test.go index b3cf65e73..7dfcd2734 100644 --- a/src/replication/dao/dao_test.go +++ b/src/replication/dao/dao_test.go @@ -33,7 +33,7 @@ func TestMain(m *testing.M) { "harbor_label", "harbor_resource_label", "harbor_user", "img_scan_job", "img_scan_overview", "job_log", "project", "project_member", "project_metadata", "properties", "registry", "replication_policy", "repository", "robot", "role", "schema_migrations", "user_group", - "replication_execution", "replication_task", "replication_schedule_job", "oidc_user";`, + "replication_execution", "replication_task", "replication_schedule_job", "oidc_user", "cve_whitelist";`, `DROP FUNCTION "update_update_time_at_column"();`, } dao.PrepareTestData(clearSqls, nil)