API for system level vulnerability whitelist

Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
Daniel Jiang 2019-06-19 20:51:08 +08:00
parent 766d5ee017
commit 4aca812ff2
11 changed files with 447 additions and 4 deletions

View File

@ -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
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"

View File

@ -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)
);

View File

@ -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
}

View File

@ -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"))
}

View File

@ -36,5 +36,6 @@ func init() {
new(AdminJob),
new(JobLog),
new(Robot),
new(OIDCUser))
new(OIDCUser),
new(CVEWhitelist))
}

View File

@ -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"
}

View File

@ -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")

View File

@ -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
}
}

View File

@ -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...)
}

View File

@ -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{})

View File

@ -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)