From f09debf935132dc9a10e8150fa75dff89b3f6172 Mon Sep 17 00:00:00 2001 From: Aleksandar Savchev <57963548+AleksandarSavchev@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:23:45 +0200 Subject: [PATCH] [Security Hardened Kubernetes Cluster] Rule 2004 implementation (#376) * Add GetServices utils func * Add service selector * Add rule 2004 * Register rule * Update docu * Add sugested changes * Use new object selector * Add minor changes to NamespacedObjectSelector * Add TODO * Fix unit test --- example/config/managedk8s.yaml | 12 +- pkg/kubernetes/utils/utils.go | 21 ++++ pkg/kubernetes/utils/utils_test.go | 72 +++++++++++ .../ruleset/securityhardenedk8s/rules/2001.go | 13 +- .../securityhardenedk8s/rules/2001_test.go | 6 +- .../ruleset/securityhardenedk8s/rules/2004.go | 112 ++++++++++++++++++ .../securityhardenedk8s/rules/2004_test.go | 93 +++++++++++++++ .../ruleset/securityhardenedk8s/rules/2008.go | 13 +- .../securityhardenedk8s/rules/2008_test.go | 22 ++-- .../securityhardenedk8s/rules/options.go | 1 + .../securityhardenedk8s/v01_ruleset.go | 15 +-- pkg/shared/kubernetes/option/options.go | 41 +++++++ .../kubernetes/option/options_suite_test.go | 17 +++ pkg/shared/kubernetes/option/options_test.go | 102 ++++++++++++++++ 14 files changed, 505 insertions(+), 35 deletions(-) create mode 100644 pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2004.go create mode 100644 pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2004_test.go create mode 100644 pkg/shared/kubernetes/option/options.go create mode 100644 pkg/shared/kubernetes/option/options_suite_test.go create mode 100644 pkg/shared/kubernetes/option/options_test.go diff --git a/example/config/managedk8s.yaml b/example/config/managedk8s.yaml index 94979f49..d5e2e38a 100644 --- a/example/config/managedk8s.yaml +++ b/example/config/managedk8s.yaml @@ -181,7 +181,15 @@ providers: # contains information about known providers # - ruleID: "2001" # args: # acceptedPods: - # - podMatchLabels: + # - matchLabels: + # foo: bar + # namespaceMatchLabels: + # foo: bar + # justification: "justification" + # - ruleID: "2004" + # args: + # acceptedServices: + # - matchLabels: # foo: bar # namespaceMatchLabels: # foo: bar @@ -189,7 +197,7 @@ providers: # contains information about known providers # - ruleID: "2008" # args: # acceptedPods: - # - podMatchLabels: + # - matchLabels: # foo: bar # namespaceMatchLabels: # foo: bar diff --git a/pkg/kubernetes/utils/utils.go b/pkg/kubernetes/utils/utils.go index f6fac2ea..9e0abea9 100644 --- a/pkg/kubernetes/utils/utils.go +++ b/pkg/kubernetes/utils/utils.go @@ -106,6 +106,27 @@ func GetPods(ctx context.Context, c client.Client, namespace string, selector la } } +// GetServices returns all services for a given namespace, or all namespaces if it's set to empty string "". +// It retrieves services by portions set by limit. +func GetServices(ctx context.Context, c client.Client, namespace string, selector labels.Selector, limit int64) ([]corev1.Service, error) { + var ( + services []corev1.Service + serviceList = &corev1.ServiceList{} + ) + + for { + if err := c.List(ctx, serviceList, client.InNamespace(namespace), client.Limit(limit), client.MatchingLabelsSelector{Selector: selector}, client.Continue(serviceList.Continue)); err != nil { + return nil, err + } + + services = append(services, serviceList.Items...) + + if len(serviceList.Continue) == 0 { + return services, nil + } + } +} + // GetReplicaSets returns all replicaSets for a given namespace, or all namespaces if it's set to empty string "". // It retrieves replicaSets by portions set by limit. func GetReplicaSets(ctx context.Context, c client.Client, namespace string, selector labels.Selector, limit int64) ([]appsv1.ReplicaSet, error) { diff --git a/pkg/kubernetes/utils/utils_test.go b/pkg/kubernetes/utils/utils_test.go index 8dd0421a..272ef79b 100644 --- a/pkg/kubernetes/utils/utils_test.go +++ b/pkg/kubernetes/utils/utils_test.go @@ -368,6 +368,78 @@ var _ = Describe("utils", func() { }) + Describe("#GetServices", func() { + var ( + fakeClient client.Client + ctx = context.TODO() + namespaceFoo = "foo" + namespaceDefault = "default" + ) + + BeforeEach(func() { + fakeClient = fakeclient.NewClientBuilder().Build() + for i := 0; i < 10; i++ { + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: strconv.Itoa(i), + Namespace: namespaceDefault, + }, + } + Expect(fakeClient.Create(ctx, service)).To(Succeed()) + } + for i := 10; i < 12; i++ { + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: strconv.Itoa(i), + Namespace: namespaceDefault, + Labels: map[string]string{ + "foo": "bar", + }, + }, + } + Expect(fakeClient.Create(ctx, service)).To(Succeed()) + } + for i := 0; i < 6; i++ { + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: strconv.Itoa(i), + Namespace: namespaceFoo, + }, + } + Expect(fakeClient.Create(ctx, service)).To(Succeed()) + } + }) + + It("should return correct number of services in default namespace", func() { + services, err := utils.GetServices(ctx, fakeClient, namespaceDefault, labels.NewSelector(), 2) + + Expect(len(services)).To(Equal(12)) + Expect(err).To(BeNil()) + }) + + It("should return correct number of services in foo namespace", func() { + services, err := utils.GetServices(ctx, fakeClient, namespaceFoo, labels.NewSelector(), 2) + + Expect(len(services)).To(Equal(6)) + Expect(err).To(BeNil()) + }) + + It("should return correct number of services in all namespaces", func() { + services, err := utils.GetServices(ctx, fakeClient, "", labels.NewSelector(), 2) + + Expect(len(services)).To(Equal(18)) + Expect(err).To(BeNil()) + }) + + It("should return correct number of labeled services in default namespace", func() { + services, err := utils.GetServices(ctx, fakeClient, namespaceDefault, labels.SelectorFromSet(labels.Set{"foo": "bar"}), 2) + + Expect(len(services)).To(Equal(2)) + Expect(err).To(BeNil()) + }) + + }) + Describe("#GetReplicaSets", func() { var ( fakeClient client.Client diff --git a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2001.go b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2001.go index 2777aa8d..2250d2b8 100644 --- a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2001.go +++ b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2001.go @@ -17,13 +17,14 @@ import ( "github.com/gardener/diki/pkg/internal/utils" kubeutils "github.com/gardener/diki/pkg/kubernetes/utils" "github.com/gardener/diki/pkg/rule" - "github.com/gardener/diki/pkg/shared/ruleset/disak8sstig/option" + "github.com/gardener/diki/pkg/shared/kubernetes/option" + disaoptions "github.com/gardener/diki/pkg/shared/ruleset/disak8sstig/option" ) var ( - _ rule.Rule = &Rule2001{} - _ rule.Severity = &Rule2001{} - _ option.Option = &Options2001{} + _ rule.Rule = &Rule2001{} + _ rule.Severity = &Rule2001{} + _ disaoptions.Option = &Options2001{} ) type Rule2001 struct { @@ -36,7 +37,7 @@ type Options2001 struct { } type AcceptedPods2001 struct { - option.PodSelector + option.NamespacedObjectSelector Justification string `json:"justification" yaml:"justification"` } @@ -131,7 +132,7 @@ func (r *Rule2001) accepted(pod corev1.Pod, namespace corev1.Namespace) (bool, s } for _, acceptedPod := range r.Options.AcceptedPods { - if utils.MatchLabels(pod.Labels, acceptedPod.PodMatchLabels) && + if utils.MatchLabels(pod.Labels, acceptedPod.MatchLabels) && utils.MatchLabels(namespace.Labels, acceptedPod.NamespaceMatchLabels) { return true, acceptedPod.Justification } diff --git a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2001_test.go b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2001_test.go index 83c9b741..ee700185 100644 --- a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2001_test.go +++ b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2001_test.go @@ -17,7 +17,7 @@ import ( "github.com/gardener/diki/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules" "github.com/gardener/diki/pkg/rule" - "github.com/gardener/diki/pkg/shared/ruleset/disak8sstig/option" + "github.com/gardener/diki/pkg/shared/kubernetes/option" ) var _ = Describe("#2001", func() { @@ -105,8 +105,8 @@ var _ = Describe("#2001", func() { rules.Options2001{ AcceptedPods: []rules.AcceptedPods2001{ { - PodSelector: option.PodSelector{ - PodMatchLabels: map[string]string{"foo": "bar"}, + NamespacedObjectSelector: option.NamespacedObjectSelector{ + MatchLabels: map[string]string{"foo": "bar"}, NamespaceMatchLabels: map[string]string{"foo": "bar"}, }, Justification: "foo justify", diff --git a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2004.go b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2004.go new file mode 100644 index 00000000..0232d0c2 --- /dev/null +++ b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2004.go @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package rules + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/gardener/diki/pkg/internal/utils" + kubeutils "github.com/gardener/diki/pkg/kubernetes/utils" + "github.com/gardener/diki/pkg/rule" + "github.com/gardener/diki/pkg/shared/kubernetes/option" + disaoptions "github.com/gardener/diki/pkg/shared/ruleset/disak8sstig/option" +) + +var ( + _ rule.Rule = &Rule2004{} + _ rule.Severity = &Rule2004{} + _ disaoptions.Option = &Options2004{} +) + +type Rule2004 struct { + Client client.Client + Options *Options2004 +} + +type Options2004 struct { + AcceptedServices []AcceptedServices2004 `json:"acceptedServices" yaml:"acceptedServices"` +} + +type AcceptedServices2004 struct { + option.NamespacedObjectSelector + Justification string `json:"justification" yaml:"justification"` +} + +// Validate validates that option configurations are correctly defined +func (o Options2004) Validate() field.ErrorList { + var allErrs field.ErrorList + + for _, s := range o.AcceptedServices { + allErrs = append(allErrs, s.Validate()...) + } + + return allErrs +} + +func (r *Rule2004) ID() string { + return "2004" +} + +func (r *Rule2004) Name() string { + return "Limit the Services of type NodePort." +} + +func (r *Rule2004) Severity() rule.SeverityLevel { + return rule.SeverityMedium +} + +func (r *Rule2004) Run(ctx context.Context) (rule.RuleResult, error) { + var checkResults []rule.CheckResult + + services, err := kubeutils.GetServices(ctx, r.Client, "", labels.NewSelector(), 300) + if err != nil { + return rule.Result(r, rule.ErroredCheckResult(err.Error(), rule.NewTarget("kind", "serviceList"))), nil + } + + namespaces, err := kubeutils.GetNamespaces(ctx, r.Client) + if err != nil { + return rule.Result(r, rule.ErroredCheckResult(err.Error(), rule.NewTarget("kind", "namespaceList"))), nil + } + + for _, service := range services { + serviceTarget := rule.NewTarget("kind", "service", "name", service.Name, "namespace", service.Namespace) + + if service.Spec.Type == corev1.ServiceTypeNodePort { + if accepted, justification := r.accepted(service, namespaces[service.Namespace]); accepted { + msg := "Service accepted to be of type NodePort." + if justification != "" { + msg = justification + } + checkResults = append(checkResults, rule.AcceptedCheckResult(msg, serviceTarget)) + } else { + checkResults = append(checkResults, rule.FailedCheckResult("Service should not be of type NodePort.", serviceTarget)) + } + } else { + checkResults = append(checkResults, rule.PassedCheckResult("Service is not of type NodePort.", serviceTarget)) + } + } + + return rule.Result(r, checkResults...), nil +} + +func (r *Rule2004) accepted(service corev1.Service, namespace corev1.Namespace) (bool, string) { + if r.Options == nil { + return false, "" + } + + for _, acceptedService := range r.Options.AcceptedServices { + if utils.MatchLabels(service.Labels, acceptedService.MatchLabels) && + utils.MatchLabels(namespace.Labels, acceptedService.NamespaceMatchLabels) { + return true, acceptedService.Justification + } + } + + return false, "" +} diff --git a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2004_test.go b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2004_test.go new file mode 100644 index 00000000..67523d1d --- /dev/null +++ b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2004_test.go @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package rules_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/gardener/diki/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules" + "github.com/gardener/diki/pkg/rule" + "github.com/gardener/diki/pkg/shared/kubernetes/option" +) + +var _ = Describe("#2004", func() { + var ( + client client.Client + service *corev1.Service + ctx = context.TODO() + namespaceName = "foo" + namespace *corev1.Namespace + ) + + BeforeEach(func() { + client = fakeclient.NewClientBuilder().Build() + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName, + Labels: map[string]string{ + "foo": "bar", + }, + }, + } + service = &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: namespaceName, + Labels: map[string]string{ + "foo": "bar", + }, + }, + } + }) + + DescribeTable("Run cases", + func(serviceSpec corev1.ServiceSpec, ruleOptions rules.Options2004, expectedResult rule.CheckResult) { + r := &rules.Rule2004{Client: client, Options: &ruleOptions} + service.Spec = serviceSpec + + Expect(client.Create(ctx, service)).To(Succeed()) + Expect(client.Create(ctx, namespace)).To(Succeed()) + + ruleResult, err := r.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(ruleResult.CheckResults).To(Equal([]rule.CheckResult{expectedResult})) + }, + + Entry("should pass when serviceSpec is not set", + corev1.ServiceSpec{}, rules.Options2004{}, + rule.CheckResult{Status: rule.Passed, Message: "Service is not of type NodePort.", Target: rule.NewTarget("kind", "service", "name", "foo", "namespace", "foo")}, + ), + Entry("should fail when service is of type NodePort", + corev1.ServiceSpec{Type: "NodePort"}, rules.Options2004{}, + rule.CheckResult{Status: rule.Failed, Message: "Service should not be of type NodePort.", Target: rule.NewTarget("kind", "service", "name", "foo", "namespace", "foo")}, + ), + Entry("should pass when service is not of type NodePort", + corev1.ServiceSpec{Type: "ClusterIP"}, rules.Options2004{}, + rule.CheckResult{Status: rule.Passed, Message: "Service is not of type NodePort.", Target: rule.NewTarget("kind", "service", "name", "foo", "namespace", "foo")}, + ), + Entry("should pass when options are set", + corev1.ServiceSpec{Type: "NodePort"}, + rules.Options2004{ + AcceptedServices: []rules.AcceptedServices2004{ + { + NamespacedObjectSelector: option.NamespacedObjectSelector{ + MatchLabels: map[string]string{"foo": "bar"}, + NamespaceMatchLabels: map[string]string{"foo": "bar"}, + }, + Justification: "foo justify", + }, + }, + }, + rule.CheckResult{Status: rule.Accepted, Message: "foo justify", Target: rule.NewTarget("kind", "service", "name", "foo", "namespace", "foo")}, + ), + ) +}) diff --git a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2008.go b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2008.go index 10d524cb..4466eb49 100644 --- a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2008.go +++ b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2008.go @@ -16,13 +16,14 @@ import ( "github.com/gardener/diki/pkg/internal/utils" kubeutils "github.com/gardener/diki/pkg/kubernetes/utils" "github.com/gardener/diki/pkg/rule" - "github.com/gardener/diki/pkg/shared/ruleset/disak8sstig/option" + "github.com/gardener/diki/pkg/shared/kubernetes/option" + disaoptions "github.com/gardener/diki/pkg/shared/ruleset/disak8sstig/option" ) var ( - _ rule.Rule = &Rule2008{} - _ rule.Severity = &Rule2008{} - _ option.Option = &Options2008{} + _ rule.Rule = &Rule2008{} + _ rule.Severity = &Rule2008{} + _ disaoptions.Option = &Options2008{} ) type Rule2008 struct { @@ -35,7 +36,7 @@ type Options2008 struct { } type AcceptedPods2008 struct { - option.PodSelector + option.NamespacedObjectSelector VolumeNames []string `json:"volumeNames" yaml:"volumeNames"` Justification string `json:"justification" yaml:"justification"` } @@ -117,7 +118,7 @@ func (r *Rule2008) accepted(pod corev1.Pod, namespace corev1.Namespace, volumeNa } for _, acceptedPod := range r.Options.AcceptedPods { - if utils.MatchLabels(pod.Labels, acceptedPod.PodMatchLabels) && + if utils.MatchLabels(pod.Labels, acceptedPod.MatchLabels) && utils.MatchLabels(namespace.Labels, acceptedPod.NamespaceMatchLabels) { if slices.Contains(acceptedPod.VolumeNames, volumeName) { return true, acceptedPod.Justification diff --git a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2008_test.go b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2008_test.go index 0ac89da7..44f01c94 100644 --- a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2008_test.go +++ b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/2008_test.go @@ -18,7 +18,7 @@ import ( "github.com/gardener/diki/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules" "github.com/gardener/diki/pkg/rule" - "github.com/gardener/diki/pkg/shared/ruleset/disak8sstig/option" + "github.com/gardener/diki/pkg/shared/kubernetes/option" ) var _ = Describe("#2008", func() { @@ -138,15 +138,15 @@ var _ = Describe("#2008", func() { options = rules.Options2008{ AcceptedPods: []rules.AcceptedPods2008{ { - PodSelector: option.PodSelector{ - PodMatchLabels: map[string]string{"foo": "bar"}, + NamespacedObjectSelector: option.NamespacedObjectSelector{ + MatchLabels: map[string]string{"foo": "bar"}, NamespaceMatchLabels: map[string]string{"foo": "not-bar"}, }, VolumeNames: []string{"bar"}, }, { - PodSelector: option.PodSelector{ - PodMatchLabels: map[string]string{"foo": "bar"}, + NamespacedObjectSelector: option.NamespacedObjectSelector{ + MatchLabels: map[string]string{"foo": "bar"}, NamespaceMatchLabels: map[string]string{"foo": "bar"}, }, Justification: "foo justify", @@ -207,8 +207,8 @@ var _ = Describe("#2008", func() { options := rules.Options2008{ AcceptedPods: []rules.AcceptedPods2008{ { - PodSelector: option.PodSelector{ - PodMatchLabels: map[string]string{ + NamespacedObjectSelector: option.NamespacedObjectSelector{ + MatchLabels: map[string]string{ "foo": "bar", }, NamespaceMatchLabels: map[string]string{ @@ -217,8 +217,8 @@ var _ = Describe("#2008", func() { }, }, { - PodSelector: option.PodSelector{ - PodMatchLabels: map[string]string{ + NamespacedObjectSelector: option.NamespacedObjectSelector{ + MatchLabels: map[string]string{ "foo": "bar", }, NamespaceMatchLabels: map[string]string{ @@ -228,8 +228,8 @@ var _ = Describe("#2008", func() { VolumeNames: []string{"foo"}, }, { - PodSelector: option.PodSelector{ - PodMatchLabels: map[string]string{ + NamespacedObjectSelector: option.NamespacedObjectSelector{ + MatchLabels: map[string]string{ "foo": "bar", }, NamespaceMatchLabels: map[string]string{ diff --git a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/options.go b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/options.go index 58d0c77c..afd1c89c 100644 --- a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/options.go +++ b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/rules/options.go @@ -6,5 +6,6 @@ package rules type RuleOption interface { Options2001 | + Options2004 | Options2008 } diff --git a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/v01_ruleset.go b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/v01_ruleset.go index 08869112..5705045f 100644 --- a/pkg/provider/managedk8s/ruleset/securityhardenedk8s/v01_ruleset.go +++ b/pkg/provider/managedk8s/ruleset/securityhardenedk8s/v01_ruleset.go @@ -26,6 +26,10 @@ func (r *Ruleset) registerV01Rules(ruleOptions map[string]config.RuleOptionsConf if err != nil { return fmt.Errorf("rule option 2001 error: %s", err.Error()) } + opts2004, err := getV01OptionOrNil[rules.Options2004](ruleOptions["2004"].Args) + if err != nil { + return fmt.Errorf("rule option 2004 error: %s", err.Error()) + } opts2008, err := getV01OptionOrNil[rules.Options2008](ruleOptions["2008"].Args) if err != nil { return fmt.Errorf("rule option 2008 error: %s", err.Error()) @@ -57,13 +61,10 @@ func (r *Ruleset) registerV01Rules(ruleOptions map[string]config.RuleOptionsConf rule.NotImplemented, rule.SkipRuleWithSeverity(rule.SeverityMedium), ), - rule.NewSkipRule( - "2004", - "Limit the Services of type NodePort.", - "Not implemented.", - rule.NotImplemented, - rule.SkipRuleWithSeverity(rule.SeverityMedium), - ), + &rules.Rule2004{ + Client: c, + Options: opts2004, + }, rule.NewSkipRule( "2005", "Container images must come from trusted repositories.", diff --git a/pkg/shared/kubernetes/option/options.go b/pkg/shared/kubernetes/option/options.go new file mode 100644 index 00000000..c47e22a8 --- /dev/null +++ b/pkg/shared/kubernetes/option/options.go @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package option + +import ( + metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/gardener/diki/pkg/shared/ruleset/disak8sstig/option" +) + +// NamespacedObjectSelector contains generalized options for matching entities by their attribute labels. +type NamespacedObjectSelector struct { + MatchLabels map[string]string `json:"matchLabels" yaml:"matchLabels"` + NamespaceMatchLabels map[string]string `json:"namespaceMatchLabels" yaml:"namespaceMatchLabels"` +} + +// TODO: Implement new Option interface in this package with Validate method, which recieves field.Path. +var _ option.Option = (*NamespacedObjectSelector)(nil) + +// Validate validates that option configurations are correctly defined. +func (s *NamespacedObjectSelector) Validate() field.ErrorList { + var ( + allErrs field.ErrorList + rootPath = field.NewPath("") + ) + + if len(s.NamespaceMatchLabels) == 0 { + allErrs = append(allErrs, field.Required(rootPath.Child("namespaceMatchLabels"), "must not be empty")) + } + + if len(s.MatchLabels) == 0 { + allErrs = append(allErrs, field.Required(rootPath.Child("matchLabels"), "must not be empty")) + } + + allErrs = append(allErrs, metav1validation.ValidateLabels(s.NamespaceMatchLabels, rootPath.Child("namespaceMatchLabels"))...) + allErrs = append(allErrs, metav1validation.ValidateLabels(s.MatchLabels, rootPath.Child("matchLabels"))...) + return allErrs +} diff --git a/pkg/shared/kubernetes/option/options_suite_test.go b/pkg/shared/kubernetes/option/options_suite_test.go new file mode 100644 index 00000000..300129ec --- /dev/null +++ b/pkg/shared/kubernetes/option/options_suite_test.go @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package option_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestOptions(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Options Test Suite") +} diff --git a/pkg/shared/kubernetes/option/options_test.go b/pkg/shared/kubernetes/option/options_test.go new file mode 100644 index 00000000..9a28dc79 --- /dev/null +++ b/pkg/shared/kubernetes/option/options_test.go @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package option_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/gardener/diki/pkg/shared/kubernetes/option" +) + +var _ = Describe("options", func() { + Describe("#ValidateNamespacedObjectSelector", func() { + It("should correctly validate labels", func() { + attributes := []option.NamespacedObjectSelector{ + { + NamespaceMatchLabels: map[string]string{"_foo": "bar"}, + MatchLabels: map[string]string{"foo": "bar."}, + }, + { + NamespaceMatchLabels: map[string]string{"fo?o": "bar"}, + MatchLabels: map[string]string{"foo": "bar"}, + }, + { + NamespaceMatchLabels: map[string]string{"foo": "bar"}, + MatchLabels: map[string]string{"at_ta": "bar"}, + }, + { + NamespaceMatchLabels: map[string]string{"this": "is_a"}, + MatchLabels: map[string]string{"Valid": "label-pair"}, + }, + { + NamespaceMatchLabels: map[string]string{"foo": "ba/r"}, + MatchLabels: map[string]string{"at$a": "bar"}, + }, + { + NamespaceMatchLabels: map[string]string{"label": "value"}, + }, + { + MatchLabels: map[string]string{"label": "value"}, + }, + { + NamespaceMatchLabels: map[string]string{}, + MatchLabels: map[string]string{"at_ta": "bar"}, + }, + { + NamespaceMatchLabels: map[string]string{"foo": "bar"}, + MatchLabels: map[string]string{}, + }, + } + + var result field.ErrorList + for _, p := range attributes { + result = append(result, p.Validate()...) + } + + Expect(result).To(ConsistOf( + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("[].namespaceMatchLabels"), + "BadValue": Equal("_foo"), + })), + PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("[].matchLabels"), + "BadValue": Equal("bar."), + })), PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("[].namespaceMatchLabels"), + "BadValue": Equal("fo?o"), + })), PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("[].namespaceMatchLabels"), + "BadValue": Equal("ba/r"), + })), PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeInvalid), + "Field": Equal("[].matchLabels"), + "BadValue": Equal("at$a"), + })), PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("[].namespaceMatchLabels"), + "Detail": Equal("must not be empty"), + })), PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("[].namespaceMatchLabels"), + "Detail": Equal("must not be empty"), + })), PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("[].matchLabels"), + "Detail": Equal("must not be empty"), + })), PointTo(MatchFields(IgnoreExtras, Fields{ + "Type": Equal(field.ErrorTypeRequired), + "Field": Equal("[].matchLabels"), + "Detail": Equal("must not be empty"), + })))) + }) + }) +})