mirror of
https://github.com/FairwindsOps/polaris.git
synced 2026-05-11 03:37:42 +00:00
Merge pull request #235 from FairwindsOps/rb/custom-checks-2
Implement custom checks
This commit is contained in:
@@ -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
|
||||
|
||||
27
checks/cpuLimitsMissing.yaml
Normal file
27
checks/cpuLimitsMissing.yaml
Normal 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: ''
|
||||
27
checks/cpuRequestsMissing.yaml
Normal file
27
checks/cpuRequestsMissing.yaml
Normal 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: ''
|
||||
25
checks/dangerousCapabilities.yaml
Normal file
25
checks/dangerousCapabilities.yaml
Normal 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
|
||||
@@ -1,5 +1,3 @@
|
||||
name: HostIPCSet
|
||||
id: hostIPCSet
|
||||
successMessage: Host IPC is not configured
|
||||
failureMessage: Host IPC should not be configured
|
||||
category: Security
|
||||
@@ -1,5 +1,3 @@
|
||||
name: HostNetworkSet
|
||||
id: hostNetworkSet
|
||||
successMessage: Host network is not configured
|
||||
failureMessage: Host network should not be configured
|
||||
category: Networking
|
||||
@@ -1,5 +1,3 @@
|
||||
name: HostPIDSet
|
||||
id: hostPIDSet
|
||||
successMessage: Host PID is not configured
|
||||
failureMessage: Host PID should not be configured
|
||||
category: Security
|
||||
@@ -1,5 +1,3 @@
|
||||
name: HostPortSet
|
||||
id: hostPortSet
|
||||
successMessage: Host port is not configured
|
||||
failureMessage: Host port should not be configured
|
||||
category: Networking
|
||||
|
||||
31
checks/insecureCapabilities.yaml
Normal file
31
checks/insecureCapabilities.yaml
Normal 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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
name: LivenessProbeMissing
|
||||
id: livenessProbeMissing
|
||||
successMessage: Liveness probe is configured
|
||||
failureMessage: Liveness probe should be configured
|
||||
category: Health Checks
|
||||
27
checks/memoryLimitsMissing.yaml
Normal file
27
checks/memoryLimitsMissing.yaml
Normal 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: ''
|
||||
27
checks/memoryRequestsMissing.yaml
Normal file
27
checks/memoryRequestsMissing.yaml
Normal 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: ''
|
||||
@@ -1,5 +1,3 @@
|
||||
name: NotReadOnlyRootFileSystem
|
||||
id: notReadOnlyRootFileSystem
|
||||
successMessage: Filesystem is read only
|
||||
failureMessage: Filesystem should be read only
|
||||
category: Security
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
name: PrivilegeEscalationAllowed
|
||||
id: privilegeEscalationAllowed
|
||||
successMessage: Privilege escalation not allowed
|
||||
failureMessage: Privilege escalation should not be allowed
|
||||
category: Security
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
name: PullPolicyNotAlways
|
||||
id: pullPolicyNotAlways
|
||||
successMessage: Image pull policy is "Always"
|
||||
failureMessage: Image pull policy should be "Always"
|
||||
category: Images
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
name: ReadinessProbeMissing
|
||||
id: readinessProbeMissing
|
||||
successMessage: Readiness probe is configured
|
||||
failureMessage: Readiness probe should be configured
|
||||
category: Health Checks
|
||||
@@ -1,5 +1,3 @@
|
||||
name: RunAsPrivileged
|
||||
id: runAsPrivileged
|
||||
successMessage: Not running as privileged
|
||||
failureMessage: Should not be running as privileged
|
||||
category: Security
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
name: TagNotSpecified
|
||||
id: tagNotSpecified
|
||||
successMessage: Image tag is specified
|
||||
failureMessage: Image tag should be specified
|
||||
category: Images
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
184
pkg/config/schema.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
258
pkg/validator/schema_test.go
Normal file
258
pkg/validator/schema_test.go
Normal 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)
|
||||
}
|
||||
17
scripts/generate-deployment-files.sh
Executable file
17
scripts/generate-deployment-files.sh
Executable 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
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user