diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c99d6e47..39ff4703 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,9 +94,8 @@ The steps are: 2. Bump the version number in: 1. main.go 2. README.md - 3. Regenerate the deployment files. Assuming you've cloned the charts repo to `./charts`: - 1. `helm template ./charts/stable/polaris/ --name polaris --namespace polaris --set templateOnly=true > deploy/dashboard.yaml` - 2. `helm template ./charts/stable/polaris/ --name polaris --namespace polaris --set templateOnly=true --set webhook.enable=true --set dashboard.enable=false > deploy/webhook.yaml` + 3. Regenerate the deployment files. Assuming you've cloned the charts repo to `~/git/charts`: + 1. `CHARTS_DIR=~/git/charts ./scripts/generate-deployment-files.sh` 4. Update CHANGELOG.md 5. Merge your PR 3. Tag the latest branch for this repo diff --git a/checks/cpuLimitsMissing.yaml b/checks/cpuLimitsMissing.yaml new file mode 100644 index 00000000..ccf1ddfe --- /dev/null +++ b/checks/cpuLimitsMissing.yaml @@ -0,0 +1,27 @@ +successMessage: CPU limits are set +failureMessage: CPU limits should be set +category: Resources +target: Container +containers: + exclude: + - initContainer +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - limits + properties: + limits: + type: object + required: + - cpu + properties: + cpu: + type: string + not: + const: '' diff --git a/checks/cpuRequestsMissing.yaml b/checks/cpuRequestsMissing.yaml new file mode 100644 index 00000000..77290d8b --- /dev/null +++ b/checks/cpuRequestsMissing.yaml @@ -0,0 +1,27 @@ +successMessage: CPU requests are set +failureMessage: CPU requests should be set +category: Resources +target: Container +containers: + exclude: + - initContainer +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - requests + properties: + requests: + type: object + required: + - cpu + properties: + cpu: + type: string + not: + const: '' diff --git a/checks/dangerousCapabilities.yaml b/checks/dangerousCapabilities.yaml new file mode 100644 index 00000000..d1daa414 --- /dev/null +++ b/checks/dangerousCapabilities.yaml @@ -0,0 +1,25 @@ +successMessage: Container does not have any dangerous capabilities +failureMessage: Container should not have dangerous capabilities +category: Security +target: Container +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + properties: + securityContext: + type: object + properties: + capabilities: + type: object + properties: + add: + type: array + not: + contains: + const: ALL + not: + contains: + const: SYS_ADMIN + not: + contains: + const: NET_ADMIN diff --git a/checks/hostIPC.yaml b/checks/hostIPCSet.yaml similarity index 88% rename from checks/hostIPC.yaml rename to checks/hostIPCSet.yaml index 158931c0..db62a24f 100644 --- a/checks/hostIPC.yaml +++ b/checks/hostIPCSet.yaml @@ -1,5 +1,3 @@ -name: HostIPCSet -id: hostIPCSet successMessage: Host IPC is not configured failureMessage: Host IPC should not be configured category: Security diff --git a/checks/hostNetwork.yaml b/checks/hostNetworkSet.yaml similarity index 87% rename from checks/hostNetwork.yaml rename to checks/hostNetworkSet.yaml index 191c174b..d54419bf 100644 --- a/checks/hostNetwork.yaml +++ b/checks/hostNetworkSet.yaml @@ -1,5 +1,3 @@ -name: HostNetworkSet -id: hostNetworkSet successMessage: Host network is not configured failureMessage: Host network should not be configured category: Networking diff --git a/checks/hostPID.yaml b/checks/hostPIDSet.yaml similarity index 88% rename from checks/hostPID.yaml rename to checks/hostPIDSet.yaml index 6f3ed894..d2b0a65c 100644 --- a/checks/hostPID.yaml +++ b/checks/hostPIDSet.yaml @@ -1,5 +1,3 @@ -name: HostPIDSet -id: hostPIDSet successMessage: Host PID is not configured failureMessage: Host PID should not be configured category: Security diff --git a/checks/hostPortSet.yaml b/checks/hostPortSet.yaml index 2c26cc2a..cd193ea6 100644 --- a/checks/hostPortSet.yaml +++ b/checks/hostPortSet.yaml @@ -1,5 +1,3 @@ -name: HostPortSet -id: hostPortSet successMessage: Host port is not configured failureMessage: Host port should not be configured category: Networking diff --git a/checks/insecureCapabilities.yaml b/checks/insecureCapabilities.yaml new file mode 100644 index 00000000..e5afcfd0 --- /dev/null +++ b/checks/insecureCapabilities.yaml @@ -0,0 +1,31 @@ +successMessage: Container does not have any insecure capabilities +failureMessage: Container should not have insecure capabilities +category: Security +target: Container +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + properties: + securityContext: + type: object + properties: + capabilities: + type: object + properties: + add: + enum: + - CHOWN + - DAC_OVERRIDE + - FSETID + - FOWNER + - MKNOD + - NET_RAW + - SETGID + - SETUID + - SETFCAP + - SETPCAP + - NET_BIND_SERVICE + - SYS_CHROOT + - KILL + - AUDIT_WRITE + diff --git a/checks/livenessProbe.yaml b/checks/livenessProbeMissing.yaml similarity index 88% rename from checks/livenessProbe.yaml rename to checks/livenessProbeMissing.yaml index a60d719f..a7827304 100644 --- a/checks/livenessProbe.yaml +++ b/checks/livenessProbeMissing.yaml @@ -1,5 +1,3 @@ -name: LivenessProbeMissing -id: livenessProbeMissing successMessage: Liveness probe is configured failureMessage: Liveness probe should be configured category: Health Checks diff --git a/checks/memoryLimitsMissing.yaml b/checks/memoryLimitsMissing.yaml new file mode 100644 index 00000000..f3c14524 --- /dev/null +++ b/checks/memoryLimitsMissing.yaml @@ -0,0 +1,27 @@ +successMessage: Memory limits are set +failureMessage: Memory limits should be set +category: Resources +target: Container +containers: + exclude: + - initContainer +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - limits + properties: + limits: + type: object + required: + - memory + properties: + memory: + type: string + not: + const: '' diff --git a/checks/memoryRequestsMissing.yaml b/checks/memoryRequestsMissing.yaml new file mode 100644 index 00000000..a3f91781 --- /dev/null +++ b/checks/memoryRequestsMissing.yaml @@ -0,0 +1,27 @@ +successMessage: Memory requests are set +failureMessage: Memory requests should be set +category: Resources +target: Container +containers: + exclude: + - initContainer +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - requests + properties: + requests: + type: object + required: + - memory + properties: + memory: + type: string + not: + const: '' diff --git a/checks/notReadOnlyRootFileSystem.yaml b/checks/notReadOnlyRootFileSystem.yaml index 509bf2e8..d4e203fe 100644 --- a/checks/notReadOnlyRootFileSystem.yaml +++ b/checks/notReadOnlyRootFileSystem.yaml @@ -1,5 +1,3 @@ -name: NotReadOnlyRootFileSystem -id: notReadOnlyRootFileSystem successMessage: Filesystem is read only failureMessage: Filesystem should be read only category: Security diff --git a/checks/privilegeEscalationAllowed.yaml b/checks/privilegeEscalationAllowed.yaml index 912db92d..6db1f836 100644 --- a/checks/privilegeEscalationAllowed.yaml +++ b/checks/privilegeEscalationAllowed.yaml @@ -1,5 +1,3 @@ -name: PrivilegeEscalationAllowed -id: privilegeEscalationAllowed successMessage: Privilege escalation not allowed failureMessage: Privilege escalation should not be allowed category: Security diff --git a/checks/pullPolicyNotAlways.yaml b/checks/pullPolicyNotAlways.yaml index 5aeac39a..89e591f5 100644 --- a/checks/pullPolicyNotAlways.yaml +++ b/checks/pullPolicyNotAlways.yaml @@ -1,5 +1,3 @@ -name: PullPolicyNotAlways -id: pullPolicyNotAlways successMessage: Image pull policy is "Always" failureMessage: Image pull policy should be "Always" category: Images diff --git a/checks/readinessProbe.yaml b/checks/readinessProbeMissing.yaml similarity index 88% rename from checks/readinessProbe.yaml rename to checks/readinessProbeMissing.yaml index c9d0dba8..1309bab8 100644 --- a/checks/readinessProbe.yaml +++ b/checks/readinessProbeMissing.yaml @@ -1,5 +1,3 @@ -name: ReadinessProbeMissing -id: readinessProbeMissing successMessage: Readiness probe is configured failureMessage: Readiness probe should be configured category: Health Checks diff --git a/checks/runAsPrivileged.yaml b/checks/runAsPrivileged.yaml index 8c9b6ff1..47be7cb7 100644 --- a/checks/runAsPrivileged.yaml +++ b/checks/runAsPrivileged.yaml @@ -1,5 +1,3 @@ -name: RunAsPrivileged -id: runAsPrivileged successMessage: Not running as privileged failureMessage: Should not be running as privileged category: Security diff --git a/checks/runAsRootAllowed.yaml b/checks/runAsRootAllowed.yaml index 468f51e6..3ae2fe49 100644 --- a/checks/runAsRootAllowed.yaml +++ b/checks/runAsRootAllowed.yaml @@ -1,5 +1,3 @@ -name: RunAsRootAllowed -id: runAsRootAllowed successMessage: Is not allowed to run as root failureMessage: Should not be allowed to run as root category: Security diff --git a/checks/tagNotSpecified.yaml b/checks/tagNotSpecified.yaml index 322d806d..102d34c7 100644 --- a/checks/tagNotSpecified.yaml +++ b/checks/tagNotSpecified.yaml @@ -1,5 +1,3 @@ -name: TagNotSpecified -id: tagNotSpecified successMessage: Image tag is specified failureMessage: Image tag should be specified category: Images diff --git a/deploy/dashboard.yaml b/deploy/dashboard.yaml index 2af9813c..ee514972 100644 --- a/deploy/dashboard.yaml +++ b/deploy/dashboard.yaml @@ -14,56 +14,131 @@ metadata: labels: app: polaris data: - config.yaml: | - resources: + config.yaml: |- + checks: + # resources cpuRequestsMissing: warning cpuLimitsMissing: warning memoryRequestsMissing: warning memoryLimitsMissing: warning - images: + # images tagNotSpecified: error - healthChecks: + pullPolicyNotAlways: ignore + # healthChecks readinessProbeMissing: warning livenessProbeMissing: warning - networking: + # networking hostNetworkSet: warning hostPortSet: warning - security: + # security hostIPCSet: error hostPIDSet: error notReadOnlyRootFileSystem: warning privilegeEscalationAllowed: error runAsRootAllowed: warning runAsPrivileged: error - capabilities: - error: - ifAnyAdded: - - SYS_ADMIN - - NET_ADMIN - - ALL - warning: - ifAnyAddedBeyond: - - CHOWN - - DAC_OVERRIDE - - FSETID - - FOWNER - - MKNOD - - NET_RAW - - SETGID - - SETUID - - SETFCAP - - SETPCAP - - NET_BIND_SERVICE - - SYS_CHROOT - - KILL - - AUDIT_WRITE - controllers_to_scan: + dangerousCapabilities: error + insecureCapabilities: warning + controllersToScan: - Deployments - StatefulSets - DaemonSets - - Jobs - CronJobs + - Jobs - ReplicationControllers + exemptions: + - controllerNames: + - dns-controller + - datadog-datadog + - kube-flannel-ds + - kube2iam + - aws-iam-authenticator + - datadog + - kube2iam + rules: + - hostNetworkSet + - controllerNames: + - aws-iam-authenticator + - aws-cluster-autoscaler + - kube-state-metrics + - dns-controller + - external-dns + - dnsmasq + - autoscaler + - kubernetes-dashboard + - install-cni + - kube2iam + rules: + - readinessProbeMissing + - livenessProbeMissing + - controllerNames: + - aws-iam-authenticator + - nginx-ingress-controller + - nginx-ingress-default-backend + - aws-cluster-autoscaler + - kube-state-metrics + - dns-controller + - external-dns + - kubedns + - dnsmasq + - autoscaler + - tiller + - kube2iam + rules: + - runAsRootAllowed + - controllerNames: + - aws-iam-authenticator + - nginx-ingress-controller + - nginx-ingress-default-backend + - aws-cluster-autoscaler + - kube-state-metrics + - dns-controller + - external-dns + - kubedns + - dnsmasq + - autoscaler + - tiller + - kube2iam + rules: + - notReadOnlyRootFileSystem + - controllerNames: + - cert-manager + - dns-controller + - kubedns + - dnsmasq + - autoscaler + - insights-agent-goldilocks-vpa-install + rules: + - cpuRequestsMissing + - cpuLimitsMissing + - memoryRequestsMissing + - memoryLimitsMissing + - controllerNames: + - kube2iam + - kube-flannel-ds + rules: + - runAsPrivileged + - controllerNames: + - kube-hunter + rules: + - hostPIDSet + - controllerNames: + - polaris + - kube-hunter + - goldilocks + - insights-agent-goldilocks-vpa-install + rules: + - notReadOnlyRootFileSystem + - controllerNames: + - insights-agent-goldilocks-controller + rules: + - livenessProbeMissing + - readinessProbeMissing + - controllerNames: + - insights-agent-goldilocks-vpa-install + - kube-hunter + rules: + - runAsRootAllowed --- # Source: polaris/templates/dashboard.rbac.yaml @@ -164,7 +239,7 @@ spec: template: metadata: annotations: - checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2' + checksum/config: 'eb6d6b194c6786d62400fc0578dd5ea5158212b5b29d93d3cde3fa14da8ac501' labels: app: polaris component: dashboard diff --git a/deploy/webhook.yaml b/deploy/webhook.yaml index 8bc52f2d..f288d958 100644 --- a/deploy/webhook.yaml +++ b/deploy/webhook.yaml @@ -27,56 +27,131 @@ metadata: labels: app: polaris data: - config.yaml: | - resources: + config.yaml: |- + checks: + # resources cpuRequestsMissing: warning cpuLimitsMissing: warning memoryRequestsMissing: warning memoryLimitsMissing: warning - images: + # images tagNotSpecified: error - healthChecks: + pullPolicyNotAlways: ignore + # healthChecks readinessProbeMissing: warning livenessProbeMissing: warning - networking: + # networking hostNetworkSet: warning hostPortSet: warning - security: + # security hostIPCSet: error hostPIDSet: error notReadOnlyRootFileSystem: warning privilegeEscalationAllowed: error runAsRootAllowed: warning runAsPrivileged: error - capabilities: - error: - ifAnyAdded: - - SYS_ADMIN - - NET_ADMIN - - ALL - warning: - ifAnyAddedBeyond: - - CHOWN - - DAC_OVERRIDE - - FSETID - - FOWNER - - MKNOD - - NET_RAW - - SETGID - - SETUID - - SETFCAP - - SETPCAP - - NET_BIND_SERVICE - - SYS_CHROOT - - KILL - - AUDIT_WRITE - controllers_to_scan: + dangerousCapabilities: error + insecureCapabilities: warning + controllersToScan: - Deployments - StatefulSets - DaemonSets - - Jobs - CronJobs + - Jobs - ReplicationControllers + exemptions: + - controllerNames: + - dns-controller + - datadog-datadog + - kube-flannel-ds + - kube2iam + - aws-iam-authenticator + - datadog + - kube2iam + rules: + - hostNetworkSet + - controllerNames: + - aws-iam-authenticator + - aws-cluster-autoscaler + - kube-state-metrics + - dns-controller + - external-dns + - dnsmasq + - autoscaler + - kubernetes-dashboard + - install-cni + - kube2iam + rules: + - readinessProbeMissing + - livenessProbeMissing + - controllerNames: + - aws-iam-authenticator + - nginx-ingress-controller + - nginx-ingress-default-backend + - aws-cluster-autoscaler + - kube-state-metrics + - dns-controller + - external-dns + - kubedns + - dnsmasq + - autoscaler + - tiller + - kube2iam + rules: + - runAsRootAllowed + - controllerNames: + - aws-iam-authenticator + - nginx-ingress-controller + - nginx-ingress-default-backend + - aws-cluster-autoscaler + - kube-state-metrics + - dns-controller + - external-dns + - kubedns + - dnsmasq + - autoscaler + - tiller + - kube2iam + rules: + - notReadOnlyRootFileSystem + - controllerNames: + - cert-manager + - dns-controller + - kubedns + - dnsmasq + - autoscaler + - insights-agent-goldilocks-vpa-install + rules: + - cpuRequestsMissing + - cpuLimitsMissing + - memoryRequestsMissing + - memoryLimitsMissing + - controllerNames: + - kube2iam + - kube-flannel-ds + rules: + - runAsPrivileged + - controllerNames: + - kube-hunter + rules: + - hostPIDSet + - controllerNames: + - polaris + - kube-hunter + - goldilocks + - insights-agent-goldilocks-vpa-install + rules: + - notReadOnlyRootFileSystem + - controllerNames: + - insights-agent-goldilocks-controller + rules: + - livenessProbeMissing + - readinessProbeMissing + - controllerNames: + - insights-agent-goldilocks-vpa-install + - kube-hunter + rules: + - runAsRootAllowed --- # Source: polaris/templates/webhook.rbac.yaml @@ -227,7 +302,7 @@ spec: template: metadata: annotations: - checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2' + checksum/config: 'eb6d6b194c6786d62400fc0578dd5ea5158212b5b29d93d3cde3fa14da8ac501' labels: app: polaris component: webhook diff --git a/docs/usage.md b/docs/usage.md index 2109e01f..610f63e0 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -20,6 +20,45 @@ Polaris validation checks fall into several different categories: - [Resources](check-documentation/resources.md) - [Security](check-documentation/security.md) +#### Custom Checks +If you'd like to create your own checks, you can use [JSON Schema](https://json-schema.org/). For example, +to disallow images from quay.io: + +```yaml +checks: + imageRegistry: warning +customChecks: + imageRegistry: + successMessage: Image comes from allowed registries + failureMessage: Image should not be from disallowed registry + category: Images + target: Container # target can be "Container" or "Pod" + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + properties: + image: + type: string + not: + pattern: ^quay.io +``` + +Schemas can also be specified as JSON strings instead of YAML, for easier copy/pasting: +```yaml +customChecks: + foo: + jsonSchema: | + { + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object" + } +``` + +We extend JSON Schema with `resourceMinimum` and `resourceMaximum` fields to help compare memory and CPU resource +strings like `1000m` and `1G`. You can see an example in [the extended config](/examples/config-full.yaml) + +There are additional examples in the [checks folder](/checks). + ### Exemptions Exemptions can be added two ways: by annotating a controller, or editing the Polaris config. diff --git a/examples/config-full.yaml b/examples/config-full.yaml index 011fe15b..fcdd01aa 100644 --- a/examples/config-full.yaml +++ b/examples/config-full.yaml @@ -1,69 +1,78 @@ -resources: +checks: + # resources cpuRequestsMissing: warning - cpuRequestRanges: - warning: - below: 50m - above: 1000m - error: - below: 500m - above: 2000m cpuLimitsMissing: warning - cpuLimitRanges: - warning: - below: 50m - above: 1000m - error: - below: 500m - above: 2000m memoryRequestsMissing: warning - memoryRequestRanges: - warning: - below: 50M - above: 2G - error: - below: 100M - above: 4G memoryLimitsMissing: warning - memoryLimitRanges: - warning: - below: 50M - above: 2G - error: - below: 100M - above: 4G -images: + # images tagNotSpecified: error - pullPolicyNotAlways: warning - whitelist: - error: - - gcr.io/* - blacklist: - warning: - - docker.io/* -healthChecks: + pullPolicyNotAlways: ignore + # healthChecks readinessProbeMissing: warning livenessProbeMissing: warning -networking: - hostNetworkSet: error - hostPortSet: error -security: + # networking + hostNetworkSet: warning + hostPortSet: warning + # security hostIPCSet: error hostPIDSet: error - runAsRootAllowed: warning - runAsPrivileged: error notReadOnlyRootFileSystem: warning privilegeEscalationAllowed: error - capabilities: - error: - ifAnyAdded: - - SYS_ADMIN - - ALL - ifAnyNotDropped: - - ALL - warning: - ifAnyAddedBeyond: - - NONE -controllers_to_scan: + runAsRootAllowed: warning + runAsPrivileged: error + dangerousCapabilities: error + insecureCapabilities: warning + +customChecks: + resourceLimits: + containers: + exclude: + - initContainer + successMessage: Resource limits are within the required range + failureMessage: Resource limits should be within the required range + category: Resources + target: Container + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - limits + properties: + limits: + type: object + required: + - memory + - cpu + properties: + memory: + type: string + resourceMinimum: 100M + resourceMaximum: 6G + cpu: + type: string + resourceMinimum: 100m + resourceMaximum: "2" + imageRegistry: + successMessage: Image comes from allowed registries + failureMessage: Image should not be from disallowed registry + category: Images + target: Container + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + properties: + image: + type: string + allOf: + not: + pattern: ^quay.io + +controllersToScan: - Deployments - StatefulSets - DaemonSets diff --git a/examples/config.yaml b/examples/config.yaml index 1e8ac06e..e468175f 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -1,47 +1,28 @@ -resources: +checks: + # resources cpuRequestsMissing: warning cpuLimitsMissing: warning memoryRequestsMissing: warning memoryLimitsMissing: warning -images: + # images tagNotSpecified: error pullPolicyNotAlways: ignore -healthChecks: + # healthChecks readinessProbeMissing: warning livenessProbeMissing: warning -networking: + # networking hostNetworkSet: warning hostPortSet: warning -security: + # security hostIPCSet: error hostPIDSet: error notReadOnlyRootFileSystem: warning privilegeEscalationAllowed: error runAsRootAllowed: warning runAsPrivileged: error - capabilities: - error: - ifAnyAdded: - - SYS_ADMIN - - NET_ADMIN - - ALL - warning: - ifAnyAddedBeyond: - - CHOWN - - DAC_OVERRIDE - - FSETID - - FOWNER - - MKNOD - - NET_RAW - - SETGID - - SETUID - - SETFCAP - - SETPCAP - - NET_BIND_SERVICE - - SYS_CHROOT - - KILL - - AUDIT_WRITE -controllers_to_scan: + dangerousCapabilities: error + insecureCapabilities: warning +controllersToScan: - Deployments - StatefulSets - DaemonSets diff --git a/pkg/config/config.go b/pkg/config/config.go index 841d8ada..11671700 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -23,22 +23,17 @@ import ( "strings" packr "github.com/gobuffalo/packr/v2" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/yaml" ) // Configuration contains all of the config for the validation checks. type Configuration struct { - DisplayName string `json:"displayName"` - Resources Resources `json:"resources"` - HealthChecks HealthChecks `json:"healthChecks"` - Images Images `json:"images"` - Networking Networking `json:"networking"` - Security Security `json:"security"` - ControllersToScan []SupportedController `json:"controllers_to_scan"` - Exemptions []Exemption `json:"exemptions"` - DisallowExemptions bool `json:"disallowExemptions"` + DisplayName string `json:"displayName"` + Checks map[string]Severity `json:"checks"` + ControllersToScan []SupportedController `json:"controllersToScan"` + CustomChecks map[string]SchemaCheck `json:"customChecks"` + Exemptions []Exemption `json:"exemptions"` + DisallowExemptions bool `json:"disallowExemptions"` } // Exemption represents an exemption to normal rules @@ -47,80 +42,6 @@ type Exemption struct { ControllerNames []string `json:"controllerNames"` } -// Resources contains config for resource requests and limits. -type Resources struct { - CPURequestsMissing Severity `json:"cpuRequestsMissing"` - CPURequestRanges ResourceRanges `json:"cpuRequestRanges"` - CPULimitsMissing Severity `json:"cpuLimitsMissing"` - CPULimitRanges ResourceRanges `json:"cpuLimitRanges"` - MemoryRequestsMissing Severity `json:"memoryRequestsMissing"` - MemoryRequestRanges ResourceRanges `json:"memoryRequestRanges"` - MemoryLimitsMissing Severity `json:"memoryLimitsMissing"` - MemoryLimitRanges ResourceRanges `json:"memoryLimitRanges"` -} - -// ResourceRanges contains config for requests or limits for a specific resource. -type ResourceRanges struct { - Warning ResourceRange `json:"warning"` - Error ResourceRange `json:"error"` -} - -// ResourceRange can contain below and above conditions for validation. -type ResourceRange struct { - Below *resource.Quantity `json:"below"` - Above *resource.Quantity `json:"above"` -} - -// HealthChecks contains config for readiness and liveness probes. -type HealthChecks struct { - ReadinessProbeMissing Severity `json:"readinessProbeMissing"` - LivenessProbeMissing Severity `json:"livenessProbeMissing"` -} - -// Images contains the config for images. -type Images struct { - TagNotSpecified Severity `json:"tagNotSpecified"` - PullPolicyNotAlways Severity `json:"pullPolicyNotAlways"` - Whitelist ErrorWarningLists `json:"whitelist"` - Blacklist ErrorWarningLists `json:"blacklist"` -} - -// ErrorWarningLists provides lists of patterns to match or avoid in image tags. -type ErrorWarningLists struct { - Error []string `json:"error"` - Warning []string `json:"warning"` -} - -// Networking contains the config for networking validations. -type Networking struct { - HostNetworkSet Severity `json:"hostNetworkSet"` - HostPortSet Severity `json:"hostPortSet"` -} - -// Security contains the config for security validations. -type Security struct { - HostIPCSet Severity `json:"hostIPCSet"` - HostPIDSet Severity `json:"hostPIDSet"` - RunAsRootAllowed Severity `json:"runAsRootAllowed"` - RunAsPrivileged Severity `json:"runAsPrivileged"` - NotReadOnlyRootFileSystem Severity `json:"notReadOnlyRootFileSystem"` - PrivilegeEscalationAllowed Severity `json:"privilegeEscalationAllowed"` - Capabilities SecurityCapabilities `json:"capabilities"` -} - -// SecurityCapabilities contains the config for security capabilities validations. -type SecurityCapabilities struct { - Error SecurityCapabilityLists `json:"error"` - Warning SecurityCapabilityLists `json:"warning"` -} - -// SecurityCapabilityLists contains the config for security capabilitie list validations. -type SecurityCapabilityLists struct { - IfAnyAdded []corev1.Capability `json:"ifAnyAdded"` - IfAnyAddedBeyond []corev1.Capability `json:"ifAnyAddedBeyond"` - IfAnyNotDropped []corev1.Capability `json:"ifAnyNotDropped"` -} - // ParseFile parses config from a file. func ParseFile(path string) (Configuration, error) { var rawBytes []byte @@ -153,9 +74,17 @@ func Parse(rawBytes []byte) (Configuration, error) { for { if err := d.Decode(&conf); err != nil { if err == io.EOF { - return conf, nil + break } return conf, fmt.Errorf("Decoding config failed: %v", err) } } + for key, check := range conf.CustomChecks { + err := check.Initialize(key) + if err != nil { + return conf, err + } + conf.CustomChecks[key] = check + } + return conf, nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index ba15b2f1..b13feb75 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -24,111 +24,74 @@ import ( "time" "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/api/resource" ) -var resourceConfInvalid1 = `test` +var confInvalid = `test` -var resourceConfYAML1 = `--- -resources: - cpuRequestRanges: - error: - below: 100m - above: 1 - warning: - below: 200m - above: 800m - memoryRequestRanges: - error: - below: 100M - above: 3G - warning: - below: 200M - above: 2G - cpuLimitRanges: - error: - below: 100m - above: 2 - warning: - below: 300m - above: 1800m - memoryLimitRanges: - error: - below: 200M - above: 6G - warning: - below: 300M - above: 4G -controllers_to_scan: +var confValidYAML = ` +checks: + cpuRequestsMissing: warning +controllersToScan: - Deployments - - StatefulSets - - Jobs - - CronJobs - - DaemonSets - - ReplicationControllers ` -var resourceConfJSON1 = `{ - "resources": { - "cpuRequestRanges": { - "error": { - "below": "100m", - "above": 1 - }, - "warning": { - "below": "200m", - "above": "800m" - } - }, - "memoryRequestRanges": { - "error": { - "below": "100M", - "above": "3G" - }, - "warning": { - "below": "200M", - "above": "2G" - } - }, - "cpuLimitRanges": { - "error": { - "below": "100m", - "above": 2 - }, - "warning": { - "below": "300m", - "above": "1800m" - } - }, - "memoryLimitRanges": { - "error": { - "below": "200M", - "above": "6G" - }, - "warning": { - "below": "300M", - "above": "4G" - } - } - }, - "controllers_to_scan": ["Deployments", "StatefulSets", "Jobs", "CronJobs", "DaemonSets", "ReplicationControllers"] -}` +var confValidJSON = ` +{ + "checks": { + "cpuRequestsMissing": "warning" + }, + "controllersToScan": ["Deployments"] +} +` + +var confCustomChecks = ` +checks: + foo: warning +customChecks: + foo: + successMessage: Security context is set + failureMessage: Security context should be set + category: Security + target: Container + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - securityContext +` + +var confCustomChecksWithJSONSchema = ` +checks: + foo: warning +customChecks: + foo: + successMessage: Security context is set + failureMessage: Security context should be set + category: Security + target: Container + jsonSchema: > + { + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "required": ["securityContext"] + } +` func TestParseError(t *testing.T) { - _, err := Parse([]byte(resourceConfInvalid1)) + _, err := Parse([]byte(confInvalid)) expectedErr := "Decoding config failed: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type config.Configuration" assert.EqualError(t, err, expectedErr) } func TestParseYaml(t *testing.T) { - parsedConf, err := Parse([]byte(resourceConfYAML1)) + parsedConf, err := Parse([]byte(confValidYAML)) assert.NoError(t, err, "Expected no error when parsing YAML config") testParsedConfig(t, &parsedConf) } func TestParseJson(t *testing.T) { - parsedConf, err := Parse([]byte(resourceConfJSON1)) + parsedConf, err := Parse([]byte(confValidJSON)) assert.NoError(t, err, "Expected no error when parsing JSON config") testParsedConfig(t, &parsedConf) @@ -139,7 +102,7 @@ func TestConfigFromURL(t *testing.T) { var parsedConf Configuration srv := &http.Server{Addr: ":8081"} http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - io.WriteString(w, resourceConfYAML1) + io.WriteString(w, confValidYAML) }) go func() { @@ -165,31 +128,37 @@ func TestConfigNoServerError(t *testing.T) { assert.Regexp(t, regexp.MustCompile("connection refused"), err.Error()) } -func testParsedConfig(t *testing.T, config *Configuration) { - cpuRequests := config.Resources.CPURequestRanges - assert.Equal(t, int64(100), cpuRequests.Error.Below.ScaledValue(resource.Milli)) - assert.Equal(t, int64(1000), cpuRequests.Error.Above.ScaledValue(resource.Milli)) - assert.Equal(t, int64(200), cpuRequests.Warning.Below.ScaledValue(resource.Milli)) - assert.Equal(t, int64(800), cpuRequests.Warning.Above.ScaledValue(resource.Milli)) +func TestConfigWithCustomChecks(t *testing.T) { + valid := map[string]interface{}{ + "securityContext": map[string]interface{}{}, + } + invalid := map[string]interface{}{ + "notSecurityContext": map[string]interface{}{}, + } - memRequests := config.Resources.MemoryRequestRanges - assert.Equal(t, int64(100), memRequests.Error.Below.ScaledValue(resource.Mega)) - assert.Equal(t, int64(3000), memRequests.Error.Above.ScaledValue(resource.Mega)) - assert.Equal(t, int64(200), memRequests.Warning.Below.ScaledValue(resource.Mega)) - assert.Equal(t, int64(2000), memRequests.Warning.Above.ScaledValue(resource.Mega)) + parsedConf, err := Parse([]byte(confCustomChecks)) + assert.NoError(t, err, "Expected no error when parsing YAML config") + assert.Equal(t, 1, len(parsedConf.CustomChecks)) + isValid, err := parsedConf.CustomChecks["foo"].CheckObject(valid) + assert.NoError(t, err) + assert.Equal(t, true, isValid) + isValid, err = parsedConf.CustomChecks["foo"].CheckObject(invalid) + assert.NoError(t, err) + assert.Equal(t, false, isValid) - cpuLimits := config.Resources.CPULimitRanges - assert.Equal(t, int64(100), cpuLimits.Error.Below.ScaledValue(resource.Milli)) - assert.Equal(t, int64(2000), cpuLimits.Error.Above.ScaledValue(resource.Milli)) - assert.Equal(t, int64(300), cpuLimits.Warning.Below.ScaledValue(resource.Milli)) - assert.Equal(t, int64(1800), cpuLimits.Warning.Above.ScaledValue(resource.Milli)) - - memLimits := config.Resources.MemoryLimitRanges - assert.Equal(t, int64(200), memLimits.Error.Below.ScaledValue(resource.Mega)) - assert.Equal(t, int64(6000), memLimits.Error.Above.ScaledValue(resource.Mega)) - assert.Equal(t, int64(300), memLimits.Warning.Below.ScaledValue(resource.Mega)) - assert.Equal(t, int64(4000), memLimits.Warning.Above.ScaledValue(resource.Mega)) - - controllersToScan := config.ControllersToScan - assert.ElementsMatch(t, []SupportedController{Deployments, StatefulSets, Jobs, CronJobs, DaemonSets, ReplicationControllers}, controllersToScan) + parsedConf, err = Parse([]byte(confCustomChecksWithJSONSchema)) + assert.NoError(t, err, "Expected no error when parsing YAML config") + assert.Equal(t, 1, len(parsedConf.CustomChecks)) + isValid, err = parsedConf.CustomChecks["foo"].CheckObject(valid) + assert.NoError(t, err) + assert.Equal(t, true, isValid) + isValid, err = parsedConf.CustomChecks["foo"].CheckObject(invalid) + assert.NoError(t, err) + assert.Equal(t, false, isValid) +} + +func testParsedConfig(t *testing.T, config *Configuration) { + assert.Equal(t, SeverityWarning, config.Checks["cpuRequestsMissing"]) + assert.Equal(t, Severity(""), config.Checks["cpuLimitsMissing"]) + assert.ElementsMatch(t, []SupportedController{Deployments}, config.ControllersToScan) } diff --git a/pkg/config/exemptions.go b/pkg/config/exemptions.go index 03f518ca..7b5f55f9 100644 --- a/pkg/config/exemptions.go +++ b/pkg/config/exemptions.go @@ -1,27 +1,14 @@ package config import ( - "reflect" "strings" ) // IsActionable determines whether a check is actionable given the current configuration -func (conf Configuration) IsActionable(subConf interface{}, ruleName, controllerName string) bool { - if subConfStr, ok := subConf.(string); ok { - subConf = conf.GetCategoryConfig(subConfStr) - } - ruleID := GetIDFromField(subConf, ruleName) - subConfRef := reflect.ValueOf(subConf) - fieldVal := reflect.Indirect(subConfRef).FieldByName(ruleName).Interface() - if severity, ok := fieldVal.(Severity); ok && !severity.IsActionable() { +func (conf Configuration) IsActionable(ruleID, controllerName string) bool { + if severity, ok := conf.Checks[ruleID]; !ok || !severity.IsActionable() { return false } - if ranges, ok := fieldVal.(ResourceRanges); ok { - if ranges.Warning.Above == nil && ranges.Warning.Below == nil && - ranges.Error.Above == nil && ranges.Error.Below == nil { - return false - } - } if conf.DisallowExemptions { return true } @@ -39,31 +26,3 @@ func (conf Configuration) IsActionable(subConf interface{}, ruleName, controller } return true } - -// GetCategoryConfig returns the configuration for a particular category name -func (conf Configuration) GetCategoryConfig(category string) interface{} { - if category == "Networking" { - return conf.Networking - } else if category == "Security" { - return conf.Security - } else if category == "Health Checks" { - return conf.HealthChecks - } else if category == "Resources" { - return conf.Resources - } else if category == "Images" { - return conf.Images - } - return nil -} - -// GetSeverity returns the severity configured for a particular check -func (conf Configuration) GetSeverity(category string, name string) Severity { - subConf := conf.GetCategoryConfig(category) - subConfRef := reflect.ValueOf(subConf) - fieldVal := reflect.Indirect(subConfRef).FieldByName(name).Interface() - if severity, ok := fieldVal.(Severity); ok { - return severity - } - // TODO: don't panic - panic("Unknown severity: " + category + "/" + name) -} diff --git a/pkg/config/schema.go b/pkg/config/schema.go new file mode 100644 index 00000000..320a3843 --- /dev/null +++ b/pkg/config/schema.go @@ -0,0 +1,184 @@ +package config + +import ( + "encoding/json" + "fmt" + + "github.com/qri-io/jsonschema" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +// TargetKind represents the part of the config to be validated +type TargetKind string + +const ( + // TargetContainer points to the container spec + TargetContainer TargetKind = "Container" + // TargetPod points to the pod spec + TargetPod TargetKind = "Pod" +) + +// SchemaCheck is a Polaris check that runs using JSON Schema +type SchemaCheck struct { + ID string `yaml:"id"` + Category string `yaml:"category"` + SuccessMessage string `yaml:"successMessage"` + FailureMessage string `yaml:"failureMessage"` + Controllers includeExcludeList `yaml:"controllers"` + Containers includeExcludeList `yaml:"containers"` + Target TargetKind `yaml:"target"` + SchemaTarget TargetKind `yaml:"schemaTarget"` + Schema jsonschema.RootSchema `yaml:"schema"` + JSONSchema string `yaml:"jsonSchema"` +} + +type resourceMinimum string +type resourceMaximum string + +func init() { + jsonschema.RegisterValidator("resourceMinimum", newResourceMinimum) + jsonschema.RegisterValidator("resourceMaximum", newResourceMaximum) +} + +type includeExcludeList struct { + Include []string `yaml:"include"` + Exclude []string `yaml:"exclude"` +} + +func newResourceMinimum() jsonschema.Validator { + return new(resourceMinimum) +} + +func newResourceMaximum() jsonschema.Validator { + return new(resourceMaximum) +} + +// Validate checks that a specified quanitity is not less than the minimum +func (min resourceMinimum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) { + err := validateRange(path, string(min), data, true) + if err != nil { + *errs = append(*errs, *err...) + } +} + +// Validate checks that a specified quanitity is not greater than the maximum +func (max resourceMaximum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) { + err := validateRange(path, string(max), data, false) + if err != nil { + *errs = append(*errs, *err...) + } +} + +func parseQuantity(i interface{}) (resource.Quantity, *[]jsonschema.ValError) { + resStr, ok := i.(string) + if !ok { + return resource.Quantity{}, &[]jsonschema.ValError{ + {Message: fmt.Sprintf("Resource quantity %v is not a string", i)}, + } + } + q, err := resource.ParseQuantity(resStr) + if err != nil { + return resource.Quantity{}, &[]jsonschema.ValError{ + {Message: fmt.Sprintf("Could not parse resource quantity: %s", resStr)}, + } + } + return q, nil +} + +func validateRange(path string, limit interface{}, data interface{}, isMinimum bool) *[]jsonschema.ValError { + limitQuantity, err := parseQuantity(limit) + if err != nil { + return err + } + actualQuantity, err := parseQuantity(data) + if err != nil { + return err + } + cmp := limitQuantity.Cmp(actualQuantity) + if isMinimum { + if cmp == 1 { + return &[]jsonschema.ValError{ + {Message: fmt.Sprintf("%s quantity %v is > %v", path, actualQuantity, limitQuantity)}, + } + } + } else { + if cmp == -1 { + return &[]jsonschema.ValError{ + {Message: fmt.Sprintf("%s quantity %v is < %v", path, actualQuantity, limitQuantity)}, + } + } + } + return nil +} + +// Initialize sets up the schema +func (check *SchemaCheck) Initialize(id string) error { + check.ID = id + if check.JSONSchema != "" { + if err := json.Unmarshal([]byte(check.JSONSchema), &check.Schema); err != nil { + return err + } + } + return nil +} + +// CheckPod checks a pod spec against the schema +func (check SchemaCheck) CheckPod(pod *corev1.PodSpec) (bool, error) { + return check.CheckObject(pod) +} + +// CheckContainer checks a container spec against the schema +func (check SchemaCheck) CheckContainer(container *corev1.Container) (bool, error) { + return check.CheckObject(container) +} + +// CheckObject checks arbitrary data against the schema +func (check SchemaCheck) CheckObject(obj interface{}) (bool, error) { + bytes, err := json.Marshal(obj) + if err != nil { + return false, err + } + errs, err := check.Schema.ValidateBytes(bytes) + return len(errs) == 0, err +} + +// IsActionable decides if this check applies to a particular target +func (check SchemaCheck) IsActionable(target TargetKind, controllerType SupportedController, isInit bool) bool { + if check.Target != target { + return false + } + isIncluded := len(check.Controllers.Include) == 0 + for _, inclusion := range check.Controllers.Include { + if GetSupportedControllerFromString(inclusion) == controllerType { + isIncluded = true + break + } + } + if !isIncluded { + return false + } + for _, exclusion := range check.Controllers.Exclude { + if GetSupportedControllerFromString(exclusion) == controllerType { + return false + } + } + if check.Target == TargetContainer { + isIncluded := len(check.Containers.Include) == 0 + for _, inclusion := range check.Containers.Include { + if (inclusion == "initContainer" && isInit) || (inclusion == "container" && !isInit) { + isIncluded = true + break + } + } + if !isIncluded { + return false + } + for _, exclusion := range check.Containers.Exclude { + if (exclusion == "initContainer" && isInit) || (exclusion == "container" && !isInit) { + return false + } + } + } + return true +} diff --git a/pkg/validator/container.go b/pkg/validator/container.go index 85beade5..0926cf71 100644 --- a/pkg/validator/container.go +++ b/pkg/validator/container.go @@ -15,12 +15,8 @@ package validator import ( - "fmt" - "github.com/fairwindsops/polaris/pkg/config" - "github.com/fairwindsops/polaris/pkg/validator/messages" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" ) // ContainerValidation tracks validation failures associated with a Container. @@ -56,16 +52,12 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, cv.parentPodSpec = parentPodResult.podSpec } - cv.validateResources(conf, controllerName) - err := applyContainerSchemaChecks(conf, controllerName, controllerType, &cv) // FIXME: don't panic if err != nil { panic(err) } - cv.validateSecurity(conf, controllerName) - cRes := ContainerResult{ Name: container.Name, Messages: cv.messages(), @@ -74,226 +66,3 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, return cRes } - -func (cv *ContainerValidation) validateResources(conf *config.Configuration, controllerName string) { - // Only validate resources for primary containers. Although it can - // be helpful to set these in certain cases, it usually isn't - if cv.IsInitContainer { - return - } - - category := messages.CategoryResources - res := cv.Container.Resources - - missingName := "CPURequestsMissing" - rangeName := "CPURequestRanges" - id := config.GetIDFromField(conf.Resources, missingName) - if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Requests.Cpu().MilliValue() == 0 { - cv.addFailure(messages.CPURequestsFailure, conf.Resources.CPURequestsMissing, category, id) - } else if conf.IsActionable(conf.Resources, rangeName, controllerName) { - id := config.GetIDFromField(conf.Resources, rangeName) - cv.validateResourceRange(id, messages.CPURequestsLabel, &conf.Resources.CPURequestRanges, res.Requests.Cpu()) - } else if conf.IsActionable(conf.Resources, missingName, controllerName) { - cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.CPURequestsLabel), category, id) - } - - missingName = "CPULimitsMissing" - rangeName = "CPULimitRanges" - id = config.GetIDFromField(conf.Resources, missingName) - if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Limits.Cpu().MilliValue() == 0 { - cv.addFailure(messages.CPULimitsFailure, conf.Resources.CPULimitsMissing, category, id) - } else if conf.IsActionable(conf.Resources, rangeName, controllerName) { - id := config.GetIDFromField(conf.Resources, rangeName) - cv.validateResourceRange(id, messages.CPULimitsLabel, &conf.Resources.CPULimitRanges, res.Requests.Cpu()) - } else if conf.IsActionable(conf.Resources, missingName, controllerName) { - cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.CPULimitsLabel), category, id) - } - - missingName = "MemoryRequestsMissing" - rangeName = "MemoryRequestRanges" - id = config.GetIDFromField(conf.Resources, missingName) - if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Requests.Memory().MilliValue() == 0 { - cv.addFailure(messages.MemoryRequestsFailure, conf.Resources.MemoryRequestsMissing, category, id) - } else if conf.IsActionable(conf.Resources, rangeName, controllerName) { - id := config.GetIDFromField(conf.Resources, rangeName) - cv.validateResourceRange(id, messages.MemoryRequestsLabel, &conf.Resources.MemoryRequestRanges, res.Requests.Memory()) - } else if conf.IsActionable(conf.Resources, missingName, controllerName) { - cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.MemoryRequestsLabel), category, id) - } - - missingName = "MemoryLimitsMissing" - rangeName = "MemoryLimitRanges" - id = config.GetIDFromField(conf.Resources, missingName) - if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Limits.Memory().MilliValue() == 0 { - cv.addFailure(messages.MemoryLimitsFailure, conf.Resources.MemoryLimitsMissing, category, id) - } else if conf.IsActionable(conf.Resources, rangeName, controllerName) { - id := config.GetIDFromField(conf.Resources, rangeName) - cv.validateResourceRange(id, messages.MemoryLimitsLabel, &conf.Resources.MemoryLimitRanges, res.Limits.Memory()) - } else if conf.IsActionable(conf.Resources, missingName, controllerName) { - cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.MemoryLimitsLabel), category, id) - } -} - -func (cv *ContainerValidation) validateResourceRange(id, resourceName string, rangeConf *config.ResourceRanges, res *resource.Quantity) { - warnAbove := rangeConf.Warning.Above - warnBelow := rangeConf.Warning.Below - errorAbove := rangeConf.Error.Above - errorBelow := rangeConf.Error.Below - category := messages.CategoryResources - - if errorAbove != nil && errorAbove.MilliValue() < res.MilliValue() { - cv.addError(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, errorAbove.String()), category, id) - } else if warnAbove != nil && warnAbove.MilliValue() < res.MilliValue() { - cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, warnAbove.String()), category, id) - } else if errorBelow != nil && errorBelow.MilliValue() > res.MilliValue() { - cv.addError(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, errorBelow.String()), category, id) - } else if warnBelow != nil && warnBelow.MilliValue() > res.MilliValue() { - cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, warnBelow.String()), category, id) - } else if errorAbove != nil && warnAbove != nil && errorBelow != nil && warnBelow != nil { - cv.addSuccess(fmt.Sprintf(messages.ResourceAmountSuccess, resourceName), category, id) - } -} - -func (cv *ContainerValidation) validateSecurity(conf *config.Configuration, controllerName string) { - securityContext := cv.Container.SecurityContext - podSecurityContext := cv.parentPodSpec.SecurityContext - - // Support an empty container security context - if securityContext == nil { - securityContext = &corev1.SecurityContext{} - } - - // Support an empty pod security context - if podSecurityContext == nil { - podSecurityContext = &corev1.PodSecurityContext{} - } - - name := "Capabilities" - if conf.IsActionable(conf.Security, name, controllerName) { - cv.validateCapabilities(&conf.Security.Capabilities.Warning, &conf.Security.Capabilities.Error) - } -} - -func (cv *ContainerValidation) validateCapabilities(warningLists *config.SecurityCapabilityLists, errorLists *config.SecurityCapabilityLists) { - category := messages.CategorySecurity - capabilities := &corev1.Capabilities{} - if cv.Container.SecurityContext != nil && cv.Container.SecurityContext.Capabilities != nil { - capabilities = cv.Container.SecurityContext.Capabilities - } - allLists := []*config.SecurityCapabilityLists{warningLists, errorLists} - - addID := "capabilitiesAdded" - hasAddFailure := false - hasAddCheck := false - for _, confLists := range allLists { - if len(confLists.IfAnyAdded) == 0 && len(confLists.IfAnyAddedBeyond) == 0 { - continue - } - hasAddCheck = true - var severity config.Severity - if confLists == warningLists { - severity = config.SeverityWarning - } else { - severity = config.SeverityError - } - badAdds := make([]corev1.Capability, 0) - if len(confLists.IfAnyAdded) > 0 { - intersectAdds := capIntersection(capabilities.Add, confLists.IfAnyAdded) - badAdds = append(badAdds, intersectAdds...) - } - if len(confLists.IfAnyAddedBeyond) > 0 { - differentAdds := capDifference(capabilities.Add, confLists.IfAnyAddedBeyond) - differentAdds = capDifference(differentAdds, badAdds) - badAdds = append(badAdds, differentAdds...) - } - if capContains(capabilities.Add, "ALL") && !capContains(badAdds, "ALL") { - badAdds = append(badAdds, "ALL") - } - if len(badAdds) > 0 { - hasAddFailure = true - capsString := commaSeparatedCapabilities(badAdds) - cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity, category, addID) - } - } - if hasAddCheck && !hasAddFailure { - cv.addSuccess(messages.SecurityCapabilitiesAddedSuccess, category, addID) - } - - dropID := "capabilitiesDropped" - hasDropCheck := false - hasDropFailure := false - for _, confLists := range allLists { - if len(confLists.IfAnyNotDropped) == 0 { - continue - } - hasDropCheck = true - var severity config.Severity - if confLists == warningLists { - severity = config.SeverityWarning - } else { - severity = config.SeverityError - } - missingDrops := capDifference(confLists.IfAnyNotDropped, capabilities.Drop) - id := "capabilitiesNotDropped" - if len(missingDrops) > 0 && !capContains(capabilities.Drop, "ALL") { - hasDropFailure = true - capsString := commaSeparatedCapabilities(missingDrops) - cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesNotDroppedFailure, capsString), severity, category, id) - } - } - if hasDropCheck && !hasDropFailure { - cv.addSuccess(messages.SecurityCapabilitiesNotDroppedSuccess, category, dropID) - } -} - -func commaSeparatedCapabilities(caps []corev1.Capability) string { - capsString := "" - for _, cap := range caps { - capsString = fmt.Sprintf("%s, %s", capsString, cap) - } - return capsString[2:] -} - -func capIntersection(a, b []corev1.Capability) []corev1.Capability { - result := []corev1.Capability{} - hash := map[corev1.Capability]bool{} - - for _, s := range a { - hash[s] = true - } - - for _, s := range b { - if hash[s] { - result = append(result, s) - } - } - - return result -} - -func capDifference(b, a []corev1.Capability) []corev1.Capability { - result := []corev1.Capability{} - hash := map[corev1.Capability]bool{} - - for _, s := range a { - hash[s] = true - } - - for _, s := range b { - if !hash[s] { - result = append(result, s) - } - } - - return result -} - -func capContains(list []corev1.Capability, val corev1.Capability) bool { - for _, s := range list { - if s == val { - return true - } - } - - return false -} diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index 20ff2c17..66d20865 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -21,43 +21,10 @@ import ( conf "github.com/fairwindsops/polaris/pkg/config" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" ) -var resourceConf1 = `--- -resources: - cpuRequestRanges: - error: - below: 100m - above: 1 - warning: - below: 200m - above: 800m - memoryRequestRanges: - error: - below: 100M - above: 3G - warning: - below: 200M - above: 2G - cpuLimitRanges: - error: - below: 100m - above: 2 - warning: - below: 300m - above: 1800m - memoryLimitRanges: - error: - below: 200M - above: 6G - warning: - below: 300M - above: 4G -` - -var resourceConf2 = `--- -resources: +var resourceConfMinimal = `--- +checks: cpuRequestsMissing: warning memoryRequestsMissing: warning cpuLimitsMissing: error @@ -65,7 +32,7 @@ resources: ` var resourceConfExemptions = `--- -resources: +checks: cpuRequestsMissing: warning memoryRequestsMissing: warning cpuLimitsMissing: error @@ -80,47 +47,7 @@ exemptions: - foo ` -var resourceConfRangeExemptions = `--- -resources: - cpuRequestRanges: - error: - below: 100m - above: 1 - warning: - below: 200m - above: 800m - memoryRequestRanges: - error: - below: 100M - above: 3G - warning: - below: 200M - above: 2G - cpuLimitRanges: - error: - below: 100m - above: 2 - warning: - below: 300m - above: 1800m - memoryLimitRanges: - error: - below: 200M - above: 6G - warning: - below: 300M - above: 4G -exemptions: - - rules: - - cpuRequestRanges - - memoryRequestRanges - - cpuLimitRanges - - memoryLimitRanges - controllerNames: - - foo -` - -func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []*ResultMessage, expectedWarnings []*ResultMessage, expectedSuccesses []*ResultMessage) { +func testValidate(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []*ResultMessage, expectedWarnings []*ResultMessage, expectedSuccesses []*ResultMessage) { cv := ContainerValidation{ Container: container, ResourceValidation: &ResourceValidation{}, @@ -129,7 +56,10 @@ func testValidateResources(t *testing.T, container *corev1.Container, resourceCo parsedConf, err := conf.Parse([]byte(*resourceConf)) assert.NoError(t, err, "Expected no error when parsing config") - cv.validateResources(&parsedConf, controllerName) + err = applyContainerSchemaChecks(&parsedConf, controllerName, conf.Deployments, &cv) + if err != nil { + panic(err) + } assert.Len(t, cv.Warnings, len(expectedWarnings)) assert.ElementsMatch(t, expectedWarnings, cv.Warnings) @@ -151,7 +81,10 @@ func TestValidateResourcesEmptyConfig(t *testing.T) { ResourceValidation: &ResourceValidation{}, } - cv.validateResources(&conf.Configuration{}, "") + err := applyContainerSchemaChecks(&conf.Configuration{}, "", conf.Deployments, &cv) + if err != nil { + panic(err) + } assert.Len(t, cv.Errors, 0) } @@ -192,181 +125,20 @@ func TestValidateResourcesEmptyContainer(t *testing.T) { expectedSuccesses := []*ResultMessage{} - testValidateResources(t, &container, &resourceConf2, "foo", expectedErrors, expectedWarnings, expectedSuccesses) -} - -func TestValidateResourcesPartiallyValid(t *testing.T) { - cpuRequest, err := resource.ParseQuantity("100m") - assert.NoError(t, err, "Error parsing quantity") - - cpuLimit, err := resource.ParseQuantity("200m") - assert.NoError(t, err, "Error parsing quantity") - - container := corev1.Container{ - Name: "Empty", - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - "cpu": cpuRequest, - }, - Limits: corev1.ResourceList{ - "cpu": cpuLimit, - }, - }, - } - - expectedWarnings := []*ResultMessage{ - { - ID: "cpuRequestRanges", - Type: "warning", - Message: "CPU requests should be higher than 200m", - Category: "Resources", - }, - { - ID: "cpuLimitRanges", - Type: "warning", - Message: "CPU limits should be higher than 300m", - Category: "Resources", - }, - } - - expectedErrors := []*ResultMessage{ - { - ID: "memoryRequestRanges", - Type: "error", - Message: "Memory requests should be higher than 100M", - Category: "Resources", - }, - { - ID: "memoryLimitRanges", - Type: "error", - Message: "Memory limits should be higher than 200M", - Category: "Resources", - }, - } - - expectedSuccesses := []*ResultMessage{} - - testValidateResources(t, &container, &resourceConf1, "foo", expectedErrors, expectedWarnings, expectedSuccesses) -} - -func TestValidateResourcesInit(t *testing.T) { - cvEmpty := ContainerValidation{ - Container: &corev1.Container{}, - ResourceValidation: &ResourceValidation{}, - } - cvInit := ContainerValidation{ - Container: &corev1.Container{}, - ResourceValidation: &ResourceValidation{}, - IsInitContainer: true, - } - - parsedConf, err := conf.Parse([]byte(resourceConf1)) - assert.NoError(t, err, "Expected no error when parsing config") - - cvEmpty.validateResources(&parsedConf, "") - assert.Len(t, cvEmpty.Errors, 4) - - cvInit.validateResources(&parsedConf, "") - assert.Len(t, cvInit.Errors, 0) -} - -func TestValidateResourcesFullyValid(t *testing.T) { - cpuRequest, err := resource.ParseQuantity("300m") - assert.NoError(t, err, "Error parsing quantity") - - cpuLimit, err := resource.ParseQuantity("400m") - assert.NoError(t, err, "Error parsing quantity") - - memoryRequest, err := resource.ParseQuantity("400Mi") - assert.NoError(t, err, "Error parsing quantity") - - memoryLimit, err := resource.ParseQuantity("500Mi") - assert.NoError(t, err, "Error parsing quantity") - - container := corev1.Container{ - Name: "Empty", - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - "cpu": cpuRequest, - "memory": memoryRequest, - }, - Limits: corev1.ResourceList{ - "cpu": cpuLimit, - "memory": memoryLimit, - }, - }, - } - - expectedSuccesses := []*ResultMessage{ - { - ID: "cpuRequestRanges", - Type: "success", - Message: "CPU requests are within the expected range", - Category: "Resources", - }, - { - ID: "memoryRequestRanges", - Type: "success", - Message: "Memory requests are within the expected range", - Category: "Resources", - }, - { - ID: "cpuLimitRanges", - Type: "success", - Message: "CPU limits are within the expected range", - Category: "Resources", - }, - { - ID: "memoryLimitRanges", - Type: "success", - Message: "Memory limits are within the expected range", - Category: "Resources", - }, - } - - testValidateResources(t, &container, &resourceConf1, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) - - expectedSuccesses = []*ResultMessage{ - { - ID: "cpuRequestsMissing", - Type: "success", - Message: "CPU requests are set", - Category: "Resources", - }, - { - ID: "memoryRequestsMissing", - Type: "success", - Message: "Memory requests are set", - Category: "Resources", - }, - { - ID: "cpuLimitsMissing", - Type: "success", - Message: "CPU limits are set", - Category: "Resources", - }, - { - ID: "memoryLimitsMissing", - Type: "success", - Message: "Memory limits are set", - Category: "Resources", - }, - } - - testValidateResources(t, &container, &resourceConf2, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) + testValidate(t, &container, &resourceConfMinimal, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } func TestValidateHealthChecks(t *testing.T) { // Test setup. - p1 := conf.HealthChecks{} - p2 := conf.HealthChecks{ - ReadinessProbeMissing: conf.SeverityIgnore, - LivenessProbeMissing: conf.SeverityIgnore, + p1 := make(map[string]conf.Severity) + p2 := map[string]conf.Severity{ + "readinessProbeMissing": conf.SeverityIgnore, + "livenessProbeMissing": conf.SeverityIgnore, } - p3 := conf.HealthChecks{ - ReadinessProbeMissing: conf.SeverityError, - LivenessProbeMissing: conf.SeverityWarning, + p3 := map[string]conf.Severity{ + "readinessProbeMissing": conf.SeverityError, + "livenessProbeMissing": conf.SeverityWarning, } probe := corev1.Probe{} @@ -396,7 +168,7 @@ func TestValidateHealthChecks(t *testing.T) { var testCases = []struct { name string - probes conf.HealthChecks + probes map[string]conf.Severity cv ContainerValidation errors *[]*ResultMessage warnings *[]*ResultMessage @@ -411,7 +183,7 @@ func TestValidateHealthChecks(t *testing.T) { for idx, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - err := applyContainerSchemaChecks(&conf.Configuration{HealthChecks: tt.probes}, "", conf.Deployments, &tt.cv) + err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.probes}, "", conf.Deployments, &tt.cv) if err != nil { panic(err) } @@ -429,14 +201,14 @@ func TestValidateHealthChecks(t *testing.T) { } func TestValidateImage(t *testing.T) { - emptyConf := conf.Images{} - standardConf := conf.Images{ - TagNotSpecified: conf.SeverityError, - PullPolicyNotAlways: conf.SeverityIgnore, + emptyConf := make(map[string]conf.Severity) + standardConf := map[string]conf.Severity{ + "tagNotSpecified": conf.SeverityError, + "pullPolicyNotAlways": conf.SeverityIgnore, } - strongConf := conf.Images{ - TagNotSpecified: conf.SeverityError, - PullPolicyNotAlways: conf.SeverityError, + strongConf := map[string]conf.Severity{ + "tagNotSpecified": conf.SeverityError, + "pullPolicyNotAlways": conf.SeverityError, } emptyCV := ContainerValidation{ @@ -458,7 +230,7 @@ func TestValidateImage(t *testing.T) { var testCases = []struct { name string - image conf.Images + image map[string]conf.Severity cv ContainerValidation expected []*ResultMessage }{ @@ -528,7 +300,7 @@ func TestValidateImage(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { tt.cv = resetCV(tt.cv) - err := applyContainerSchemaChecks(&conf.Configuration{Images: tt.image}, "", conf.Deployments, &tt.cv) + err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.image}, "", conf.Deployments, &tt.cv) if err != nil { panic(err) } @@ -540,12 +312,12 @@ func TestValidateImage(t *testing.T) { func TestValidateNetworking(t *testing.T) { // Test setup. - emptyConf := conf.Networking{} - standardConf := conf.Networking{ - HostPortSet: conf.SeverityWarning, + emptyConf := make(map[string]conf.Severity) + standardConf := map[string]conf.Severity{ + "hostPortSet": conf.SeverityWarning, } - strongConf := conf.Networking{ - HostPortSet: conf.SeverityError, + strongConf := map[string]conf.Severity{ + "hostPortSet": conf.SeverityError, } emptyCV := ContainerValidation{ @@ -574,7 +346,7 @@ func TestValidateNetworking(t *testing.T) { var testCases = []struct { name string - networkConf conf.Networking + networkConf map[string]conf.Severity cv ContainerValidation expectedMessages []*ResultMessage }{ @@ -650,7 +422,7 @@ func TestValidateNetworking(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { tt.cv = resetCV(tt.cv) - err := applyContainerSchemaChecks(&conf.Configuration{Networking: tt.networkConf}, "", conf.Deployments, &tt.cv) + err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.networkConf}, "", conf.Deployments, &tt.cv) if err != nil { panic(err) } @@ -665,35 +437,22 @@ func TestValidateSecurity(t *testing.T) { falseVar := false // Test setup. - emptyConf := conf.Security{} - standardConf := conf.Security{ - RunAsRootAllowed: conf.SeverityWarning, - RunAsPrivileged: conf.SeverityError, - NotReadOnlyRootFileSystem: conf.SeverityWarning, - PrivilegeEscalationAllowed: conf.SeverityError, - Capabilities: conf.SecurityCapabilities{ - Error: conf.SecurityCapabilityLists{ - IfAnyAdded: []corev1.Capability{"ALL", "SYS_ADMIN", "NET_ADMIN"}, - }, - Warning: conf.SecurityCapabilityLists{ - IfAnyAddedBeyond: []corev1.Capability{"NONE"}, - }, - }, + emptyConf := map[string]conf.Severity{} + standardConf := map[string]conf.Severity{ + "runAsRootAllowed": conf.SeverityWarning, + "runAsPrivileged": conf.SeverityError, + "notReadOnlyRootFileSystem": conf.SeverityWarning, + "privilegeEscalationAllowed": conf.SeverityError, + "dangerousCapabilities": conf.SeverityError, + "insecureCapabilities": conf.SeverityWarning, } - strongConf := conf.Security{ - RunAsRootAllowed: conf.SeverityError, - RunAsPrivileged: conf.SeverityError, - NotReadOnlyRootFileSystem: conf.SeverityError, - PrivilegeEscalationAllowed: conf.SeverityError, - Capabilities: conf.SecurityCapabilities{ - Error: conf.SecurityCapabilityLists{ - IfAnyAdded: []corev1.Capability{"ALL", "SYS_ADMIN", "NET_ADMIN"}, - IfAnyNotDropped: []corev1.Capability{"NET_BIND_SERVICE", "DAC_OVERRIDE", "SYS_CHROOT"}, - }, - Warning: conf.SecurityCapabilityLists{ - IfAnyAddedBeyond: []corev1.Capability{"NONE"}, - }, - }, + strongConf := map[string]conf.Severity{ + "runAsRootAllowed": conf.SeverityError, + "runAsPrivileged": conf.SeverityError, + "notReadOnlyRootFileSystem": conf.SeverityError, + "privilegeEscalationAllowed": conf.SeverityError, + "dangerousCapabilities": conf.SeverityError, + "insecureCapabilities": conf.SeverityError, } emptyCV := ContainerValidation{ @@ -708,7 +467,7 @@ func TestValidateSecurity(t *testing.T) { Privileged: &trueVar, AllowPrivilegeEscalation: &trueVar, Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"}, + Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"}, }, }}, ResourceValidation: &ResourceValidation{}, @@ -721,7 +480,7 @@ func TestValidateSecurity(t *testing.T) { Privileged: &trueVar, AllowPrivilegeEscalation: &trueVar, Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"}, + Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"}, }, }}, ResourceValidation: &ResourceValidation{}, @@ -739,7 +498,7 @@ func TestValidateSecurity(t *testing.T) { Privileged: &trueVar, AllowPrivilegeEscalation: &trueVar, Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"}, + Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"}, }, }}, ResourceValidation: &ResourceValidation{}, @@ -814,7 +573,7 @@ func TestValidateSecurity(t *testing.T) { var testCases = []struct { name string - securityConf conf.Security + securityConf map[string]conf.Severity cv ContainerValidation expectedMessages []*ResultMessage }{ @@ -849,8 +608,13 @@ func TestValidateSecurity(t *testing.T) { Type: "success", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "Disallowed security capabilities have not been added", + ID: "insecureCapabilities", + Message: "Container does not have any insecure capabilities", + Type: "success", + Category: "Security", + }, { + ID: "dangerousCapabilities", + Message: "Container does not have any dangerous capabilities", Type: "success", Category: "Security", }}, @@ -860,8 +624,8 @@ func TestValidateSecurity(t *testing.T) { securityConf: standardConf, cv: badCV, expectedMessages: []*ResultMessage{{ - ID: "capabilitiesAdded", - Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN", + ID: "dangerousCapabilities", + Message: "Container should not have dangerous capabilities", Type: "error", Category: "Security", }, { @@ -875,8 +639,8 @@ func TestValidateSecurity(t *testing.T) { Type: "error", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN", + ID: "insecureCapabilities", + Message: "Container should not have insecure capabilities", Type: "warning", Category: "Security", }, { @@ -896,8 +660,8 @@ func TestValidateSecurity(t *testing.T) { securityConf: standardConf, cv: badCVWithGoodPodSpec, expectedMessages: []*ResultMessage{{ - ID: "capabilitiesAdded", - Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN", + ID: "dangerousCapabilities", + Message: "Container should not have dangerous capabilities", Type: "error", Category: "Security", }, { @@ -911,8 +675,8 @@ func TestValidateSecurity(t *testing.T) { Type: "error", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN", + ID: "insecureCapabilities", + Message: "Container should not have insecure capabilities", Type: "warning", Category: "Security", }, { @@ -932,10 +696,15 @@ func TestValidateSecurity(t *testing.T) { securityConf: standardConf, cv: badCVWithBadPodSpec, expectedMessages: []*ResultMessage{{ - ID: "capabilitiesAdded", - Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN", + ID: "dangerousCapabilities", + Message: "Container should not have dangerous capabilities", Type: "error", Category: "Security", + }, { + ID: "insecureCapabilities", + Message: "Container should not have insecure capabilities", + Type: "warning", + Category: "Security", }, { ID: "privilegeEscalationAllowed", Message: "Privilege escalation should not be allowed", @@ -946,11 +715,6 @@ func TestValidateSecurity(t *testing.T) { Message: "Should not be running as privileged", Type: "error", Category: "Security", - }, { - ID: "capabilitiesAdded", - Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN", - Type: "warning", - Category: "Security", }, { ID: "runAsRootAllowed", Message: "Should not be allowed to run as root", @@ -988,8 +752,13 @@ func TestValidateSecurity(t *testing.T) { Type: "success", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "Disallowed security capabilities have not been added", + ID: "dangerousCapabilities", + Message: "Container does not have any dangerous capabilities", + Type: "success", + Category: "Security", + }, { + ID: "insecureCapabilities", + Message: "Container does not have any insecure capabilities", Type: "success", Category: "Security", }}, @@ -999,13 +768,13 @@ func TestValidateSecurity(t *testing.T) { securityConf: strongConf, cv: goodCV, expectedMessages: []*ResultMessage{{ - ID: "capabilitiesNotDropped", - Message: "The following security capabilities should be dropped: DAC_OVERRIDE, SYS_CHROOT", - Type: "error", + ID: "dangerousCapabilities", + Message: "Container does not have any dangerous capabilities", + Type: "success", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "Disallowed security capabilities have not been added", + ID: "insecureCapabilities", + Message: "Container does not have any insecure capabilities", Type: "success", Category: "Security", }, { @@ -1055,13 +824,13 @@ func TestValidateSecurity(t *testing.T) { Type: "success", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "Disallowed security capabilities have not been added", + ID: "dangerousCapabilities", + Message: "Container does not have any dangerous capabilities", Type: "success", Category: "Security", }, { - ID: "capabilitiesDropped", - Message: "All disallowed security capabilities have been dropped", + ID: "insecureCapabilities", + Message: "Container does not have any insecure capabilities", Type: "success", Category: "Security", }}, @@ -1091,13 +860,13 @@ func TestValidateSecurity(t *testing.T) { Type: "success", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "Disallowed security capabilities have not been added", + ID: "dangerousCapabilities", + Message: "Container does not have any dangerous capabilities", Type: "success", Category: "Security", }, { - ID: "capabilitiesDropped", - Message: "All disallowed security capabilities have been dropped", + ID: "insecureCapabilities", + Message: "Container does not have any insecure capabilities", Type: "success", Category: "Security", }}, @@ -1127,13 +896,13 @@ func TestValidateSecurity(t *testing.T) { Type: "success", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "Disallowed security capabilities have not been added", + ID: "dangerousCapabilities", + Message: "Container does not have any dangerous capabilities", Type: "success", Category: "Security", }, { - ID: "capabilitiesDropped", - Message: "All disallowed security capabilities have been dropped", + ID: "insecureCapabilities", + Message: "Container does not have any insecure capabilities", Type: "success", Category: "Security", }}, @@ -1143,13 +912,12 @@ func TestValidateSecurity(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { tt.cv = resetCV(tt.cv) - err := applyContainerSchemaChecks(&conf.Configuration{Security: tt.securityConf}, "", conf.Deployments, &tt.cv) + err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.securityConf}, "", conf.Deployments, &tt.cv) if err != nil { panic(err) } - tt.cv.validateSecurity(&conf.Configuration{Security: tt.securityConf}, "") assert.Len(t, tt.cv.messages(), len(tt.expectedMessages)) - assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages) + assert.ElementsMatch(t, tt.expectedMessages, tt.cv.messages()) }) } } @@ -1160,8 +928,8 @@ func TestValidateRunAsRoot(t *testing.T) { nonRootUser := int64(1000) rootUser := int64(0) config := conf.Configuration{ - Security: conf.Security{ - RunAsRootAllowed: conf.SeverityWarning, + Checks: map[string]conf.Severity{ + "runAsRootAllowed": conf.SeverityWarning, }, } testCases := []struct { @@ -1306,7 +1074,7 @@ func TestValidateResourcesExemption(t *testing.T) { expectedErrors := []*ResultMessage{} expectedSuccesses := []*ResultMessage{} - testValidateResources(t, &container, &resourceConfExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses) + testValidate(t, &container, &resourceConfExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses) expectedWarnings = []*ResultMessage{ { @@ -1340,19 +1108,7 @@ func TestValidateResourcesExemption(t *testing.T) { disallowExemptionsConf := resourceConfExemptions + "\ndisallowExemptions: true" - testValidateResources(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses) -} - -func TestValidateResourceRangeExemption(t *testing.T) { - container := corev1.Container{ - Name: "Empty", - } - - expectedWarnings := []*ResultMessage{} - expectedErrors := []*ResultMessage{} - expectedSuccesses := []*ResultMessage{} - - testValidateResources(t, &container, &resourceConfRangeExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses) + testValidate(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } func resetCV(cv ContainerValidation) ContainerValidation { diff --git a/pkg/validator/controller_test.go b/pkg/validator/controller_test.go index 860984f1..90a67486 100644 --- a/pkg/validator/controller_test.go +++ b/pkg/validator/controller_test.go @@ -29,9 +29,9 @@ import ( func TestValidateController(t *testing.T) { c := conf.Configuration{ - Security: conf.Security{ - HostIPCSet: conf.SeverityError, - HostPIDSet: conf.SeverityError, + Checks: map[string]conf.Severity{ + "hostIPCSet": conf.SeverityError, + "hostPIDSet": conf.SeverityError, }, } deployment := controller.NewDeploymentController(test.MockDeploy()) @@ -64,9 +64,9 @@ func TestValidateController(t *testing.T) { func TestSkipHealthChecks(t *testing.T) { c := conf.Configuration{ - HealthChecks: conf.HealthChecks{ - ReadinessProbeMissing: conf.SeverityError, - LivenessProbeMissing: conf.SeverityWarning, + Checks: map[string]conf.Severity{ + "readinessProbeMissing": conf.SeverityError, + "livenessProbeMissing": conf.SeverityWarning, }, ControllersToScan: []conf.SupportedController{ conf.Deployments, @@ -139,9 +139,9 @@ func TestSkipHealthChecks(t *testing.T) { func TestControllerExemptions(t *testing.T) { c := conf.Configuration{ - HealthChecks: conf.HealthChecks{ - ReadinessProbeMissing: conf.SeverityError, - LivenessProbeMissing: conf.SeverityWarning, + Checks: map[string]conf.Severity{ + "readinessProbeMissing": conf.SeverityError, + "livenessProbeMissing": conf.SeverityWarning, }, ControllersToScan: []conf.SupportedController{ conf.Deployments, diff --git a/pkg/validator/fullaudit_test.go b/pkg/validator/fullaudit_test.go index 863e35e4..af9411c0 100644 --- a/pkg/validator/fullaudit_test.go +++ b/pkg/validator/fullaudit_test.go @@ -16,9 +16,9 @@ func TestGetTemplateData(t *testing.T) { assert.Equal(t, err, nil, "error should be nil") c := conf.Configuration{ - HealthChecks: conf.HealthChecks{ - ReadinessProbeMissing: conf.SeverityError, - LivenessProbeMissing: conf.SeverityWarning, + Checks: map[string]conf.Severity{ + "readinessProbeMissing": conf.SeverityError, + "livenessProbeMissing": conf.SeverityWarning, }, ControllersToScan: []conf.SupportedController{ conf.Deployments, diff --git a/pkg/validator/messages/messages.go b/pkg/validator/messages/messages.go deleted file mode 100644 index 9c1193f8..00000000 --- a/pkg/validator/messages/messages.go +++ /dev/null @@ -1,101 +0,0 @@ -package messages - -const ( - // CategoryHealthChecks category - CategoryHealthChecks = "Health Checks" - // CategorySecurity category - CategorySecurity = "Security" - // CategoryNetworking category - CategoryNetworking = "Networking" - // CategoryResources category - CategoryResources = "Resources" - // CategoryImages category - CategoryImages = "Images" - - // CPURequestsLabel label - CPURequestsLabel = "CPU requests" - // CPULimitsLabel label - CPULimitsLabel = "CPU limits" - // MemoryRequestsLabel label - MemoryRequestsLabel = "Memory requests" - // MemoryLimitsLabel label - MemoryLimitsLabel = "Memory limits" - - // CPURequestsFailure message - CPURequestsFailure = "CPU requests should be set" - // CPULimitsFailure message - CPULimitsFailure = "CPU limits should be set" - // MemoryRequestsFailure message - MemoryRequestsFailure = "Memory requests should be set" - // MemoryLimitsFailure message - MemoryLimitsFailure = "Memory limits should be set" - // ResourceAmountTooHighFailure message - ResourceAmountTooHighFailure = "%s should be lower than %s" - // ResourceAmountTooLowFailure message - ResourceAmountTooLowFailure = "%s should be higher than %s" - // ResourceAmountSuccess message - ResourceAmountSuccess = "%s are within the expected range" - // ResourcePresentSuccess message - ResourcePresentSuccess = "%s are set" - // ReadinessProbeFailure message - ReadinessProbeFailure = "Readiness probe should be configured" - // ReadinessProbeSuccess message - ReadinessProbeSuccess = "Readiness probe configured" - // LivenessProbeFailure message - LivenessProbeFailure = "Liveness probe should be configured" - // LivenessProbeSuccess message - LivenessProbeSuccess = "Liveness probe is configured" - // ImageTagFailure message - ImageTagFailure = "Image tag should be specified" - // ImageTagSuccess message - ImageTagSuccess = "Image tag is specified" - // ImagePullPolicyFailure message - ImagePullPolicyFailure = "Image pull policy should be \"Always\"" - // ImagePullPolicySuccess message - ImagePullPolicySuccess = "Image pull policy is \"Always\"" - // HostPortFailure message - HostPortFailure = "Host port should not be configured" - // HostPortSuccess message - HostPortSuccess = "Host port is not configured" - // RunAsRootFailure message - RunAsRootFailure = "Should not be allowed to run as root" - // RunAsRootSuccess message - RunAsRootSuccess = "Is not allowed to run as root" - // RunAsPrivilegedFailure message - RunAsPrivilegedFailure = "Should not be running as privileged" - // RunAsPrivilegedSuccess message - RunAsPrivilegedSuccess = "Not running as privileged" - // ReadOnlyFilesystemSuccess message - ReadOnlyFilesystemSuccess = "Filesystem is read only" - // ReadOnlyFilesystemFailure message - ReadOnlyFilesystemFailure = "Filesystem should be read only" - // PrivilegeEscalationFailure message - PrivilegeEscalationFailure = "Privilege escalation should not be allowed" - // PrivilegeEscalationSuccess message - PrivilegeEscalationSuccess = "Privilege escalation not allowed" - // SecurityCapabilitiesAddedSuccess message - SecurityCapabilitiesAddedSuccess = "Disallowed security capabilities have not been added" - // SecurityCapabilitiesAddedFailure message - SecurityCapabilitiesAddedFailure = "The following security capabilities should not be added: %v" - // SecurityCapabilitiesNotDroppedSuccess message - SecurityCapabilitiesNotDroppedSuccess = "All disallowed security capabilities have been dropped" - // SecurityCapabilitiesNotDroppedFailure message - SecurityCapabilitiesNotDroppedFailure = "The following security capabilities should be dropped: %v" - - // HostAliasFailure message - HostAliasFailure = "Host alias should not be configured" - // HostAliasSuccess message - HostAliasSuccess = "Host alias is not configured" - // HostIPCFailure message - HostIPCFailure = "Host IPC should not be configured" - // HostIPCSuccess message - HostIPCSuccess = "Host IPC is not configured" - // HostPIDFailure message - HostPIDFailure = "Host PID should not be configured" - // HostPIDSuccess message - HostPIDSuccess = "Host PID is not configured" - // HostNetworkFailure message - HostNetworkFailure = "Host network should not be configured" - // HostNetworkSuccess message - HostNetworkSuccess = "Host network is not configured" -) diff --git a/pkg/validator/pod_test.go b/pkg/validator/pod_test.go index d0552a70..fd9df744 100644 --- a/pkg/validator/pod_test.go +++ b/pkg/validator/pod_test.go @@ -24,13 +24,11 @@ import ( func TestValidatePod(t *testing.T) { c := conf.Configuration{ - Security: conf.Security{ - HostIPCSet: conf.SeverityError, - HostPIDSet: conf.SeverityError, - }, - Networking: conf.Networking{ - HostNetworkSet: conf.SeverityWarning, - HostPortSet: conf.SeverityError, + Checks: map[string]conf.Severity{ + "hostIPCSet": conf.SeverityError, + "hostPIDSet": conf.SeverityError, + "hostNetworkSet": conf.SeverityWarning, + "hostPortSet": conf.SeverityError, }, } @@ -59,8 +57,8 @@ func TestValidatePod(t *testing.T) { expectedMessages := []*ResultMessage{ {ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"}, - {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"}, {ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"}, + {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"}, } actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments) @@ -72,13 +70,11 @@ func TestValidatePod(t *testing.T) { func TestInvalidIPCPod(t *testing.T) { c := conf.Configuration{ - Security: conf.Security{ - HostIPCSet: conf.SeverityError, - HostPIDSet: conf.SeverityError, - }, - Networking: conf.Networking{ - HostNetworkSet: conf.SeverityWarning, - HostPortSet: conf.SeverityError, + Checks: map[string]conf.Severity{ + "hostIPCSet": conf.SeverityError, + "hostPIDSet": conf.SeverityError, + "hostNetworkSet": conf.SeverityWarning, + "hostPortSet": conf.SeverityError, }, } @@ -107,8 +103,8 @@ func TestInvalidIPCPod(t *testing.T) { } expectedMessages := []*ResultMessage{ {ID: "hostIPCSet", Message: "Host IPC should not be configured", Type: "error", Category: "Security"}, - {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"}, {ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"}, + {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"}, } actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments) @@ -120,13 +116,11 @@ func TestInvalidIPCPod(t *testing.T) { func TestInvalidNeworkPod(t *testing.T) { c := conf.Configuration{ - Networking: conf.Networking{ - HostNetworkSet: conf.SeverityWarning, - HostPortSet: conf.SeverityError, - }, - Security: conf.Security{ - HostIPCSet: conf.SeverityError, - HostPIDSet: conf.SeverityError, + Checks: map[string]conf.Severity{ + "hostNetworkSet": conf.SeverityWarning, + "hostPortSet": conf.SeverityError, + "hostIPCSet": conf.SeverityError, + "hostPIDSet": conf.SeverityError, }, } @@ -170,13 +164,11 @@ func TestInvalidNeworkPod(t *testing.T) { func TestInvalidPIDPod(t *testing.T) { c := conf.Configuration{ - Security: conf.Security{ - HostIPCSet: conf.SeverityError, - HostPIDSet: conf.SeverityError, - }, - Networking: conf.Networking{ - HostNetworkSet: conf.SeverityWarning, - HostPortSet: conf.SeverityError, + Checks: map[string]conf.Severity{ + "hostIPCSet": conf.SeverityError, + "hostPIDSet": conf.SeverityError, + "hostNetworkSet": conf.SeverityWarning, + "hostPortSet": conf.SeverityError, }, } @@ -219,13 +211,11 @@ func TestInvalidPIDPod(t *testing.T) { func TestExemption(t *testing.T) { c := conf.Configuration{ - Security: conf.Security{ - HostIPCSet: conf.SeverityError, - HostPIDSet: conf.SeverityError, - }, - Networking: conf.Networking{ - HostNetworkSet: conf.SeverityWarning, - HostPortSet: conf.SeverityError, + Checks: map[string]conf.Severity{ + "hostIPCSet": conf.SeverityError, + "hostNetworkSet": conf.SeverityWarning, + "hostPIDSet": conf.SeverityError, + "hostPortSet": conf.SeverityError, }, Exemptions: []conf.Exemption{ conf.Exemption{ @@ -259,8 +249,8 @@ func TestExemption(t *testing.T) { Errors: uint(0), } expectedMessages := []*ResultMessage{ - {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"}, {ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"}, + {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"}, } actualPodResult := ValidatePod(c, &pod.Spec, "foo", conf.Deployments) diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go index f6762381..2060991d 100644 --- a/pkg/validator/schema.go +++ b/pkg/validator/schema.go @@ -2,60 +2,34 @@ package validator import ( "bytes" - "encoding/json" "fmt" "io" + "sort" packr "github.com/gobuffalo/packr/v2" - "github.com/qri-io/jsonschema" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/yaml" "github.com/fairwindsops/polaris/pkg/config" ) -type includeExcludeList struct { - Include []string `yaml:"include"` - Exclude []string `yaml:"exclude"` -} - -type target string - -const ( - targetContainer target = "Container" - targetPod target = "Pod" -) - -// SchemaCheck is a Polaris check that runs using JSON Schema -type SchemaCheck struct { - Name string `yaml:"name"` - ID string `yaml:"id"` - Category string `yaml:"category"` - SuccessMessage string `yaml:"successMessage"` - FailureMessage string `yaml:"failureMessage"` - Controllers includeExcludeList `yaml:"controllers"` - Containers includeExcludeList `yaml:"containers"` - Target target `yaml:"target"` - SchemaTarget target `yaml:"schemaTarget"` - Schema jsonschema.RootSchema `yaml:"schema"` -} - var ( - schemaBox = (*packr.Box)(nil) - checks = map[target][]SchemaCheck{ - targetContainer: []SchemaCheck{}, - targetPod: []SchemaCheck{}, - } + schemaBox = (*packr.Box)(nil) + builtInChecks = map[string]config.SchemaCheck{} // We explicitly set the order to avoid thrash in the // tests as we migrate toward JSON schema checkOrder = []string{ // Pod checks - "hostIPC", - "hostPID", - "hostNetwork", + "hostIPCSet", + "hostPIDSet", + "hostNetworkSet", // Container checks - "readinessProbe", - "livenessProbe", + "memoryLimitsMissing", + "memoryRequestsMissing", + "cpuLimitsMissing", + "cpuRequestsMissing", + "readinessProbeMissing", + "livenessProbeMissing", "pullPolicyNotAlways", "tagNotSpecified", "hostPortSet", @@ -63,13 +37,15 @@ var ( "runAsPrivileged", "notReadOnlyRootFileSystem", "privilegeEscalationAllowed", + "dangerousCapabilities", + "insecureCapabilities", } ) func init() { schemaBox = packr.New("Schemas", "../../checks") - for _, file := range checkOrder { - contents, err := schemaBox.Find(file + ".yaml") + for _, checkID := range checkOrder { + contents, err := schemaBox.Find(checkID + ".yaml") if err != nil { panic(err) } @@ -77,13 +53,14 @@ func init() { if err != nil { panic(err) } - checks[check.Target] = append(checks[check.Target], check) + check.ID = checkID + builtInChecks[checkID] = check } } -func parseCheck(rawBytes []byte) (SchemaCheck, error) { +func parseCheck(rawBytes []byte) (config.SchemaCheck, error) { reader := bytes.NewReader(rawBytes) - check := SchemaCheck{} + check := config.SchemaCheck{} d := yaml.NewYAMLOrJSONDecoder(reader, 4096) for { if err := d.Decode(&check); err != nil { @@ -95,78 +72,30 @@ func parseCheck(rawBytes []byte) (SchemaCheck, error) { } } -func (check SchemaCheck) checkPod(pod *corev1.PodSpec) (bool, error) { - return check.checkObject(pod) -} - -func (check SchemaCheck) checkContainer(container *corev1.Container) (bool, error) { - return check.checkObject(container) -} - -func (check SchemaCheck) checkObject(obj interface{}) (bool, error) { - bytes, err := json.Marshal(obj) - if err != nil { - return false, err - } - errors, err := check.Schema.ValidateBytes(bytes) - return len(errors) == 0, err -} - -func (check SchemaCheck) isActionable(target target, controllerType config.SupportedController, isInit bool) bool { - if check.Target != target { - return false - } - isIncluded := len(check.Controllers.Include) == 0 - for _, inclusion := range check.Controllers.Include { - if config.GetSupportedControllerFromString(inclusion) == controllerType { - isIncluded = true - break - } - } - if !isIncluded { - return false - } - for _, exclusion := range check.Controllers.Exclude { - if config.GetSupportedControllerFromString(exclusion) == controllerType { - return false - } - } - if check.Target == targetContainer { - isIncluded := len(check.Containers.Include) == 0 - for _, inclusion := range check.Containers.Include { - if (inclusion == "initContainer" && isInit) || (inclusion == "container" && !isInit) { - isIncluded = true - break - } - } - if !isIncluded { - return false - } - for _, exclusion := range check.Containers.Exclude { - if (exclusion == "initContainer" && isInit) || (exclusion == "container" && !isInit) { - return false - } - } - } - return true -} - func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController, pv *PodValidation) error { - for _, check := range checks[targetPod] { - if !conf.IsActionable(check.Category, check.Name, controllerName) { + checkIDs := getSortedKeys(conf.Checks) + for _, checkID := range checkIDs { + check, ok := conf.CustomChecks[checkID] + if !ok { + check, ok = builtInChecks[checkID] + } + if !ok { + return fmt.Errorf("Check %s not found", checkID) + } + if !conf.IsActionable(check.ID, controllerName) { continue } - if !check.isActionable(targetPod, controllerType, false) { + if !check.IsActionable(config.TargetPod, controllerType, false) { continue } - severity := conf.GetSeverity(check.Category, check.Name) - passes, err := check.checkPod(pod) + passes, err := check.CheckPod(pod) if err != nil { return err } if passes { pv.addSuccess(check.SuccessMessage, check.Category, check.ID) } else { + severity := conf.Checks[checkID] pv.addFailure(check.FailureMessage, severity, check.Category, check.ID) } } @@ -174,22 +103,29 @@ func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, contr } func applyContainerSchemaChecks(conf *config.Configuration, controllerName string, controllerType config.SupportedController, cv *ContainerValidation) error { - for _, check := range checks[targetContainer] { - if !conf.IsActionable(check.Category, check.Name, controllerName) { + checkIDs := getSortedKeys(conf.Checks) + for _, checkID := range checkIDs { + check, ok := conf.CustomChecks[checkID] + if !ok { + check, ok = builtInChecks[checkID] + } + if !ok { + return fmt.Errorf("Check %s not found", checkID) + } + if !conf.IsActionable(check.ID, controllerName) { continue } - if !check.isActionable(targetContainer, controllerType, cv.IsInitContainer) { + if !check.IsActionable(config.TargetContainer, controllerType, cv.IsInitContainer) { continue } - severity := conf.GetSeverity(check.Category, check.Name) var passes bool var err error - if check.SchemaTarget == targetPod { + if check.SchemaTarget == config.TargetPod { cv.parentPodSpec.Containers = []corev1.Container{*cv.Container} - passes, err = check.checkPod(&cv.parentPodSpec) + passes, err = check.CheckPod(&cv.parentPodSpec) cv.parentPodSpec.Containers = []corev1.Container{} } else { - passes, err = check.checkContainer(cv.Container) + passes, err = check.CheckContainer(cv.Container) } if err != nil { return err @@ -197,8 +133,18 @@ func applyContainerSchemaChecks(conf *config.Configuration, controllerName strin if passes { cv.addSuccess(check.SuccessMessage, check.Category, check.ID) } else { + severity := conf.Checks[checkID] cv.addFailure(check.FailureMessage, severity, check.Category, check.ID) } } return nil } + +func getSortedKeys(m map[string]config.Severity) []string { + keys := make([]string, 0, len(m)) + for key := range m { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} diff --git a/pkg/validator/schema_test.go b/pkg/validator/schema_test.go new file mode 100644 index 00000000..a6d8737b --- /dev/null +++ b/pkg/validator/schema_test.go @@ -0,0 +1,258 @@ +package validator + +import ( + "testing" + + conf "github.com/fairwindsops/polaris/pkg/config" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +var customCheckExemptions = ` +checks: + foo: error +customChecks: + foo: + successMessage: success! + failureMessage: fail! + target: Container + category: Security + schema: + properties: + image: + pattern: ^quay.io +exemptions: +- controllerNames: + - exempt + rules: + - foo +` + +var resourceConfRanges = ` +checks: + memoryRequestsRange: error + memoryLimitsRange: warning +customChecks: + memoryLimitsRange: + containers: + exclude: + - initContainer + successMessage: Memory limits are within the required range + failureMessage: Memory limits should be within the required range + category: Resources + target: Container + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - limits + properties: + limits: + type: object + required: + - memory + properties: + memory: + type: string + resourceMinimum: 200M + resourceMaximum: 6G + memoryRequestsRange: + successMessage: Memory requests are within the required range + failureMessage: Memory requests should be within the required range + category: Resources + target: Container + containers: + exclude: + - initContainer + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - requests + properties: + requests: + required: + - memory + properties: + memory: + type: string + resourceMinimum: 200M + resourceMaximum: 3G +` + +func TestValidateResourcesPartiallyValid(t *testing.T) { + cpuRequest, err := resource.ParseQuantity("100m") + assert.NoError(t, err, "Error parsing quantity") + + cpuLimit, err := resource.ParseQuantity("200m") + assert.NoError(t, err, "Error parsing quantity") + + container := corev1.Container{ + Name: "Empty", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + "cpu": cpuRequest, + }, + Limits: corev1.ResourceList{ + "cpu": cpuLimit, + }, + }, + } + + expectedWarnings := []*ResultMessage{ + { + ID: "memoryLimitsRange", + Type: "warning", + Message: "Memory limits should be within the required range", + Category: "Resources", + }, + } + + expectedErrors := []*ResultMessage{ + { + ID: "memoryRequestsRange", + Type: "error", + Message: "Memory requests should be within the required range", + Category: "Resources", + }, + } + + expectedSuccesses := []*ResultMessage{} + + testValidate(t, &container, &resourceConfRanges, "foo", expectedErrors, expectedWarnings, expectedSuccesses) +} + +func TestValidateResourcesInit(t *testing.T) { + cvEmpty := ContainerValidation{ + Container: &corev1.Container{}, + ResourceValidation: &ResourceValidation{}, + } + cvInit := ContainerValidation{ + Container: &corev1.Container{}, + ResourceValidation: &ResourceValidation{}, + IsInitContainer: true, + } + + parsedConf, err := conf.Parse([]byte(resourceConfRanges)) + assert.NoError(t, err, "Expected no error when parsing config") + + err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvEmpty) + if err != nil { + panic(err) + } + assert.Len(t, cvEmpty.Errors, 1) + assert.Len(t, cvEmpty.Warnings, 1) + + err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvInit) + if err != nil { + panic(err) + } + assert.Len(t, cvInit.Errors, 0) +} + +func TestValidateResourcesFullyValid(t *testing.T) { + cpuRequest, err := resource.ParseQuantity("300m") + assert.NoError(t, err, "Error parsing quantity") + + cpuLimit, err := resource.ParseQuantity("400m") + assert.NoError(t, err, "Error parsing quantity") + + memoryRequest, err := resource.ParseQuantity("400Mi") + assert.NoError(t, err, "Error parsing quantity") + + memoryLimit, err := resource.ParseQuantity("500Mi") + assert.NoError(t, err, "Error parsing quantity") + + container := corev1.Container{ + Name: "Empty", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + "cpu": cpuRequest, + "memory": memoryRequest, + }, + Limits: corev1.ResourceList{ + "cpu": cpuLimit, + "memory": memoryLimit, + }, + }, + } + + expectedSuccesses := []*ResultMessage{ + { + ID: "memoryRequestsRange", + Type: "success", + Message: "Memory requests are within the required range", + Category: "Resources", + }, + { + ID: "memoryLimitsRange", + Type: "success", + Message: "Memory limits are within the required range", + Category: "Resources", + }, + } + + testValidate(t, &container, &resourceConfRanges, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) + + expectedSuccesses = []*ResultMessage{ + { + ID: "cpuRequestsMissing", + Type: "success", + Message: "CPU requests are set", + Category: "Resources", + }, + { + ID: "memoryRequestsMissing", + Type: "success", + Message: "Memory requests are set", + Category: "Resources", + }, + { + ID: "cpuLimitsMissing", + Type: "success", + Message: "CPU limits are set", + Category: "Resources", + }, + { + ID: "memoryLimitsMissing", + Type: "success", + Message: "Memory limits are set", + Category: "Resources", + }, + } + + testValidate(t, &container, &resourceConfMinimal, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) +} + +func TestValidateCustomCheckExemptions(t *testing.T) { + container := corev1.Container{ + Name: "example", + Image: "hub.docker.com/foo", + } + + expectedWarnings := []*ResultMessage{} + expectedErrors := []*ResultMessage{} + expectedSuccesses := []*ResultMessage{} + testValidate(t, &container, &customCheckExemptions, "exempt", expectedErrors, expectedWarnings, expectedSuccesses) + + expectedErrors = []*ResultMessage{ + { + ID: "foo", + Type: "error", + Message: "fail!", + Category: "Security", + }, + } + testValidate(t, &container, &customCheckExemptions, "notexempt", expectedErrors, expectedWarnings, expectedSuccesses) +} diff --git a/scripts/generate-deployment-files.sh b/scripts/generate-deployment-files.sh new file mode 100755 index 00000000..252f412d --- /dev/null +++ b/scripts/generate-deployment-files.sh @@ -0,0 +1,17 @@ +# /bin/bash + +set -eo pipefail + +helm template $CHARTS_DIR/stable/polaris/ \ + --name polaris --namespace polaris \ + --set templateOnly=true \ + --set config="$(cat ./examples/config.yaml)" \ + > deploy/dashboard.yaml + +helm template $CHARTS_DIR/stable/polaris/ \ + --name polaris --namespace polaris \ + --set templateOnly=true \ + --set webhook.enable=true \ + --set dashboard.enable=false \ + --set config="$(cat ./examples/config.yaml)" \ + > deploy/webhook.yaml diff --git a/test/webhook_test.sh b/test/webhook_test.sh index c5d299fe..9909ad32 100755 --- a/test/webhook_test.sh +++ b/test/webhook_test.sh @@ -70,6 +70,7 @@ for filename in test/webhook_cases/passing_test.*.yaml; do if ! kubectl apply -f $filename &> /dev/null; then ALL_TESTS_PASSED=0 echo "Test Failed: Polaris prevented a deployment with no configuration issues." + kubectl logs -n polaris $(kubectl get po -oname -n polaris | grep webhook) fi done @@ -79,6 +80,7 @@ for filename in test/webhook_cases/failing_test.*.yaml; do if kubectl apply -f $filename &> /dev/null; then ALL_TESTS_PASSED=0 echo "Test Failed: Polaris should have prevented this deployment due to configuration issues." + kubectl logs -n polaris $(kubectl get po -oname -n polaris | grep webhook) fi done