mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-20 07:37:38 +01:00
feat(preheat):add artifact filters for preheat policy
- add new selector based on vulnerability severity criteria - add new selector based on signature(signed) criteria - do change to the select factory method definition - do changes to selector.Candidate model - add preheat policy filter interface and default implementation - add UT cases to cover new code Signed-off-by: Steven Zou <szou@vmware.com> misspelling
This commit is contained in:
parent
fb29a6ae87
commit
d8e88ef5bc
35
src/controller/p2p/preheat/enforcer.go
Normal file
35
src/controller/p2p/preheat/enforcer.go
Normal file
@ -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 preheat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/models/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enforcer defines policy enforcement operations.
|
||||||
|
type Enforcer interface {
|
||||||
|
// Enforce the specified policy.
|
||||||
|
//
|
||||||
|
// Arguments:
|
||||||
|
// p *policy.Schema : the being enforced policy
|
||||||
|
// art ...*artifact.Artifact (optional): the relevant artifact referred by the happening events
|
||||||
|
// that defined in the event-based policy p.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - ID of the execution
|
||||||
|
// - non-nil error if any error occurred during the enforcement
|
||||||
|
Enforce(p *policy.Schema, art ...*artifact.Artifact) (int64, error)
|
||||||
|
}
|
@ -18,9 +18,9 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -85,6 +85,12 @@ type Candidate struct {
|
|||||||
CreationTime int64 `json:"create_time_second"`
|
CreationTime int64 `json:"create_time_second"`
|
||||||
// Labels attached with the candidate
|
// Labels attached with the candidate
|
||||||
Labels []string `json:"labels"`
|
Labels []string `json:"labels"`
|
||||||
|
// Overall severity of the candidate
|
||||||
|
// Use severity code value here to avoid pkg dependency issue.
|
||||||
|
VulnerabilitySeverity uint `json:"vulnerability_severity"`
|
||||||
|
// Signatures of the above Tags
|
||||||
|
// This is not technical correct, just for keeping compatibilities with the original definition.
|
||||||
|
Signatures map[string]bool `json:"signatures"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash code based on the candidate info for differentiation
|
// Hash code based on the candidate info for differentiation
|
||||||
|
@ -27,4 +27,6 @@ type Selector interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Factory is factory method to return a selector implementation
|
// Factory is factory method to return a selector implementation
|
||||||
type Factory func(decoration string, pattern string, extras string) Selector
|
// Pattern can be any type of data.
|
||||||
|
// TODO: 'extras' can also be an optional interface{} to accept more complicated data.
|
||||||
|
type Factory func(decoration string, pattern interface{}, extras string) Selector
|
||||||
|
@ -16,6 +16,7 @@ package doublestar
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/bmatcuk/doublestar"
|
"github.com/bmatcuk/doublestar"
|
||||||
iselector "github.com/goharbor/harbor/src/lib/selector"
|
iselector "github.com/goharbor/harbor/src/lib/selector"
|
||||||
)
|
)
|
||||||
@ -132,7 +133,7 @@ func (s *selector) tagSelectExclude(artifact *iselector.Candidate) (selected boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New is factory method for doublestar selector
|
// New is factory method for doublestar selector
|
||||||
func New(decoration string, pattern string, extras string) iselector.Selector {
|
func New(decoration string, pattern interface{}, extras string) iselector.Selector {
|
||||||
untagged := true // default behavior for upgrade, active keep the untagged images
|
untagged := true // default behavior for upgrade, active keep the untagged images
|
||||||
if decoration == Excludes {
|
if decoration == Excludes {
|
||||||
untagged = false
|
untagged = false
|
||||||
@ -145,9 +146,15 @@ func New(decoration string, pattern string, extras string) iselector.Selector {
|
|||||||
untagged = extraObj.Untagged
|
untagged = extraObj.Untagged
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var p string
|
||||||
|
if pattern != nil {
|
||||||
|
p, _ = pattern.(string)
|
||||||
|
}
|
||||||
|
|
||||||
return &selector{
|
return &selector{
|
||||||
decoration: decoration,
|
decoration: decoration,
|
||||||
pattern: pattern,
|
pattern: p,
|
||||||
untagged: untagged,
|
untagged: untagged,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,9 @@
|
|||||||
package label
|
package label
|
||||||
|
|
||||||
import (
|
import (
|
||||||
iselector "github.com/goharbor/harbor/src/lib/selector"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
iselector "github.com/goharbor/harbor/src/lib/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -48,11 +49,15 @@ func (s *selector) Select(artifacts []*iselector.Candidate) (selected []*iselect
|
|||||||
return selected, nil
|
return selected, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// New is factory method for list selector
|
// New is factory method for label selector
|
||||||
func New(decoration string, pattern string, extras string) iselector.Selector {
|
func New(decoration string, pattern interface{}, extras string) iselector.Selector {
|
||||||
labels := make([]string, 0)
|
labels := make([]string, 0)
|
||||||
if len(pattern) > 0 {
|
|
||||||
labels = append(labels, strings.Split(pattern, ",")...)
|
if pattern != nil {
|
||||||
|
labelText, ok := pattern.(string)
|
||||||
|
if ok && len(labelText) > 0 {
|
||||||
|
labels = append(labels, strings.Split(labelText, ",")...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &selector{
|
return &selector{
|
||||||
|
95
src/lib/selector/selectors/severity/selector.go
Normal file
95
src/lib/selector/selectors/severity/selector.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
// 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 severity
|
||||||
|
|
||||||
|
import (
|
||||||
|
sl "github.com/goharbor/harbor/src/lib/selector"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Kind of this selector
|
||||||
|
Kind = "severity"
|
||||||
|
// Gte decoration: Severity of candidate should be greater than or equal to the expected severity.
|
||||||
|
Gte = "gte"
|
||||||
|
// Gt decoration: Severity of candidate should be greater than the expected severity.
|
||||||
|
Gt = "gt"
|
||||||
|
// Equal decoration: Severity of candidate should be equal to the expected severity.
|
||||||
|
Equal = "equal"
|
||||||
|
// Lte decoration: Severity of candidate should be less than or equal to the expected severity.
|
||||||
|
Lte = "lte"
|
||||||
|
// Lt decoration: Severity of candidate should be less than the expected severity.
|
||||||
|
Lt = "lt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// selector filters the candidates by comparing the vulnerability severity
|
||||||
|
type selector struct {
|
||||||
|
// Pre defined pattern decorations
|
||||||
|
// "gte", "gt", "equal", "lte" or "lt"
|
||||||
|
decoration string
|
||||||
|
|
||||||
|
// expected severity value
|
||||||
|
severity uint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select candidates by comparing the vulnerability severity of the candidate
|
||||||
|
func (s *selector) Select(artifacts []*sl.Candidate) (selected []*sl.Candidate, err error) {
|
||||||
|
for _, a := range artifacts {
|
||||||
|
matched := false
|
||||||
|
|
||||||
|
switch s.decoration {
|
||||||
|
case Gte:
|
||||||
|
if a.VulnerabilitySeverity >= s.severity {
|
||||||
|
matched = true
|
||||||
|
}
|
||||||
|
case Gt:
|
||||||
|
if a.VulnerabilitySeverity > s.severity {
|
||||||
|
matched = true
|
||||||
|
}
|
||||||
|
case Equal:
|
||||||
|
if a.VulnerabilitySeverity == s.severity {
|
||||||
|
matched = true
|
||||||
|
}
|
||||||
|
case Lte:
|
||||||
|
if a.VulnerabilitySeverity <= s.severity {
|
||||||
|
matched = true
|
||||||
|
}
|
||||||
|
case Lt:
|
||||||
|
if a.VulnerabilitySeverity < s.severity {
|
||||||
|
matched = true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched {
|
||||||
|
selected = append(selected, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// New is factory method for vulnerability severity selector
|
||||||
|
func New(decoration string, pattern interface{}, extras string) sl.Selector {
|
||||||
|
var sev int
|
||||||
|
if pattern != nil {
|
||||||
|
sev, _ = pattern.(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &selector{
|
||||||
|
decoration: decoration,
|
||||||
|
severity: (uint)(sev),
|
||||||
|
}
|
||||||
|
}
|
130
src/lib/selector/selectors/severity/selector_test.go
Normal file
130
src/lib/selector/selectors/severity/selector_test.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
// 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 severity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
sl "github.com/goharbor/harbor/src/lib/selector"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SeveritySelectorTestSuite is a test suite of testing severity selector
|
||||||
|
type SeveritySelectorTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
candidates []*sl.Candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeveritySelector is an entry method of running SeveritySelectorTestSuite
|
||||||
|
func TestSeveritySelector(t *testing.T) {
|
||||||
|
suite.Run(t, &SeveritySelectorTestSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite prepares the env of running SeveritySelectorTestSuite.
|
||||||
|
func (suite *SeveritySelectorTestSuite) SetupSuite() {
|
||||||
|
suite.candidates = []*sl.Candidate{
|
||||||
|
{
|
||||||
|
Namespace: "test",
|
||||||
|
NamespaceID: 1,
|
||||||
|
Repository: "busybox",
|
||||||
|
Kind: "image",
|
||||||
|
Digest: "sha256@fake",
|
||||||
|
Tags: []string{
|
||||||
|
"latest",
|
||||||
|
"1.0",
|
||||||
|
},
|
||||||
|
VulnerabilitySeverity: 3, // medium
|
||||||
|
}, {
|
||||||
|
Namespace: "test",
|
||||||
|
NamespaceID: 1,
|
||||||
|
Repository: "core",
|
||||||
|
Kind: "image",
|
||||||
|
Digest: "sha256@fake",
|
||||||
|
Tags: []string{
|
||||||
|
"latest",
|
||||||
|
"1.1",
|
||||||
|
},
|
||||||
|
VulnerabilitySeverity: 4, // high
|
||||||
|
}, {
|
||||||
|
Namespace: "test",
|
||||||
|
NamespaceID: 1,
|
||||||
|
Repository: "portal",
|
||||||
|
Kind: "image",
|
||||||
|
Digest: "sha256@fake",
|
||||||
|
Tags: []string{
|
||||||
|
"latest",
|
||||||
|
"1.2",
|
||||||
|
},
|
||||||
|
VulnerabilitySeverity: 5, // critical
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGte test >=
|
||||||
|
func (suite *SeveritySelectorTestSuite) TestGte() {
|
||||||
|
s := New(Gte, 3, "")
|
||||||
|
l, err := s.Select(suite.candidates)
|
||||||
|
require.NoError(suite.T(), err, "filter candidates by vulnerability severity")
|
||||||
|
suite.Equal(3, len(l), "number of matched candidates")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGte test >
|
||||||
|
func (suite *SeveritySelectorTestSuite) TestGt() {
|
||||||
|
s := New(Gt, 3, "")
|
||||||
|
l, err := s.Select(suite.candidates)
|
||||||
|
require.NoError(suite.T(), err, "filter candidates by vulnerability severity")
|
||||||
|
require.Equal(suite.T(), 2, len(l), "number of matched candidates")
|
||||||
|
suite.Condition(func() (success bool) {
|
||||||
|
for _, a := range l {
|
||||||
|
if a.VulnerabilitySeverity <= 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, "severity checking of matched candidates")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGte test =
|
||||||
|
func (suite *SeveritySelectorTestSuite) TestEqual() {
|
||||||
|
s := New(Equal, 3, "")
|
||||||
|
l, err := s.Select(suite.candidates)
|
||||||
|
require.NoError(suite.T(), err, "filter candidates by vulnerability severity")
|
||||||
|
require.Equal(suite.T(), 1, len(l), "number of matched candidates")
|
||||||
|
suite.Equal("busybox", l[0].Repository, "repository comparison of matched candidate")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGte test <=
|
||||||
|
func (suite *SeveritySelectorTestSuite) TestLte() {
|
||||||
|
s := New(Lte, 4, "")
|
||||||
|
l, err := s.Select(suite.candidates)
|
||||||
|
require.NoError(suite.T(), err, "filter candidates by vulnerability severity")
|
||||||
|
require.Equal(suite.T(), 2, len(l), "number of matched candidates")
|
||||||
|
suite.Equal("busybox", l[0].Repository, "repository comparison of matched candidate")
|
||||||
|
suite.Equal("core", l[1].Repository, "repository comparison of matched candidate")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGte test <
|
||||||
|
func (suite *SeveritySelectorTestSuite) TestLt() {
|
||||||
|
s := New(Lt, 5, "")
|
||||||
|
l, err := s.Select(suite.candidates)
|
||||||
|
require.NoError(suite.T(), err, "filter candidates by vulnerability severity")
|
||||||
|
require.Equal(suite.T(), 2, len(l), "number of matched candidates")
|
||||||
|
suite.Equal("busybox", l[0].Repository, "repository comparison of matched candidate")
|
||||||
|
suite.Equal("core", l[1].Repository, "repository comparison of matched candidate")
|
||||||
|
}
|
77
src/lib/selector/selectors/signature/selector.go
Normal file
77
src/lib/selector/selectors/signature/selector.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// 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 signature
|
||||||
|
|
||||||
|
import (
|
||||||
|
sl "github.com/goharbor/harbor/src/lib/selector"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Kind of this selector
|
||||||
|
Kind = "signature"
|
||||||
|
// Any tag of the artifact candidate is signed
|
||||||
|
Any = "any"
|
||||||
|
// All the tags of the artifact candidate are signed
|
||||||
|
All = "all"
|
||||||
|
)
|
||||||
|
|
||||||
|
// selector filters the candidates by signing status (signature)
|
||||||
|
type selector struct {
|
||||||
|
// Pre defined pattern decorations
|
||||||
|
// "any" or "all"
|
||||||
|
decoration string
|
||||||
|
|
||||||
|
// expected status of signing
|
||||||
|
expected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select candidates by the signing status of the candidate
|
||||||
|
func (s *selector) Select(artifacts []*sl.Candidate) (selected []*sl.Candidate, err error) {
|
||||||
|
for _, a := range artifacts {
|
||||||
|
matched := 0
|
||||||
|
for _, t := range a.Tags {
|
||||||
|
if a.Signatures[t] == s.expected {
|
||||||
|
matched++
|
||||||
|
if s.decoration == Any {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if s.decoration == All {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.decoration == Any && matched > 0) ||
|
||||||
|
(s.decoration == All && matched == len(a.Tags)) {
|
||||||
|
selected = append(selected, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// New is factory method for signature selector
|
||||||
|
func New(decoration string, pattern interface{}, extras string) sl.Selector {
|
||||||
|
var e bool
|
||||||
|
if pattern != nil {
|
||||||
|
e, _ = pattern.(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &selector{
|
||||||
|
decoration: decoration,
|
||||||
|
expected: e,
|
||||||
|
}
|
||||||
|
}
|
122
src/lib/selector/selectors/signature/selector_test.go
Normal file
122
src/lib/selector/selectors/signature/selector_test.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
// 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 signature
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sl "github.com/goharbor/harbor/src/lib/selector"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SignatureSelectorTestSuite is a test suite for testing the signature selector
|
||||||
|
type SignatureSelectorTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
candidates []*sl.Candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSignatureSelector is the entry method of running SignatureSelectorTestSuite
|
||||||
|
func TestSignatureSelector(t *testing.T) {
|
||||||
|
suite.Run(t, &SignatureSelectorTestSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite prepares the env for running SeveritySelectorTestSuite
|
||||||
|
func (suite *SignatureSelectorTestSuite) SetupSuite() {
|
||||||
|
suite.candidates = []*sl.Candidate{
|
||||||
|
{
|
||||||
|
Namespace: "test",
|
||||||
|
NamespaceID: 1,
|
||||||
|
Repository: "busybox",
|
||||||
|
Kind: "image",
|
||||||
|
Digest: "sha256@fake",
|
||||||
|
Tags: []string{
|
||||||
|
"latest",
|
||||||
|
"1.0",
|
||||||
|
},
|
||||||
|
Signatures: map[string]bool{
|
||||||
|
"latest": false,
|
||||||
|
"1.0": true,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Namespace: "test",
|
||||||
|
NamespaceID: 1,
|
||||||
|
Repository: "core",
|
||||||
|
Kind: "image",
|
||||||
|
Digest: "sha256@fake",
|
||||||
|
Tags: []string{
|
||||||
|
"latest",
|
||||||
|
"1.1",
|
||||||
|
},
|
||||||
|
Signatures: map[string]bool{
|
||||||
|
"latest": false,
|
||||||
|
"1.1": false,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Namespace: "test",
|
||||||
|
NamespaceID: 1,
|
||||||
|
Repository: "portal",
|
||||||
|
Kind: "image",
|
||||||
|
Digest: "sha256@fake",
|
||||||
|
Tags: []string{
|
||||||
|
"latest",
|
||||||
|
"1.2",
|
||||||
|
},
|
||||||
|
Signatures: map[string]bool{
|
||||||
|
"latest": true,
|
||||||
|
"1.2": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAnySigned tests the 'any' decoration with expected=true
|
||||||
|
func (suite *SignatureSelectorTestSuite) TestAnySigned() {
|
||||||
|
s := New(Any, true, "")
|
||||||
|
l, err := s.Select(suite.candidates)
|
||||||
|
require.NoError(suite.T(), err, "filter candidates by signature")
|
||||||
|
suite.Equal(2, len(l), "number of matched candidates")
|
||||||
|
suite.Equal("busybox", l[0].Repository)
|
||||||
|
suite.Equal("portal", l[1].Repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAnyUnSigned tests the 'any' decoration with expected=false
|
||||||
|
func (suite *SignatureSelectorTestSuite) TestAnyUnSigned() {
|
||||||
|
s := New(Any, false, "")
|
||||||
|
l, err := s.Select(suite.candidates)
|
||||||
|
require.NoError(suite.T(), err, "filter candidates by signature")
|
||||||
|
suite.Equal(2, len(l), "number of matched candidates")
|
||||||
|
suite.Equal("busybox", l[0].Repository)
|
||||||
|
suite.Equal("core", l[1].Repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAllSigned tests the 'all' decoration with expected=true
|
||||||
|
func (suite *SignatureSelectorTestSuite) TestAllSigned() {
|
||||||
|
s := New(All, true, "")
|
||||||
|
l, err := s.Select(suite.candidates)
|
||||||
|
require.NoError(suite.T(), err, "filter candidates by signature")
|
||||||
|
suite.Equal(1, len(l), "number of matched candidates")
|
||||||
|
suite.Equal("portal", l[0].Repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAllUnSigned tests the 'all' decoration with expected=false
|
||||||
|
func (suite *SignatureSelectorTestSuite) TestAllUnSigned() {
|
||||||
|
s := New(All, false, "")
|
||||||
|
l, err := s.Select(suite.candidates)
|
||||||
|
require.NoError(suite.T(), err, "filter candidates by signature")
|
||||||
|
suite.Equal(1, len(l), "number of matched candidates")
|
||||||
|
suite.Equal("core", l[0].Repository)
|
||||||
|
}
|
@ -27,8 +27,8 @@ const (
|
|||||||
// Repository : type=Repository value=name text (double star pattern used)
|
// Repository : type=Repository value=name text (double star pattern used)
|
||||||
// Tag: type=Tag value=tag text (double star pattern used)
|
// Tag: type=Tag value=tag text (double star pattern used)
|
||||||
// Signature: type=Signature value=bool (True/False)
|
// Signature: type=Signature value=bool (True/False)
|
||||||
// Vulnerability: type=Vulnerability value=Severity (expected bar)
|
// Vulnerability: type=Vulnerability value=Severity (int) (expected bar)
|
||||||
// Label: type=Label value=label array
|
// Label: type=Label value=label array (with format: lb1,lb2,lb3)
|
||||||
|
|
||||||
// FilterTypeRepository represents the repository filter type
|
// FilterTypeRepository represents the repository filter type
|
||||||
FilterTypeRepository FilterType = "repository"
|
FilterTypeRepository FilterType = "repository"
|
||||||
|
181
src/pkg/p2p/preheat/policy/filter.go
Normal file
181
src/pkg/p2p/preheat/policy/filter.go
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
// 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 policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
|
"github.com/goharbor/harbor/src/lib/selector"
|
||||||
|
"github.com/goharbor/harbor/src/lib/selector/selectors/doublestar"
|
||||||
|
"github.com/goharbor/harbor/src/lib/selector/selectors/label"
|
||||||
|
"github.com/goharbor/harbor/src/lib/selector/selectors/severity"
|
||||||
|
"github.com/goharbor/harbor/src/lib/selector/selectors/signature"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/models/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter defines the filter operations of the preheat policy.
|
||||||
|
type Filter interface {
|
||||||
|
// Build filter from the given policy schema
|
||||||
|
BuildFrom(pl *policy.Schema) Filter
|
||||||
|
// Filter the inputting candidates and return the matched ones.
|
||||||
|
Filter(candidates []*selector.Candidate) ([]*selector.Candidate, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultFilter struct {
|
||||||
|
// all kinds of underlying selectors
|
||||||
|
selectors []selector.Selector
|
||||||
|
// keep internal error
|
||||||
|
error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFilter constructs a filter
|
||||||
|
func NewFilter() Filter {
|
||||||
|
return &defaultFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter candidates
|
||||||
|
func (df *defaultFilter) Filter(candidates []*selector.Candidate) ([]*selector.Candidate, error) {
|
||||||
|
if len(df.selectors) == 0 {
|
||||||
|
return nil, errors.New("no underlying filters")
|
||||||
|
}
|
||||||
|
|
||||||
|
if df.error != nil {
|
||||||
|
// Internal error occurred
|
||||||
|
return nil, df.error
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// At the beginning
|
||||||
|
filtered = candidates
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Do filters
|
||||||
|
for _, sl := range df.selectors {
|
||||||
|
filtered, err = sl.Select(filtered)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "do filter error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filtered) == 0 {
|
||||||
|
// Return earlier
|
||||||
|
return filtered, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final filtered ones
|
||||||
|
return filtered, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildFrom builds filter from the given policy schema
|
||||||
|
func (df *defaultFilter) BuildFrom(pl *policy.Schema) Filter {
|
||||||
|
if pl != nil && len(pl.Filters) > 0 {
|
||||||
|
filters := make([]*policy.Filter, 0)
|
||||||
|
// Copy filters and sort the filter list
|
||||||
|
for _, fl := range pl.Filters {
|
||||||
|
filters = append(filters, fl)
|
||||||
|
}
|
||||||
|
// Sort
|
||||||
|
sort.SliceStable(filters, func(i, j int) bool {
|
||||||
|
return filterOrder(filters[i].Type) < filterOrder(filters[j].Type)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Build executable selector based on the filter
|
||||||
|
if df.selectors == nil || len(df.selectors) > 0 {
|
||||||
|
// make or reset
|
||||||
|
df.selectors = make([]selector.Selector, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fl := range filters {
|
||||||
|
sl, err := buildFilter(fl)
|
||||||
|
if err != nil {
|
||||||
|
df.error = errors.Wrap(err, "build filter error")
|
||||||
|
// Return earlier
|
||||||
|
return df
|
||||||
|
}
|
||||||
|
|
||||||
|
df.selectors = append(df.selectors, sl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return df
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign the filter with different order weight and then do filters with fixed order.
|
||||||
|
func filterOrder(t policy.FilterType) uint {
|
||||||
|
switch t {
|
||||||
|
case policy.FilterTypeRepository:
|
||||||
|
return 0
|
||||||
|
case policy.FilterTypeTag:
|
||||||
|
return 1
|
||||||
|
case policy.FilterTypeLabel:
|
||||||
|
return 2
|
||||||
|
case policy.FilterTypeSignature:
|
||||||
|
return 3
|
||||||
|
case policy.FilterTypeVulnerability:
|
||||||
|
return 4
|
||||||
|
default:
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildFilter constructs the selector with the given filter object.
|
||||||
|
// The filter function leverages the selector lib.
|
||||||
|
func buildFilter(f *policy.Filter) (selector.Selector, error) {
|
||||||
|
if f == nil {
|
||||||
|
return nil, errors.New("nil policy filter object")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value should not be nil as all the following filters need pattern data,
|
||||||
|
// even the pattern is empty string or zero int (not nil object).
|
||||||
|
if f.Value == nil {
|
||||||
|
return nil, errors.Errorf("pattern value is missing for filter: %s", f.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check value type
|
||||||
|
switch f.Type {
|
||||||
|
case policy.FilterTypeRepository,
|
||||||
|
policy.FilterTypeTag,
|
||||||
|
policy.FilterTypeLabel:
|
||||||
|
if _, ok := f.Value.(string); !ok {
|
||||||
|
return nil, errors.Errorf("invalid string pattern format for filter: %s", f.Type)
|
||||||
|
}
|
||||||
|
case policy.FilterTypeSignature:
|
||||||
|
if _, ok := f.Value.(bool); !ok {
|
||||||
|
return nil, errors.Errorf("invalid boolean pattern format for filter: %s", f.Type)
|
||||||
|
}
|
||||||
|
case policy.FilterTypeVulnerability:
|
||||||
|
if _, ok := f.Value.(int); !ok {
|
||||||
|
return nil, errors.Errorf("invalid integer pattern format for filter: %s", f.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build selectors
|
||||||
|
switch f.Type {
|
||||||
|
case policy.FilterTypeRepository:
|
||||||
|
return doublestar.New(doublestar.RepoMatches, f.Value, ""), nil
|
||||||
|
case policy.FilterTypeTag:
|
||||||
|
return doublestar.New(doublestar.Matches, f.Value, ""), nil
|
||||||
|
case policy.FilterTypeLabel:
|
||||||
|
return label.New(label.With, f.Value, ""), nil
|
||||||
|
case policy.FilterTypeSignature:
|
||||||
|
return signature.New(signature.All, f.Value, ""), nil
|
||||||
|
case policy.FilterTypeVulnerability:
|
||||||
|
return severity.New(severity.Lt, f.Value, ""), nil
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("unknown filter type: %s", f.Type)
|
||||||
|
}
|
||||||
|
}
|
189
src/pkg/p2p/preheat/policy/filter_test.go
Normal file
189
src/pkg/p2p/preheat/policy/filter_test.go
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// 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 policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/lib/selector"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/p2p/preheat/models/policy"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FilterTestSuite is a test suite of testing policy filter
|
||||||
|
type FilterTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
candidates []*selector.Candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFilter is an entry method of running FilterTestSuite
|
||||||
|
func TestFilter(t *testing.T) {
|
||||||
|
suite.Run(t, &FilterTestSuite{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite prepares env for running FilterTestSuite
|
||||||
|
func (suite *FilterTestSuite) SetupSuite() {
|
||||||
|
suite.candidates = []*selector.Candidate{
|
||||||
|
{
|
||||||
|
NamespaceID: 1,
|
||||||
|
Namespace: "test",
|
||||||
|
Kind: "image",
|
||||||
|
Repository: "sub/busybox",
|
||||||
|
Tags: []string{"prod"},
|
||||||
|
Digest: "sha256@fake",
|
||||||
|
Labels: []string{"prod_ready", "approved"},
|
||||||
|
Signatures: map[string]bool{"prod": true},
|
||||||
|
VulnerabilitySeverity: 3, // medium
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NamespaceID: 1,
|
||||||
|
Namespace: "test",
|
||||||
|
Kind: "image",
|
||||||
|
Repository: "sub/busybox",
|
||||||
|
Tags: []string{"qa"},
|
||||||
|
Digest: "sha256@fake2",
|
||||||
|
Labels: []string{"prod_ready", "approved"},
|
||||||
|
Signatures: map[string]bool{"qa": true},
|
||||||
|
VulnerabilitySeverity: 3, // medium
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NamespaceID: 1,
|
||||||
|
Namespace: "test",
|
||||||
|
Kind: "image",
|
||||||
|
Repository: "portal",
|
||||||
|
Tags: []string{"prod"},
|
||||||
|
Digest: "sha256@fake3",
|
||||||
|
Labels: []string{"prod_ready", "approved"},
|
||||||
|
Signatures: map[string]bool{"prod": true},
|
||||||
|
VulnerabilitySeverity: 3, // medium
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NamespaceID: 1,
|
||||||
|
Namespace: "test",
|
||||||
|
Kind: "image",
|
||||||
|
Repository: "sub/busybox",
|
||||||
|
Tags: []string{"prod2"},
|
||||||
|
Digest: "sha256@fake4",
|
||||||
|
Labels: []string{"prod_ready", "approved"},
|
||||||
|
Signatures: map[string]bool{"prod2": true},
|
||||||
|
VulnerabilitySeverity: 5, // critical
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NamespaceID: 1,
|
||||||
|
Namespace: "test",
|
||||||
|
Kind: "image",
|
||||||
|
Repository: "sub/busybox",
|
||||||
|
Tags: []string{"prod3"},
|
||||||
|
Digest: "sha256@fake5",
|
||||||
|
Labels: []string{"prod_ready", "approved"},
|
||||||
|
Signatures: map[string]bool{"prod3": false},
|
||||||
|
VulnerabilitySeverity: 3, // medium
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NamespaceID: 1,
|
||||||
|
Namespace: "test",
|
||||||
|
Kind: "image",
|
||||||
|
Repository: "sub/busybox",
|
||||||
|
Tags: []string{"prod4"},
|
||||||
|
Digest: "sha256@fake6",
|
||||||
|
Labels: []string{"prod_ready"},
|
||||||
|
Signatures: map[string]bool{"prod4": true},
|
||||||
|
VulnerabilitySeverity: 3, // medium
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NamespaceID: 1,
|
||||||
|
Namespace: "test",
|
||||||
|
Kind: "image",
|
||||||
|
Repository: "portal",
|
||||||
|
Tags: []string{"qa"},
|
||||||
|
Digest: "sha256@fake7",
|
||||||
|
Labels: []string{"staged"},
|
||||||
|
Signatures: map[string]bool{"qa": false},
|
||||||
|
VulnerabilitySeverity: 4, // high
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInvalidFilters tests the invalid filters
|
||||||
|
func (suite *FilterTestSuite) TestInvalidFilters() {
|
||||||
|
p1 := &policy.Schema{
|
||||||
|
Filters: []*policy.Filter{
|
||||||
|
{
|
||||||
|
Type: policy.FilterTypeRepository,
|
||||||
|
Value: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fl := NewFilter()
|
||||||
|
_, err := fl.BuildFrom(p1).Filter(suite.candidates)
|
||||||
|
suite.Errorf(err, "invalid filter: %s", policy.FilterTypeRepository)
|
||||||
|
|
||||||
|
p2 := &policy.Schema{
|
||||||
|
Filters: []*policy.Filter{
|
||||||
|
{
|
||||||
|
Type: policy.FilterTypeSignature,
|
||||||
|
Value: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = fl.BuildFrom(p2).Filter(suite.candidates)
|
||||||
|
suite.Errorf(err, "invalid filter: %s", policy.FilterTypeSignature)
|
||||||
|
|
||||||
|
p3 := &policy.Schema{
|
||||||
|
Filters: []*policy.Filter{
|
||||||
|
{
|
||||||
|
Type: policy.FilterTypeVulnerability,
|
||||||
|
Value: "3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = fl.BuildFrom(p3).Filter(suite.candidates)
|
||||||
|
suite.Errorf(err, "invalid filter: %s", policy.FilterTypeVulnerability)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFilters test all the supported filters with candidates
|
||||||
|
func (suite *FilterTestSuite) TestFilters() {
|
||||||
|
p := &policy.Schema{
|
||||||
|
Filters: []*policy.Filter{
|
||||||
|
{
|
||||||
|
Type: policy.FilterTypeRepository,
|
||||||
|
Value: "sub/**",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: policy.FilterTypeTag,
|
||||||
|
Value: "prod*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: policy.FilterTypeLabel,
|
||||||
|
Value: "prod_ready,approved",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: policy.FilterTypeSignature,
|
||||||
|
Value: true, // signed
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: policy.FilterTypeVulnerability,
|
||||||
|
Value: 4, // < high
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := NewFilter().BuildFrom(p).Filter(suite.candidates)
|
||||||
|
require.NoError(suite.T(), err, "do filters")
|
||||||
|
require.Equal(suite.T(), 1, len(res), "number of matched candidates")
|
||||||
|
suite.Equal("sha256@fake", res[0].Digest, "digest of matched candidate")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user