From 5e2425e2e1442d1563bad18051c39917c03fa59e Mon Sep 17 00:00:00 2001 From: Viswajith Venugopal Date: Wed, 21 Oct 2020 21:27:25 -0700 Subject: [PATCH] Refactor parameters to support arbitrary types (#10) Instead of a map[string]string, support (almost) arbitrary parameters via a map[string]interface{}. Use k8s-style methodology to describe the objects: ie, describe them as Go structs, embedding metadata in comments, and parse out the properties from that using the same library k8s uses, and then use code generation for the validation etc. Also add a new check template (and built-in check) for forbidden API object versions. --- .circleci/config.yml | 4 +- Makefile | 9 +- docs/generated/checks.md | 11 +- docs/generated/templates.md | 165 +++++++++- go.mod | 2 + go.sum | 4 + .../yamls/no-extensions-v1beta.yaml | 9 + internal/check/check.go | 10 +- internal/check/parameter_desc.go | 85 ++++++ internal/check/template.go | 19 +- internal/command/checks/command.go | 8 +- internal/command/common/markdown.go | 4 +- internal/command/templates/command.go | 69 ++++- internal/defaultchecks/default_checks.go | 1 + internal/extract/gvk.go | 11 + .../instantiatedcheck/instantiated_check.go | 20 +- internal/pointers/pointers.go | 6 + internal/stringutils/split.go | 16 + internal/templates/all/all.go | 1 + internal/templates/all/all_test.go | 21 ++ internal/templates/codegen/main.go | 288 ++++++++++++++++++ .../internal/params/gen-params.go | 97 ++++++ .../disallowedgvk/internal/params/params.go | 19 ++ internal/templates/disallowedgvk/template.go | 49 +++ .../envvar/internal/params/gen-params.go | 79 +++++ .../envvar/internal/params/params.go | 12 + internal/templates/envvar/template.go | 25 +- internal/templates/gen.go | 3 + .../privileged/internal/params/gen-params.go | 50 +++ .../privileged/internal/params/params.go | 5 + internal/templates/privileged/template.go | 11 +- .../internal/params/gen-params.go | 50 +++ .../readonlyrootfs/internal/params/params.go | 5 + internal/templates/readonlyrootfs/template.go | 11 +- internal/templates/registry.go | 8 +- .../internal/params/gen-params.go | 79 +++++ .../requiredlabel/internal/params/params.go | 12 + internal/templates/requiredlabel/template.go | 25 +- .../internal/params/gen-params.go | 50 +++ .../runasnonroot/internal/params/params.go | 5 + internal/templates/runasnonroot/template.go | 11 +- internal/templates/util/json.go | 20 ++ internal/templates/util/map_structure.go | 19 ++ internal/utils/ignore_error.go | 7 + 44 files changed, 1300 insertions(+), 115 deletions(-) create mode 100644 internal/builtinchecks/yamls/no-extensions-v1beta.yaml create mode 100644 internal/check/parameter_desc.go create mode 100644 internal/extract/gvk.go create mode 100644 internal/pointers/pointers.go create mode 100644 internal/stringutils/split.go create mode 100644 internal/templates/all/all_test.go create mode 100644 internal/templates/codegen/main.go create mode 100644 internal/templates/disallowedgvk/internal/params/gen-params.go create mode 100644 internal/templates/disallowedgvk/internal/params/params.go create mode 100644 internal/templates/disallowedgvk/template.go create mode 100644 internal/templates/envvar/internal/params/gen-params.go create mode 100644 internal/templates/envvar/internal/params/params.go create mode 100644 internal/templates/gen.go create mode 100644 internal/templates/privileged/internal/params/gen-params.go create mode 100644 internal/templates/privileged/internal/params/params.go create mode 100644 internal/templates/readonlyrootfs/internal/params/gen-params.go create mode 100644 internal/templates/readonlyrootfs/internal/params/params.go create mode 100644 internal/templates/requiredlabel/internal/params/gen-params.go create mode 100644 internal/templates/requiredlabel/internal/params/params.go create mode 100644 internal/templates/runasnonroot/internal/params/gen-params.go create mode 100644 internal/templates/runasnonroot/internal/params/params.go create mode 100644 internal/templates/util/json.go create mode 100644 internal/templates/util/map_structure.go create mode 100644 internal/utils/ignore_error.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 7979824ad..db946359d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,9 +22,9 @@ jobs: make lint - run: - name: Ensure generated docs are up-to-date + name: Ensure generated files are up-to-date command: | - make generated-docs + make generated-srcs git diff --exit-code HEAD test: diff --git a/Makefile b/Makefile index 0cb17e777..bf06cb8a4 100644 --- a/Makefile +++ b/Makefile @@ -70,11 +70,18 @@ lint: golangci-lint staticcheck ## Code generation # #################### +.PHONY: go-generated-srcs +go-generated-srcs: deps + go generate ./... + .PHONY: generated-docs -generated-docs: build +generated-docs: go-generated-srcs build kube-linter templates list --format markdown > docs/generated/templates.md kube-linter checks list --format markdown > docs/generated/checks.md +.PHONY: generated-srcs +generated-srcs: go-generated-srcs generated-docs + .PHONY: packr packr: $(PACKR_BIN) packr diff --git a/docs/generated/checks.md b/docs/generated/checks.md index 39d949f86..8867b253d 100644 --- a/docs/generated/checks.md +++ b/docs/generated/checks.md @@ -2,8 +2,9 @@ The following table enumerates built-in checks: | Name | Enabled by default | Description | Template | Parameters | | ---- | ------------------ | ----------- | -------- | ---------- | - | env-var-secret | Yes | Alert on objects using a secret in an environment variable | env-var |- `name`: `.*secret.*`
| - | no-read-only-root-fs | Yes | Alert on containers not running with a read-only root filesystem | read-only-root-fs | none | - | privileged-container | Yes | Alert on deployments with containers running in privileged mode | privileged | none | - | required-label-owner | No | Alert on objects without the 'owner' label | required-label |- `key`: `owner`
| - | run-as-non-root | Yes | Alert on containers not set to runAsNonRoot | run-as-non-root | none | + | env-var-secret | Yes | Alert on objects using a secret in an environment variable | env-var | `{"name":".*secret.*"}` | + | no-extensions-v1beta | Yes | Alert on objects using deprecated API versions under extensions v1beta | disallowed-api-obj | `{"group":"extensions","version":"v1beta.+"}` | + | no-read-only-root-fs | Yes | Alert on containers not running with a read-only root filesystem | read-only-root-fs | `{}` | + | privileged-container | Yes | Alert on deployments with containers running in privileged mode | privileged | `{}` | + | required-label-owner | No | Alert on objects without the 'owner' label | required-label | `{"key":"owner"}` | + | run-as-non-root | Yes | Alert on containers not set to runAsNonRoot | run-as-non-root | `{}` | diff --git a/docs/generated/templates.md b/docs/generated/templates.md index 627a4d7c8..a8194c29d 100644 --- a/docs/generated/templates.md +++ b/docs/generated/templates.md @@ -1,9 +1,156 @@ -The following table enumerates supported check templates: - -| Name | Description | Supported Objects | Parameters | -| ---- | ----------- | ----------------- | ---------- | - | env-var | Flag environment variables that match the provided patterns | DeploymentLike |- `name` (required): A regex for the env var name
- `value`: A regex for the env var value
| - | privileged | Flag privileged containers | DeploymentLike | none | - | read-only-root-fs | Flag containers without read-only root file systems | DeploymentLike | none | - | required-label | Flag objects not carrying at least one label matching the provided patterns | Any |- `key` (required): A regex for the key of the required label
- `value`: A regex for the value of the required label
| - | run-as-non-root | Flag containers set to run as a root user | DeploymentLike | none | +This page lists supported check templates. + +## Disallowed API Objects + +**Key**: `disallowed-api-obj` + +**Description**: Flag disallowed API object kinds + +**Supported Objects**: Any + +**Parameters**: +``` +[ + { + "name": "group", + "type": "string", + "description": "The disallowed object group.", + "required": false, + "examples": [ + "apps" + ], + "regexAllowed": true, + "negationAllowed": true + }, + { + "name": "version", + "type": "string", + "description": "The disallowed object API version.", + "required": false, + "examples": [ + "v1", + "v1beta1" + ], + "regexAllowed": true, + "negationAllowed": true + }, + { + "name": "kind", + "type": "string", + "description": "The disallowed kind.", + "required": false, + "examples": [ + "Deployment", + "DaemonSet" + ], + "regexAllowed": true, + "negationAllowed": true + } +] + +``` + +## Environment Variables + +**Key**: `env-var` + +**Description**: Flag environment variables that match the provided patterns + +**Supported Objects**: DeploymentLike + +**Parameters**: +``` +[ + { + "name": "name", + "type": "string", + "description": "The name of the environment variable.", + "required": true, + "regexAllowed": true, + "negationAllowed": true + }, + { + "name": "value", + "type": "string", + "description": "The value of the environment variable.", + "required": false, + "regexAllowed": true, + "negationAllowed": true + } +] + +``` + +## Privileged Containers + +**Key**: `privileged` + +**Description**: Flag privileged containers + +**Supported Objects**: DeploymentLike + +**Parameters**: +``` +[] + +``` + +## Read-only Root Filesystems + +**Key**: `read-only-root-fs` + +**Description**: Flag containers without read-only root file systems + +**Supported Objects**: DeploymentLike + +**Parameters**: +``` +[] + +``` + +## Required Label + +**Key**: `required-label` + +**Description**: Flag objects not carrying at least one label matching the provided patterns + +**Supported Objects**: Any + +**Parameters**: +``` +[ + { + "name": "key", + "type": "string", + "description": "Key of the required label.", + "required": true, + "regexAllowed": true, + "negationAllowed": true + }, + { + "name": "value", + "type": "string", + "description": "Value of the required label.", + "required": false, + "regexAllowed": true, + "negationAllowed": true + } +] + +``` + +## Run as non-root user + +**Key**: `run-as-non-root` + +**Description**: Flag containers set to run as a root user + +**Supported Objects**: DeploymentLike + +**Parameters**: +``` +[] + +``` + diff --git a/go.mod b/go.mod index e49d881da..a66a1129e 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/gobuffalo/packr v1.30.1 github.com/golangci/golangci-lint v1.30.0 + github.com/mitchellh/mapstructure v1.1.2 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.0.0 github.com/stretchr/objx v0.2.0 // indirect @@ -18,4 +19,5 @@ require ( k8s.io/api v0.19.1 k8s.io/apimachinery v0.19.1 k8s.io/client-go v0.19.0 + k8s.io/gengo v0.0.0-20200728071708-7794989d0000 ) diff --git a/go.sum b/go.sum index ab0ed8b89..780a0e81e 100644 --- a/go.sum +++ b/go.sum @@ -671,6 +671,7 @@ golang.org/x/tools v0.0.0-20200321224714-0d839f3cf2ed/go.mod h1:Sl4aGygMT6LrqrWc golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200519015757-0d0afa43d58a/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -765,10 +766,13 @@ k8s.io/apimachinery v0.19.1/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlm k8s.io/client-go v0.19.0 h1:1+0E0zfWFIWeyRhQYWzimJOyAk2UT7TiARaLNwJCf7k= k8s.io/client-go v0.19.0/go.mod h1:H9E/VT95blcFQnlyShFgnFT9ZnJOAceiUHM3MlRC+mU= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200728071708-7794989d0000 h1:XgICMZutMLbopSVIJJrhUun6Hbuh1NTZBv2sd0lvypU= +k8s.io/gengo v0.0.0-20200728071708-7794989d0000/go.mod h1:aG2eeomYfcUw8sE3fa7YdkjgnGtyY56TjZlaJJ0ZoWo= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= +k8s.io/utils v0.0.0-20200729134348-d5654de09c73 h1:uJmqzgNWG7XyClnU/mLPBWwfKKF1K8Hf8whTseBgJcg= k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= mvdan.cc/gofumpt v0.0.0-20200709182408-4fd085cb6d5f h1:gi7cb8HTDZ6q8VqsUpkdoFi3vxwHMneQ6+Q5Ap5hjPE= mvdan.cc/gofumpt v0.0.0-20200709182408-4fd085cb6d5f/go.mod h1:9VQ397fNXEnF84t90W4r4TRCQK+pg9f8ugVfyj+S26w= diff --git a/internal/builtinchecks/yamls/no-extensions-v1beta.yaml b/internal/builtinchecks/yamls/no-extensions-v1beta.yaml new file mode 100644 index 000000000..f7b7b46c9 --- /dev/null +++ b/internal/builtinchecks/yamls/no-extensions-v1beta.yaml @@ -0,0 +1,9 @@ +name: "no-extensions-v1beta" +description: "Alert on objects using deprecated API versions under extensions v1beta" +scope: + objectKinds: + - Any +template: "disallowed-api-obj" +params: + group: "extensions" + version: "v1beta.+" diff --git a/internal/check/check.go b/internal/check/check.go index 062b7555e..55d22aa86 100644 --- a/internal/check/check.go +++ b/internal/check/check.go @@ -2,9 +2,9 @@ package check // A Check represents a single check. It is serializable. type Check struct { - Name string `json:"name"` - Description string `json:"description"` - Scope *ObjectKindsDesc `json:"scope"` - Template string `json:"template"` - Params map[string]string `json:"params,omitempty"` + Name string `json:"name"` + Description string `json:"description"` + Scope *ObjectKindsDesc `json:"scope"` + Template string `json:"template"` + Params map[string]interface{} `json:"params,omitempty"` } diff --git a/internal/check/parameter_desc.go b/internal/check/parameter_desc.go new file mode 100644 index 000000000..41eadcfaa --- /dev/null +++ b/internal/check/parameter_desc.go @@ -0,0 +1,85 @@ +package check + +import ( + "golang.stackrox.io/kube-linter/internal/pointers" +) + +// ParameterType represents the expected type of a particular parameter. +type ParameterType string + +// This block enumerates all known type names. +// These type names are chosen to be aligned with OpenAPI/JSON schema. +const ( + StringType ParameterType = "string" + IntegerType ParameterType = "integer" + BooleanType ParameterType = "boolean" + NumberType ParameterType = "number" + ObjectType ParameterType = "object" +) + +// ParameterDesc describes a parameter. +type ParameterDesc struct { + Name string + Type ParameterType + Description string + + Examples []string + + // SubParameters are the child parameters of the given parameter. + // Only relevant if Type is "object". + SubParameters []ParameterDesc + + // Required denotes whether the parameter is required. + Required bool + + // NoRegex is set if the parameter does not support regexes. + // Only relevant if Type is "string". + NoRegex bool + + // NotNegatable is set if the parameter does not support negation via a leading !. + // OnlyRelevant if Type is "string". + NotNegatable bool + + // Fields below are for internal use only. + + XXXStructFieldName string +} + +// HumanReadableParamDesc is a human-friendly representation of a ParameterDesc. +// It is intended only for API documentation/JSON marshaling, and must NOT be used for +// any business logic. +type HumanReadableParamDesc struct { + Name string `json:"name"` + Type ParameterType `json:"type"` + Description string `json:"description"` + Required bool `json:"required"` + Examples []string `json:"examples,omitempty"` + RegexAllowed *bool `json:"regexAllowed,omitempty"` + NegationAllowed *bool `json:"negationAllowed,omitempty"` + SubParameters []HumanReadableParamDesc `json:"subParameters,omitempty"` +} + +// HumanReadableFields returns a human-friendly representation of this ParameterDesc. +func (p *ParameterDesc) HumanReadableFields() HumanReadableParamDesc { + out := HumanReadableParamDesc{ + Name: p.Name, + Type: p.Type, + Description: p.Description, + Required: p.Required, + Examples: p.Examples, + } + + if p.Type == StringType { + out.RegexAllowed = pointers.Bool(!p.NoRegex) + out.NegationAllowed = pointers.Bool(!p.NotNegatable) + } + + if len(p.SubParameters) > 0 { + subParamFields := make([]HumanReadableParamDesc, 0, len(p.SubParameters)) + for _, subParam := range p.SubParameters { + subParamFields = append(subParamFields, subParam.HumanReadableFields()) + } + out.SubParameters = subParamFields + } + return out +} diff --git a/internal/check/template.go b/internal/check/template.go index 15b44c088..d92278da0 100644 --- a/internal/check/template.go +++ b/internal/check/template.go @@ -10,13 +10,6 @@ import ( // object passed in the second argument. type Func func(lintCtx *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic -// A ParameterDesc describes a parameter to a check template. -type ParameterDesc struct { - ParamName string - Required bool - Description string -} - // ObjectKindsDesc describes a list of supported object kinds for a check template. type ObjectKindsDesc struct { ObjectKinds []string `json:"objectKinds"` @@ -24,9 +17,15 @@ type ObjectKindsDesc struct { // A Template is a template for a check. type Template struct { - Name string + // HumanName is a human-friendly name for the template. + // It is to be used ONLY for documentation, and has no + // semantic relevance. + HumanName string + Key string Description string SupportedObjectKinds ObjectKindsDesc - Parameters []ParameterDesc - Instantiate func(params map[string]string) (Func, error) + + Parameters []ParameterDesc + ParseAndValidateParams func(params map[string]interface{}) (interface{}, error) + Instantiate func(parsedParams interface{}) (Func, error) } diff --git a/internal/command/checks/command.go b/internal/command/checks/command.go index d0ae7093f..3eb351364 100644 --- a/internal/command/checks/command.go +++ b/internal/command/checks/command.go @@ -40,17 +40,13 @@ const ( | Name | Enabled by default | Description | Template | Parameters | | ---- | ------------------ | ----------- | -------- | ---------- | -{{ range . }} | {{ .Check.Name}} | {{ if .Default }}Yes{{ else }}No{{ end }} | {{.Check.Description}} | {{.Check.Template}} | -{{- range $key, $value := .Check.Params -}} -- {{backtick}}{{$key}}{{backtick}}: {{backtick}}{{$value}}{{backtick}}
-{{- else }} none {{ end -}} -| +{{ range . }} | {{ .Check.Name}} | {{ if .Default }}Yes{{ else }}No{{ end }} | {{.Check.Description}} | {{.Check.Template}} | {{ backtick }}{{ mustToJson (default (dict) .Check.Params ) }}{{ backtick }} | {{ end -}} ` ) var ( - markDownTemplate = common.MustInstantiateTemplate(markDownTemplateStr) + markDownTemplate = common.MustInstantiateTemplate(markDownTemplateStr, nil) ) func renderMarkdown(checks []check.Check, out io.Writer) error { diff --git a/internal/command/common/markdown.go b/internal/command/common/markdown.go index fe0b3d085..e5e865dc8 100644 --- a/internal/command/common/markdown.go +++ b/internal/command/common/markdown.go @@ -9,14 +9,14 @@ import ( // MustInstantiateTemplate instanties the given go template with a common list of // functions. It panics if there is an error. -func MustInstantiateTemplate(templateStr string) *template.Template { +func MustInstantiateTemplate(templateStr string, customFuncMap template.FuncMap) *template.Template { tpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Funcs( template.FuncMap{ "backtick": func() string { return "`" }, }, - ).Parse(templateStr) + ).Funcs(customFuncMap).Parse(templateStr) utils.Must(err) return tpl diff --git a/internal/command/templates/command.go b/internal/command/templates/command.go index 5b591aee2..8120db05f 100644 --- a/internal/command/templates/command.go +++ b/internal/command/templates/command.go @@ -1,9 +1,13 @@ package templates import ( + "bytes" + "encoding/json" "fmt" "io" "os" + "strings" + "text/template" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -23,33 +27,70 @@ var ( ) const ( - markDownTemplateStr = `The following table enumerates supported check templates: - -| Name | Description | Supported Objects | Parameters | -| ---- | ----------- | ----------------- | ---------- | -{{ range . }} | {{ .Name}} | {{ .Description }} | {{ join "," .SupportedObjectKinds.ObjectKinds }} | -{{- range .Parameters -}} -- {{backtick}}{{.ParamName}}{{backtick}}{{ if .Required }} (required){{ end }}: {{ .Description }}
-{{- else }} none {{ end -}} -| + markDownTemplateStr = `This page lists supported check templates. + +{{ range . -}} +## {{ .HumanName }} + +**Key**: {{ backtick }}{{ .Key }}{{ backtick }} + +**Description**: {{ .Description }} + +**Supported Objects**: {{ join "," .SupportedObjectKinds.ObjectKinds }} + +**Parameters**: +{{ backtick }}{{ backtick }}{{ backtick }} +{{ getParametersJSON .Parameters }} +{{ backtick }}{{ backtick }}{{ backtick }} + {{ end -}} ` ) var ( - markDownTemplate = common.MustInstantiateTemplate(markDownTemplateStr) + markDownTemplate = common.MustInstantiateTemplate(markDownTemplateStr, template.FuncMap{ + "getParametersJSON": func(params []check.ParameterDesc) (string, error) { + out := make([]check.HumanReadableParamDesc, 0, len(params)) + for _, param := range params { + out = append(out, param.HumanReadableFields()) + } + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetIndent("", "\t") + if err := enc.Encode(out); err != nil { + return "", err + } + return buf.String(), nil + }, + }) ) +func renderParameters(numTabs int, params []check.ParameterDesc, out io.Writer) { + tabs := stringutils.Repeat("\t", numTabs) + for _, param := range params { + fmt.Fprintf(out, "%s%s:\n%s\tDescription: %s\n%s\tRequired: %v\n", tabs, param.Name, tabs, param.Description, tabs, param.Required) + if len(param.Examples) > 0 { + quotedExamples := make([]string, 0, len(param.Examples)) + for _, ex := range param.Examples { + quotedExamples = append(quotedExamples, fmt.Sprintf(`"%s"`, ex)) + } + fmt.Fprintf(out, "%s\tExample values: %s\n", tabs, strings.Join(quotedExamples, ", ")) + } + if len(param.SubParameters) > 0 { + fmt.Fprintf(out, "%s\tSub-parameters:\n", tabs) + renderParameters(numTabs+1, param.SubParameters, out) + } + } +} + func renderPlain(templates []check.Template, out io.Writer) error { //nolint:unparam // The function signature is required to match formatToRenderFuncs for i, template := range templates { - fmt.Fprintf(out, "Name: %s\nDescription: %s\nSupported Objects: %v\n", template.Name, template.Description, template.SupportedObjectKinds.ObjectKinds) + fmt.Fprintf(out, "Name: %s\nKey: %s\nDescription: %s\nSupported Objects: %v\n", template.HumanName, template.Key, template.Description, template.SupportedObjectKinds.ObjectKinds) if len(template.Parameters) == 0 { fmt.Fprintln(out, "Parameters: none") } else { fmt.Fprintf(out, "Parameters:\n") - for _, param := range template.Parameters { - fmt.Fprintf(out, "\t%s:\n\t\tDescription: %s\n\t\tRequired: %v\n", param.ParamName, param.Description, param.Required) - } + renderParameters(1, template.Parameters, out) } if i != len(templates)-1 { fmt.Fprintf(out, "\n%s\n\n", dashes) diff --git a/internal/defaultchecks/default_checks.go b/internal/defaultchecks/default_checks.go index 53ec130e9..04d351465 100644 --- a/internal/defaultchecks/default_checks.go +++ b/internal/defaultchecks/default_checks.go @@ -11,5 +11,6 @@ var ( "env-var-secret", "no-read-only-root-fs", "run-as-non-root", + "no-extensions-v1beta", ) ) diff --git a/internal/extract/gvk.go b/internal/extract/gvk.go new file mode 100644 index 000000000..47da02d2f --- /dev/null +++ b/internal/extract/gvk.go @@ -0,0 +1,11 @@ +package extract + +import ( + "golang.stackrox.io/kube-linter/internal/k8sutil" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GVK extracts the GroupVersionKind of an object. +func GVK(object k8sutil.Object) schema.GroupVersionKind { + return object.GetObjectKind().GroupVersionKind() +} diff --git a/internal/instantiatedcheck/instantiated_check.go b/internal/instantiatedcheck/instantiated_check.go index f7ffe28d6..fb10d2967 100644 --- a/internal/instantiatedcheck/instantiated_check.go +++ b/internal/instantiatedcheck/instantiated_check.go @@ -5,7 +5,6 @@ import ( "golang.stackrox.io/kube-linter/internal/check" "golang.stackrox.io/kube-linter/internal/errorhelpers" "golang.stackrox.io/kube-linter/internal/objectkinds" - "golang.stackrox.io/kube-linter/internal/set" "golang.stackrox.io/kube-linter/internal/templates" ) @@ -30,20 +29,11 @@ func ValidateAndInstantiate(c *check.Check) (*InstantiatedCheck, error) { return nil, validationErrs.ToError() } - supportedParams := set.NewStringSet() - for _, param := range template.Parameters { - if param.Required { - if _, found := c.Params[param.ParamName]; !found { - validationErrs.AddStringf("required param %q not specified", param.ParamName) - } - } - supportedParams.Add(param.ParamName) - } - for passedParam := range c.Params { - if !supportedParams.Contains(passedParam) { - validationErrs.AddStringf("unknown param %q passed", passedParam) - } + params, err := template.ParseAndValidateParams(c.Params) + if err != nil { + return nil, errors.Wrap(err, "validating and instantiating params") } + if err := validationErrs.ToError(); err != nil { return nil, err } @@ -60,7 +50,7 @@ func ValidateAndInstantiate(c *check.Check) (*InstantiatedCheck, error) { return nil, err } i.Matcher = matcher - checkFunc, err := template.Instantiate(c.Params) + checkFunc, err := template.Instantiate(params) if err != nil { return nil, errors.Wrap(err, "instantiating check") } diff --git a/internal/pointers/pointers.go b/internal/pointers/pointers.go new file mode 100644 index 000000000..867285240 --- /dev/null +++ b/internal/pointers/pointers.go @@ -0,0 +1,6 @@ +package pointers + +// Bool returns a pointer to a bool. +func Bool(b bool) *bool { + return &b +} diff --git a/internal/stringutils/split.go b/internal/stringutils/split.go new file mode 100644 index 000000000..47996cc9c --- /dev/null +++ b/internal/stringutils/split.go @@ -0,0 +1,16 @@ +package stringutils + +import ( + "strings" +) + +// Split2 splits the given string at the given separator, returning the part before and after the separator as two +// separate return values. +// If the string does not contain `sep`, the entire string is returned as the first return value. +func Split2(str, sep string) (string, string) { + splitIdx := strings.Index(str, sep) + if splitIdx == -1 { + return str, "" + } + return str[:splitIdx], str[splitIdx+len(sep):] +} diff --git a/internal/templates/all/all.go b/internal/templates/all/all.go index bb2b978bc..80f55bcda 100644 --- a/internal/templates/all/all.go +++ b/internal/templates/all/all.go @@ -2,6 +2,7 @@ package all import ( // Import all check templates. + _ "golang.stackrox.io/kube-linter/internal/templates/disallowedgvk" _ "golang.stackrox.io/kube-linter/internal/templates/envvar" _ "golang.stackrox.io/kube-linter/internal/templates/privileged" _ "golang.stackrox.io/kube-linter/internal/templates/readonlyrootfs" diff --git a/internal/templates/all/all_test.go b/internal/templates/all/all_test.go new file mode 100644 index 000000000..2d1fdf7fd --- /dev/null +++ b/internal/templates/all/all_test.go @@ -0,0 +1,21 @@ +package all + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "golang.stackrox.io/kube-linter/internal/templates" +) + +func TestTemplatesAreValid(t *testing.T) { + for _, template := range templates.List() { + t.Run(template.HumanName, func(t *testing.T) { + assert.NotEmpty(t, template.HumanName, "human name") + assert.NotEmpty(t, template.Key, "name") + assert.NotEmpty(t, template.Description, "description") + assert.NotNil(t, template.ParseAndValidateParams, "parse and validate params") + assert.NotNil(t, template.Parameters, "params") // We want people to use the generated code and explicitly set it to an empty list. + assert.NotNil(t, template.Instantiate, "instantiate") + }) + } +} diff --git a/internal/templates/codegen/main.go b/internal/templates/codegen/main.go new file mode 100644 index 000000000..327cb968c --- /dev/null +++ b/internal/templates/codegen/main.go @@ -0,0 +1,288 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "strings" + "text/template" + + "github.com/Masterminds/sprig/v3" + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/internal/check" + "golang.stackrox.io/kube-linter/internal/set" + "golang.stackrox.io/kube-linter/internal/stringutils" + "golang.stackrox.io/kube-linter/internal/utils" + "k8s.io/gengo/parser" + "k8s.io/gengo/types" +) + +var ( + knownNonTemplateDirs = set.NewFrozenStringSet("all", "codegen", "util") +) + +const ( + metadataMarker = "+" + + paramsStructName = "Params" +) + +type templateElem struct { + ParamDesc check.ParameterDesc + ParamJSON string +} + +const ( + fileTemplateStr = `// Code generated by kube-linter template codegen. DO NOT EDIT. +// +build !templatecodegen + +package params + +import ( + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/internal/check" + "golang.stackrox.io/kube-linter/internal/templates/util" +) + +var ( + // Use this in case it doesn't get used otherwise. + _ = util.MustParseParameterDesc + +{{ range . }} + {{ .ParamDesc.Name}}ParamDesc = util.MustParseParameterDesc({{backtick}} +{{- .ParamJSON -}} +{{backtick}}) +{{- end }} + + ParamDescs = []check.ParameterDesc{ + {{- range . }} + {{ .ParamDesc.Name}}ParamDesc, + {{- end }} + } +) + +func (p *Params) Validate() error { + var missingRequiredParams []string + {{- range . }} + {{- if eq .ParamDesc.Type "object" }} + return errors.Errorf("parameter validation not yet supported for object type \"{{ .ParamDesc.Key }}\"") + {{- end }} + {{- if .ParamDesc.Required }} + {{- if ne .ParamDesc.Type "string" }} + return errors.Errorf("required parameter validation is currently only supported for strings, but {{ .ParamDesc.Key }} is not") + {{- end }} + if p.{{ .ParamDesc.XXXStructFieldName }} == "" { + missingRequiredParams = append(missingRequiredParams, "{{.ParamDesc.Name}}") + } + {{- end }} + {{- end }} + if len(missingRequiredParams) > 0 { + return errors.Errorf("required params %v not found", missingRequiredParams) + } + 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)) + } +} +` +) + +var ( + fileTemplate = template.Must(template.New("gen").Funcs(sprig.TxtFuncMap()).Funcs(template.FuncMap{ + "backtick": func() string { + return "`" + }, + }).Parse(fileTemplateStr)) +) + +func lowerCaseFirstLetter(s string) string { + return strings.ToLower(s[:1]) + s[1:] +} + +func getName(member types.Member) string { + if jsonTag := reflect.StructTag(member.Tags).Get("json"); jsonTag != "" { + name, _ := stringutils.Split2(jsonTag, ",") + if name != "" { + return name + } + } + return lowerCaseFirstLetter(member.Name) +} + +func getDescription(member types.Member) string { + firstCommentLineWithMetadata := len(member.CommentLines) + for i, commentLine := range member.CommentLines { + if strings.HasPrefix(commentLine, metadataMarker) { + firstCommentLineWithMetadata = i + break + } + } + return strings.Join(member.CommentLines[:firstCommentLineWithMetadata], " ") +} + +func setBoolBasedOnPresenceOfTag(valToSet *bool, tag string, extractedTags map[string][]string) error { + if val, exists := extractedTags[tag]; exists { + if len(val) > 1 || (len(val) == 0 && val[0] != "") { + return errors.Errorf("invalid value for tag %s: %v; tag is only supported WITHOUT values", tag, val) + } + *valToSet = true + } + return nil +} + +func constructParameterDescsFromStruct(typeSpec *types.Type) ([]check.ParameterDesc, error) { + var paramDescs []check.ParameterDesc + for _, member := range typeSpec.Members { + if member.Embedded { + return nil, errors.Errorf("cannot handle embedded member %s in %+v", member.Name, typeSpec) + } + + desc := check.ParameterDesc{ + Name: getName(member), + Description: getDescription(member), + XXXStructFieldName: member.Name, + } + switch kind := member.Type.Kind; kind { + case types.Builtin: + switch member.Type { + case types.String: + desc.Type = check.StringType + case types.Int: + desc.Type = check.IntegerType + case types.Float32, types.Float64: + desc.Type = check.NumberType + case types.Bool: + desc.Type = check.BooleanType + default: + return nil, errors.Errorf("currently unsupported type %v", member.Type) + } + case types.Struct: + desc.Type = check.ObjectType + subParams, err := constructParameterDescsFromStruct(member.Type) + if err != nil { + return nil, errors.Wrapf(err, "handling field %v", member.Name) + } + desc.SubParameters = subParams + } + + extractedTags := types.ExtractCommentTags(metadataMarker, member.CommentLines) + desc.Examples = extractedTags["example"] + if err := setBoolBasedOnPresenceOfTag(&desc.Required, "required", extractedTags); err != nil { + return nil, err + } + if err := setBoolBasedOnPresenceOfTag(&desc.NoRegex, "noregex", extractedTags); err != nil { + return nil, err + } + if err := setBoolBasedOnPresenceOfTag(&desc.NotNegatable, "notnegatable", extractedTags); err != nil { + return nil, err + } + paramDescs = append(paramDescs, desc) + } + return paramDescs, nil +} + +func processTemplate(dir string) error { + b := parser.New() + // This avoids parsing generated files in the package (since we add +build !templatecodegen to them, + // which makes the parsing much quicker since the parser doesn't have to load any imported packages). + b.AddBuildTags("templatecodegen") + if err := b.AddDir(fmt.Sprintf("./%s/internal/params", dir)); err != nil { + return err + } + typeUniverse, err := b.FindTypes() + if err != nil { + return err + } + pkgNames := b.FindPackages() + if len(pkgNames) != 1 { + return errors.Errorf("found unexpected number of packages in %+v: %d", pkgNames, len(pkgNames)) + } + + pkg := typeUniverse.Package(pkgNames[0]) + paramsType := pkg.Type(paramsStructName) + + if paramsType.Kind != types.Struct { + return errors.Errorf("unexpected param type: %+v", paramsType) + } + paramDescs, err := constructParameterDescsFromStruct(paramsType) + if err != nil { + return err + } + + var templateObj []templateElem + + for _, paramDesc := range paramDescs { + buf := bytes.NewBuffer(nil) + enc := json.NewEncoder(buf) + enc.SetIndent("", "\t") + if err := enc.Encode(paramDesc); err != nil { + return errors.Wrapf(err, "couldn't marshal param %v", paramDesc) + } + + templateObj = append(templateObj, templateElem{ + ParamDesc: paramDesc, + ParamJSON: buf.String(), + }) + } + + outFileName := filepath.Join(dir, "internal", "params", "gen-params.go") + outF, err := os.Create(outFileName) + if err != nil { + return errors.Wrap(err, "creating output file") + } + defer utils.IgnoreError(outF.Close) + if err := fileTemplate.Execute(outF, templateObj); err != nil { + return err + } + return nil +} + +func mainCmd() error { + fileInfos, err := ioutil.ReadDir(".") + if err != nil { + return err + } + for _, fileInfo := range fileInfos { + if !fileInfo.IsDir() { + continue + } + if knownNonTemplateDirs.Contains(fileInfo.Name()) { + continue + } + if err := processTemplate(fileInfo.Name()); err != nil { + return errors.Wrapf(err, "processing dir %v", fileInfo.Name()) + } + } + return nil +} + +func main() { + if err := mainCmd(); err != nil { + fmt.Printf("Error executing command: %v", err) + os.Exit(1) + } + +} diff --git a/internal/templates/disallowedgvk/internal/params/gen-params.go b/internal/templates/disallowedgvk/internal/params/gen-params.go new file mode 100644 index 000000000..b1a787305 --- /dev/null +++ b/internal/templates/disallowedgvk/internal/params/gen-params.go @@ -0,0 +1,97 @@ +// Code generated by kube-linter template codegen. DO NOT EDIT. +// +build !templatecodegen + +package params + +import ( + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/internal/check" + "golang.stackrox.io/kube-linter/internal/templates/util" +) + +var ( + // Use this in case it doesn't get used otherwise. + _ = util.MustParseParameterDesc + + + groupParamDesc = util.MustParseParameterDesc(`{ + "Name": "group", + "Type": "string", + "Description": "The disallowed object group.", + "Examples": [ + "apps" + ], + "SubParameters": null, + "Required": false, + "NoRegex": false, + "NotNegatable": false, + "XXXStructFieldName": "Group" +} +`) + versionParamDesc = util.MustParseParameterDesc(`{ + "Name": "version", + "Type": "string", + "Description": "The disallowed object API version.", + "Examples": [ + "v1", + "v1beta1" + ], + "SubParameters": null, + "Required": false, + "NoRegex": false, + "NotNegatable": false, + "XXXStructFieldName": "Version" +} +`) + kindParamDesc = util.MustParseParameterDesc(`{ + "Name": "kind", + "Type": "string", + "Description": "The disallowed kind.", + "Examples": [ + "Deployment", + "DaemonSet" + ], + "SubParameters": null, + "Required": false, + "NoRegex": false, + "NotNegatable": false, + "XXXStructFieldName": "Kind" +} +`) + + ParamDescs = []check.ParameterDesc{ + groupParamDesc, + versionParamDesc, + kindParamDesc, + } +) + +func (p *Params) Validate() error { + var missingRequiredParams []string + if len(missingRequiredParams) > 0 { + return errors.Errorf("required params %v not found", missingRequiredParams) + } + 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/internal/templates/disallowedgvk/internal/params/params.go b/internal/templates/disallowedgvk/internal/params/params.go new file mode 100644 index 000000000..6190bbf92 --- /dev/null +++ b/internal/templates/disallowedgvk/internal/params/params.go @@ -0,0 +1,19 @@ +package params + +// Params represents the params accepted by this template. +type Params struct { + + // The disallowed object group. + // +example=apps + Group string `json:"group"` + + // The disallowed object API version. + // +example=v1 + // +example=v1beta1 + Version string + + // The disallowed kind. + // +example=Deployment + // +example=DaemonSet + Kind string +} diff --git a/internal/templates/disallowedgvk/template.go b/internal/templates/disallowedgvk/template.go new file mode 100644 index 000000000..5725e0b7a --- /dev/null +++ b/internal/templates/disallowedgvk/template.go @@ -0,0 +1,49 @@ +package disallowedgvk + +import ( + "fmt" + + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/internal/check" + "golang.stackrox.io/kube-linter/internal/diagnostic" + "golang.stackrox.io/kube-linter/internal/extract" + "golang.stackrox.io/kube-linter/internal/lintcontext" + "golang.stackrox.io/kube-linter/internal/matcher" + "golang.stackrox.io/kube-linter/internal/objectkinds" + "golang.stackrox.io/kube-linter/internal/templates" + "golang.stackrox.io/kube-linter/internal/templates/disallowedgvk/internal/params" +) + +func init() { + templates.Register(check.Template{ + HumanName: "Disallowed API Objects", + Key: "disallowed-api-obj", + Description: "Flag disallowed API object kinds", + SupportedObjectKinds: check.ObjectKindsDesc{ + ObjectKinds: []string{objectkinds.Any}, + }, + Parameters: params.ParamDescs, + ParseAndValidateParams: params.ParseAndValidate, + Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) { + groupMatcher, err := matcher.ForString(p.Group) + if err != nil { + return nil, errors.Wrap(err, "invalid group") + } + versionMatcher, err := matcher.ForString(p.Version) + if err != nil { + return nil, errors.Wrap(err, "invalid version") + } + kindMatcher, err := matcher.ForString(p.Kind) + if err != nil { + return nil, errors.Wrap(err, "invalid kind") + } + return func(_ *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic { + gvk := extract.GVK(object.K8sObject) + if groupMatcher(gvk.Group) && versionMatcher(gvk.Version) && kindMatcher(gvk.Kind) { + return []diagnostic.Diagnostic{{Message: fmt.Sprintf("disallowed API object found: %s", gvk)}} + } + return nil + }, nil + }), + }) +} diff --git a/internal/templates/envvar/internal/params/gen-params.go b/internal/templates/envvar/internal/params/gen-params.go new file mode 100644 index 000000000..5f739a63e --- /dev/null +++ b/internal/templates/envvar/internal/params/gen-params.go @@ -0,0 +1,79 @@ +// Code generated by kube-linter template codegen. DO NOT EDIT. +// +build !templatecodegen + +package params + +import ( + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/internal/check" + "golang.stackrox.io/kube-linter/internal/templates/util" +) + +var ( + // Use this in case it doesn't get used otherwise. + _ = util.MustParseParameterDesc + + + nameParamDesc = util.MustParseParameterDesc(`{ + "Name": "name", + "Type": "string", + "Description": "The name of the environment variable.", + "Examples": null, + "SubParameters": null, + "Required": true, + "NoRegex": false, + "NotNegatable": false, + "XXXStructFieldName": "Name" +} +`) + valueParamDesc = util.MustParseParameterDesc(`{ + "Name": "value", + "Type": "string", + "Description": "The value of the environment variable.", + "Examples": null, + "SubParameters": null, + "Required": false, + "NoRegex": false, + "NotNegatable": false, + "XXXStructFieldName": "Value" +} +`) + + ParamDescs = []check.ParameterDesc{ + nameParamDesc, + valueParamDesc, + } +) + +func (p *Params) Validate() error { + var missingRequiredParams []string + if p.Name == "" { + missingRequiredParams = append(missingRequiredParams, "name") + } + if len(missingRequiredParams) > 0 { + return errors.Errorf("required params %v not found", missingRequiredParams) + } + 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/internal/templates/envvar/internal/params/params.go b/internal/templates/envvar/internal/params/params.go new file mode 100644 index 000000000..69bb6184e --- /dev/null +++ b/internal/templates/envvar/internal/params/params.go @@ -0,0 +1,12 @@ +package params + +// Params represents the params accepted by this template. +type Params struct { + + // The name of the environment variable. + // +required + Name string + + // The value of the environment variable. + Value string +} diff --git a/internal/templates/envvar/template.go b/internal/templates/envvar/template.go index 5d60f7d33..891176f1b 100644 --- a/internal/templates/envvar/template.go +++ b/internal/templates/envvar/template.go @@ -11,30 +11,25 @@ import ( "golang.stackrox.io/kube-linter/internal/matcher" "golang.stackrox.io/kube-linter/internal/objectkinds" "golang.stackrox.io/kube-linter/internal/templates" -) - -const ( - nameParamName = "name" - valueParamName = "value" + "golang.stackrox.io/kube-linter/internal/templates/envvar/internal/params" ) func init() { templates.Register(check.Template{ - Name: "env-var", + HumanName: "Environment Variables", + Key: "env-var", Description: "Flag environment variables that match the provided patterns", SupportedObjectKinds: check.ObjectKindsDesc{ ObjectKinds: []string{objectkinds.DeploymentLike}, }, - Parameters: []check.ParameterDesc{ - {ParamName: nameParamName, Required: true, Description: "A regex for the env var name"}, - {ParamName: valueParamName, Description: "A regex for the env var value"}, - }, - Instantiate: func(params map[string]string) (check.Func, error) { - nameMatcher, err := matcher.ForString(params[nameParamName]) + Parameters: params.ParamDescs, + ParseAndValidateParams: params.ParseAndValidate, + Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) { + nameMatcher, err := matcher.ForString(p.Name) if err != nil { - return nil, errors.Wrap(err, "invalid key") + return nil, errors.Wrap(err, "invalid name") } - valueMatcher, err := matcher.ForString(params[valueParamName]) + valueMatcher, err := matcher.ForString(p.Value) if err != nil { return nil, errors.Wrap(err, "invalid value") } @@ -56,6 +51,6 @@ func init() { } return results }, nil - }, + }), }) } diff --git a/internal/templates/gen.go b/internal/templates/gen.go new file mode 100644 index 000000000..3c739a572 --- /dev/null +++ b/internal/templates/gen.go @@ -0,0 +1,3 @@ +package templates + +//go:generate go run ./codegen diff --git a/internal/templates/privileged/internal/params/gen-params.go b/internal/templates/privileged/internal/params/gen-params.go new file mode 100644 index 000000000..8ca851fde --- /dev/null +++ b/internal/templates/privileged/internal/params/gen-params.go @@ -0,0 +1,50 @@ +// Code generated by kube-linter template codegen. DO NOT EDIT. +// +build !templatecodegen + +package params + +import ( + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/internal/check" + "golang.stackrox.io/kube-linter/internal/templates/util" +) + +var ( + // Use this in case it doesn't get used otherwise. + _ = util.MustParseParameterDesc + + + + ParamDescs = []check.ParameterDesc{ + } +) + +func (p *Params) Validate() error { + var missingRequiredParams []string + if len(missingRequiredParams) > 0 { + return errors.Errorf("required params %v not found", missingRequiredParams) + } + 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/internal/templates/privileged/internal/params/params.go b/internal/templates/privileged/internal/params/params.go new file mode 100644 index 000000000..578cc3aa8 --- /dev/null +++ b/internal/templates/privileged/internal/params/params.go @@ -0,0 +1,5 @@ +package params + +// Params represents the params accepted by this template. +type Params struct { +} diff --git a/internal/templates/privileged/template.go b/internal/templates/privileged/template.go index 96518ed85..09b806859 100644 --- a/internal/templates/privileged/template.go +++ b/internal/templates/privileged/template.go @@ -9,17 +9,20 @@ import ( "golang.stackrox.io/kube-linter/internal/lintcontext" "golang.stackrox.io/kube-linter/internal/objectkinds" "golang.stackrox.io/kube-linter/internal/templates" + "golang.stackrox.io/kube-linter/internal/templates/privileged/internal/params" ) func init() { templates.Register(check.Template{ - Name: "privileged", + HumanName: "Privileged Containers", + Key: "privileged", Description: "Flag privileged containers", SupportedObjectKinds: check.ObjectKindsDesc{ ObjectKinds: []string{objectkinds.DeploymentLike}, }, - Parameters: nil, - Instantiate: func(_ map[string]string) (check.Func, error) { + Parameters: params.ParamDescs, + ParseAndValidateParams: params.ParseAndValidate, + Instantiate: params.WrapInstantiateFunc(func(_ params.Params) (check.Func, error) { return func(_ *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic { podSpec, found := extract.PodSpec(object.K8sObject) if !found { @@ -36,6 +39,6 @@ func init() { } return results }, nil - }, + }), }) } diff --git a/internal/templates/readonlyrootfs/internal/params/gen-params.go b/internal/templates/readonlyrootfs/internal/params/gen-params.go new file mode 100644 index 000000000..8ca851fde --- /dev/null +++ b/internal/templates/readonlyrootfs/internal/params/gen-params.go @@ -0,0 +1,50 @@ +// Code generated by kube-linter template codegen. DO NOT EDIT. +// +build !templatecodegen + +package params + +import ( + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/internal/check" + "golang.stackrox.io/kube-linter/internal/templates/util" +) + +var ( + // Use this in case it doesn't get used otherwise. + _ = util.MustParseParameterDesc + + + + ParamDescs = []check.ParameterDesc{ + } +) + +func (p *Params) Validate() error { + var missingRequiredParams []string + if len(missingRequiredParams) > 0 { + return errors.Errorf("required params %v not found", missingRequiredParams) + } + 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/internal/templates/readonlyrootfs/internal/params/params.go b/internal/templates/readonlyrootfs/internal/params/params.go new file mode 100644 index 000000000..578cc3aa8 --- /dev/null +++ b/internal/templates/readonlyrootfs/internal/params/params.go @@ -0,0 +1,5 @@ +package params + +// Params represents the params accepted by this template. +type Params struct { +} diff --git a/internal/templates/readonlyrootfs/template.go b/internal/templates/readonlyrootfs/template.go index 370cbc03a..37abf7e37 100644 --- a/internal/templates/readonlyrootfs/template.go +++ b/internal/templates/readonlyrootfs/template.go @@ -9,17 +9,20 @@ import ( "golang.stackrox.io/kube-linter/internal/lintcontext" "golang.stackrox.io/kube-linter/internal/objectkinds" "golang.stackrox.io/kube-linter/internal/templates" + "golang.stackrox.io/kube-linter/internal/templates/readonlyrootfs/internal/params" ) func init() { templates.Register(check.Template{ - Name: "read-only-root-fs", + HumanName: "Read-only Root Filesystems", + Key: "read-only-root-fs", Description: "Flag containers without read-only root file systems", SupportedObjectKinds: check.ObjectKindsDesc{ ObjectKinds: []string{objectkinds.DeploymentLike}, }, - Parameters: nil, - Instantiate: func(_ map[string]string) (check.Func, error) { + Parameters: params.ParamDescs, + ParseAndValidateParams: params.ParseAndValidate, + Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) { return func(_ *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic { podSpec, found := extract.PodSpec(object.K8sObject) if !found { @@ -34,6 +37,6 @@ func init() { } return results }, nil - }, + }), }) } diff --git a/internal/templates/registry.go b/internal/templates/registry.go index 6d1fa24d3..470397146 100644 --- a/internal/templates/registry.go +++ b/internal/templates/registry.go @@ -14,10 +14,10 @@ var ( // Register registers a template with the given name. // Intended to be called at program init time. func Register(t check.Template) { - if _, ok := allTemplates[t.Name]; ok { - panic(fmt.Sprintf("duplicate template: %v", t.Name)) + if _, ok := allTemplates[t.Key]; ok { + panic(fmt.Sprintf("duplicate template: %v", t.Key)) } - allTemplates[t.Name] = t + allTemplates[t.Key] = t } // Get gets a template by name, returning a boolean indicating whether it was found. @@ -33,7 +33,7 @@ func List() []check.Template { out = append(out, t) } sort.Slice(out, func(i, j int) bool { - return out[i].Name < out[j].Name + return out[i].Key < out[j].Key }) return out } diff --git a/internal/templates/requiredlabel/internal/params/gen-params.go b/internal/templates/requiredlabel/internal/params/gen-params.go new file mode 100644 index 000000000..6b2d04d81 --- /dev/null +++ b/internal/templates/requiredlabel/internal/params/gen-params.go @@ -0,0 +1,79 @@ +// Code generated by kube-linter template codegen. DO NOT EDIT. +// +build !templatecodegen + +package params + +import ( + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/internal/check" + "golang.stackrox.io/kube-linter/internal/templates/util" +) + +var ( + // Use this in case it doesn't get used otherwise. + _ = util.MustParseParameterDesc + + + keyParamDesc = util.MustParseParameterDesc(`{ + "Name": "key", + "Type": "string", + "Description": "Key of the required label.", + "Examples": null, + "SubParameters": null, + "Required": true, + "NoRegex": false, + "NotNegatable": false, + "XXXStructFieldName": "Key" +} +`) + valueParamDesc = util.MustParseParameterDesc(`{ + "Name": "value", + "Type": "string", + "Description": "Value of the required label.", + "Examples": null, + "SubParameters": null, + "Required": false, + "NoRegex": false, + "NotNegatable": false, + "XXXStructFieldName": "Value" +} +`) + + ParamDescs = []check.ParameterDesc{ + keyParamDesc, + valueParamDesc, + } +) + +func (p *Params) Validate() error { + var missingRequiredParams []string + if p.Key == "" { + missingRequiredParams = append(missingRequiredParams, "key") + } + if len(missingRequiredParams) > 0 { + return errors.Errorf("required params %v not found", missingRequiredParams) + } + 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/internal/templates/requiredlabel/internal/params/params.go b/internal/templates/requiredlabel/internal/params/params.go new file mode 100644 index 000000000..c0187b32c --- /dev/null +++ b/internal/templates/requiredlabel/internal/params/params.go @@ -0,0 +1,12 @@ +package params + +// Params represents the params accepted by this template. +type Params struct { + + // Key of the required label. + // +required + Key string + + // Value of the required label. + Value string +} diff --git a/internal/templates/requiredlabel/template.go b/internal/templates/requiredlabel/template.go index 5d4b70003..b83d44f0f 100644 --- a/internal/templates/requiredlabel/template.go +++ b/internal/templates/requiredlabel/template.go @@ -12,30 +12,25 @@ import ( "golang.stackrox.io/kube-linter/internal/objectkinds" "golang.stackrox.io/kube-linter/internal/stringutils" "golang.stackrox.io/kube-linter/internal/templates" -) - -const ( - keyParamName = "key" - valueParamName = "value" + "golang.stackrox.io/kube-linter/internal/templates/requiredlabel/internal/params" ) func init() { templates.Register(check.Template{ - Name: "required-label", + HumanName: "Required Label", + Key: "required-label", Description: "Flag objects not carrying at least one label matching the provided patterns", SupportedObjectKinds: check.ObjectKindsDesc{ ObjectKinds: []string{objectkinds.Any}, }, - Parameters: []check.ParameterDesc{ - {ParamName: keyParamName, Required: true, Description: "A regex for the key of the required label"}, - {ParamName: valueParamName, Required: false, Description: "A regex for the value of the required label"}, - }, - Instantiate: func(params map[string]string) (check.Func, error) { - keyMatcher, err := matcher.ForString(params[keyParamName]) + Parameters: params.ParamDescs, + ParseAndValidateParams: params.ParseAndValidate, + Instantiate: params.WrapInstantiateFunc(func(p params.Params) (check.Func, error) { + keyMatcher, err := matcher.ForString(p.Key) if err != nil { return nil, errors.Wrap(err, "invalid key") } - valueMatcher, err := matcher.ForString(params[valueParamName]) + valueMatcher, err := matcher.ForString(p.Value) if err != nil { return nil, errors.Wrap(err, "invalid value") } @@ -48,9 +43,9 @@ func init() { } } return []diagnostic.Diagnostic{{ - Message: fmt.Sprintf("no label matching \"%s=%s\" found", params[keyParamName], stringutils.OrDefault(params[valueParamName], "")), + Message: fmt.Sprintf("no label matching \"%s=%s\" found", p.Key, stringutils.OrDefault(p.Value, "")), }} }, nil - }, + }), }) } diff --git a/internal/templates/runasnonroot/internal/params/gen-params.go b/internal/templates/runasnonroot/internal/params/gen-params.go new file mode 100644 index 000000000..8ca851fde --- /dev/null +++ b/internal/templates/runasnonroot/internal/params/gen-params.go @@ -0,0 +1,50 @@ +// Code generated by kube-linter template codegen. DO NOT EDIT. +// +build !templatecodegen + +package params + +import ( + "github.com/pkg/errors" + "golang.stackrox.io/kube-linter/internal/check" + "golang.stackrox.io/kube-linter/internal/templates/util" +) + +var ( + // Use this in case it doesn't get used otherwise. + _ = util.MustParseParameterDesc + + + + ParamDescs = []check.ParameterDesc{ + } +) + +func (p *Params) Validate() error { + var missingRequiredParams []string + if len(missingRequiredParams) > 0 { + return errors.Errorf("required params %v not found", missingRequiredParams) + } + 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/internal/templates/runasnonroot/internal/params/params.go b/internal/templates/runasnonroot/internal/params/params.go new file mode 100644 index 000000000..578cc3aa8 --- /dev/null +++ b/internal/templates/runasnonroot/internal/params/params.go @@ -0,0 +1,5 @@ +package params + +// Params represents the params accepted by this template. +type Params struct { +} diff --git a/internal/templates/runasnonroot/template.go b/internal/templates/runasnonroot/template.go index 0c13e8ddb..6c2b7dc60 100644 --- a/internal/templates/runasnonroot/template.go +++ b/internal/templates/runasnonroot/template.go @@ -9,6 +9,7 @@ import ( "golang.stackrox.io/kube-linter/internal/lintcontext" "golang.stackrox.io/kube-linter/internal/objectkinds" "golang.stackrox.io/kube-linter/internal/templates" + "golang.stackrox.io/kube-linter/internal/templates/runasnonroot/internal/params" v1 "k8s.io/api/core/v1" ) @@ -34,13 +35,15 @@ func effectiveRunAsUser(podSC *v1.PodSecurityContext, containerSC *v1.SecurityCo func init() { templates.Register(check.Template{ - Name: "run-as-non-root", + HumanName: "Run as non-root user", + Key: "run-as-non-root", Description: "Flag containers set to run as a root user", SupportedObjectKinds: check.ObjectKindsDesc{ ObjectKinds: []string{objectkinds.DeploymentLike}, }, - Parameters: nil, - Instantiate: func(_ map[string]string) (check.Func, error) { + Parameters: params.ParamDescs, + ParseAndValidateParams: params.ParseAndValidate, + Instantiate: params.WrapInstantiateFunc(func(_ params.Params) (check.Func, error) { return func(_ *lintcontext.LintContext, object lintcontext.Object) []diagnostic.Diagnostic { podSpec, found := extract.PodSpec(object.K8sObject) if !found { @@ -67,6 +70,6 @@ func init() { } return results }, nil - }, + }), }) } diff --git a/internal/templates/util/json.go b/internal/templates/util/json.go new file mode 100644 index 000000000..b5b762fa2 --- /dev/null +++ b/internal/templates/util/json.go @@ -0,0 +1,20 @@ +package util + +import ( + "encoding/json" + "strings" + + "golang.stackrox.io/kube-linter/internal/check" +) + +// MustParseParameterDesc unmarshals the given JSON into a templates.ParameterDesc. +func MustParseParameterDesc(asJSON string) check.ParameterDesc { + var out check.ParameterDesc + + decoder := json.NewDecoder(strings.NewReader(asJSON)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&out); err != nil { + panic(err) + } + return out +} diff --git a/internal/templates/util/map_structure.go b/internal/templates/util/map_structure.go new file mode 100644 index 000000000..ab1ac05b5 --- /dev/null +++ b/internal/templates/util/map_structure.go @@ -0,0 +1,19 @@ +package util + +import ( + "github.com/mitchellh/mapstructure" +) + +// DecodeMapStructure decodes the given map[string]interface{} into the given out variable, typically +// a pointer to a struct. +func DecodeMapStructure(m map[string]interface{}, out interface{}) error { + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + ErrorUnused: true, + TagName: "json", + Result: out, + }) + if err != nil { + return err + } + return dec.Decode(m) +} diff --git a/internal/utils/ignore_error.go b/internal/utils/ignore_error.go new file mode 100644 index 000000000..c38c0b38d --- /dev/null +++ b/internal/utils/ignore_error.go @@ -0,0 +1,7 @@ +package utils + +// IgnoreError is useful when you want to defer a func that returns an error, +// but ignore the error without having the linter complain. +func IgnoreError(f func() error) { + _ = f() +}