Repository: FairwindsOps/polaris
Pull request #235 (2020-01-02 20:09:16)

Implement custom checks
This change does a few things:
* refactors the configuration format a bit
* adds `customChecks` to the config, which can be specified using JSON Schema in YAML or JSON
* implements our two configurable checks as JSON schema
  * we now have two `capabilities` checks built in, which implement our old defaults
  * first-class support for resource ranges is now gone, but we provide an example for creating a custom check with resource ranges

This also adds `resourceMinimum` and `resourceMaximum` extensions to JSON schema to support resource ranges.


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c99d6e47..39ff4703 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -94,9 +94,8 @@ The steps are:
     2. Bump the version number in:
         1. main.go
         2. README.md
-    3. Regenerate the deployment files. Assuming you've cloned the charts repo to `./charts`:
-        1. `helm template ./charts/stable/polaris/ --name polaris --namespace polaris --set templateOnly=true > deploy/dashboard.yaml`
-        2. `helm template ./charts/stable/polaris/ --name polaris --namespace polaris --set templateOnly=true --set webhook.enable=true --set dashboard.enable=false > deploy/webhook.yaml`
+    3. Regenerate the deployment files. Assuming you've cloned the charts repo to `~/git/charts`:
+        1. `CHARTS_DIR=~/git/charts ./scripts/generate-deployment-files.sh`
     4. Update CHANGELOG.md
     5. Merge your PR
 3. Tag the latest branch for this repo
diff --git a/checks/cpuLimitsMissing.yaml b/checks/cpuLimitsMissing.yaml
new file mode 100644
index 00000000..ccf1ddfe
--- /dev/null
+++ b/checks/cpuLimitsMissing.yaml
@@ -0,0 +1,27 @@
+successMessage: CPU limits are set
+failureMessage: CPU limits should be set
+category: Resources
+target: Container
+containers:
+  exclude:
+  - initContainer
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  required:
+  - resources
+  properties:
+    resources:
+      type: object
+      required:
+      - limits
+      properties:
+        limits:
+          type: object
+          required:
+          - cpu
+          properties:
+            cpu:
+              type: string
+              not:
+                const: ''
diff --git a/checks/cpuRequestsMissing.yaml b/checks/cpuRequestsMissing.yaml
new file mode 100644
index 00000000..77290d8b
--- /dev/null
+++ b/checks/cpuRequestsMissing.yaml
@@ -0,0 +1,27 @@
+successMessage: CPU requests are set
+failureMessage: CPU requests should be set
+category: Resources
+target: Container
+containers:
+  exclude:
+  - initContainer
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  required:
+  - resources
+  properties:
+    resources:
+      type: object
+      required:
+      - requests
+      properties:
+        requests:
+          type: object
+          required:
+          - cpu
+          properties:
+            cpu:
+              type: string
+              not:
+                const: ''
diff --git a/checks/dangerousCapabilities.yaml b/checks/dangerousCapabilities.yaml
new file mode 100644
index 00000000..d1daa414
--- /dev/null
+++ b/checks/dangerousCapabilities.yaml
@@ -0,0 +1,25 @@
+successMessage: Container does not have any dangerous capabilities
+failureMessage: Container should not have dangerous capabilities
+category: Security
+target: Container
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  properties:
+    securityContext:
+      type: object
+      properties:
+        capabilities:
+          type: object
+          properties:
+            add:
+              type: array
+              not:
+                contains:
+                  const: ALL
+              not:
+                contains:
+                  const: SYS_ADMIN
+              not:
+                contains:
+                  const: NET_ADMIN
diff --git a/checks/hostIPC.yaml b/checks/hostIPCSet.yaml
similarity index 88%
rename from checks/hostIPC.yaml
rename to checks/hostIPCSet.yaml
index 158931c0..db62a24f 100644
--- a/checks/hostIPC.yaml
+++ b/checks/hostIPCSet.yaml
@@ -1,5 +1,3 @@
-name: HostIPCSet
-id: hostIPCSet
 successMessage: Host IPC is not configured
 failureMessage: Host IPC should not be configured
 category: Security
diff --git a/checks/hostNetwork.yaml b/checks/hostNetworkSet.yaml
similarity index 87%
rename from checks/hostNetwork.yaml
rename to checks/hostNetworkSet.yaml
index 191c174b..d54419bf 100644
--- a/checks/hostNetwork.yaml
+++ b/checks/hostNetworkSet.yaml
@@ -1,5 +1,3 @@
-name: HostNetworkSet
-id: hostNetworkSet
 successMessage: Host network is not configured
 failureMessage: Host network should not be configured
 category: Networking
diff --git a/checks/hostPID.yaml b/checks/hostPIDSet.yaml
similarity index 88%
rename from checks/hostPID.yaml
rename to checks/hostPIDSet.yaml
index 6f3ed894..d2b0a65c 100644
--- a/checks/hostPID.yaml
+++ b/checks/hostPIDSet.yaml
@@ -1,5 +1,3 @@
-name: HostPIDSet
-id: hostPIDSet
 successMessage: Host PID is not configured
 failureMessage: Host PID should not be configured
 category: Security
diff --git a/checks/hostPortSet.yaml b/checks/hostPortSet.yaml
index 2c26cc2a..cd193ea6 100644
--- a/checks/hostPortSet.yaml
+++ b/checks/hostPortSet.yaml
@@ -1,5 +1,3 @@
-name: HostPortSet
-id: hostPortSet
 successMessage: Host port is not configured
 failureMessage: Host port should not be configured
 category: Networking
diff --git a/checks/insecureCapabilities.yaml b/checks/insecureCapabilities.yaml
new file mode 100644
index 00000000..e5afcfd0
--- /dev/null
+++ b/checks/insecureCapabilities.yaml
@@ -0,0 +1,31 @@
+successMessage: Container does not have any insecure capabilities
+failureMessage: Container should not have insecure capabilities
+category: Security
+target: Container
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  properties:
+    securityContext:
+      type: object
+      properties:
+        capabilities:
+          type: object
+          properties:
+            add:
+              enum:
+              - CHOWN
+              - DAC_OVERRIDE
+              - FSETID
+              - FOWNER
+              - MKNOD
+              - NET_RAW
+              - SETGID
+              - SETUID
+              - SETFCAP
+              - SETPCAP
+              - NET_BIND_SERVICE
+              - SYS_CHROOT
+              - KILL
+              - AUDIT_WRITE
+
diff --git a/checks/livenessProbe.yaml b/checks/livenessProbeMissing.yaml
similarity index 88%
rename from checks/livenessProbe.yaml
rename to checks/livenessProbeMissing.yaml
index a60d719f..a7827304 100644
--- a/checks/livenessProbe.yaml
+++ b/checks/livenessProbeMissing.yaml
@@ -1,5 +1,3 @@
-name: LivenessProbeMissing
-id: livenessProbeMissing
 successMessage: Liveness probe is configured
 failureMessage: Liveness probe should be configured
 category: Health Checks
diff --git a/checks/memoryLimitsMissing.yaml b/checks/memoryLimitsMissing.yaml
new file mode 100644
index 00000000..f3c14524
--- /dev/null
+++ b/checks/memoryLimitsMissing.yaml
@@ -0,0 +1,27 @@
+successMessage: Memory limits are set
+failureMessage: Memory limits should be set
+category: Resources
+target: Container
+containers:
+  exclude:
+  - initContainer
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  required:
+  - resources
+  properties:
+    resources:
+      type: object
+      required:
+      - limits
+      properties:
+        limits:
+          type: object
+          required:
+          - memory
+          properties:
+            memory:
+              type: string
+              not:
+                const: ''
diff --git a/checks/memoryRequestsMissing.yaml b/checks/memoryRequestsMissing.yaml
new file mode 100644
index 00000000..a3f91781
--- /dev/null
+++ b/checks/memoryRequestsMissing.yaml
@@ -0,0 +1,27 @@
+successMessage: Memory requests are set
+failureMessage: Memory requests should be set
+category: Resources
+target: Container
+containers:
+  exclude:
+  - initContainer
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  required:
+  - resources
+  properties:
+    resources:
+      type: object
+      required:
+      - requests
+      properties:
+        requests:
+          type: object
+          required:
+          - memory
+          properties:
+            memory:
+              type: string
+              not:
+                const: ''
diff --git a/checks/notReadOnlyRootFileSystem.yaml b/checks/notReadOnlyRootFileSystem.yaml
index 509bf2e8..d4e203fe 100644
--- a/checks/notReadOnlyRootFileSystem.yaml
+++ b/checks/notReadOnlyRootFileSystem.yaml
@@ -1,5 +1,3 @@
-name: NotReadOnlyRootFileSystem
-id: notReadOnlyRootFileSystem
 successMessage: Filesystem is read only
 failureMessage: Filesystem should be read only
 category: Security
diff --git a/checks/privilegeEscalationAllowed.yaml b/checks/privilegeEscalationAllowed.yaml
index 912db92d..6db1f836 100644
--- a/checks/privilegeEscalationAllowed.yaml
+++ b/checks/privilegeEscalationAllowed.yaml
@@ -1,5 +1,3 @@
-name: PrivilegeEscalationAllowed
-id: privilegeEscalationAllowed
 successMessage: Privilege escalation not allowed
 failureMessage: Privilege escalation should not be allowed
 category: Security
diff --git a/checks/pullPolicyNotAlways.yaml b/checks/pullPolicyNotAlways.yaml
index 5aeac39a..89e591f5 100644
--- a/checks/pullPolicyNotAlways.yaml
+++ b/checks/pullPolicyNotAlways.yaml
@@ -1,5 +1,3 @@
-name: PullPolicyNotAlways
-id: pullPolicyNotAlways
 successMessage: Image pull policy is "Always"
 failureMessage: Image pull policy should be "Always"
 category: Images
diff --git a/checks/readinessProbe.yaml b/checks/readinessProbeMissing.yaml
similarity index 88%
rename from checks/readinessProbe.yaml
rename to checks/readinessProbeMissing.yaml
index c9d0dba8..1309bab8 100644
--- a/checks/readinessProbe.yaml
+++ b/checks/readinessProbeMissing.yaml
@@ -1,5 +1,3 @@
-name: ReadinessProbeMissing
-id: readinessProbeMissing
 successMessage: Readiness probe is configured
 failureMessage: Readiness probe should be configured
 category: Health Checks
diff --git a/checks/runAsPrivileged.yaml b/checks/runAsPrivileged.yaml
index 8c9b6ff1..47be7cb7 100644
--- a/checks/runAsPrivileged.yaml
+++ b/checks/runAsPrivileged.yaml
@@ -1,5 +1,3 @@
-name: RunAsPrivileged
-id: runAsPrivileged
 successMessage: Not running as privileged
 failureMessage: Should not be running as privileged
 category: Security
diff --git a/checks/runAsRootAllowed.yaml b/checks/runAsRootAllowed.yaml
index 468f51e6..3ae2fe49 100644
--- a/checks/runAsRootAllowed.yaml
+++ b/checks/runAsRootAllowed.yaml
@@ -1,5 +1,3 @@
-name: RunAsRootAllowed
-id: runAsRootAllowed
 successMessage: Is not allowed to run as root
 failureMessage: Should not be allowed to run as root
 category: Security
diff --git a/checks/tagNotSpecified.yaml b/checks/tagNotSpecified.yaml
index 322d806d..102d34c7 100644
--- a/checks/tagNotSpecified.yaml
+++ b/checks/tagNotSpecified.yaml
@@ -1,5 +1,3 @@
-name: TagNotSpecified
-id: tagNotSpecified
 successMessage: Image tag is specified
 failureMessage: Image tag should be specified
 category: Images
diff --git a/deploy/dashboard.yaml b/deploy/dashboard.yaml
index 2af9813c..ee514972 100644
--- a/deploy/dashboard.yaml
+++ b/deploy/dashboard.yaml
@@ -14,56 +14,131 @@ metadata:
   labels:
     app: polaris
 data:
-  config.yaml:  |
-    resources:
+  config.yaml:  |-
+    checks:
+      # resources
       cpuRequestsMissing: warning
       cpuLimitsMissing: warning
       memoryRequestsMissing: warning
       memoryLimitsMissing: warning
-    images:
+      # images
       tagNotSpecified: error
-    healthChecks:
+      pullPolicyNotAlways: ignore
+      # healthChecks
       readinessProbeMissing: warning
       livenessProbeMissing: warning
-    networking:
+      # networking
       hostNetworkSet: warning
       hostPortSet: warning
-    security:
+      # security
       hostIPCSet: error
       hostPIDSet: error
       notReadOnlyRootFileSystem: warning
       privilegeEscalationAllowed: error
       runAsRootAllowed: warning
       runAsPrivileged: error
-      capabilities:
-        error:
-          ifAnyAdded:
-            - SYS_ADMIN
-            - NET_ADMIN
-            - ALL
-        warning:
-          ifAnyAddedBeyond:
-            - CHOWN
-            - DAC_OVERRIDE
-            - FSETID
-            - FOWNER
-            - MKNOD
-            - NET_RAW
-            - SETGID
-            - SETUID
-            - SETFCAP
-            - SETPCAP
-            - NET_BIND_SERVICE
-            - SYS_CHROOT
-            - KILL
-            - AUDIT_WRITE
-    controllers_to_scan:
+      dangerousCapabilities: error
+      insecureCapabilities: warning
+    controllersToScan:
       - Deployments
       - StatefulSets
       - DaemonSets
-      - Jobs
       - CronJobs
+      - Jobs
       - ReplicationControllers
