From b7b01f5a85e0479e087cae9ae45885e42fe40e85 Mon Sep 17 00:00:00 2001 From: Vedant Mahabaleshwarkar Date: Tue, 11 Apr 2023 15:17:44 -0400 Subject: [PATCH 1/4] add image-sha check --- docs/generated/checks.md | 16 ++++ docs/generated/templates.md | 30 +++++++ e2etests/bats-tests.sh | 17 ++++ pkg/builtinchecks/yamls/image-sha.yaml | 9 ++ pkg/templates/all/all.go | 1 + .../imageshatag/internal/params/gen-params.go | 86 +++++++++++++++++++ .../imageshatag/internal/params/params.go | 11 +++ pkg/templates/imageshatag/template.go | 77 +++++++++++++++++ pkg/templates/imageshatag/template_test.go | 79 +++++++++++++++++ tests/checks/image-sha.yml | 44 ++++++++++ 10 files changed, 370 insertions(+) create mode 100644 pkg/builtinchecks/yamls/image-sha.yaml create mode 100644 pkg/templates/imageshatag/internal/params/gen-params.go create mode 100644 pkg/templates/imageshatag/internal/params/params.go create mode 100644 pkg/templates/imageshatag/template.go create mode 100644 pkg/templates/imageshatag/template_test.go create mode 100644 tests/checks/image-sha.yml diff --git a/docs/generated/checks.md b/docs/generated/checks.md index 930ee1ac0..7b94ae48d 100644 --- a/docs/generated/checks.md +++ b/docs/generated/checks.md @@ -259,6 +259,22 @@ forbiddenServiceTypes: ```yaml minReplicas: 3 ``` +## image-sha + +**Enabled by default**: No + +**Description**: Indicates if there are any image references that are not references by sha256 tags + +**Remediation**: Reference all images using their sha256 tags. + +**Template**: [image-sha](templates.md#latest-tag) + +**Parameters**: + +```yaml +AllowList: +- .*:[a-fA-F0-9]{64}$ +``` ## invalid-target-ports **Enabled by default**: Yes diff --git a/docs/generated/templates.md b/docs/generated/templates.md index d45f86d70..629b8412f 100644 --- a/docs/generated/templates.md +++ b/docs/generated/templates.md @@ -407,6 +407,36 @@ KubeLinter supports the following templates: ## Latest Tag +**Key**: `image-sha` + +**Description**: Flag applications running container images that do not satisfies "allowList" & "blockList" parameters criteria. + +**Supported Objects**: DeploymentLike + + +**Parameters**: + +```yaml +- arrayElemType: string + description: list of regular expressions specifying pattern(s) for container images + that will be blocked. */ + name: blockList + negationAllowed: true + regexAllowed: true + required: false + type: array +- arrayElemType: string + description: list of regular expressions specifying pattern(s) for container images + that will be allowed. + name: allowList + negationAllowed: true + regexAllowed: true + required: false + type: array +``` + +## Latest Tag + **Key**: `latest-tag` **Description**: Flag applications running container images that do not satisfies "allowList" & "blockList" parameters criteria. diff --git a/e2etests/bats-tests.sh b/e2etests/bats-tests.sh index 8674d0ea0..14a7472b7 100755 --- a/e2etests/bats-tests.sh +++ b/e2etests/bats-tests.sh @@ -389,6 +389,23 @@ get_value_from() { [[ "${count}" == "2" ]] } +@test "image-sha" { + tmp="tests/checks/image-sha.yml" + cmd="${KUBE_LINTER_BIN} lint --include image-sha --do-not-auto-add-defaults --format json ${tmp}" + run ${cmd} + + print_info "${status}" "${output}" "${cmd}" "${tmp}" + [ "$status" -eq 1 ] + + message1=$(get_value_from "${lines[0]}" '.Reports[0].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[0].Diagnostic.Message') + message2=$(get_value_from "${lines[0]}" '.Reports[1].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[1].Diagnostic.Message') + count=$(get_value_from "${lines[0]}" '.Reports | length') + + [[ "${message1}" == "Deployment: The container \"app\" is using an invalid container image, \"app:v1\". Please reference the image using a SHA256 tag." ]] + [[ "${message2}" == "DeploymentConfig: The container \"app\" is using an invalid container image, \"app:v1\". Please reference the image using a SHA256 tag." ]] + [[ "${count}" == "2" ]] +} + @test "minimum-three-replicas" { tmp="tests/checks/minimum-three-replicas.yml" cmd="${KUBE_LINTER_BIN} lint --include minimum-three-replicas --do-not-auto-add-defaults --format json ${tmp}" diff --git a/pkg/builtinchecks/yamls/image-sha.yaml b/pkg/builtinchecks/yamls/image-sha.yaml new file mode 100644 index 000000000..cb2f8a485 --- /dev/null +++ b/pkg/builtinchecks/yamls/image-sha.yaml @@ -0,0 +1,9 @@ +name: "image-sha" +description: "Indicates if there are any image references that are not references by sha256 tags" +remediation: "Reference all images using their sha256 tags." +scope: + objectKinds: + - DeploymentLike +template: "image-sha" +params: + AllowList: [".*:[a-fA-F0-9]{64}$"] diff --git a/pkg/templates/all/all.go b/pkg/templates/all/all.go index db88467cb..9e0056da4 100644 --- a/pkg/templates/all/all.go +++ b/pkg/templates/all/all.go @@ -24,6 +24,7 @@ import ( _ "golang.stackrox.io/kube-linter/pkg/templates/hostpid" _ "golang.stackrox.io/kube-linter/pkg/templates/hpareplicas" _ "golang.stackrox.io/kube-linter/pkg/templates/imagepullpolicy" + _ "golang.stackrox.io/kube-linter/pkg/templates/imageshatag" _ "golang.stackrox.io/kube-linter/pkg/templates/latesttag" _ "golang.stackrox.io/kube-linter/pkg/templates/livenessprobe" _ "golang.stackrox.io/kube-linter/pkg/templates/memoryrequirements" diff --git a/pkg/templates/imageshatag/internal/params/gen-params.go b/pkg/templates/imageshatag/internal/params/gen-params.go new file mode 100644 index 000000000..20c6bb6c0 --- /dev/null +++ b/pkg/templates/imageshatag/internal/params/gen-params.go @@ -0,0 +1,86 @@ +// Code generated by kube-linter template codegen. DO NOT EDIT. +// +build !templatecodegen + +package params + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/pkg/check" + "golang.stackrox.io/kube-linter/pkg/templates/util" +) + +var ( + // Use some imports in case they don't get used otherwise. + _ = util.MustParseParameterDesc + _ = fmt.Sprintf + + blockListParamDesc = util.MustParseParameterDesc(`{ + "Name": "blockList", + "Type": "array", + "Description": "list of regular expressions specifying pattern(s) for container images that will be blocked. */", + "Examples": null, + "Enum": null, + "SubParameters": null, + "ArrayElemType": "string", + "Required": false, + "NoRegex": false, + "NotNegatable": false, + "XXXStructFieldName": "BlockList", + "XXXIsPointer": false +} +`) + + allowListParamDesc = util.MustParseParameterDesc(`{ + "Name": "allowList", + "Type": "array", + "Description": "list of regular expressions specifying pattern(s) for container images that will be allowed.", + "Examples": null, + "Enum": null, + "SubParameters": null, + "ArrayElemType": "string", + "Required": false, + "NoRegex": false, + "NotNegatable": false, + "XXXStructFieldName": "AllowList", + "XXXIsPointer": false +} +`) + + ParamDescs = []check.ParameterDesc{ + blockListParamDesc, + allowListParamDesc, + } +) + +func (p *Params) Validate() error { + var validationErrors []string + if len(validationErrors) > 0 { + return errors.Errorf("invalid parameters: %s", strings.Join(validationErrors, ", ")) + } + return nil +} + +// ParseAndValidate instantiates a Params object out of the passed map[string]interface{}, +// validates it, and returns it. +// The return type is interface{} to satisfy the type in the Template struct. +func ParseAndValidate(m map[string]interface{}) (interface{}, error) { + var p Params + if err := util.DecodeMapStructure(m, &p); err != nil { + return nil, err + } + if err := p.Validate(); err != nil { + return nil, err + } + return p, nil +} + +// WrapInstantiateFunc is a convenience wrapper that wraps an untyped instantiate function +// into a typed one. +func WrapInstantiateFunc(f func(p Params) (check.Func, error)) func (interface{}) (check.Func, error) { + return func(paramsInt interface{}) (check.Func, error) { + return f(paramsInt.(Params)) + } +} diff --git a/pkg/templates/imageshatag/internal/params/params.go b/pkg/templates/imageshatag/internal/params/params.go new file mode 100644 index 000000000..cef7c04e7 --- /dev/null +++ b/pkg/templates/imageshatag/internal/params/params.go @@ -0,0 +1,11 @@ +package params + +// Params represents the params accepted by this template. +type Params struct { + + // list of regular expressions specifying pattern(s) for container images that will be blocked. */ + BlockList []string + + // list of regular expressions specifying pattern(s) for container images that will be allowed. + AllowList []string +} diff --git a/pkg/templates/imageshatag/template.go b/pkg/templates/imageshatag/template.go new file mode 100644 index 000000000..1f5dede4e --- /dev/null +++ b/pkg/templates/imageshatag/template.go @@ -0,0 +1,77 @@ +package imageshatag + +import ( + "fmt" + "regexp" + + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/pkg/check" + "golang.stackrox.io/kube-linter/pkg/config" + "golang.stackrox.io/kube-linter/pkg/diagnostic" + "golang.stackrox.io/kube-linter/pkg/objectkinds" + "golang.stackrox.io/kube-linter/pkg/templates" + "golang.stackrox.io/kube-linter/pkg/templates/imageshatag/internal/params" + "golang.stackrox.io/kube-linter/pkg/templates/util" + v1 "k8s.io/api/core/v1" +) + +const ( + templateKey = "image-sha" +) + +func init() { + templates.Register(check.Template{ + HumanName: "Latest Tag", + Key: templateKey, + Description: "Flag applications running container images that do not satisfies \"allowList\" & \"blockList\" parameters criteria.", + SupportedObjectKinds: config.ObjectKindsDesc{ + ObjectKinds: []string{objectkinds.DeploymentLike}, + }, + Parameters: params.ParamDescs, + ParseAndValidateParams: params.ParseAndValidate, + Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) { + + blockedRegexes := make([]*regexp.Regexp, 0, len(p.BlockList)) + for _, res := range p.BlockList { + rg, err := regexp.Compile(res) + if err != nil { + return nil, errors.Wrapf(err, "invalid regex %s", res) + } + blockedRegexes = append(blockedRegexes, rg) + } + + allowedRegexes := make([]*regexp.Regexp, 0, len(p.AllowList)) + for _, res := range p.AllowList { + rg, err := regexp.Compile(res) + if err != nil { + return nil, errors.Wrapf(err, "invalid regex %s", res) + } + allowedRegexes = append(allowedRegexes, rg) + } + + if len(blockedRegexes) > 0 && len(allowedRegexes) > 0 { + err := fmt.Errorf("check has both \"allowList\" & \"blockList\" parameter's values set") + return nil, errors.Wrapf(err, "only one of the paramater lists can be used at a time") + } + + return util.PerContainerCheck(func(container *v1.Container) (results []diagnostic.Diagnostic) { + if len(blockedRegexes) > 0 && isInList(blockedRegexes, container.Image) { + results = append(results, diagnostic.Diagnostic{Message: fmt.Sprintf("The container %q is using an invalid container image, %q. Please reference the image using a SHA256 tag.", container.Name, container.Image)}) + } else if len(allowedRegexes) > 0 && !isInList(allowedRegexes, container.Image) { + results = append(results, diagnostic.Diagnostic{Message: fmt.Sprintf("The container %q is using an invalid container image, %q. Please reference the image using a SHA256 tag.", container.Name, container.Image)}) + } + return results + }), nil + }), + }) +} + +// isInList returns true if a match found in the list for the given name +func isInList(regexlist []*regexp.Regexp, name string) bool { + for _, regex := range regexlist { + if regex.MatchString(name) { + return true + } + } + return false +} diff --git a/pkg/templates/imageshatag/template_test.go b/pkg/templates/imageshatag/template_test.go new file mode 100644 index 000000000..2154fb15f --- /dev/null +++ b/pkg/templates/imageshatag/template_test.go @@ -0,0 +1,79 @@ +package imageshatag + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "golang.stackrox.io/kube-linter/pkg/diagnostic" + "golang.stackrox.io/kube-linter/pkg/lintcontext/mocks" + "golang.stackrox.io/kube-linter/pkg/templates" + "golang.stackrox.io/kube-linter/pkg/templates/imageshatag/internal/params" + + v1 "k8s.io/api/core/v1" +) + +var ( + containerName = "test-container" +) + +func TestContainerImage(t *testing.T) { + suite.Run(t, new(ContainerImageTestSuite)) +} + +type ContainerImageTestSuite struct { + templates.TemplateTestSuite + + ctx *mocks.MockLintContext +} + +func (s *ContainerImageTestSuite) SetupTest() { + s.Init(templateKey) + s.ctx = mocks.NewMockContext() +} + +func (s *ContainerImageTestSuite) addDeploymentWithContainerImage(name, containerImage string) { + s.ctx.AddMockDeployment(s.T(), name) + s.ctx.AddContainerToDeployment(s.T(), name, v1.Container{Name: containerName, Image: containerImage}) +} + +func (s *ContainerImageTestSuite) TestImproperContainerImage() { + const ( + depwithNotAllowedImageTag = "dep-with-not-allowed-image-tag" + ) + + s.addDeploymentWithContainerImage(depwithNotAllowedImageTag, "example.com/test:latest") + + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{ + AllowList: []string{".*:[a-fA-F0-9]{64}$"}, + }, + Diagnostics: map[string][]diagnostic.Diagnostic{ + depwithNotAllowedImageTag: { + {Message: "The container \"test-container\" is using an invalid container image, \"example.com/test:latest\". Please reference the image using a SHA256 tag."}, + }, + }, + ExpectInstantiationError: false, + }, + }) +} + +func (s *ContainerImageTestSuite) TestAcceptableContainerImage() { + const ( + depWithAcceptableImageTag = "dep-with-acceptable-container-image" + ) + + s.addDeploymentWithContainerImage(depWithAcceptableImageTag, "example.com/latest@sha256:75bf9b911b6481dcf29f7942240d1555adaa607eec7fc61bedb7f624f87c36d4") + s.Validate(s.ctx, []templates.TestCase{ + { + Param: params.Params{ + AllowList: []string{".*:[a-fA-F0-9]{64}$"}, + }, + Diagnostics: map[string][]diagnostic.Diagnostic{ + depWithAcceptableImageTag: nil, + }, + ExpectInstantiationError: false, + }, + }) +} diff --git a/tests/checks/image-sha.yml b/tests/checks/image-sha.yml new file mode 100644 index 000000000..49ada26fb --- /dev/null +++ b/tests/checks/image-sha.yml @@ -0,0 +1,44 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dont-fire +spec: + template: + spec: + containers: + - name: app + image: app:v1 +--- +apiVersion: apps.openshift.io/v1 +kind: DeploymentConfig +metadata: + name: dont-fire +spec: + template: + spec: + containers: + - name: app + image: app:v1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app +spec: + template: + spec: + containers: + - name: app + image: app:75bf9b911b6481dcf29f7942240d1555adaa607eec7fc61bedb7f624f87c36d4 +--- +apiVersion: apps.openshift.io/v1 +kind: DeploymentConfig +metadata: + name: app +spec: + template: + spec: + containers: + - name: app + image: app:75bf9b911b6481dcf29f7942240d1555adaa607eec7fc61bedb7f624f87c36d4 \ No newline at end of file From ca14c6f00fa83b91a4ae675424a3773f8e9c5eca Mon Sep 17 00:00:00 2001 From: Vedant Mahabaleshwarkar Date: Tue, 2 May 2023 15:12:28 -0400 Subject: [PATCH 2/4] reorder tests --- e2etests/bats-tests.sh | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/e2etests/bats-tests.sh b/e2etests/bats-tests.sh index 14a7472b7..37e049145 100755 --- a/e2etests/bats-tests.sh +++ b/e2etests/bats-tests.sh @@ -347,6 +347,23 @@ get_value_from() { [[ "${count}" == "1" ]] } +@test "image-sha" { + tmp="tests/checks/image-sha.yml" + cmd="${KUBE_LINTER_BIN} lint --include image-sha --do-not-auto-add-defaults --format json ${tmp}" + run ${cmd} + + print_info "${status}" "${output}" "${cmd}" "${tmp}" + [ "$status" -eq 1 ] + + message1=$(get_value_from "${lines[0]}" '.Reports[0].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[0].Diagnostic.Message') + message2=$(get_value_from "${lines[0]}" '.Reports[1].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[1].Diagnostic.Message') + count=$(get_value_from "${lines[0]}" '.Reports | length') + + [[ "${message1}" == "Deployment: The container \"app\" is using an invalid container image, \"app:v1\". Please reference the image using a SHA256 tag." ]] + [[ "${message2}" == "DeploymentConfig: The container \"app\" is using an invalid container image, \"app:v1\". Please reference the image using a SHA256 tag." ]] + [[ "${count}" == "2" ]] +} + @test "invalid-target-ports" { tmp="tests/checks/invalid-target-ports.yaml" cmd="${KUBE_LINTER_BIN} lint --include invalid-target-ports --do-not-auto-add-defaults --format json ${tmp}" @@ -389,23 +406,6 @@ get_value_from() { [[ "${count}" == "2" ]] } -@test "image-sha" { - tmp="tests/checks/image-sha.yml" - cmd="${KUBE_LINTER_BIN} lint --include image-sha --do-not-auto-add-defaults --format json ${tmp}" - run ${cmd} - - print_info "${status}" "${output}" "${cmd}" "${tmp}" - [ "$status" -eq 1 ] - - message1=$(get_value_from "${lines[0]}" '.Reports[0].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[0].Diagnostic.Message') - message2=$(get_value_from "${lines[0]}" '.Reports[1].Object.K8sObject.GroupVersionKind.Kind + ": " + .Reports[1].Diagnostic.Message') - count=$(get_value_from "${lines[0]}" '.Reports | length') - - [[ "${message1}" == "Deployment: The container \"app\" is using an invalid container image, \"app:v1\". Please reference the image using a SHA256 tag." ]] - [[ "${message2}" == "DeploymentConfig: The container \"app\" is using an invalid container image, \"app:v1\". Please reference the image using a SHA256 tag." ]] - [[ "${count}" == "2" ]] -} - @test "minimum-three-replicas" { tmp="tests/checks/minimum-three-replicas.yml" cmd="${KUBE_LINTER_BIN} lint --include minimum-three-replicas --do-not-auto-add-defaults --format json ${tmp}" From 0e80b9c045c3635c6086414038509850394c79f2 Mon Sep 17 00:00:00 2001 From: Vedant Mahabaleshwarkar Date: Sun, 14 May 2023 15:34:17 -0400 Subject: [PATCH 3/4] allow any yaml --- pkg/builtinchecks/yamls/image-sha.yaml | 2 +- pkg/templates/imageshatag/template.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/builtinchecks/yamls/image-sha.yaml b/pkg/builtinchecks/yamls/image-sha.yaml index cb2f8a485..dd5eeb833 100644 --- a/pkg/builtinchecks/yamls/image-sha.yaml +++ b/pkg/builtinchecks/yamls/image-sha.yaml @@ -3,7 +3,7 @@ description: "Indicates if there are any image references that are not reference remediation: "Reference all images using their sha256 tags." scope: objectKinds: - - DeploymentLike + - "Any" template: "image-sha" params: AllowList: [".*:[a-fA-F0-9]{64}$"] diff --git a/pkg/templates/imageshatag/template.go b/pkg/templates/imageshatag/template.go index 1f5dede4e..15438e481 100644 --- a/pkg/templates/imageshatag/template.go +++ b/pkg/templates/imageshatag/template.go @@ -25,7 +25,7 @@ func init() { Key: templateKey, Description: "Flag applications running container images that do not satisfies \"allowList\" & \"blockList\" parameters criteria.", SupportedObjectKinds: config.ObjectKindsDesc{ - ObjectKinds: []string{objectkinds.DeploymentLike}, + ObjectKinds: []string{objectkinds.Any}, }, Parameters: params.ParamDescs, ParseAndValidateParams: params.ParseAndValidate, From 8fc082ce1ed64b798f6a585e18fdb696b8176397 Mon Sep 17 00:00:00 2001 From: Vedant Mahabaleshwarkar Date: Mon, 5 Jun 2023 14:08:45 -0400 Subject: [PATCH 4/4] add docs --- docs/generated/templates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/generated/templates.md b/docs/generated/templates.md index 629b8412f..48444f803 100644 --- a/docs/generated/templates.md +++ b/docs/generated/templates.md @@ -411,7 +411,7 @@ KubeLinter supports the following templates: **Description**: Flag applications running container images that do not satisfies "allowList" & "blockList" parameters criteria. -**Supported Objects**: DeploymentLike +**Supported Objects**: Any **Parameters**: