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