Merge pull request #235 from FairwindsOps/rb/custom-checks-2

Implement custom checks
This commit is contained in:
Robert Brennan
2020-01-09 08:32:31 -05:00
committed by GitHub
38 changed files with 1255 additions and 1259 deletions

View File

@@ -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

View File

@@ -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: ''

View File

@@ -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: ''

View File

@@ -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

View File

@@ -1,5 +1,3 @@
name: HostIPCSet
id: hostIPCSet
successMessage: Host IPC is not configured
failureMessage: Host IPC should not be configured
category: Security

View File

@@ -1,5 +1,3 @@
name: HostNetworkSet
id: hostNetworkSet
successMessage: Host network is not configured
failureMessage: Host network should not be configured
category: Networking

View File

@@ -1,5 +1,3 @@
name: HostPIDSet
id: hostPIDSet
successMessage: Host PID is not configured
failureMessage: Host PID should not be configured
category: Security

View File

@@ -1,5 +1,3 @@
name: HostPortSet
id: hostPortSet
successMessage: Host port is not configured
failureMessage: Host port should not be configured
category: Networking

View File

@@ -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

View File

@@ -1,5 +1,3 @@
name: LivenessProbeMissing
id: livenessProbeMissing
successMessage: Liveness probe is configured
failureMessage: Liveness probe should be configured
category: Health Checks

View File

@@ -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: ''

View File

@@ -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: ''

View File

@@ -1,5 +1,3 @@
name: NotReadOnlyRootFileSystem
id: notReadOnlyRootFileSystem
successMessage: Filesystem is read only
failureMessage: Filesystem should be read only
category: Security

View File

@@ -1,5 +1,3 @@
name: PrivilegeEscalationAllowed
id: privilegeEscalationAllowed
successMessage: Privilege escalation not allowed
failureMessage: Privilege escalation should not be allowed
category: Security

View File

@@ -1,5 +1,3 @@
name: PullPolicyNotAlways
id: pullPolicyNotAlways
successMessage: Image pull policy is "Always"
failureMessage: Image pull policy should be "Always"
category: Images

View File

@@ -1,5 +1,3 @@
name: ReadinessProbeMissing
id: readinessProbeMissing
successMessage: Readiness probe is configured
failureMessage: Readiness probe should be configured
category: Health Checks

View File

@@ -1,5 +1,3 @@
name: RunAsPrivileged
id: runAsPrivileged
successMessage: Not running as privileged
failureMessage: Should not be running as privileged
category: Security

View File

@@ -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

View File

@@ -1,5 +1,3 @@
name: TagNotSpecified
id: tagNotSpecified
successMessage: Image tag is specified
failureMessage: Image tag should be specified
category: Images

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

184
pkg/config/schema.go Normal file
View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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"
)

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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