+    exemptions:
+      - controllerNames:
+          - dns-controller
+          - datadog-datadog
+          - kube-flannel-ds
+          - kube2iam
+          - aws-iam-authenticator
+          - datadog
+          - kube2iam
+        rules:
+          - hostNetworkSet
+      - controllerNames:
+          - aws-iam-authenticator
+          - aws-cluster-autoscaler
+          - kube-state-metrics
+          - dns-controller
+          - external-dns
+          - dnsmasq
+          - autoscaler
+          - kubernetes-dashboard
+          - install-cni
+          - kube2iam
+        rules:
+          - readinessProbeMissing
+          - livenessProbeMissing
+      - controllerNames:
+          - aws-iam-authenticator
+          - nginx-ingress-controller
+          - nginx-ingress-default-backend
+          - aws-cluster-autoscaler
+          - kube-state-metrics
+          - dns-controller
+          - external-dns
+          - kubedns
+          - dnsmasq
+          - autoscaler
+          - tiller
+          - kube2iam
+        rules:
+          - runAsRootAllowed
+      - controllerNames:
+          - aws-iam-authenticator
+          - nginx-ingress-controller
+          - nginx-ingress-default-backend
+          - aws-cluster-autoscaler
+          - kube-state-metrics
+          - dns-controller
+          - external-dns
+          - kubedns
+          - dnsmasq
+          - autoscaler
+          - tiller
+          - kube2iam
+        rules:
+          - notReadOnlyRootFileSystem
+      - controllerNames:
+          - cert-manager
+          - dns-controller
+          - kubedns
+          - dnsmasq
+          - autoscaler
+          - insights-agent-goldilocks-vpa-install
+        rules:
+          - cpuRequestsMissing
+          - cpuLimitsMissing
+          - memoryRequestsMissing
+          - memoryLimitsMissing
+      - controllerNames:
+          - kube2iam
+          - kube-flannel-ds
+        rules:
+          - runAsPrivileged
+      - controllerNames:
+          - kube-hunter
+        rules:
+          - hostPIDSet
+      - controllerNames:
+          - polaris
+          - kube-hunter
+          - goldilocks
+          - insights-agent-goldilocks-vpa-install
+        rules:
+          - notReadOnlyRootFileSystem
+      - controllerNames:
+          - insights-agent-goldilocks-controller
+        rules:
+          - livenessProbeMissing
+          - readinessProbeMissing
+      - controllerNames:
+          - insights-agent-goldilocks-vpa-install
+          - kube-hunter
+        rules:
+          - runAsRootAllowed
   
 ---
 # Source: polaris/templates/dashboard.rbac.yaml
@@ -164,7 +239,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2'
+        checksum/config: 'eb6d6b194c6786d62400fc0578dd5ea5158212b5b29d93d3cde3fa14da8ac501'
       labels:
         app: polaris
         component: dashboard
diff --git a/deploy/webhook.yaml b/deploy/webhook.yaml
index 8bc52f2d..f288d958 100644
--- a/deploy/webhook.yaml
+++ b/deploy/webhook.yaml
@@ -27,56 +27,131 @@ metadata:
   labels:
     app: polaris
 data:
-  config.yaml:  |
-    resources:
+  config.yaml:  |-
+    checks:
+      # resources
       cpuRequestsMissing: warning
       cpuLimitsMissing: warning
       memoryRequestsMissing: warning
       memoryLimitsMissing: warning
-    images:
+      # images
       tagNotSpecified: error
-    healthChecks:
+      pullPolicyNotAlways: ignore
+      # healthChecks
       readinessProbeMissing: warning
       livenessProbeMissing: warning
-    networking:
+      # networking
       hostNetworkSet: warning
       hostPortSet: warning
-    security:
+      # security
       hostIPCSet: error
       hostPIDSet: error
       notReadOnlyRootFileSystem: warning
       privilegeEscalationAllowed: error
       runAsRootAllowed: warning
       runAsPrivileged: error
-      capabilities:
-        error:
-          ifAnyAdded:
-            - SYS_ADMIN
-            - NET_ADMIN
-            - ALL
-        warning:
-          ifAnyAddedBeyond:
-            - CHOWN
-            - DAC_OVERRIDE
-            - FSETID
-            - FOWNER
-            - MKNOD
-            - NET_RAW
-            - SETGID
-            - SETUID
-            - SETFCAP
-            - SETPCAP
-            - NET_BIND_SERVICE
-            - SYS_CHROOT
-            - KILL
-            - AUDIT_WRITE
-    controllers_to_scan:
+      dangerousCapabilities: error
+      insecureCapabilities: warning
+    controllersToScan:
       - Deployments
       - StatefulSets
       - DaemonSets
-      - Jobs
       - CronJobs
+      - Jobs
       - ReplicationControllers
+    exemptions:
+      - controllerNames:
+          - dns-controller
+          - datadog-datadog
+          - kube-flannel-ds
+          - kube2iam
+          - aws-iam-authenticator
+          - datadog
+          - kube2iam
+        rules:
+          - hostNetworkSet
+      - controllerNames:
+          - aws-iam-authenticator
+          - aws-cluster-autoscaler
+          - kube-state-metrics
+          - dns-controller
+          - external-dns
+          - dnsmasq
+          - autoscaler
+          - kubernetes-dashboard
+          - install-cni
+          - kube2iam
+        rules:
+          - readinessProbeMissing
+          - livenessProbeMissing
+      - controllerNames:
+          - aws-iam-authenticator
+          - nginx-ingress-controller
+          - nginx-ingress-default-backend
+          - aws-cluster-autoscaler
+          - kube-state-metrics
+          - dns-controller
+          - external-dns
+          - kubedns
+          - dnsmasq
+          - autoscaler
+          - tiller
+          - kube2iam
+        rules:
+          - runAsRootAllowed
+      - controllerNames:
+          - aws-iam-authenticator
+          - nginx-ingress-controller
+          - nginx-ingress-default-backend
+          - aws-cluster-autoscaler
+          - kube-state-metrics
+          - dns-controller
+          - external-dns
+          - kubedns
+          - dnsmasq
+          - autoscaler
+          - tiller
+          - kube2iam
+        rules:
+          - notReadOnlyRootFileSystem
+      - controllerNames:
+          - cert-manager
+          - dns-controller
+          - kubedns
+          - dnsmasq
+          - autoscaler
+          - insights-agent-goldilocks-vpa-install
+        rules:
+          - cpuRequestsMissing
+          - cpuLimitsMissing
+          - memoryRequestsMissing
+          - memoryLimitsMissing
+      - controllerNames:
+          - kube2iam
+          - kube-flannel-ds
+        rules:
+          - runAsPrivileged
+      - controllerNames:
+          - kube-hunter
+        rules:
+          - hostPIDSet
+      - controllerNames:
+          - polaris
+          - kube-hunter
+          - goldilocks
+          - insights-agent-goldilocks-vpa-install
+        rules:
+          - notReadOnlyRootFileSystem
+      - controllerNames:
+          - insights-agent-goldilocks-controller
+        rules:
+          - livenessProbeMissing
+          - readinessProbeMissing
+      - controllerNames:
+          - insights-agent-goldilocks-vpa-install
+          - kube-hunter
+        rules:
+          - runAsRootAllowed
   
 ---
 # Source: polaris/templates/webhook.rbac.yaml
@@ -227,7 +302,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2'
+        checksum/config: 'eb6d6b194c6786d62400fc0578dd5ea5158212b5b29d93d3cde3fa14da8ac501'
       labels:
         app: polaris
         component: webhook
diff --git a/docs/usage.md b/docs/usage.md
index 2109e01f..610f63e0 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -20,6 +20,45 @@ Polaris validation checks fall into several different categories:
 - [Resources](check-documentation/resources.md)
 - [Security](check-documentation/security.md)
 
+#### Custom Checks
+If you'd like to create your own checks, you can use [JSON Schema](https://json-schema.org/). For example,
+to disallow images from quay.io:
+
+```yaml
+checks:
+  imageRegistry: warning
+customChecks:
+  imageRegistry:
+    successMessage: Image comes from allowed registries
+    failureMessage: Image should not be from disallowed registry
+    category: Images
+    target: Container # target can be "Container" or "Pod"
+    schema:
+      '$schema': http://json-schema.org/draft-07/schema
+      type: object
+      properties:
+        image:
+          type: string
+          not:
+            pattern: ^quay.io
+```
+
+Schemas can also be specified as JSON strings instead of YAML, for easier copy/pasting:
+```yaml
+customChecks:
+  foo:
+    jsonSchema: |
+      {
+        "$schema": "http://json-schema.org/draft-07/schema",
+        "type": "object"
+      }
+```
+
+We extend JSON Schema with `resourceMinimum` and `resourceMaximum` fields to help compare memory and CPU resource
+strings like `1000m` and `1G`. You can see an example in [the extended config](/examples/config-full.yaml)
+
+There are additional examples in the [checks folder](/checks).
+
 ### Exemptions
 Exemptions can be added two ways: by annotating a controller, or editing the Polaris config.
 
diff --git a/examples/config-full.yaml b/examples/config-full.yaml
index 011fe15b..fcdd01aa 100644
--- a/examples/config-full.yaml
+++ b/examples/config-full.yaml
@@ -1,69 +1,78 @@
-resources:
+checks:
+  # resources
   cpuRequestsMissing: warning
-  cpuRequestRanges:
-    warning:
-      below: 50m
-      above: 1000m
-    error:
-      below: 500m
-      above: 2000m
   cpuLimitsMissing: warning
-  cpuLimitRanges:
-    warning:
-      below: 50m
-      above: 1000m
-    error:
-      below: 500m
-      above: 2000m
   memoryRequestsMissing: warning
-  memoryRequestRanges:
-    warning:
-      below: 50M
-      above: 2G
-    error:
-      below: 100M
-      above: 4G
   memoryLimitsMissing: warning
-  memoryLimitRanges:
-    warning:
-      below: 50M
-      above: 2G
-    error:
-      below: 100M
-      above: 4G
-images:
+  # images
   tagNotSpecified: error
