From d8e88ef5bcfbd75654d507c2d75faa4ad2c627f5 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Tue, 30 Jun 2020 00:59:01 +0800 Subject: [PATCH] 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 misspelling --- src/controller/p2p/preheat/enforcer.go | 35 ++++ src/lib/selector/candidate.go | 8 +- src/lib/selector/selector.go | 4 +- .../selector/selectors/doublestar/selector.go | 11 +- src/lib/selector/selectors/label/selector.go | 15 +- .../selector/selectors/severity/selector.go | 95 +++++++++ .../selectors/severity/selector_test.go | 130 ++++++++++++ .../selector/selectors/signature/selector.go | 77 +++++++ .../selectors/signature/selector_test.go | 122 +++++++++++ src/pkg/p2p/preheat/models/policy/policy.go | 4 +- src/pkg/p2p/preheat/policy/filter.go | 181 +++++++++++++++++ src/pkg/p2p/preheat/policy/filter_test.go | 189 ++++++++++++++++++ 12 files changed, 860 insertions(+), 11 deletions(-) create mode 100644 src/controller/p2p/preheat/enforcer.go create mode 100644 src/lib/selector/selectors/severity/selector.go create mode 100644 src/lib/selector/selectors/severity/selector_test.go create mode 100644 src/lib/selector/selectors/signature/selector.go create mode 100644 src/lib/selector/selectors/signature/selector_test.go create mode 100644 src/pkg/p2p/preheat/policy/filter.go create mode 100644 src/pkg/p2p/preheat/policy/filter_test.go diff --git a/src/controller/p2p/preheat/enforcer.go b/src/controller/p2p/preheat/enforcer.go new file mode 100644 index 000000000..1cb8d866f --- /dev/null +++ b/src/controller/p2p/preheat/enforcer.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 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) +} diff --git a/src/lib/selector/candidate.go b/src/lib/selector/candidate.go index f89f3cb77..a9bb06212 100644 --- a/src/lib/selector/candidate.go +++ b/src/lib/selector/candidate.go @@ -18,9 +18,9 @@ import ( "encoding/base64" "encoding/json" "fmt" - "github.com/goharbor/harbor/src/lib/log" "github.com/goharbor/harbor/src/lib/errors" + "github.com/goharbor/harbor/src/lib/log" ) const ( @@ -85,6 +85,12 @@ type Candidate struct { CreationTime int64 `json:"create_time_second"` // Labels attached with the candidate 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 diff --git a/src/lib/selector/selector.go b/src/lib/selector/selector.go index 2d660073b..68e07aef8 100644 --- a/src/lib/selector/selector.go +++ b/src/lib/selector/selector.go @@ -27,4 +27,6 @@ type Selector interface { } // 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 diff --git a/src/lib/selector/selectors/doublestar/selector.go b/src/lib/selector/selectors/doublestar/selector.go index 3a246be0c..0867df2f7 100644 --- a/src/lib/selector/selectors/doublestar/selector.go +++ b/src/lib/selector/selectors/doublestar/selector.go @@ -16,6 +16,7 @@ package doublestar import ( "encoding/json" + "github.com/bmatcuk/doublestar" 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 -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 if decoration == Excludes { untagged = false @@ -145,9 +146,15 @@ func New(decoration string, pattern string, extras string) iselector.Selector { untagged = extraObj.Untagged } } + + var p string + if pattern != nil { + p, _ = pattern.(string) + } + return &selector{ decoration: decoration, - pattern: pattern, + pattern: p, untagged: untagged, } } diff --git a/src/lib/selector/selectors/label/selector.go b/src/lib/selector/selectors/label/selector.go index 7523ce42f..a95c0754b 100644 --- a/src/lib/selector/selectors/label/selector.go +++ b/src/lib/selector/selectors/label/selector.go @@ -15,8 +15,9 @@ package label import ( - iselector "github.com/goharbor/harbor/src/lib/selector" "strings" + + iselector "github.com/goharbor/harbor/src/lib/selector" ) const ( @@ -48,11 +49,15 @@ func (s *selector) Select(artifacts []*iselector.Candidate) (selected []*iselect return selected, nil } -// New is factory method for list selector -func New(decoration string, pattern string, extras string) iselector.Selector { +// New is factory method for label selector +func New(decoration string, pattern interface{}, extras string) iselector.Selector { 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{ diff --git a/src/lib/selector/selectors/severity/selector.go b/src/lib/selector/selectors/severity/selector.go new file mode 100644 index 000000000..60c6cd096 --- /dev/null +++ b/src/lib/selector/selectors/severity/selector.go @@ -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), + } +} diff --git a/src/lib/selector/selectors/severity/selector_test.go b/src/lib/selector/selectors/severity/selector_test.go new file mode 100644 index 000000000..37c07e6ba --- /dev/null +++ b/src/lib/selector/selectors/severity/selector_test.go @@ -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") +} diff --git a/src/lib/selector/selectors/signature/selector.go b/src/lib/selector/selectors/signature/selector.go new file mode 100644 index 000000000..cb4a1a166 --- /dev/null +++ b/src/lib/selector/selectors/signature/selector.go @@ -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, + } +} diff --git a/src/lib/selector/selectors/signature/selector_test.go b/src/lib/selector/selectors/signature/selector_test.go new file mode 100644 index 000000000..0601190c3 --- /dev/null +++ b/src/lib/selector/selectors/signature/selector_test.go @@ -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) +} diff --git a/src/pkg/p2p/preheat/models/policy/policy.go b/src/pkg/p2p/preheat/models/policy/policy.go index af6102320..1afb41606 100644 --- a/src/pkg/p2p/preheat/models/policy/policy.go +++ b/src/pkg/p2p/preheat/models/policy/policy.go @@ -27,8 +27,8 @@ const ( // Repository : type=Repository value=name text (double star pattern used) // Tag: type=Tag value=tag text (double star pattern used) // Signature: type=Signature value=bool (True/False) - // Vulnerability: type=Vulnerability value=Severity (expected bar) - // Label: type=Label value=label array + // Vulnerability: type=Vulnerability value=Severity (int) (expected bar) + // Label: type=Label value=label array (with format: lb1,lb2,lb3) // FilterTypeRepository represents the repository filter type FilterTypeRepository FilterType = "repository" diff --git a/src/pkg/p2p/preheat/policy/filter.go b/src/pkg/p2p/preheat/policy/filter.go new file mode 100644 index 000000000..d0b05e335 --- /dev/null +++ b/src/pkg/p2p/preheat/policy/filter.go @@ -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) + } +} diff --git a/src/pkg/p2p/preheat/policy/filter_test.go b/src/pkg/p2p/preheat/policy/filter_test.go new file mode 100644 index 000000000..bc128a7a3 --- /dev/null +++ b/src/pkg/p2p/preheat/policy/filter_test.go @@ -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") +}