diff --git a/api/v2beta3/policy_types.go b/api/v2beta3/policy_types.go index db929fc..09f46b8 100644 --- a/api/v2beta3/policy_types.go +++ b/api/v2beta3/policy_types.go @@ -78,7 +78,7 @@ type PolicyExclusions struct { // +optional // Labels is a list of Kubernetes labels that are needed to excluded the policy against a resource // this filter is statisfied if only one label existed, using * for value make it so it will match if the key exists regardless of its value - Labels []map[string]string `json:"labels"` + Labels map[string]string `json:"labels"` } // PolicySpec defines the desired state of Policy diff --git a/api/v2beta3/zz_generated.deepcopy.go b/api/v2beta3/zz_generated.deepcopy.go index f69cced..d1c1227 100644 --- a/api/v2beta3/zz_generated.deepcopy.go +++ b/api/v2beta3/zz_generated.deepcopy.go @@ -226,15 +226,9 @@ func (in *PolicyExclusions) DeepCopyInto(out *PolicyExclusions) { } if in.Labels != nil { in, out := &in.Labels, &out.Labels - *out = make([]map[string]string, len(*in)) - for i := range *in { - if (*in)[i] != nil { - in, out := &(*in)[i], &(*out)[i] - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val } } } diff --git a/config/crd/bases/pac.weave.works_policies.yaml b/config/crd/bases/pac.weave.works_policies.yaml index f50b700..ad870b2 100644 --- a/config/crd/bases/pac.weave.works_policies.yaml +++ b/config/crd/bases/pac.weave.works_policies.yaml @@ -533,15 +533,13 @@ spec: list properties: labels: + additionalProperties: + type: string description: Labels is a list of Kubernetes labels that are needed to excluded the policy against a resource this filter is statisfied if only one label existed, using * for value make it so it will match if the key exists regardless of its value - items: - additionalProperties: - type: string - type: object - type: array + type: object namespaces: description: Namespaces is a list of Kubernetes namespaces that a resource needs to be a part of to excluded from this policy diff --git a/helm/crds/pac.weave.works_policies.yaml b/helm/crds/pac.weave.works_policies.yaml index f50b700..ad870b2 100644 --- a/helm/crds/pac.weave.works_policies.yaml +++ b/helm/crds/pac.weave.works_policies.yaml @@ -533,15 +533,13 @@ spec: list properties: labels: + additionalProperties: + type: string description: Labels is a list of Kubernetes labels that are needed to excluded the policy against a resource this filter is statisfied if only one label existed, using * for value make it so it will match if the key exists regardless of its value - items: - additionalProperties: - type: string - type: object - type: array + type: object namespaces: description: Namespaces is a list of Kubernetes namespaces that a resource needs to be a part of to excluded from this policy diff --git a/pkg/policy-core/domain/policy.go b/pkg/policy-core/domain/policy.go index 3f5a47f..8a249d2 100644 --- a/pkg/policy-core/domain/policy.go +++ b/pkg/policy-core/domain/policy.go @@ -27,9 +27,9 @@ type PolicyStandard struct { // PolicyExclusions are the structure which resources should not be evaluated against the policy type PolicyExclusions struct { - Namespaces []string `json:"namespaces"` - Resources []string `json:"resources"` - Labels []map[string]string `json:"labels"` + Namespaces []string `json:"namespaces"` + Resources []string `json:"resources"` + Labels map[string]string `json:"labels"` } // Policy represents a policy diff --git a/pkg/policy-core/validation/common.go b/pkg/policy-core/validation/common.go index 81148e1..895d8cd 100644 --- a/pkg/policy-core/validation/common.go +++ b/pkg/policy-core/validation/common.go @@ -73,15 +73,13 @@ func isExcluded(entity domain.Entity, policy domain.Policy) bool { } } - for _, obj := range policy.Exclude.Labels { - for key, val := range obj { - entityVal, ok := entity.Labels[key] - if ok { - if val != "*" && val != entityVal { - continue - } - return true + for key, val := range policy.Exclude.Labels { + entityVal, ok := entity.Labels[key] + if ok { + if val != "*" && val != entityVal { + continue } + return true } } diff --git a/pkg/policy-core/validation/opa_validation_test.go b/pkg/policy-core/validation/opa_validation_test.go index 3a5c3de..d89ffb9 100644 --- a/pkg/policy-core/validation/opa_validation_test.go +++ b/pkg/policy-core/validation/opa_validation_test.go @@ -674,7 +674,7 @@ func TestOpaValidator_Validate(t *testing.T) { init: init{ writeCompliance: false, loadStubs: func(policiesSource *mock.MockPoliciesSource, sink *mock.MockPolicyValidationSink) { - imageTag := testdata.Policies["imageTagExcluded"] + imageTag := testdata.Policies["imageTagExcludedNamespaces"] policiesSource.EXPECT().GetAll(gomock.Any()). Times(1).Return([]domain.Policy{ imageTag, @@ -694,7 +694,7 @@ func TestOpaValidator_Validate(t *testing.T) { init: init{ writeCompliance: false, loadStubs: func(policiesSource *mock.MockPoliciesSource, sink *mock.MockPolicyValidationSink) { - imageTag := testdata.Policies["imageTagExcluded"] + imageTag := testdata.Policies["imageTagExcludedLabels"] policiesSource.EXPECT().GetAll(gomock.Any()). Times(1).Return([]domain.Policy{ imageTag, @@ -715,7 +715,7 @@ func TestOpaValidator_Validate(t *testing.T) { writeCompliance: false, loadStubs: func(policiesSource *mock.MockPoliciesSource, sink *mock.MockPolicyValidationSink) { missingOwner := testdata.Policies["missingOwner"] - imageTag := testdata.Policies["imageTag"] + imageTag := testdata.Policies["imageTagExcludedResources"] imageTag.Exclude.Resources = []string{"unit-testing/nginx-deployment"} policiesSource.EXPECT().GetAll(gomock.Any()). Times(1).Return([]domain.Policy{ diff --git a/pkg/policy-core/validation/testdata/testdata.go b/pkg/policy-core/validation/testdata/testdata.go index f064fe4..7706ef7 100644 --- a/pkg/policy-core/validation/testdata/testdata.go +++ b/pkg/policy-core/validation/testdata/testdata.go @@ -544,15 +544,175 @@ var ( }, }, }, - "imageTagExcluded": { + "imageTagExcludedNamespaces": { Name: "Using latest image tag in container", Exclude: domain.PolicyExclusions{ Namespaces: []string{"unit-testing", "flux-system"}, - Resources: []string{"unit-testing/nginx-deployment"}, - Labels: []map[string]string{ - { - "app": "nginx", - }, + }, + Enforce: true, + ID: uuid.NewV4().String(), + Code: ` + package magalix.advisor.images.image_tag_enforce + + image_tag := input.parameters.image_tag + + violation[result] { + some i + containers = controller_spec.containers[i] + splittedUrl = split(containers.image, "/") + image = splittedUrl[count(splittedUrl)-1] + not contains(image, ":") + result = { + "issue detected": true, + "msg": "Image is not tagged", + "violating_key": sprintf("spec.template.spec.containers[%v].image", [i]) + } + } + + violation[result] { + some i + containers = controller_spec.containers[i] + splittedUrl = split(containers.image, "/") + image = splittedUrl[count(splittedUrl)-1] + count(split(image, ":")) == 2 + [image_name, tag] = split(image, ":") + tag == image_tag + result = { + "issue detected": true, + "msg": sprintf("Image contains unapproved tag '%v'", [image_tag]), + "image": image, + "violating_key": sprintf("spec.template.spec.containers[%v].image", [i]) + } + } + + violation[result] { + some i + containers = controller_spec.containers[i] + splittedUrl = split(containers.image, "/") + image = splittedUrl[count(splittedUrl)-1] + count(split(image, ":")) == 3 + [image_name, port, tag] = split(image, ":") + tag == image_tag + result = { + "issue detected": true, + "msg": sprintf("Image contains unapproved tag:'%v'", [image_tag]), + "image": image, + "violating_key": sprintf("spec.template.spec.containers[%v].image", [i]) + } + } + + # Controller input + controller_input = input.review.object + + # controller_container acts as an iterator to get containers from the template + controller_spec = controller_input.spec.template.spec { + contains_kind(controller_input.kind, {"StatefulSet" , "DaemonSet", "Deployment", "Job"}) + } else = controller_input.spec { + controller_input.kind == "Pod" + } else = controller_input.spec.jobTemplate.spec.template.spec { + controller_input.kind == "CronJob" + } + + contains_kind(kind, kinds) { + kinds[_] = kind + }`, + Mutate: true, + Parameters: []domain.PolicyParameters{ + { + Name: "image_tag", + Type: "string", + Required: true, + Value: "latest", + }, + }, + }, + "imageTagExcludedResources": { + Name: "Using latest image tag in container", + Exclude: domain.PolicyExclusions{ + Resources: []string{"unit-testing/nginx-deployment"}, + }, + Enforce: true, + ID: uuid.NewV4().String(), + Code: ` + package magalix.advisor.images.image_tag_enforce + + image_tag := input.parameters.image_tag + + violation[result] { + some i + containers = controller_spec.containers[i] + splittedUrl = split(containers.image, "/") + image = splittedUrl[count(splittedUrl)-1] + not contains(image, ":") + result = { + "issue detected": true, + "msg": "Image is not tagged", + "violating_key": sprintf("spec.template.spec.containers[%v].image", [i]) + } + } + + violation[result] { + some i + containers = controller_spec.containers[i] + splittedUrl = split(containers.image, "/") + image = splittedUrl[count(splittedUrl)-1] + count(split(image, ":")) == 2 + [image_name, tag] = split(image, ":") + tag == image_tag + result = { + "issue detected": true, + "msg": sprintf("Image contains unapproved tag '%v'", [image_tag]), + "image": image, + "violating_key": sprintf("spec.template.spec.containers[%v].image", [i]) + } + } + + violation[result] { + some i + containers = controller_spec.containers[i] + splittedUrl = split(containers.image, "/") + image = splittedUrl[count(splittedUrl)-1] + count(split(image, ":")) == 3 + [image_name, port, tag] = split(image, ":") + tag == image_tag + result = { + "issue detected": true, + "msg": sprintf("Image contains unapproved tag:'%v'", [image_tag]), + "image": image, + "violating_key": sprintf("spec.template.spec.containers[%v].image", [i]) + } + } + + # Controller input + controller_input = input.review.object + + # controller_container acts as an iterator to get containers from the template + controller_spec = controller_input.spec.template.spec { + contains_kind(controller_input.kind, {"StatefulSet" , "DaemonSet", "Deployment", "Job"}) + } else = controller_input.spec { + controller_input.kind == "Pod" + } else = controller_input.spec.jobTemplate.spec.template.spec { + controller_input.kind == "CronJob" + } + + contains_kind(kind, kinds) { + kinds[_] = kind + }`, + Mutate: true, + Parameters: []domain.PolicyParameters{ + { + Name: "image_tag", + Type: "string", + Required: true, + Value: "latest", + }, + }, + }, + "imageTagExcludedLabels": { + Name: "Using latest image tag in container", + Exclude: domain.PolicyExclusions{ + Labels: map[string]string{ + "app": "nginx", }, }, Enforce: true,