-  pullPolicyNotAlways: warning
-  whitelist:
-    error:
-      - gcr.io/*
-  blacklist:
-    warning:
-      - docker.io/*
-healthChecks:
+  pullPolicyNotAlways: ignore
+  # healthChecks
   readinessProbeMissing: warning
   livenessProbeMissing: warning
-networking:
-  hostNetworkSet: error
-  hostPortSet: error
-security:
+  # networking
+  hostNetworkSet: warning
+  hostPortSet: warning
+  # security
   hostIPCSet: error
   hostPIDSet: error
-  runAsRootAllowed: warning
-  runAsPrivileged: error
   notReadOnlyRootFileSystem: warning
   privilegeEscalationAllowed: error
-  capabilities:
-    error:
-      ifAnyAdded:
-        - SYS_ADMIN
-        - ALL
-      ifAnyNotDropped:
-        - ALL
-    warning:
-      ifAnyAddedBeyond:
-        - NONE
-controllers_to_scan:
+  runAsRootAllowed: warning
+  runAsPrivileged: error
+  dangerousCapabilities: error
+  insecureCapabilities: warning
+
+customChecks:
+  resourceLimits:
+    containers:
+      exclude:
+      - initContainer
+    successMessage: Resource limits are within the required range
+    failureMessage: Resource limits should be within the required range
+    category: Resources
+    target: Container
+    schema:
+      '$schema': http://json-schema.org/draft-07/schema
+      type: object
+      required:
+      - resources
+      properties:
+        resources:
+          type: object
+          required:
+          - limits
+          properties:
+            limits:
+              type: object
+              required:
+              - memory
+              - cpu
+              properties:
+                memory:
+                  type: string
+                  resourceMinimum: 100M
+                  resourceMaximum: 6G
+                cpu:
+                  type: string
+                  resourceMinimum: 100m
+                  resourceMaximum: "2"
+  imageRegistry:
+    successMessage: Image comes from allowed registries
+    failureMessage: Image should not be from disallowed registry
+    category: Images
+    target: Container
+    schema:
+      '$schema': http://json-schema.org/draft-07/schema
+      type: object
+      properties:
+        image:
+          type: string
+          allOf:
+            not:
+              pattern: ^quay.io
+
+controllersToScan:
   - Deployments
   - StatefulSets
   - DaemonSets
diff --git a/examples/config.yaml b/examples/config.yaml
index 1e8ac06e..e468175f 100644
--- a/examples/config.yaml
+++ b/examples/config.yaml
@@ -1,47 +1,28 @@
-resources:
+checks:
+  # resources
   cpuRequestsMissing: warning
   cpuLimitsMissing: warning
   memoryRequestsMissing: warning
   memoryLimitsMissing: warning
-images:
+  # images
   tagNotSpecified: error
   pullPolicyNotAlways: ignore
-healthChecks:
+  # healthChecks
   readinessProbeMissing: warning
   livenessProbeMissing: warning
-networking:
+  # networking
   hostNetworkSet: warning
   hostPortSet: warning
-security:
+  # security
   hostIPCSet: error
   hostPIDSet: error
   notReadOnlyRootFileSystem: warning
   privilegeEscalationAllowed: error
   runAsRootAllowed: warning
   runAsPrivileged: error
-  capabilities:
-    error:
-      ifAnyAdded:
-        - SYS_ADMIN
-        - NET_ADMIN
-        - ALL
-    warning:
-      ifAnyAddedBeyond:
-        - CHOWN
-        - DAC_OVERRIDE
-        - FSETID
-        - FOWNER
-        - MKNOD
-        - NET_RAW
-        - SETGID
-        - SETUID
-        - SETFCAP
-        - SETPCAP
-        - NET_BIND_SERVICE
-        - SYS_CHROOT
-        - KILL
-        - AUDIT_WRITE
-controllers_to_scan:
+  dangerousCapabilities: error
+  insecureCapabilities: warning
+controllersToScan:
   - Deployments
   - StatefulSets
   - DaemonSets
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 841d8ada..11671700 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -23,22 +23,17 @@ import (
 	"strings"
 
 	packr "github.com/gobuffalo/packr/v2"
-	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/resource"
 	"k8s.io/apimachinery/pkg/util/yaml"
 )
 
 // Configuration contains all of the config for the validation checks.
 type Configuration struct {
-	DisplayName        string                `json:"displayName"`
-	Resources          Resources             `json:"resources"`
-	HealthChecks       HealthChecks          `json:"healthChecks"`
-	Images             Images                `json:"images"`
-	Networking         Networking            `json:"networking"`
-	Security           Security              `json:"security"`
-	ControllersToScan  []SupportedController `json:"controllers_to_scan"`
-	Exemptions         []Exemption           `json:"exemptions"`
-	DisallowExemptions bool                  `json:"disallowExemptions"`
+	DisplayName        string                 `json:"displayName"`
+	Checks             map[string]Severity    `json:"checks"`
+	ControllersToScan  []SupportedController  `json:"controllersToScan"`
+	CustomChecks       map[string]SchemaCheck `json:"customChecks"`
+	Exemptions         []Exemption            `json:"exemptions"`
+	DisallowExemptions bool                   `json:"disallowExemptions"`
 }
 
 // Exemption represents an exemption to normal rules
@@ -47,80 +42,6 @@ type Exemption struct {
 	ControllerNames []string `json:"controllerNames"`
 }
 
-// Resources contains config for resource requests and limits.
-type Resources struct {
-	CPURequestsMissing    Severity       `json:"cpuRequestsMissing"`
-	CPURequestRanges      ResourceRanges `json:"cpuRequestRanges"`
-	CPULimitsMissing      Severity       `json:"cpuLimitsMissing"`
-	CPULimitRanges        ResourceRanges `json:"cpuLimitRanges"`
-	MemoryRequestsMissing Severity       `json:"memoryRequestsMissing"`
-	MemoryRequestRanges   ResourceRanges `json:"memoryRequestRanges"`
-	MemoryLimitsMissing   Severity       `json:"memoryLimitsMissing"`
-	MemoryLimitRanges     ResourceRanges `json:"memoryLimitRanges"`
-}
-
-// ResourceRanges contains config for requests or limits for a specific resource.
-type ResourceRanges struct {
-	Warning ResourceRange `json:"warning"`
-	Error   ResourceRange `json:"error"`
-}
-
-// ResourceRange can contain below and above conditions for validation.
-type ResourceRange struct {
-	Below *resource.Quantity `json:"below"`
-	Above *resource.Quantity `json:"above"`
-}
-
-// HealthChecks contains config for readiness and liveness probes.
-type HealthChecks struct {
-	ReadinessProbeMissing Severity `json:"readinessProbeMissing"`
-	LivenessProbeMissing  Severity `json:"livenessProbeMissing"`
-}
-
-// Images contains the config for images.
-type Images struct {
-	TagNotSpecified     Severity          `json:"tagNotSpecified"`
-	PullPolicyNotAlways Severity          `json:"pullPolicyNotAlways"`
-	Whitelist           ErrorWarningLists `json:"whitelist"`
-	Blacklist           ErrorWarningLists `json:"blacklist"`
-}
-
-// ErrorWarningLists provides lists of patterns to match or avoid in image tags.
-type ErrorWarningLists struct {
-	Error   []string `json:"error"`
-	Warning []string `json:"warning"`
-}
-
-// Networking contains the config for networking validations.
-type Networking struct {
-	HostNetworkSet Severity `json:"hostNetworkSet"`
-	HostPortSet    Severity `json:"hostPortSet"`
-}
-
-// Security contains the config for security validations.
-type Security struct {
-	HostIPCSet                 Severity             `json:"hostIPCSet"`
-	HostPIDSet                 Severity             `json:"hostPIDSet"`
-	RunAsRootAllowed           Severity             `json:"runAsRootAllowed"`
-	RunAsPrivileged            Severity             `json:"runAsPrivileged"`
-	NotReadOnlyRootFileSystem  Severity             `json:"notReadOnlyRootFileSystem"`
-	PrivilegeEscalationAllowed Severity             `json:"privilegeEscalationAllowed"`
-	Capabilities               SecurityCapabilities `json:"capabilities"`
-}
-
-// SecurityCapabilities contains the config for security capabilities validations.
-type SecurityCapabilities struct {
-	Error   SecurityCapabilityLists `json:"error"`
-	Warning SecurityCapabilityLists `json:"warning"`
-}
-
-// SecurityCapabilityLists contains the config for security capabilitie list validations.
-type SecurityCapabilityLists struct {
-	IfAnyAdded       []corev1.Capability `json:"ifAnyAdded"`
-	IfAnyAddedBeyond []corev1.Capability `json:"ifAnyAddedBeyond"`
-	IfAnyNotDropped  []corev1.Capability `json:"ifAnyNotDropped"`
-}
-
 // ParseFile parses config from a file.
 func ParseFile(path string) (Configuration, error) {
 	var rawBytes []byte
@@ -153,9 +74,17 @@ func Parse(rawBytes []byte) (Configuration, error) {
 	for {
 		if err := d.Decode(&conf); err != nil {
 			if err == io.EOF {
-				return conf, nil
+				break
 			}
 			return conf, fmt.Errorf("Decoding config failed: %v", err)
 		}
 	}
+	for key, check := range conf.CustomChecks {
+		err := check.Initialize(key)
+		if err != nil {
+			return conf, err
+		}
+		conf.CustomChecks[key] = check
+	}
+	return conf, nil
 }
diff --git a/pkg/config/exemptions.go b/pkg/config/exemptions.go
index 03f518ca..7b5f55f9 100644
--- a/pkg/config/exemptions.go
+++ b/pkg/config/exemptions.go
@@ -1,27 +1,14 @@
 package config
 
 import (
-	"reflect"
 	"strings"
 )
 
 // IsActionable determines whether a check is actionable given the current configuration
-func (conf Configuration) IsActionable(subConf interface{}, ruleName, controllerName string) bool {
-	if subConfStr, ok := subConf.(string); ok {
-		subConf = conf.GetCategoryConfig(subConfStr)
-	}
-	ruleID := GetIDFromField(subConf, ruleName)
-	subConfRef := reflect.ValueOf(subConf)
-	fieldVal := reflect.Indirect(subConfRef).FieldByName(ruleName).Interface()
-	if severity, ok := fieldVal.(Severity); ok && !severity.IsActionable() {
+func (conf Configuration) IsActionable(ruleID, controllerName string) bool {
+	if severity, ok := conf.Checks[ruleID]; !ok || !severity.IsActionable() {
 		return false
 	}
-	if ranges, ok := fieldVal.(ResourceRanges); ok {
-		if ranges.Warning.Above == nil && ranges.Warning.Below == nil &&
-			ranges.Error.Above == nil && ranges.Error.Below == nil {
-			return false
-		}
-	}
 	if conf.DisallowExemptions {
 		return true
 	}
@@ -39,31 +26,3 @@ func (conf Configuration) IsActionable(subConf interface{}, ruleName, controller
 	}
 	return true
 }
-
-// GetCategoryConfig returns the configuration for a particular category name
-func (conf Configuration) GetCategoryConfig(category string) interface{} {
-	if category == "Networking" {
-		return conf.Networking
-	} else if category == "Security" {
-		return conf.Security
-	} else if category == "Health Checks" {
-		return conf.HealthChecks
-	} else if category == "Resources" {
-		return conf.Resources
-	} else if category == "Images" {
-		return conf.Images
-	}
-	return nil
-}
-
-// GetSeverity returns the severity configured for a particular check
-func (conf Configuration) GetSeverity(category string, name string) Severity {
-	subConf := conf.GetCategoryConfig(category)
-	subConfRef := reflect.ValueOf(subConf)
-	fieldVal := reflect.Indirect(subConfRef).FieldByName(name).Interface()
-	if severity, ok := fieldVal.(Severity); ok {
-		return severity
-	}
-	// TODO: don't panic
-	panic("Unknown severity: " + category + "/" + name)
-}
diff --git a/pkg/config/schema.go b/pkg/config/schema.go
new file mode 100644
index 00000000..320a3843
--- /dev/null
+++ b/pkg/config/schema.go
@@ -0,0 +1,184 @@
+package config
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/qri-io/jsonschema"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+// TargetKind represents the part of the config to be validated
+type TargetKind string
+
+const (
+	// TargetContainer points to the container spec
+	TargetContainer TargetKind = "Container"
+	// TargetPod points to the pod spec
+	TargetPod TargetKind = "Pod"
+)
+
+// SchemaCheck is a Polaris check that runs using JSON Schema
+type SchemaCheck struct {
+	ID             string                `yaml:"id"`
+	Category       string                `yaml:"category"`
+	SuccessMessage string                `yaml:"successMessage"`
+	FailureMessage string                `yaml:"failureMessage"`
+	Controllers    includeExcludeList    `yaml:"controllers"`
+	Containers     includeExcludeList    `yaml:"containers"`
+	Target         TargetKind            `yaml:"target"`
+	SchemaTarget   TargetKind            `yaml:"schemaTarget"`
+	Schema         jsonschema.RootSchema `yaml:"schema"`
+	JSONSchema     string                `yaml:"jsonSchema"`
+}
+
+type resourceMinimum string
+type resourceMaximum string
+
+func init() {
+	jsonschema.RegisterValidator("resourceMinimum", newResourceMinimum)
+	jsonschema.RegisterValidator("resourceMaximum", newResourceMaximum)
+}
+
+type includeExcludeList struct {
+	Include []string `yaml:"include"`
+	Exclude []string `yaml:"exclude"`
+}
+
+func newResourceMinimum() jsonschema.Validator {
+	return new(resourceMinimum)
+}
+
+func newResourceMaximum() jsonschema.Validator {
+	return new(resourceMaximum)
+}
+
+// Validate checks that a specified quanitity is not less than the minimum
+func (min resourceMinimum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) {
+	err := validateRange(path, string(min), data, true)
+	if err != nil {
+		*errs = append(*errs, *err...)
+	}
+}
+
+// Validate checks that a specified quanitity is not greater than the maximum
+func (max resourceMaximum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) {
+	err := validateRange(path, string(max), data, false)
+	if err != nil {
+		*errs = append(*errs, *err...)
+	}
+}
+
+func parseQuantity(i interface{}) (resource.Quantity, *[]jsonschema.ValError) {
+	resStr, ok := i.(string)
+	if !ok {
+		return resource.Quantity{}, &[]jsonschema.ValError{
+			{Message: fmt.Sprintf("Resource quantity %v is not a string", i)},
+		}
+	}
+	q, err := resource.ParseQuantity(resStr)
+	if err != nil {
+		return resource.Quantity{}, &[]jsonschema.ValError{
+			{Message: fmt.Sprintf("Could not parse resource quantity: %s", resStr)},
+		}
+	}
+	return q, nil
+}
+
+func validateRange(path string, limit interface{}, data interface{}, isMinimum bool) *[]jsonschema.ValError {
+	limitQuantity, err := parseQuantity(limit)
+	if err != nil {
+		return err
+	}
+	actualQuantity, err := parseQuantity(data)
+	if err != nil {
+		return err
+	}
+	cmp := limitQuantity.Cmp(actualQuantity)
+	if isMinimum {
+		if cmp == 1 {
+			return &[]jsonschema.ValError{
+				{Message: fmt.Sprintf("%s quantity %v is > %v", path, actualQuantity, limitQuantity)},
+			}
+		}
+	} else {
+		if cmp == -1 {
+			return &[]jsonschema.ValError{
+				{Message: fmt.Sprintf("%s quantity %v is < %v", path, actualQuantity, limitQuantity)},
+			}
+		}
+	}
+	return nil
+}
+
+// Initialize sets up the schema
+func (check *SchemaCheck) Initialize(id string) error {
+	check.ID = id
+	if check.JSONSchema != "" {
+		if err := json.Unmarshal([]byte(check.JSONSchema), &check.Schema); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// CheckPod checks a pod spec against the schema
+func (check SchemaCheck) CheckPod(pod *corev1.PodSpec) (bool, error) {
+	return check.CheckObject(pod)
+}
+
+// CheckContainer checks a container spec against the schema
+func (check SchemaCheck) CheckContainer(container *corev1.Container) (bool, error) {
+	return check.CheckObject(container)
+}
+
+// CheckObject checks arbitrary data against the schema
+func (check SchemaCheck) CheckObject(obj interface{}) (bool, error) {
+	bytes, err := json.Marshal(obj)
+	if err != nil {
+		return false, err
+	}
+	errs, err := check.Schema.ValidateBytes(bytes)
+	return len(errs) == 0, err
+}
+
+// IsActionable decides if this check applies to a particular target
+func (check SchemaCheck) IsActionable(target TargetKind, controllerType SupportedController, isInit bool) bool {
+	if check.Target != target {
+		return false
+	}
+	isIncluded := len(check.Controllers.Include) == 0
+	for _, inclusion := range check.Controllers.Include {
+		if GetSupportedControllerFromString(inclusion) == controllerType {
+			isIncluded = true
+			break
+		}
+	}
+	if !isIncluded {
+		return false
+	}
+	for _, exclusion := range check.Controllers.Exclude {
+		if GetSupportedControllerFromString(exclusion) == controllerType {
+			return false
+		}
+	}
+	if check.Target == TargetContainer {
+		isIncluded := len(check.Containers.Include) == 0
+		for _, inclusion := range check.Containers.Include {
+			if (inclusion == "initContainer" && isInit) || (inclusion == "container" && !isInit) {
+				isIncluded = true
+				break
+			}
+		}
+		if !isIncluded {
+			return false
+		}
+		for _, exclusion := range check.Containers.Exclude {
+			if (exclusion == "initContainer" && isInit) || (exclusion == "container" && !isInit) {
+				return false
+			}
+		}
+	}
+	return true
+}
diff --git a/pkg/validator/container.go b/pkg/validator/container.go
index 85beade5..0926cf71 100644
--- a/pkg/validator/container.go
+++ b/pkg/validator/container.go
@@ -15,12 +15,8 @@
 package validator
 
 import (
-	"fmt"
-
 	"github.com/fairwindsops/polaris/pkg/config"
-	"github.com/fairwindsops/polaris/pkg/validator/messages"
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/resource"
 )
 
 // ContainerValidation tracks validation failures associated with a Container.
@@ -56,16 +52,12 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult,
 		cv.parentPodSpec = parentPodResult.podSpec
 	}
 
-	cv.validateResources(conf, controllerName)
-
 	err := applyContainerSchemaChecks(conf, controllerName, controllerType, &cv)
 	// FIXME: don't panic
 	if err != nil {
 		panic(err)
 	}
 
-	cv.validateSecurity(conf, controllerName)
-
 	cRes := ContainerResult{
 		Name:     container.Name,
 		Messages: cv.messages(),
@@ -74,226 +66,3 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult,
 
 	return cRes
 }
-
-func (cv *ContainerValidation) validateResources(conf *config.Configuration, controllerName string) {
-	// Only validate resources for primary containers. Although it can
-	// be helpful to set these in certain cases, it usually isn't
-	if cv.IsInitContainer {
-		return
-	}
-
-	category := messages.CategoryResources
-	res := cv.Container.Resources
-
-	missingName := "CPURequestsMissing"
-	rangeName := "CPURequestRanges"
-	id := config.GetIDFromField(conf.Resources, missingName)
-	if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Requests.Cpu().MilliValue() == 0 {
-		cv.addFailure(messages.CPURequestsFailure, conf.Resources.CPURequestsMissing, category, id)
-	} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
-		id := config.GetIDFromField(conf.Resources, rangeName)
-		cv.validateResourceRange(id, messages.CPURequestsLabel, &conf.Resources.CPURequestRanges, res.Requests.Cpu())
-	} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
-		cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.CPURequestsLabel), category, id)
-	}
-
-	missingName = "CPULimitsMissing"
-	rangeName = "CPULimitRanges"
-	id = config.GetIDFromField(conf.Resources, missingName)
-	if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Limits.Cpu().MilliValue() == 0 {
-		cv.addFailure(messages.CPULimitsFailure, conf.Resources.CPULimitsMissing, category, id)
-	} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
-		id := config.GetIDFromField(conf.Resources, rangeName)
-		cv.validateResourceRange(id, messages.CPULimitsLabel, &conf.Resources.CPULimitRanges, res.Requests.Cpu())
-	} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
-		cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.CPULimitsLabel), category, id)
-	}
-
-	missingName = "MemoryRequestsMissing"
-	rangeName = "MemoryRequestRanges"
-	id = config.GetIDFromField(conf.Resources, missingName)
-	if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Requests.Memory().MilliValue() == 0 {
-		cv.addFailure(messages.MemoryRequestsFailure, conf.Resources.MemoryRequestsMissing, category, id)
-	} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
-		id := config.GetIDFromField(conf.Resources, rangeName)
-		cv.validateResourceRange(id, messages.MemoryRequestsLabel, &conf.Resources.MemoryRequestRanges, res.Requests.Memory())
-	} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
-		cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.MemoryRequestsLabel), category, id)
-	}
-
-	missingName = "MemoryLimitsMissing"
-	rangeName = "MemoryLimitRanges"
-	id = config.GetIDFromField(conf.Resources, missingName)
-	if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Limits.Memory().MilliValue() == 0 {
-		cv.addFailure(messages.MemoryLimitsFailure, conf.Resources.MemoryLimitsMissing, category, id)
-	} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
-		id := config.GetIDFromField(conf.Resources, rangeName)
-		cv.validateResourceRange(id, messages.MemoryLimitsLabel, &conf.Resources.MemoryLimitRanges, res.Limits.Memory())
-	} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
-		cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.MemoryLimitsLabel), category, id)
-	}
-}
-
-func (cv *ContainerValidation) validateResourceRange(id, resourceName string, rangeConf *config.ResourceRanges, res *resource.Quantity) {
-	warnAbove := rangeConf.Warning.Above
-	warnBelow := rangeConf.Warning.Below
-	errorAbove := rangeConf.Error.Above
-	errorBelow := rangeConf.Error.Below
-	category := messages.CategoryResources
-
-	if errorAbove != nil && errorAbove.MilliValue() < res.MilliValue() {
-		cv.addError(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, errorAbove.String()), category, id)
-	} else if warnAbove != nil && warnAbove.MilliValue() < res.MilliValue() {
-		cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, warnAbove.String()), category, id)
-	} else if errorBelow != nil && errorBelow.MilliValue() > res.MilliValue() {
-		cv.addError(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, errorBelow.String()), category, id)
-	} else if warnBelow != nil && warnBelow.MilliValue() > res.MilliValue() {
-		cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, warnBelow.String()), category, id)
-	} else if errorAbove != nil && warnAbove != nil && errorBelow != nil && warnBelow != nil {
-		cv.addSuccess(fmt.Sprintf(messages.ResourceAmountSuccess, resourceName), category, id)
-	}
-}
-
-func (cv *ContainerValidation) validateSecurity(conf *config.Configuration, controllerName string) {
-	securityContext := cv.Container.SecurityContext
-	podSecurityContext := cv.parentPodSpec.SecurityContext
-
-	// Support an empty container security context
-	if securityContext == nil {
-		securityContext = &corev1.SecurityContext{}
-	}
-
-	// Support an empty pod security context
-	if podSecurityContext == nil {
-		podSecurityContext = &corev1.PodSecurityContext{}
-	}
-
-	name := "Capabilities"
-	if conf.IsActionable(conf.Security, name, controllerName) {
-		cv.validateCapabilities(&conf.Security.Capabilities.Warning, &conf.Security.Capabilities.Error)
-	}
-}
-
-func (cv *ContainerValidation) validateCapabilities(warningLists *config.SecurityCapabilityLists, errorLists *config.SecurityCapabilityLists) {
-	category := messages.CategorySecurity
-	capabilities := &corev1.Capabilities{}
-	if cv.Container.SecurityContext != nil && cv.Container.SecurityContext.Capabilities != nil {
-		capabilities = cv.Container.SecurityContext.Capabilities
-	}
-	allLists := []*config.SecurityCapabilityLists{warningLists, errorLists}
-
-	addID := "capabilitiesAdded"
-	hasAddFailure := false
-	hasAddCheck := false
-	for _, confLists := range allLists {
-		if len(confLists.IfAnyAdded) == 0 && len(confLists.IfAnyAddedBeyond) == 0 {
-			continue
-		}
-		hasAddCheck = true
-		var severity config.Severity
-		if confLists == warningLists {
-			severity = config.SeverityWarning
-		} else {
-			severity = config.SeverityError
-		}
-		badAdds := make([]corev1.Capability, 0)
-		if len(confLists.IfAnyAdded) > 0 {
-			intersectAdds := capIntersection(capabilities.Add, confLists.IfAnyAdded)
-			badAdds = append(badAdds, intersectAdds...)
-		}
-		if len(confLists.IfAnyAddedBeyond) > 0 {
-			differentAdds := capDifference(capabilities.Add, confLists.IfAnyAddedBeyond)
-			differentAdds = capDifference(differentAdds, badAdds)
-			badAdds = append(badAdds, differentAdds...)
-		}
-		if capContains(capabilities.Add, "ALL") && !capContains(badAdds, "ALL") {
-			badAdds = append(badAdds, "ALL")
-		}
-		if len(badAdds) > 0 {
-			hasAddFailure = true
-			capsString := commaSeparatedCapabilities(badAdds)
-			cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity, category, addID)
-		}
-	}
-	if hasAddCheck && !hasAddFailure {
-		cv.addSuccess(messages.SecurityCapabilitiesAddedSuccess, category, addID)
-	}
-
-	dropID := "capabilitiesDropped"
-	hasDropCheck := false
-	hasDropFailure := false
-	for _, confLists := range allLists {
-		if len(confLists.IfAnyNotDropped) == 0 {
-			continue
-		}
-		hasDropCheck = true
-		var severity config.Severity
-		if confLists == warningLists {
-			severity = config.SeverityWarning
-		} else {
-			severity = config.SeverityError
-		}
-		missingDrops := capDifference(confLists.IfAnyNotDropped, capabilities.Drop)
-		id := "capabilitiesNotDropped"
-		if len(missingDrops) > 0 && !capContains(capabilities.Drop, "ALL") {
-			hasDropFailure = true
-			capsString := commaSeparatedCapabilities(missingDrops)
-			cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesNotDroppedFailure, capsString), severity, category, id)
-		}
-	}
-	if hasDropCheck && !hasDropFailure {
-		cv.addSuccess(messages.SecurityCapabilitiesNotDroppedSuccess, category, dropID)
-	}
-}
-
-func commaSeparatedCapabilities(caps []corev1.Capability) string {
-	capsString := ""
-	for _, cap := range caps {
-		capsString = fmt.Sprintf("%s, %s", capsString, cap)
-	}
-	return capsString[2:]
-}
-
-func capIntersection(a, b []corev1.Capability) []corev1.Capability {
-	result := []corev1.Capability{}
-	hash := map[corev1.Capability]bool{}
-
-	for _, s := range a {
-		hash[s] = true
-	}
-
-	for _, s := range b {
-		if hash[s] {
-			result = append(result, s)
-		}
-	}
-
-	return result
-}
-
-func capDifference(b, a []corev1.Capability) []corev1.Capability {
-	result := []corev1.Capability{}
-	hash := map[corev1.Capability]bool{}
-
-	for _, s := range a {
-		hash[s] = true
-	}
-
-	for _, s := range b {
-		if !hash[s] {
-			result = append(result, s)
-		}
-	}
-
-	return result
-}
-
-func capContains(list []corev1.Capability, val corev1.Capability) bool {
-	for _, s := range list {
-		if s == val {
-			return true
-		}
-	}
-
-	return false
-}
diff --git a/pkg/validator/messages/messages.go b/pkg/validator/messages/messages.go
deleted file mode 100644
index 9c1193f8..00000000
--- a/pkg/validator/messages/messages.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package messages
-
-const (
-	// CategoryHealthChecks category
-	CategoryHealthChecks = "Health Checks"
-	// CategorySecurity category
-	CategorySecurity = "Security"
-	// CategoryNetworking category
-	CategoryNetworking = "Networking"
-	// CategoryResources category
-	CategoryResources = "Resources"
-	// CategoryImages category
-	CategoryImages = "Images"
-
-	// CPURequestsLabel label
-	CPURequestsLabel = "CPU requests"
-	// CPULimitsLabel label
-	CPULimitsLabel = "CPU limits"
-	// MemoryRequestsLabel label
-	MemoryRequestsLabel = "Memory requests"
-	// MemoryLimitsLabel label
-	MemoryLimitsLabel = "Memory limits"
-
-	// CPURequestsFailure message
-	CPURequestsFailure = "CPU requests should be set"
-	// CPULimitsFailure message
-	CPULimitsFailure = "CPU limits should be set"
-	// MemoryRequestsFailure message
-	MemoryRequestsFailure = "Memory requests should be set"
-	// MemoryLimitsFailure message
-	MemoryLimitsFailure = "Memory limits should be set"
-	// ResourceAmountTooHighFailure message
-	ResourceAmountTooHighFailure = "%s should be lower than %s"
-	// ResourceAmountTooLowFailure message
-	ResourceAmountTooLowFailure = "%s should be higher than %s"
-	// ResourceAmountSuccess message
-	ResourceAmountSuccess = "%s are within the expected range"
-	// ResourcePresentSuccess message
-	ResourcePresentSuccess = "%s are set"
-	// ReadinessProbeFailure message
-	ReadinessProbeFailure = "Readiness probe should be configured"
-	// ReadinessProbeSuccess message
-	ReadinessProbeSuccess = "Readiness probe configured"
-	// LivenessProbeFailure message
-	LivenessProbeFailure = "Liveness probe should be configured"
-	// LivenessProbeSuccess message
-	LivenessProbeSuccess = "Liveness probe is configured"
-	// ImageTagFailure message
-	ImageTagFailure = "Image tag should be specified"
-	// ImageTagSuccess message
-	ImageTagSuccess = "Image tag is specified"
-	// ImagePullPolicyFailure message
-	ImagePullPolicyFailure = "Image pull policy should be \"Always\""
-	// ImagePullPolicySuccess message
-	ImagePullPolicySuccess = "Image pull policy is \"Always\""
-	// HostPortFailure message
-	HostPortFailure = "Host port should not be configured"
-	// HostPortSuccess message
-	HostPortSuccess = "Host port is not configured"
-	// RunAsRootFailure message
-	RunAsRootFailure = "Should not be allowed to run as root"
-	// RunAsRootSuccess message
-	RunAsRootSuccess = "Is not allowed to run as root"
-	// RunAsPrivilegedFailure message
-	RunAsPrivilegedFailure = "Should not be running as privileged"
-	// RunAsPrivilegedSuccess message
-	RunAsPrivilegedSuccess = "Not running as privileged"
-	// ReadOnlyFilesystemSuccess message
-	ReadOnlyFilesystemSuccess = "Filesystem is read only"
-	// ReadOnlyFilesystemFailure message
-	ReadOnlyFilesystemFailure = "Filesystem should be read only"
-	// PrivilegeEscalationFailure message
-	PrivilegeEscalationFailure = "Privilege escalation should not be allowed"
-	// PrivilegeEscalationSuccess message
-	PrivilegeEscalationSuccess = "Privilege escalation not allowed"
-	// SecurityCapabilitiesAddedSuccess message
-	SecurityCapabilitiesAddedSuccess = "Disallowed security capabilities have not been added"
-	// SecurityCapabilitiesAddedFailure message
-	SecurityCapabilitiesAddedFailure = "The following security capabilities should not be added: %v"
-	// SecurityCapabilitiesNotDroppedSuccess message
-	SecurityCapabilitiesNotDroppedSuccess = "All disallowed security capabilities have been dropped"
-	// SecurityCapabilitiesNotDroppedFailure message
-	SecurityCapabilitiesNotDroppedFailure = "The following security capabilities should be dropped: %v"
-
-	// HostAliasFailure message
-	HostAliasFailure = "Host alias should not be configured"
-	// HostAliasSuccess message
-	HostAliasSuccess = "Host alias is not configured"
-	// HostIPCFailure message
-	HostIPCFailure = "Host IPC should not be configured"
-	// HostIPCSuccess message
-	HostIPCSuccess = "Host IPC is not configured"
-	// HostPIDFailure message
-	HostPIDFailure = "Host PID should not be configured"
-	// HostPIDSuccess message
-	HostPIDSuccess = "Host PID is not configured"
-	// HostNetworkFailure message
-	HostNetworkFailure = "Host network should not be configured"
-	// HostNetworkSuccess message
-	HostNetworkSuccess = "Host network is not configured"
-)
diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go
index f6762381..2060991d 100644
--- a/pkg/validator/schema.go
+++ b/pkg/validator/schema.go
@@ -2,60 +2,34 @@ package validator
 
 import (
 	"bytes"
-	"encoding/json"
 	"fmt"
 	"io"
+	"sort"
 
 	packr "github.com/gobuffalo/packr/v2"
-	"github.com/qri-io/jsonschema"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/util/yaml"
 
 	"github.com/fairwindsops/polaris/pkg/config"
 )
 
-type includeExcludeList struct {
-	Include []string `yaml:"include"`
-	Exclude []string `yaml:"exclude"`
-}
-
-type target string
-
-const (
-	targetContainer target = "Container"
-	targetPod       target = "Pod"
-)
-
-// SchemaCheck is a Polaris check that runs using JSON Schema
-type SchemaCheck struct {
-	Name           string                `yaml:"name"`
-	ID             string                `yaml:"id"`
-	Category       string                `yaml:"category"`
-	SuccessMessage string                `yaml:"successMessage"`
-	FailureMessage string                `yaml:"failureMessage"`
-	Controllers    includeExcludeList    `yaml:"controllers"`
-	Containers     includeExcludeList    `yaml:"containers"`
-	Target         target                `yaml:"target"`
-	SchemaTarget   target                `yaml:"schemaTarget"`
-	Schema         jsonschema.RootSchema `yaml:"schema"`
-}
-
 var (
-	schemaBox = (*packr.Box)(nil)
-	checks    = map[target][]SchemaCheck{
-		targetContainer: []SchemaCheck{},
-		targetPod:       []SchemaCheck{},
-	}
+	schemaBox     = (*packr.Box)(nil)
+	builtInChecks = map[string]config.SchemaCheck{}
 	// We explicitly set the order to avoid thrash in the
 	// tests as we migrate toward JSON schema
 	checkOrder = []string{
 		// Pod checks
-		"hostIPC",
-		"hostPID",
-		"hostNetwork",
+		"hostIPCSet",
+		"hostPIDSet",
+		"hostNetworkSet",
 		// Container checks
-		"readinessProbe",
-		"livenessProbe",
+		"memoryLimitsMissing",
+		"memoryRequestsMissing",
+		"cpuLimitsMissing",
+		"cpuRequestsMissing",
+		"readinessProbeMissing",
+		"livenessProbeMissing",
 		"pullPolicyNotAlways",
 		"tagNotSpecified",
 		"hostPortSet",
@@ -63,13 +37,15 @@ var (
 		"runAsPrivileged",
 		"notReadOnlyRootFileSystem",
 		"privilegeEscalationAllowed",
+		"dangerousCapabilities",
+		"insecureCapabilities",
 	}
 )
 
 func init() {
 	schemaBox = packr.New("Schemas", "../../checks")
-	for _, file := range checkOrder {
-		contents, err := schemaBox.Find(file + ".yaml")
+	for _, checkID := range checkOrder {
+		contents, err := schemaBox.Find(checkID + ".yaml")
 		if err != nil {
 			panic(err)
 		}
@@ -77,13 +53,14 @@ func init() {
 		if err != nil {
 			panic(err)
 		}
-		checks[check.Target] = append(checks[check.Target], check)
+		check.ID = checkID
+		builtInChecks[checkID] = check
 	}
 }
 
-func parseCheck(rawBytes []byte) (SchemaCheck, error) {
+func parseCheck(rawBytes []byte) (config.SchemaCheck, error) {
 	reader := bytes.NewReader(rawBytes)
-	check := SchemaCheck{}
+	check := config.SchemaCheck{}
 	d := yaml.NewYAMLOrJSONDecoder(reader, 4096)
 	for {
 		if err := d.Decode(&check); err != nil {
@@ -95,78 +72,30 @@ func parseCheck(rawBytes []byte) (SchemaCheck, error) {
 	}
 }
 
-func (check SchemaCheck) checkPod(pod *corev1.PodSpec) (bool, error) {
-	return check.checkObject(pod)
-}
-
-func (check SchemaCheck) checkContainer(container *corev1.Container) (bool, error) {
-	return check.checkObject(container)
-}
-
-func (check SchemaCheck) checkObject(obj interface{}) (bool, error) {
-	bytes, err := json.Marshal(obj)
-	if err != nil {
-		return false, err
-	}
-	errors, err := check.Schema.ValidateBytes(bytes)
-	return len(errors) == 0, err
-}
-
-func (check SchemaCheck) isActionable(target target, controllerType config.SupportedController, isInit bool) bool {
-	if check.Target != target {
-		return false
-	}
-	isIncluded := len(check.Controllers.Include) == 0
-	for _, inclusion := range check.Controllers.Include {
-		if config.GetSupportedControllerFromString(inclusion) == controllerType {
-			isIncluded = true
-			break
-		}
-	}
-	if !isIncluded {
-		return false
-	}
-	for _, exclusion := range check.Controllers.Exclude {
-		if config.GetSupportedControllerFromString(exclusion) == controllerType {
-			return false
-		}
-	}
-	if check.Target == targetContainer {
-		isIncluded := len(check.Containers.Include) == 0
-		for _, inclusion := range check.Containers.Include {
-			if (inclusion == "initContainer" && isInit) || (inclusion == "container" && !isInit) {
-				isIncluded = true
-				break
-			}
-		}
-		if !isIncluded {
-			return false
+func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController, pv *PodValidation) error {
+	checkIDs := getSortedKeys(conf.Checks)
+	for _, checkID := range checkIDs {
+		check, ok := conf.CustomChecks[checkID]
+		if !ok {
+			check, ok = builtInChecks[checkID]
 		}
-		for _, exclusion := range check.Containers.Exclude {
-			if (exclusion == "initContainer" && isInit) || (exclusion == "container" && !isInit) {
-				return false
-			}
+		if !ok {
+			return fmt.Errorf("Check %s not found", checkID)
 		}
-	}
-	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) {
+		if !conf.IsActionable(check.ID, controllerName) {
 			continue
 		}
-		if !check.isActionable(targetPod, controllerType, false) {
+		if !check.IsActionable(config.TargetPod, controllerType, false) {
 			continue
 		}
-		severity := conf.GetSeverity(check.Category, check.Name)
-		passes, err := check.checkPod(pod)
+		passes, err := check.CheckPod(pod)
 		if err != nil {
 			return err
 		}
 		if passes {
 			pv.addSuccess(check.SuccessMessage, check.Category, check.ID)
 		} else {
+			severity := conf.Checks[checkID]
 			pv.addFailure(check.FailureMessage, severity, check.Category, check.ID)
 		}
 	}
@@ -174,22 +103,29 @@ func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, contr
 }
 
 func applyContainerSchemaChecks(conf *config.Configuration, controllerName string, controllerType config.SupportedController, cv *ContainerValidation) error {
-	for _, check := range checks[targetContainer] {
-		if !conf.IsActionable(check.Category, check.Name, controllerName) {
+	checkIDs := getSortedKeys(conf.Checks)
+	for _, checkID := range checkIDs {
+		check, ok := conf.CustomChecks[checkID]
+		if !ok {
+			check, ok = builtInChecks[checkID]
+		}
+		if !ok {
+			return fmt.Errorf("Check %s not found", checkID)
+		}
+		if !conf.IsActionable(check.ID, controllerName) {
 			continue
 		}
-		if !check.isActionable(targetContainer, controllerType, cv.IsInitContainer) {
+		if !check.IsActionable(config.TargetContainer, controllerType, cv.IsInitContainer) {
 			continue
 		}
-		severity := conf.GetSeverity(check.Category, check.Name)
 		var passes bool
 		var err error
-		if check.SchemaTarget == targetPod {
+		if check.SchemaTarget == config.TargetPod {
 			cv.parentPodSpec.Containers = []corev1.Container{*cv.Container}
-			passes, err = check.checkPod(&cv.parentPodSpec)
+			passes, err = check.CheckPod(&cv.parentPodSpec)
 			cv.parentPodSpec.Containers = []corev1.Container{}
 		} else {
-			passes, err = check.checkContainer(cv.Container)
+			passes, err = check.CheckContainer(cv.Container)
 		}
 		if err != nil {
 			return err
@@ -197,8 +133,18 @@ func applyContainerSchemaChecks(conf *config.Configuration, controllerName strin
 		if passes {
 			cv.addSuccess(check.SuccessMessage, check.Category, check.ID)
 		} else {
+			severity := conf.Checks[checkID]
 			cv.addFailure(check.FailureMessage, severity, check.Category, check.ID)
 		}
 	}
 	return nil
 }
+
+func getSortedKeys(m map[string]config.Severity) []string {
+	keys := make([]string, 0, len(m))
+	for key := range m {
+		keys = append(keys, key)
+	}
+	sort.Strings(keys)
+	return keys
+}
diff --git a/scripts/generate-deployment-files.sh b/scripts/generate-deployment-files.sh
new file mode 100755
index 00000000..252f412d
--- /dev/null
+++ b/scripts/generate-deployment-files.sh
@@ -0,0 +1,17 @@
+# /bin/bash
+
+set -eo pipefail
+
+helm template $CHARTS_DIR/stable/polaris/ \
+  --name polaris --namespace polaris \
+  --set templateOnly=true \
+  --set config="$(cat ./examples/config.yaml)" \
+  > deploy/dashboard.yaml
+
+helm template $CHARTS_DIR/stable/polaris/ \
+  --name polaris --namespace polaris \
+  --set templateOnly=true \
+  --set webhook.enable=true \
+  --set dashboard.enable=false \
+  --set config="$(cat ./examples/config.yaml)" \
+  > deploy/webhook.yaml


Test patch:
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index ba15b2f1..b13feb75 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -24,111 +24,74 @@ import (
 	"time"
 
 	"github.com/stretchr/testify/assert"
-	"k8s.io/apimachinery/pkg/api/resource"
 )
 
-var resourceConfInvalid1 = `test`
-
-var 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 confInvalid = `test`
+
+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 TestConfigWithCustomChecks(t *testing.T) {
+	valid := map[string]interface{}{
+		"securityContext": map[string]interface{}{},
+	}
+	invalid := map[string]interface{}{
+		"notSecurityContext": map[string]interface{}{},
+	}
+
+	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)
+
+	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) {
-	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))
-
-	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))
-
-	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)
+	assert.Equal(t, SeverityWarning, config.Checks["cpuRequestsMissing"])
+	assert.Equal(t, Severity(""), config.Checks["cpuLimitsMissing"])
+	assert.ElementsMatch(t, []SupportedController{Deployments}, config.ControllersToScan)
 }
diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go
index 20ff2c17..66d20865 100644
--- a/pkg/validator/container_test.go
+++ b/pkg/validator/container_test.go
@@ -21,43 +21,10 @@ import (
 	conf "github.com/fairwindsops/polaris/pkg/config"
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/resource"
 )
 
-var resourceConf1 = `---
-resources:
-  cpuRequestRanges:
-    error:
-      below: 100m
-      above: 1
-    warning:
-      below: 200m
-      above: 800m
-  memoryRequestRanges:
-    error:
-      below: 100M
-      above: 3G
-    warning:
-      below: 200M
-      above: 2G
-  cpuLimitRanges:
-    error:
-      below: 100m
-      above: 2
-    warning:
-      below: 300m
-      above: 1800m
-  memoryLimitRanges:
-    error:
-      below: 200M
-      above: 6G
-    warning:
-      below: 300M
-      above: 4G
-`
-
-var resourceConf2 = `---
-resources:
+var resourceConfMinimal = `---
+checks:
   cpuRequestsMissing: warning
   memoryRequestsMissing: warning
   cpuLimitsMissing: error
@@ -65,7 +32,7 @@ resources:
 `
 
 var resourceConfExemptions = `---
-resources:
+checks:
   cpuRequestsMissing: warning
   memoryRequestsMissing: warning
   cpuLimitsMissing: error
@@ -80,47 +47,7 @@ exemptions:
     - foo
 `
 
-var resourceConfRangeExemptions = `---
-resources:
-  cpuRequestRanges:
-    error:
-      below: 100m
-      above: 1
-    warning:
-      below: 200m
-      above: 800m
-  memoryRequestRanges:
-    error:
-      below: 100M
-      above: 3G
-    warning:
-      below: 200M
-      above: 2G
-  cpuLimitRanges:
-    error:
-      below: 100m
-      above: 2
-    warning:
-      below: 300m
-      above: 1800m
-  memoryLimitRanges:
-    error:
-      below: 200M
-      above: 6G
-    warning:
-      below: 300M
-      above: 4G
-exemptions:
-  - rules:
-    - cpuRequestRanges
-    - memoryRequestRanges
-    - cpuLimitRanges
-    - memoryLimitRanges
-    controllerNames:
-    - foo
-`
-
-func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []*ResultMessage, expectedWarnings []*ResultMessage, expectedSuccesses []*ResultMessage) {
+func testValidate(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []*ResultMessage, expectedWarnings []*ResultMessage, expectedSuccesses []*ResultMessage) {
 	cv := ContainerValidation{
 		Container:          container,
 		ResourceValidation: &ResourceValidation{},
@@ -129,7 +56,10 @@ func testValidateResources(t *testing.T, container *corev1.Container, resourceCo
 	parsedConf, err := conf.Parse([]byte(*resourceConf))
 	assert.NoError(t, err, "Expected no error when parsing config")
 
-	cv.validateResources(&parsedConf, controllerName)
+	err = applyContainerSchemaChecks(&parsedConf, controllerName, conf.Deployments, &cv)
+	if err != nil {
+		panic(err)
+	}
 
 	assert.Len(t, cv.Warnings, len(expectedWarnings))
 	assert.ElementsMatch(t, expectedWarnings, cv.Warnings)
@@ -151,7 +81,10 @@ func TestValidateResourcesEmptyConfig(t *testing.T) {
 		ResourceValidation: &ResourceValidation{},
 	}
 
-	cv.validateResources(&conf.Configuration{}, "")
+	err := applyContainerSchemaChecks(&conf.Configuration{}, "", conf.Deployments, &cv)
+	if err != nil {
+		panic(err)
+	}
 	assert.Len(t, cv.Errors, 0)
 }
 
@@ -192,181 +125,20 @@ func TestValidateResourcesEmptyContainer(t *testing.T) {
 
 	expectedSuccesses := []*ResultMessage{}
 
-	testValidateResources(t, &container, &resourceConf2, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
-}
-
-func TestValidateResourcesPartiallyValid(t *testing.T) {
-	cpuRequest, err := resource.ParseQuantity("100m")
-	assert.NoError(t, err, "Error parsing quantity")
-
-	cpuLimit, err := resource.ParseQuantity("200m")
-	assert.NoError(t, err, "Error parsing quantity")
-
-	container := corev1.Container{
-		Name: "Empty",
-		Resources: corev1.ResourceRequirements{
-			Requests: corev1.ResourceList{
-				"cpu": cpuRequest,
-			},
-			Limits: corev1.ResourceList{
-				"cpu": cpuLimit,
-			},
-		},
-	}
-
-	expectedWarnings := []*ResultMessage{
-		{
-			ID:       "cpuRequestRanges",
-			Type:     "warning",
-			Message:  "CPU requests should be higher than 200m",
-			Category: "Resources",
-		},
-		{
-			ID:       "cpuLimitRanges",
-			Type:     "warning",
-			Message:  "CPU limits should be higher than 300m",
-			Category: "Resources",
-		},
-	}
-
-	expectedErrors := []*ResultMessage{
-		{
-			ID:       "memoryRequestRanges",
-			Type:     "error",
-			Message:  "Memory requests should be higher than 100M",
-			Category: "Resources",
-		},
-		{
-			ID:       "memoryLimitRanges",
-			Type:     "error",
-			Message:  "Memory limits should be higher than 200M",
-			Category: "Resources",
-		},
-	}
-
-	expectedSuccesses := []*ResultMessage{}
-
-	testValidateResources(t, &container, &resourceConf1, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
-}
-
-func TestValidateResourcesInit(t *testing.T) {
-	cvEmpty := ContainerValidation{
-		Container:          &corev1.Container{},
-		ResourceValidation: &ResourceValidation{},
-	}
-	cvInit := ContainerValidation{
-		Container:          &corev1.Container{},
-		ResourceValidation: &ResourceValidation{},
-		IsInitContainer:    true,
-	}
-
-	parsedConf, err := conf.Parse([]byte(resourceConf1))
-	assert.NoError(t, err, "Expected no error when parsing config")
-
-	cvEmpty.validateResources(&parsedConf, "")
-	assert.Len(t, cvEmpty.Errors, 4)
-
-	cvInit.validateResources(&parsedConf, "")
-	assert.Len(t, cvInit.Errors, 0)
-}
-
-func TestValidateResourcesFullyValid(t *testing.T) {
-	cpuRequest, err := resource.ParseQuantity("300m")
-	assert.NoError(t, err, "Error parsing quantity")
-
-	cpuLimit, err := resource.ParseQuantity("400m")
-	assert.NoError(t, err, "Error parsing quantity")
-
-	memoryRequest, err := resource.ParseQuantity("400Mi")
-	assert.NoError(t, err, "Error parsing quantity")
-
-	memoryLimit, err := resource.ParseQuantity("500Mi")
-	assert.NoError(t, err, "Error parsing quantity")
-
-	container := corev1.Container{
-		Name: "Empty",
-		Resources: corev1.ResourceRequirements{
-			Requests: corev1.ResourceList{
-				"cpu":    cpuRequest,
-				"memory": memoryRequest,
-			},
-			Limits: corev1.ResourceList{
-				"cpu":    cpuLimit,
-				"memory": memoryLimit,
-			},
-		},
-	}
-
-	expectedSuccesses := []*ResultMessage{
-		{
-			ID:       "cpuRequestRanges",
-			Type:     "success",
-			Message:  "CPU requests are within the expected range",
-			Category: "Resources",
-		},
-		{
-			ID:       "memoryRequestRanges",
-			Type:     "success",
-			Message:  "Memory requests are within the expected range",
-			Category: "Resources",
-		},
-		{
-			ID:       "cpuLimitRanges",
-			Type:     "success",
-			Message:  "CPU limits are within the expected range",
-			Category: "Resources",
-		},
-		{
-			ID:       "memoryLimitRanges",
-			Type:     "success",
-			Message:  "Memory limits are within the expected range",
-			Category: "Resources",
-		},
-	}
-
-	testValidateResources(t, &container, &resourceConf1, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses)
-
-	expectedSuccesses = []*ResultMessage{
-		{
-			ID:       "cpuRequestsMissing",
-			Type:     "success",
-			Message:  "CPU requests are set",
-			Category: "Resources",
-		},
-		{
-			ID:       "memoryRequestsMissing",
-			Type:     "success",
-			Message:  "Memory requests are set",
-			Category: "Resources",
-		},
-		{
-			ID:       "cpuLimitsMissing",
-			Type:     "success",
-			Message:  "CPU limits are set",
-			Category: "Resources",
-		},
-		{
-			ID:       "memoryLimitsMissing",
-			Type:     "success",
-			Message:  "Memory limits are set",
-			Category: "Resources",
-		},
-	}
-
-	testValidateResources(t, &container, &resourceConf2, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses)
+	testValidate(t, &container, &resourceConfMinimal, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
 }
 
 func TestValidateHealthChecks(t *testing.T) {
 
 	// Test setup.
-	p1 := conf.HealthChecks{}
-	p2 := conf.HealthChecks{
-		ReadinessProbeMissing: conf.SeverityIgnore,
-		LivenessProbeMissing:  conf.SeverityIgnore,
+	p1 := make(map[string]conf.Severity)
+	p2 := map[string]conf.Severity{
+		"readinessProbeMissing": conf.SeverityIgnore,
+		"livenessProbeMissing":  conf.SeverityIgnore,
 	}
-	p3 := conf.HealthChecks{
-		ReadinessProbeMissing: conf.SeverityError,
-		LivenessProbeMissing:  conf.SeverityWarning,
+	p3 := map[string]conf.Severity{
+		"readinessProbeMissing": conf.SeverityError,
+		"livenessProbeMissing":  conf.SeverityWarning,
 	}
 
 	probe := corev1.Probe{}
@@ -396,7 +168,7 @@ func TestValidateHealthChecks(t *testing.T) {
 
 	var testCases = []struct {
 		name     string
-		probes   conf.HealthChecks
+		probes   map[string]conf.Severity
 		cv       ContainerValidation
 		errors   *[]*ResultMessage
 		warnings *[]*ResultMessage
@@ -411,7 +183,7 @@ func TestValidateHealthChecks(t *testing.T) {
 
 	for idx, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
-			err := applyContainerSchemaChecks(&conf.Configuration{HealthChecks: tt.probes}, "", conf.Deployments, &tt.cv)
+			err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.probes}, "", conf.Deployments, &tt.cv)
 			if err != nil {
 				panic(err)
 			}
@@ -429,14 +201,14 @@ func TestValidateHealthChecks(t *testing.T) {
 }
 
 func TestValidateImage(t *testing.T) {
-	emptyConf := conf.Images{}
-	standardConf := conf.Images{
-		TagNotSpecified:     conf.SeverityError,
-		PullPolicyNotAlways: conf.SeverityIgnore,
+	emptyConf := make(map[string]conf.Severity)
+	standardConf := map[string]conf.Severity{
+		"tagNotSpecified":     conf.SeverityError,
+		"pullPolicyNotAlways": conf.SeverityIgnore,
 	}
-	strongConf := conf.Images{
-		TagNotSpecified:     conf.SeverityError,
-		PullPolicyNotAlways: conf.SeverityError,
+	strongConf := map[string]conf.Severity{
+		"tagNotSpecified":     conf.SeverityError,
+		"pullPolicyNotAlways": conf.SeverityError,
 	}
 
 	emptyCV := ContainerValidation{
@@ -458,7 +230,7 @@ func TestValidateImage(t *testing.T) {
 
 	var testCases = []struct {
 		name     string
-		image    conf.Images
+		image    map[string]conf.Severity
 		cv       ContainerValidation
 		expected []*ResultMessage
 	}{
@@ -528,7 +300,7 @@ func TestValidateImage(t *testing.T) {
 	for _, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
 			tt.cv = resetCV(tt.cv)
-			err := applyContainerSchemaChecks(&conf.Configuration{Images: tt.image}, "", conf.Deployments, &tt.cv)
+			err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.image}, "", conf.Deployments, &tt.cv)
 			if err != nil {
 				panic(err)
 			}
@@ -540,12 +312,12 @@ func TestValidateImage(t *testing.T) {
 
 func TestValidateNetworking(t *testing.T) {
 	// Test setup.
-	emptyConf := conf.Networking{}
-	standardConf := conf.Networking{
-		HostPortSet: conf.SeverityWarning,
+	emptyConf := make(map[string]conf.Severity)
+	standardConf := map[string]conf.Severity{
+		"hostPortSet": conf.SeverityWarning,
 	}
-	strongConf := conf.Networking{
-		HostPortSet: conf.SeverityError,
+	strongConf := map[string]conf.Severity{
+		"hostPortSet": conf.SeverityError,
 	}
 
 	emptyCV := ContainerValidation{
@@ -574,7 +346,7 @@ func TestValidateNetworking(t *testing.T) {
 
 	var testCases = []struct {
 		name             string
-		networkConf      conf.Networking
+		networkConf      map[string]conf.Severity
 		cv               ContainerValidation
 		expectedMessages []*ResultMessage
 	}{
@@ -650,7 +422,7 @@ func TestValidateNetworking(t *testing.T) {
 	for _, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
 			tt.cv = resetCV(tt.cv)
-			err := applyContainerSchemaChecks(&conf.Configuration{Networking: tt.networkConf}, "", conf.Deployments, &tt.cv)
+			err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.networkConf}, "", conf.Deployments, &tt.cv)
 			if err != nil {
 				panic(err)
 			}
@@ -665,35 +437,22 @@ func TestValidateSecurity(t *testing.T) {
 	falseVar := false
 
 	// Test setup.
-	emptyConf := conf.Security{}
-	standardConf := conf.Security{
-		RunAsRootAllowed:           conf.SeverityWarning,
-		RunAsPrivileged:            conf.SeverityError,
-		NotReadOnlyRootFileSystem:  conf.SeverityWarning,
-		PrivilegeEscalationAllowed: conf.SeverityError,
-		Capabilities: conf.SecurityCapabilities{
-			Error: conf.SecurityCapabilityLists{
-				IfAnyAdded: []corev1.Capability{"ALL", "SYS_ADMIN", "NET_ADMIN"},
-			},
-			Warning: conf.SecurityCapabilityLists{
-				IfAnyAddedBeyond: []corev1.Capability{"NONE"},
-			},
-		},
+	emptyConf := map[string]conf.Severity{}
+	standardConf := map[string]conf.Severity{
+		"runAsRootAllowed":           conf.SeverityWarning,
+		"runAsPrivileged":            conf.SeverityError,
+		"notReadOnlyRootFileSystem":  conf.SeverityWarning,
+		"privilegeEscalationAllowed": conf.SeverityError,
+		"dangerousCapabilities":      conf.SeverityError,
+		"insecureCapabilities":       conf.SeverityWarning,
 	}
-	strongConf := conf.Security{
-		RunAsRootAllowed:           conf.SeverityError,
-		RunAsPrivileged:            conf.SeverityError,
-		NotReadOnlyRootFileSystem:  conf.SeverityError,
-		PrivilegeEscalationAllowed: conf.SeverityError,
-		Capabilities: conf.SecurityCapabilities{
-			Error: conf.SecurityCapabilityLists{
-				IfAnyAdded:      []corev1.Capability{"ALL", "SYS_ADMIN", "NET_ADMIN"},
-				IfAnyNotDropped: []corev1.Capability{"NET_BIND_SERVICE", "DAC_OVERRIDE", "SYS_CHROOT"},
-			},
-			Warning: conf.SecurityCapabilityLists{
-				IfAnyAddedBeyond: []corev1.Capability{"NONE"},
-			},
-		},
+	strongConf := map[string]conf.Severity{
+		"runAsRootAllowed":           conf.SeverityError,
+		"runAsPrivileged":            conf.SeverityError,
+		"notReadOnlyRootFileSystem":  conf.SeverityError,
+		"privilegeEscalationAllowed": conf.SeverityError,
+		"dangerousCapabilities":      conf.SeverityError,
+		"insecureCapabilities":       conf.SeverityError,
 	}
 
 	emptyCV := ContainerValidation{
@@ -708,7 +467,7 @@ func TestValidateSecurity(t *testing.T) {
 			Privileged:               &trueVar,
 			AllowPrivilegeEscalation: &trueVar,
 			Capabilities: &corev1.Capabilities{
-				Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"},
+				Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"},
 			},
 		}},
 		ResourceValidation: &ResourceValidation{},
@@ -721,7 +480,7 @@ func TestValidateSecurity(t *testing.T) {
 			Privileged:               &trueVar,
 			AllowPrivilegeEscalation: &trueVar,
 			Capabilities: &corev1.Capabilities{
-				Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"},
+				Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"},
 			},
 		}},
 		ResourceValidation: &ResourceValidation{},
@@ -739,7 +498,7 @@ func TestValidateSecurity(t *testing.T) {
 			Privileged:               &trueVar,
 			AllowPrivilegeEscalation: &trueVar,
 			Capabilities: &corev1.Capabilities{
-				Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"},
+				Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"},
 			},
 		}},
 		ResourceValidation: &ResourceValidation{},
@@ -814,7 +573,7 @@ func TestValidateSecurity(t *testing.T) {
 
 	var testCases = []struct {
 		name             string
-		securityConf     conf.Security
+		securityConf     map[string]conf.Severity
 		cv               ContainerValidation
 		expectedMessages []*ResultMessage
 	}{
@@ -849,8 +608,13 @@ func TestValidateSecurity(t *testing.T) {
 				Type:     "success",
 				Category: "Security",
 			}, {
-				ID:       "capabilitiesAdded",
-				Message:  "Disallowed security capabilities have not been added",
+				ID:       "insecureCapabilities",
+				Message:  "Container does not have any insecure capabilities",
+				Type:     "success",
+				Category: "Security",
+			}, {
+				ID:       "dangerousCapabilities",
+				Message:  "Container does not have any dangerous capabilities",
 				Type:     "success",
 				Category: "Security",
 			}},
@@ -860,8 +624,8 @@ func TestValidateSecurity(t *testing.T) {
 			securityConf: standardConf,
 			cv:           badCV,
 			expectedMessages: []*ResultMessage{{
-				ID:       "capabilitiesAdded",
-				Message:  "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
+				ID:       "dangerousCapabilities",
+				Message:  "Container should not have dangerous capabilities",
 				Type:     "error",
 				Category: "Security",
 			}, {
@@ -875,8 +639,8 @@ func TestValidateSecurity(t *testing.T) {
 				Type:     "error",
 				Category: "Security",
 			}, {
-				ID:       "capabilitiesAdded",
-				Message:  "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
+				ID:       "insecureCapabilities",
+				Message:  "Container should not have insecure capabilities",
 				Type:     "warning",
 				Category: "Security",
 			}, {
@@ -896,8 +660,8 @@ func TestValidateSecurity(t *testing.T) {
 			securityConf: standardConf,
 			cv:           badCVWithGoodPodSpec,
 			expectedMessages: []*ResultMessage{{
-				ID:       "capabilitiesAdded",
-				Message:  "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
+				ID:       "dangerousCapabilities",
+				Message:  "Container should not have dangerous capabilities",
 				Type:     "error",
 				Category: "Security",
 			}, {
@@ -911,8 +675,8 @@ func TestValidateSecurity(t *testing.T) {
 				Type:     "error",
 				Category: "Security",
 			}, {
-				ID:       "capabilitiesAdded",
-				Message:  "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
+				ID:       "insecureCapabilities",
+				Message:  "Container should not have insecure capabilities",
 				Type:     "warning",
 				Category: "Security",
 			}, {
@@ -932,10 +696,15 @@ func TestValidateSecurity(t *testing.T) {
 			securityConf: standardConf,
 			cv:           badCVWithBadPodSpec,
 			expectedMessages: []*ResultMessage{{
-				ID:       "capabilitiesAdded",
-				Message:  "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
+				ID:       "dangerousCapabilities",
+				Message:  "Container should not have dangerous capabilities",
 				Type:     "error",
 				Category: "Security",
+			}, {
+				ID:       "insecureCapabilities",
+				Message:  "Container should not have insecure capabilities",
+				Type:     "warning",
+				Category: "Security",
 			}, {
 				ID:       "privilegeEscalationAllowed",
 				Message:  "Privilege escalation should not be allowed",
@@ -946,11 +715,6 @@ func TestValidateSecurity(t *testing.T) {
 				Message:  "Should not be running as privileged",
 				Type:     "error",
 				Category: "Security",
-			}, {
-				ID:       "capabilitiesAdded",
-				Message:  "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
-				Type:     "warning",
-				Category: "Security",
 			}, {
 				ID:       "runAsRootAllowed",
 				Message:  "Should not be allowed to run as root",
@@ -988,8 +752,13 @@ func TestValidateSecurity(t *testing.T) {
 				Type:     "success",
 				Category: "Security",
 			}, {
-				ID:       "capabilitiesAdded",
-				Message:  "Disallowed security capabilities have not been added",
+				ID:       "dangerousCapabilities",
+				Message:  "Container does not have any dangerous capabilities",
+				Type:     "success",
+				Category: "Security",
+			}, {
+				ID:       "insecureCapabilities",
+				Message:  "Container does not have any insecure capabilities",
 				Type:     "success",
 				Category: "Security",
 			}},
@@ -999,13 +768,13 @@ func TestValidateSecurity(t *testing.T) {
 			securityConf: strongConf,
 			cv:           goodCV,
 			expectedMessages: []*ResultMessage{{
-				ID:       "capabilitiesNotDropped",
-				Message:  "The following security capabilities should be dropped: DAC_OVERRIDE, SYS_CHROOT",
-				Type:     "error",
+				ID:       "dangerousCapabilities",
+				Message:  "Container does not have any dangerous capabilities",
+				Type:     "success",
 				Category: "Security",
 			}, {
-				ID:       "capabilitiesAdded",
-				Message:  "Disallowed security capabilities have not been added",
+				ID:       "insecureCapabilities",
+				Message:  "Container does not have any insecure capabilities",
 				Type:     "success",
 				Category: "Security",
 			}, {
@@ -1055,13 +824,13 @@ func TestValidateSecurity(t *testing.T) {
 				Type:     "success",
 				Category: "Security",
 			}, {
-				ID:       "capabilitiesAdded",
-				Message:  "Disallowed security capabilities have not been added",
+				ID:       "dangerousCapabilities",
+				Message:  "Container does not have any dangerous capabilities",
 				Type:     "success",
 				Category: "Security",
 			}, {
-				ID:       "capabilitiesDropped",
-				Message:  "All disallowed security capabilities have been dropped",
+				ID:       "insecureCapabilities",
+				Message:  "Container does not have any insecure capabilities",
 				Type:     "success",
 				Category: "Security",
 			}},
@@ -1091,13 +860,13 @@ func TestValidateSecurity(t *testing.T) {
 				Type:     "success",
 				Category: "Security",
 			}, {
-				ID:       "capabilitiesAdded",
-				Message:  "Disallowed security capabilities have not been added",
+				ID:       "dangerousCapabilities",
+				Message:  "Container does not have any dangerous capabilities",
 				Type:     "success",
 				Category: "Security",
 			}, {
-				ID:       "capabilitiesDropped",
-				Message:  "All disallowed security capabilities have been dropped",
+				ID:       "insecureCapabilities",
+				Message:  "Container does not have any insecure capabilities",
 				Type:     "success",
 				Category: "Security",
 			}},
@@ -1127,13 +896,13 @@ func TestValidateSecurity(t *testing.T) {
 				Type:     "success",
 				Category: "Security",
 			}, {
-				ID:       "capabilitiesAdded",
-				Message:  "Disallowed security capabilities have not been added",
+				ID:       "dangerousCapabilities",
+				Message:  "Container does not have any dangerous capabilities",
 				Type:     "success",
 				Category: "Security",
 			}, {
-				ID:       "capabilitiesDropped",
-				Message:  "All disallowed security capabilities have been dropped",
+				ID:       "insecureCapabilities",
+				Message:  "Container does not have any insecure capabilities",
 				Type:     "success",
 				Category: "Security",
 			}},
@@ -1143,13 +912,12 @@ func TestValidateSecurity(t *testing.T) {
 	for _, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
 			tt.cv = resetCV(tt.cv)
-			err := applyContainerSchemaChecks(&conf.Configuration{Security: tt.securityConf}, "", conf.Deployments, &tt.cv)
+			err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.securityConf}, "", conf.Deployments, &tt.cv)
 			if err != nil {
 				panic(err)
 			}
-			tt.cv.validateSecurity(&conf.Configuration{Security: tt.securityConf}, "")
 			assert.Len(t, tt.cv.messages(), len(tt.expectedMessages))
-			assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages)
+			assert.ElementsMatch(t, tt.expectedMessages, tt.cv.messages())
 		})
 	}
 }
@@ -1160,8 +928,8 @@ func TestValidateRunAsRoot(t *testing.T) {
 	nonRootUser := int64(1000)
 	rootUser := int64(0)
 	config := conf.Configuration{
-		Security: conf.Security{
-			RunAsRootAllowed: conf.SeverityWarning,
+		Checks: map[string]conf.Severity{
+			"runAsRootAllowed": conf.SeverityWarning,
 		},
 	}
 	testCases := []struct {
@@ -1306,7 +1074,7 @@ func TestValidateResourcesExemption(t *testing.T) {
 	expectedErrors := []*ResultMessage{}
 	expectedSuccesses := []*ResultMessage{}
 
-	testValidateResources(t, &container, &resourceConfExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
+	testValidate(t, &container, &resourceConfExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
 
 	expectedWarnings = []*ResultMessage{
 		{
@@ -1340,19 +1108,7 @@ func TestValidateResourcesExemption(t *testing.T) {
 
 	disallowExemptionsConf := resourceConfExemptions + "\ndisallowExemptions: true"
 
-	testValidateResources(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
-}
-
-func TestValidateResourceRangeExemption(t *testing.T) {
-	container := corev1.Container{
-		Name: "Empty",
-	}
-
-	expectedWarnings := []*ResultMessage{}
-	expectedErrors := []*ResultMessage{}
-	expectedSuccesses := []*ResultMessage{}
-
-	testValidateResources(t, &container, &resourceConfRangeExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
+	testValidate(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
 }
 
 func resetCV(cv ContainerValidation) ContainerValidation {
diff --git a/pkg/validator/controller_test.go b/pkg/validator/controller_test.go
index 860984f1..90a67486 100644
--- a/pkg/validator/controller_test.go
+++ b/pkg/validator/controller_test.go
@@ -29,9 +29,9 @@ import (
 
 func TestValidateController(t *testing.T) {
 	c := conf.Configuration{
-		Security: conf.Security{
-			HostIPCSet: conf.SeverityError,
-			HostPIDSet: conf.SeverityError,
+		Checks: map[string]conf.Severity{
+			"hostIPCSet": conf.SeverityError,
+			"hostPIDSet": conf.SeverityError,
 		},
 	}
 	deployment := controller.NewDeploymentController(test.MockDeploy())
@@ -64,9 +64,9 @@ func TestValidateController(t *testing.T) {
 
 func TestSkipHealthChecks(t *testing.T) {
 	c := conf.Configuration{
-		HealthChecks: conf.HealthChecks{
-			ReadinessProbeMissing: conf.SeverityError,
-			LivenessProbeMissing:  conf.SeverityWarning,
+		Checks: map[string]conf.Severity{
+			"readinessProbeMissing": conf.SeverityError,
+			"livenessProbeMissing":  conf.SeverityWarning,
 		},
 		ControllersToScan: []conf.SupportedController{
 			conf.Deployments,
@@ -139,9 +139,9 @@ func TestSkipHealthChecks(t *testing.T) {
 
 func TestControllerExemptions(t *testing.T) {
 	c := conf.Configuration{
-		HealthChecks: conf.HealthChecks{
-			ReadinessProbeMissing: conf.SeverityError,
-			LivenessProbeMissing:  conf.SeverityWarning,
+		Checks: map[string]conf.Severity{
+			"readinessProbeMissing": conf.SeverityError,
+			"livenessProbeMissing":  conf.SeverityWarning,
 		},
 		ControllersToScan: []conf.SupportedController{
 			conf.Deployments,
diff --git a/pkg/validator/fullaudit_test.go b/pkg/validator/fullaudit_test.go
index 863e35e4..af9411c0 100644
--- a/pkg/validator/fullaudit_test.go
+++ b/pkg/validator/fullaudit_test.go
@@ -16,9 +16,9 @@ func TestGetTemplateData(t *testing.T) {
 	assert.Equal(t, err, nil, "error should be nil")
 
 	c := conf.Configuration{
-		HealthChecks: conf.HealthChecks{
-			ReadinessProbeMissing: conf.SeverityError,
-			LivenessProbeMissing:  conf.SeverityWarning,
+		Checks: map[string]conf.Severity{
+			"readinessProbeMissing": conf.SeverityError,
+			"livenessProbeMissing":  conf.SeverityWarning,
 		},
 		ControllersToScan: []conf.SupportedController{
 			conf.Deployments,
diff --git a/pkg/validator/pod_test.go b/pkg/validator/pod_test.go
index d0552a70..fd9df744 100644
--- a/pkg/validator/pod_test.go
+++ b/pkg/validator/pod_test.go
@@ -24,13 +24,11 @@ import (
 
 func TestValidatePod(t *testing.T) {
 	c := conf.Configuration{
-		Security: conf.Security{
-			HostIPCSet: conf.SeverityError,
-			HostPIDSet: conf.SeverityError,
-		},
-		Networking: conf.Networking{
-			HostNetworkSet: conf.SeverityWarning,
-			HostPortSet:    conf.SeverityError,
+		Checks: map[string]conf.Severity{
+			"hostIPCSet":     conf.SeverityError,
+			"hostPIDSet":     conf.SeverityError,
+			"hostNetworkSet": conf.SeverityWarning,
+			"hostPortSet":    conf.SeverityError,
 		},
 	}
 
@@ -59,8 +57,8 @@ func TestValidatePod(t *testing.T) {
 
 	expectedMessages := []*ResultMessage{
 		{ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"},
-		{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
 		{ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"},
+		{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
 	}
 
 	actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
@@ -72,13 +70,11 @@ func TestValidatePod(t *testing.T) {
 
 func TestInvalidIPCPod(t *testing.T) {
 	c := conf.Configuration{
-		Security: conf.Security{
-			HostIPCSet: conf.SeverityError,
-			HostPIDSet: conf.SeverityError,
-		},
-		Networking: conf.Networking{
-			HostNetworkSet: conf.SeverityWarning,
-			HostPortSet:    conf.SeverityError,
+		Checks: map[string]conf.Severity{
+			"hostIPCSet":     conf.SeverityError,
+			"hostPIDSet":     conf.SeverityError,
+			"hostNetworkSet": conf.SeverityWarning,
+			"hostPortSet":    conf.SeverityError,
 		},
 	}
 
@@ -107,8 +103,8 @@ func TestInvalidIPCPod(t *testing.T) {
 	}
 	expectedMessages := []*ResultMessage{
 		{ID: "hostIPCSet", Message: "Host IPC should not be configured", Type: "error", Category: "Security"},
-		{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
 		{ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"},
+		{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
 	}
 
 	actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
@@ -120,13 +116,11 @@ func TestInvalidIPCPod(t *testing.T) {
 
 func TestInvalidNeworkPod(t *testing.T) {
 	c := conf.Configuration{
-		Networking: conf.Networking{
-			HostNetworkSet: conf.SeverityWarning,
-			HostPortSet:    conf.SeverityError,
-		},
-		Security: conf.Security{
-			HostIPCSet: conf.SeverityError,
-			HostPIDSet: conf.SeverityError,
+		Checks: map[string]conf.Severity{
+			"hostNetworkSet": conf.SeverityWarning,
+			"hostPortSet":    conf.SeverityError,
+			"hostIPCSet":     conf.SeverityError,
+			"hostPIDSet":     conf.SeverityError,
 		},
 	}
 
@@ -170,13 +164,11 @@ func TestInvalidNeworkPod(t *testing.T) {
 
 func TestInvalidPIDPod(t *testing.T) {
 	c := conf.Configuration{
-		Security: conf.Security{
-			HostIPCSet: conf.SeverityError,
-			HostPIDSet: conf.SeverityError,
-		},
-		Networking: conf.Networking{
-			HostNetworkSet: conf.SeverityWarning,
-			HostPortSet:    conf.SeverityError,
+		Checks: map[string]conf.Severity{
+			"hostIPCSet":     conf.SeverityError,
+			"hostPIDSet":     conf.SeverityError,
+			"hostNetworkSet": conf.SeverityWarning,
+			"hostPortSet":    conf.SeverityError,
 		},
 	}
 
@@ -219,13 +211,11 @@ func TestInvalidPIDPod(t *testing.T) {
 
 func TestExemption(t *testing.T) {
 	c := conf.Configuration{
-		Security: conf.Security{
-			HostIPCSet: conf.SeverityError,
-			HostPIDSet: conf.SeverityError,
-		},
-		Networking: conf.Networking{
-			HostNetworkSet: conf.SeverityWarning,
-			HostPortSet:    conf.SeverityError,
+		Checks: map[string]conf.Severity{
+			"hostIPCSet":     conf.SeverityError,
+			"hostNetworkSet": conf.SeverityWarning,
+			"hostPIDSet":     conf.SeverityError,
+			"hostPortSet":    conf.SeverityError,
 		},
 		Exemptions: []conf.Exemption{
 			conf.Exemption{
@@ -259,8 +249,8 @@ func TestExemption(t *testing.T) {
 		Errors:    uint(0),
 	}
 	expectedMessages := []*ResultMessage{
-		{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
 		{ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"},
+		{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
 	}
 
 	actualPodResult := ValidatePod(c, &pod.Spec, "foo", conf.Deployments)
diff --git a/pkg/validator/schema_test.go b/pkg/validator/schema_test.go
new file mode 100644
index 00000000..a6d8737b
--- /dev/null
+++ b/pkg/validator/schema_test.go
@@ -0,0 +1,258 @@
+package validator
+
+import (
+	"testing"
+
+	conf "github.com/fairwindsops/polaris/pkg/config"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+var customCheckExemptions = `
+checks:
+  foo: error
+customChecks:
+  foo:
+    successMessage: success!
+    failureMessage: fail!
+    target: Container
+    category: Security
+    schema:
+      properties:
+        image:
+          pattern: ^quay.io
+exemptions:
+- controllerNames:
+  - exempt
+  rules:
+  - foo
+`
+
+var resourceConfRanges = `
+checks:
+  memoryRequestsRange: error
+  memoryLimitsRange: warning
+customChecks:
+  memoryLimitsRange:
+    containers:
+      exclude:
+      - initContainer
+    successMessage: Memory limits are within the required range
+    failureMessage: Memory limits should be within the required range
+    category: Resources
+    target: Container
+    schema:
+      '$schema': http://json-schema.org/draft-07/schema
+      type: object
+      required:
+      - resources
+      properties:
+        resources:
+          type: object
+          required:
+          - limits
+          properties:
+            limits:
+              type: object
+              required:
+              - memory
+              properties:
+                memory:
+                  type: string
+                  resourceMinimum: 200M
+                  resourceMaximum: 6G
+  memoryRequestsRange:
+    successMessage: Memory requests are within the required range
+    failureMessage: Memory requests should be within the required range
+    category: Resources
+    target: Container
+    containers:
+      exclude:
+      - initContainer
+    schema:
+      '$schema': http://json-schema.org/draft-07/schema
+      type: object
+      required:
+      - resources
+      properties:
+        resources:
+          type: object
+          required:
+          - requests
+          properties:
+            requests:
+              required:
+              - memory
+              properties:
+                memory:
+                  type: string
+                  resourceMinimum: 200M
+                  resourceMaximum: 3G
+`
+
+func TestValidateResourcesPartiallyValid(t *testing.T) {
+	cpuRequest, err := resource.ParseQuantity("100m")
+	assert.NoError(t, err, "Error parsing quantity")
+
+	cpuLimit, err := resource.ParseQuantity("200m")
+	assert.NoError(t, err, "Error parsing quantity")
+
+	container := corev1.Container{
+		Name: "Empty",
+		Resources: corev1.ResourceRequirements{
+			Requests: corev1.ResourceList{
+				"cpu": cpuRequest,
+			},
+			Limits: corev1.ResourceList{
+				"cpu": cpuLimit,
+			},
+		},
+	}
+
+	expectedWarnings := []*ResultMessage{
+		{
+			ID:       "memoryLimitsRange",
+			Type:     "warning",
+			Message:  "Memory limits should be within the required range",
+			Category: "Resources",
+		},
+	}
+
+	expectedErrors := []*ResultMessage{
+		{
+			ID:       "memoryRequestsRange",
+			Type:     "error",
+			Message:  "Memory requests should be within the required range",
+			Category: "Resources",
+		},
+	}
+
+	expectedSuccesses := []*ResultMessage{}
+
+	testValidate(t, &container, &resourceConfRanges, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
+}
+
+func TestValidateResourcesInit(t *testing.T) {
+	cvEmpty := ContainerValidation{
+		Container:          &corev1.Container{},
+		ResourceValidation: &ResourceValidation{},
+	}
+	cvInit := ContainerValidation{
+		Container:          &corev1.Container{},
+		ResourceValidation: &ResourceValidation{},
+		IsInitContainer:    true,
+	}
+
+	parsedConf, err := conf.Parse([]byte(resourceConfRanges))
+	assert.NoError(t, err, "Expected no error when parsing config")
+
+	err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvEmpty)
+	if err != nil {
+		panic(err)
+	}
+	assert.Len(t, cvEmpty.Errors, 1)
+	assert.Len(t, cvEmpty.Warnings, 1)
+
+	err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvInit)
+	if err != nil {
+		panic(err)
+	}
+	assert.Len(t, cvInit.Errors, 0)
+}
+
+func TestValidateResourcesFullyValid(t *testing.T) {
+	cpuRequest, err := resource.ParseQuantity("300m")
+	assert.NoError(t, err, "Error parsing quantity")
+
+	cpuLimit, err := resource.ParseQuantity("400m")
+	assert.NoError(t, err, "Error parsing quantity")
+
+	memoryRequest, err := resource.ParseQuantity("400Mi")
+	assert.NoError(t, err, "Error parsing quantity")
+
+	memoryLimit, err := resource.ParseQuantity("500Mi")
+	assert.NoError(t, err, "Error parsing quantity")
+
+	container := corev1.Container{
+		Name: "Empty",
+		Resources: corev1.ResourceRequirements{
+			Requests: corev1.ResourceList{
+				"cpu":    cpuRequest,
+				"memory": memoryRequest,
+			},
+			Limits: corev1.ResourceList{
+				"cpu":    cpuLimit,
+				"memory": memoryLimit,
+			},
+		},
+	}
+
+	expectedSuccesses := []*ResultMessage{
+		{
+			ID:       "memoryRequestsRange",
+			Type:     "success",
+			Message:  "Memory requests are within the required range",
+			Category: "Resources",
+		},
+		{
+			ID:       "memoryLimitsRange",
+			Type:     "success",
+			Message:  "Memory limits are within the required range",
+			Category: "Resources",
+		},
+	}
+
+	testValidate(t, &container, &resourceConfRanges, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses)
+
+	expectedSuccesses = []*ResultMessage{
+		{
+			ID:       "cpuRequestsMissing",
+			Type:     "success",
+			Message:  "CPU requests are set",
+			Category: "Resources",
+		},
+		{
+			ID:       "memoryRequestsMissing",
+			Type:     "success",
+			Message:  "Memory requests are set",
+			Category: "Resources",
+		},
+		{
+			ID:       "cpuLimitsMissing",
+			Type:     "success",
+			Message:  "CPU limits are set",
+			Category: "Resources",
+		},
+		{
+			ID:       "memoryLimitsMissing",
+			Type:     "success",
+			Message:  "Memory limits are set",
+			Category: "Resources",
+		},
+	}
+
+	testValidate(t, &container, &resourceConfMinimal, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses)
+}
+
+func TestValidateCustomCheckExemptions(t *testing.T) {
+	container := corev1.Container{
+		Name:  "example",
+		Image: "hub.docker.com/foo",
+	}
+
+	expectedWarnings := []*ResultMessage{}
+	expectedErrors := []*ResultMessage{}
+	expectedSuccesses := []*ResultMessage{}
+	testValidate(t, &container, &customCheckExemptions, "exempt", expectedErrors, expectedWarnings, expectedSuccesses)
+
+	expectedErrors = []*ResultMessage{
+		{
+			ID:       "foo",
+			Type:     "error",
+			Message:  "fail!",
+			Category: "Security",
+		},
+	}
+	testValidate(t, &container, &customCheckExemptions, "notexempt", expectedErrors, expectedWarnings, expectedSuccesses)
+}
diff --git a/test/webhook_test.sh b/test/webhook_test.sh
index c5d299fe..9909ad32 100755
--- a/test/webhook_test.sh
+++ b/test/webhook_test.sh
@@ -70,6 +70,7 @@ for filename in test/webhook_cases/passing_test.*.yaml; do
     if ! kubectl apply -f $filename &> /dev/null; then
         ALL_TESTS_PASSED=0
         echo "Test Failed: Polaris prevented a deployment with no configuration issues." 
+        kubectl logs -n polaris $(kubectl get po -oname -n polaris | grep webhook)
     fi
 done
 
@@ -79,6 +80,7 @@ for filename in test/webhook_cases/failing_test.*.yaml; do
     if kubectl apply -f $filename &> /dev/null; then
         ALL_TESTS_PASSED=0
         echo "Test Failed: Polaris should have prevented this deployment due to configuration issues."
+        kubectl logs -n polaris $(kubectl get po -oname -n polaris | grep webhook)
     fi
 done