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() +}