Repository: FairwindsOps/polaris
Pull request #234 (2019-12-19 21:33:41)

JSON schema checks
This is step 1 toward supporting custom checks written in JSON schema.

I think rewriting many of our current checks using JSON schema will help clean up the codebase a bunch too.


Patch:
diff --git a/checks/hostIPC.yaml b/checks/hostIPC.yaml
new file mode 100644
index 00000000..158931c0
--- /dev/null
+++ b/checks/hostIPC.yaml
@@ -0,0 +1,13 @@
+name: HostIPCSet
+id: hostIPCSet
+successMessage: Host IPC is not configured
+failureMessage: Host IPC should not be configured
+category: Security
+target: Pod
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  properties:
+    hostIPC:
+      not:
+        const: true
diff --git a/checks/hostNetwork.yaml b/checks/hostNetwork.yaml
new file mode 100644
index 00000000..191c174b
--- /dev/null
+++ b/checks/hostNetwork.yaml
@@ -0,0 +1,13 @@
+name: HostNetworkSet
+id: hostNetworkSet
+successMessage: Host network is not configured
+failureMessage: Host network should not be configured
+category: Networking
+target: Pod
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  properties:
+    hostNetwork:
+      not:
+        const: true
diff --git a/checks/hostPID.yaml b/checks/hostPID.yaml
new file mode 100644
index 00000000..6f3ed894
--- /dev/null
+++ b/checks/hostPID.yaml
@@ -0,0 +1,13 @@
+name: HostPIDSet
+id: hostPIDSet
+successMessage: Host PID is not configured
+failureMessage: Host PID should not be configured
+category: Security
+target: Pod
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  properties:
+    hostPID:
+      not:
+        const: true
diff --git a/checks/hostPortSet.yaml b/checks/hostPortSet.yaml
new file mode 100644
index 00000000..2c26cc2a
--- /dev/null
+++ b/checks/hostPortSet.yaml
@@ -0,0 +1,17 @@
+name: HostPortSet
+id: hostPortSet
+successMessage: Host port is not configured
+failureMessage: Host port should not be configured
+category: Networking
+target: Container
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  required:
+  properties:
+    ports:
+      type: array
+      items:
+        properties:
+          hostPort:
+            const: 0
diff --git a/checks/livenessProbe.yaml b/checks/livenessProbe.yaml
new file mode 100644
index 00000000..a60d719f
--- /dev/null
+++ b/checks/livenessProbe.yaml
@@ -0,0 +1,23 @@
+name: LivenessProbeMissing
+id: livenessProbeMissing
+successMessage: Liveness probe is configured
+failureMessage: Liveness probe should be configured
+category: Health Checks
+controllers:
+  exclude:
+  - Job
+  - CronJob
+containers:
+  exclude:
+  - initContainer
+target: Container
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  required:
+  - livenessProbe
+  properties:
+    livenessProbe:
+      type: object
+      not:
+        const: null
diff --git a/checks/notReadOnlyRootFileSystem.yaml b/checks/notReadOnlyRootFileSystem.yaml
new file mode 100644
index 00000000..509bf2e8
--- /dev/null
+++ b/checks/notReadOnlyRootFileSystem.yaml
@@ -0,0 +1,18 @@
+name: NotReadOnlyRootFileSystem
+id: notReadOnlyRootFileSystem
+successMessage: Filesystem is read only
+failureMessage: Filesystem should be read only
+category: Security
+target: Container
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  required:
+  - securityContext
+  properties:
+    securityContext:
+      required:
+      - readOnlyRootFilesystem
+      properties:
+        readOnlyRootFilesystem:
+          const: true
diff --git a/checks/privilegeEscalationAllowed.yaml b/checks/privilegeEscalationAllowed.yaml
new file mode 100644
index 00000000..912db92d
--- /dev/null
+++ b/checks/privilegeEscalationAllowed.yaml
@@ -0,0 +1,15 @@
+name: PrivilegeEscalationAllowed
+id: privilegeEscalationAllowed
+successMessage: Privilege escalation not allowed
+failureMessage: Privilege escalation should not be allowed
+category: Security
+target: Container
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  properties:
+    securityContext:
+      properties:
+        allowPrivilegeEscalation:
+          not:
+            const: true
diff --git a/checks/pullPolicyNotAlways.yaml b/checks/pullPolicyNotAlways.yaml
new file mode 100644
index 00000000..5aeac39a
--- /dev/null
+++ b/checks/pullPolicyNotAlways.yaml
@@ -0,0 +1,13 @@
+name: PullPolicyNotAlways
+id: pullPolicyNotAlways
+successMessage: Image pull policy is "Always"
+failureMessage: Image pull policy should be "Always"
+category: Images
+target: Container
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  required:
+  - imagePullPolicy
+  properties:
+    imagePullPolicy:
+      const: Always
diff --git a/checks/readinessProbe.yaml b/checks/readinessProbe.yaml
new file mode 100644
index 00000000..c9d0dba8
--- /dev/null
+++ b/checks/readinessProbe.yaml
@@ -0,0 +1,23 @@
+name: ReadinessProbeMissing
+id: readinessProbeMissing
+successMessage: Readiness probe is configured
+failureMessage: Readiness probe should be configured
+category: Health Checks
+controllers:
+  exclude:
+  - Job
+  - CronJob
+containers:
+  exclude:
+  - initContainer
+target: Container
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  required:
+  - readinessProbe
+  properties:
+    readinessProbe:
+      type: object
+      not:
+        const: null
diff --git a/checks/runAsPrivileged.yaml b/checks/runAsPrivileged.yaml
new file mode 100644
index 00000000..8c9b6ff1
--- /dev/null
+++ b/checks/runAsPrivileged.yaml
@@ -0,0 +1,15 @@
+name: RunAsPrivileged
+id: runAsPrivileged
+successMessage: Not running as privileged
+failureMessage: Should not be running as privileged
+category: Security
+target: Container
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  properties:
+    securityContext:
+      properties:
+        privileged:
+          not:
+            const: true
diff --git a/checks/runAsRootAllowed.yaml b/checks/runAsRootAllowed.yaml
new file mode 100644
index 00000000..468f51e6
--- /dev/null
+++ b/checks/runAsRootAllowed.yaml
@@ -0,0 +1,54 @@
+name: RunAsRootAllowed
+id: runAsRootAllowed
+successMessage: Is not allowed to run as root
+failureMessage: Should not be allowed to run as root
+category: Security
+target: Container
+schemaTarget: Pod
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  definitions:
+    goodSecurityContext:
+      type: object
+      anyOf:
+      - required:
+        - runAsUser
+        properties:
+          runAsUser:
+            minimum: 1
+      - required:
+        - runAsNonRoot
+        properties:
+          runAsNonRoot:
+            const: true
+    notBadSecurityContext:
+      type: object
+      properties:
+        runAsUser:
+          minimum: 1
+        runAsNonRoot:
+          const: true
+  type: object
+  anyOf:
+  # non-root specified at pod-level, and not overridden at container level
+  - required:
+    - securityContext
+    properties:
+      securityContext:
+        $ref: "#/definitions/goodSecurityContext"
+      containers:
+        type: array
+        items:
+          properties:
+            securityContext:
+              $ref: "#/definitions/notBadSecurityContext"
+  # non-root specified at container level
+  - properties:
+      containers:
+        type: array
+        items:
+          required:
+          - securityContext
+          properties:
+            securityContext:
+              $ref: "#/definitions/goodSecurityContext"
diff --git a/checks/tagNotSpecified.yaml b/checks/tagNotSpecified.yaml
new file mode 100644
index 00000000..322d806d
--- /dev/null
+++ b/checks/tagNotSpecified.yaml
@@ -0,0 +1,18 @@
+name: TagNotSpecified
+id: tagNotSpecified
+successMessage: Image tag is specified
+failureMessage: Image tag should be specified
+category: Images
+target: Container
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  required:
+  - image
+  allOf:
+  - properties:
+      image:
+        pattern: ^.+:.+$
+  - properties:
+      image:
+        not:
+          pattern: ^.+:latest$
diff --git a/pkg/config/exemptions.go b/pkg/config/exemptions.go
index b132585b..03f518ca 100644
--- a/pkg/config/exemptions.go
+++ b/pkg/config/exemptions.go
@@ -6,7 +6,10 @@ import (
 )
 
 // IsActionable determines whether a check is actionable given the current configuration
-func (conf *Configuration) IsActionable(subConf interface{}, ruleName, controllerName string) bool {
+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()
@@ -36,3 +39,31 @@ func (conf *Configuration) IsActionable(subConf interface{}, ruleName, controlle
 	}
 	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/supportedcontrollers.go b/pkg/config/supportedcontrollers.go
index 2efc20ac..808a9538 100644
--- a/pkg/config/supportedcontrollers.go
+++ b/pkg/config/supportedcontrollers.go
@@ -84,9 +84,9 @@ func (s *SupportedController) UnmarshalJSON(b []byte) error {
 		return err
 	}
 
-	*s, err = GetSupportedControllerFromString(j)
-	if err != nil {
-		return err
+	*s = GetSupportedControllerFromString(j)
+	if *s == Unsupported {
+		return fmt.Errorf("Unsupported controller kind: %s", j)
 	}
 	return nil
 }
@@ -124,21 +124,19 @@ func (s SupportedController) ListSupportedAPIVersions() []runtime.Object {
 }
 
 // GetSupportedControllerFromString fuzzy matches a string with a SupportedController Enum
-func GetSupportedControllerFromString(str string) (SupportedController, error) {
+func GetSupportedControllerFromString(str string) SupportedController {
 	lowerStr := strings.ToLower(str)
 	controller, keyFound := stringLookupForSupportedControllers[lowerStr]
-	if !keyFound || controller == Unsupported {
-		return 0, fmt.Errorf("Value ('%v') in configuration was not found in Supported Controllers: (%v)", str, strings.Join(ControllerStrings, ","))
+	if !keyFound {
+		controller = Unsupported
 	}
-	return controller, nil
+	return controller
 }
 
 // CheckIfKindIsConfiguredForValidation takes a kind (in string format) and checks if Polaris is configured to scan this type of controller
 func (c Configuration) CheckIfKindIsConfiguredForValidation(kind string) bool {
-	controller, err := GetSupportedControllerFromString(kind)
-	// if no errors then we found the kind in supported controller types
-	if err == nil {
-		// see if the kind exists in the controllers to scan config
+	controller := GetSupportedControllerFromString(kind)
+	if controller != Unsupported {
 		for _, controllerToScan := range c.ControllersToScan {
 			if controller == controllerToScan {
 				return true
diff --git a/pkg/validator/container.go b/pkg/validator/container.go
index 22117203..85beade5 100644
--- a/pkg/validator/container.go
+++ b/pkg/validator/container.go
@@ -16,7 +16,6 @@ package validator
 
 import (
 	"fmt"
-	"strings"
 
 	"github.com/fairwindsops/polaris/pkg/config"
 	"github.com/fairwindsops/polaris/pkg/validator/messages"
@@ -58,11 +57,13 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult,
 	}
 
 	cv.validateResources(conf, controllerName)
-	if !isInit && controllerType != config.Jobs && controllerType != config.CronJobs {
-		cv.validateHealthChecks(conf, controllerName)
+
+	err := applyContainerSchemaChecks(conf, controllerName, controllerType, &cv)
+	// FIXME: don't panic
+	if err != nil {
+		panic(err)
 	}
-	cv.validateImage(conf, controllerName)
-	cv.validateNetworking(conf, controllerName)
+
 	cv.validateSecurity(conf, controllerName)
 
 	cRes := ContainerResult{
@@ -153,80 +154,7 @@ func (cv *ContainerValidation) validateResourceRange(id, resourceName string, ra
 	}
 }
 
-func (cv *ContainerValidation) validateHealthChecks(conf *config.Configuration, controllerName string) {
-	category := messages.CategoryHealthChecks
-
-	name := "ReadinessProbeMissing"
-	// Don't validate readiness probes on init containers
-	if !cv.IsInitContainer && conf.IsActionable(conf.HealthChecks, name, controllerName) {
-		id := config.GetIDFromField(conf.HealthChecks, name)
-		if cv.Container.ReadinessProbe == nil {
-			cv.addFailure(messages.ReadinessProbeFailure, conf.HealthChecks.ReadinessProbeMissing, category, id)
-		} else {
-			cv.addSuccess(messages.ReadinessProbeSuccess, category, id)
-		}
-	}
-
-	name = "LivenessProbeMissing"
-	if conf.IsActionable(conf.HealthChecks, name, controllerName) {
-		id := config.GetIDFromField(conf.HealthChecks, "LivenessProbeMissing")
-		if cv.Container.LivenessProbe == nil {
-			cv.addFailure(messages.LivenessProbeFailure, conf.HealthChecks.LivenessProbeMissing, category, id)
-		} else {
-			cv.addSuccess(messages.LivenessProbeSuccess, category, id)
-		}
-	}
-}
-
-func (cv *ContainerValidation) validateImage(conf *config.Configuration, controllerName string) {
-	category := messages.CategoryImages
-
-	name := "PullPolicyNotAlways"
-	if conf.IsActionable(conf.Images, name, controllerName) {
-		id := config.GetIDFromField(conf.Images, name)
-		if cv.Container.ImagePullPolicy != corev1.PullAlways {
-			cv.addFailure(messages.ImagePullPolicyFailure, conf.Images.PullPolicyNotAlways, category, id)
-		} else {
-			cv.addSuccess(messages.ImagePullPolicySuccess, category, id)
-		}
-	}
-
-	name = "TagNotSpecified"
-	if conf.IsActionable(conf.Images, name, controllerName) {
-		id := config.GetIDFromField(conf.Images, name)
-		img := strings.Split(cv.Container.Image, ":")
-		if len(img) == 1 || img[1] == "latest" {
-			cv.addFailure(messages.ImageTagFailure, conf.Images.TagNotSpecified, category, id)
-		} else {
-			cv.addSuccess(messages.ImageTagSuccess, category, id)
-		}
-	}
-}
-
-func (cv *ContainerValidation) validateNetworking(conf *config.Configuration, controllerName string) {
-	category := messages.CategoryNetworking
-
-	name := "HostPortSet"
-	if conf.IsActionable(conf.Networking, name, controllerName) {
-		hostPortSet := false
-		for _, port := range cv.Container.Ports {
-			if port.HostPort != 0 {
-				hostPortSet = true
-				break
-			}
-		}
-
-		id := config.GetIDFromField(conf.Networking, name)
-		if hostPortSet {
-			cv.addFailure(messages.HostPortFailure, conf.Networking.HostPortSet, category, id)
-		} else {
-			cv.addSuccess(messages.HostPortSuccess, category, id)
-		}
-	}
-}
-
 func (cv *ContainerValidation) validateSecurity(conf *config.Configuration, controllerName string) {
-	category := messages.CategorySecurity
 	securityContext := cv.Container.SecurityContext
 	podSecurityContext := cv.parentPodSpec.SecurityContext
 
@@ -240,55 +168,7 @@ func (cv *ContainerValidation) validateSecurity(conf *config.Configuration, cont
 		podSecurityContext = &corev1.PodSecurityContext{}
 	}
 
-	name := "RunAsRootAllowed"
-	if conf.IsActionable(conf.Security, name, controllerName) {
-		id := config.GetIDFromField(conf.Security, name)
-		runAsRootSuccess := false
-		if getBoolValue(securityContext.RunAsNonRoot) || (securityContext.RunAsUser != nil && *securityContext.RunAsUser > 0) {
-			// Check if the container is explicitly set to True (pass)
-			runAsRootSuccess = true
-		} else if securityContext.RunAsNonRoot == nil && securityContext.RunAsUser == nil {
-			// Or if the container values are unset, check the pod values
-			runAsRootSuccess = getBoolValue(podSecurityContext.RunAsNonRoot) || (podSecurityContext.RunAsUser != nil && *podSecurityContext.RunAsUser > 0)
-		}
-		if runAsRootSuccess {
-			cv.addSuccess(messages.RunAsRootSuccess, category, id)
-		} else {
-			cv.addFailure(messages.RunAsRootFailure, conf.Security.RunAsRootAllowed, category, id)
-		}
-	}
-
-	name = "RunAsPrivileged"
-	if conf.IsActionable(conf.Security, name, controllerName) {
-		id := config.GetIDFromField(conf.Security, name)
-		if getBoolValue(securityContext.Privileged) {
-			cv.addFailure(messages.RunAsPrivilegedFailure, conf.Security.RunAsPrivileged, category, id)
-		} else {
-			cv.addSuccess(messages.RunAsPrivilegedSuccess, category, id)
-		}
-	}
-
-	name = "NotReadOnlyRootFileSystem"
-	if conf.IsActionable(conf.Security, name, controllerName) {
-		id := config.GetIDFromField(conf.Security, name)
-		if getBoolValue(securityContext.ReadOnlyRootFilesystem) {
-			cv.addSuccess(messages.ReadOnlyFilesystemSuccess, category, id)
-		} else {
-			cv.addFailure(messages.ReadOnlyFilesystemFailure, conf.Security.NotReadOnlyRootFileSystem, category, id)
-		}
-	}
-
-	name = "PrivilegeEscalationAllowed"
-	if conf.IsActionable(conf.Security, name, controllerName) {
-		id := config.GetIDFromField(conf.Security, name)
-		if getBoolValue(securityContext.AllowPrivilegeEscalation) {
-			cv.addFailure(messages.PrivilegeEscalationFailure, conf.Security.PrivilegeEscalationAllowed, category, id)
-		} else {
-			cv.addSuccess(messages.PrivilegeEscalationSuccess, category, id)
-		}
-	}
-
-	name = "Capabilities"
+	name := "Capabilities"
 	if conf.IsActionable(conf.Security, name, controllerName) {
 		cv.validateCapabilities(&conf.Security.Capabilities.Warning, &conf.Security.Capabilities.Error)
 	}
@@ -417,12 +297,3 @@ func capContains(list []corev1.Capability, val corev1.Capability) bool {
 
 	return false
 }
-
-// getBoolValue returns false if nil or returns the value of the bool pointer
-func getBoolValue(val *bool) bool {
-	if val == nil {
-		return false
-	}
-
-	return *val
-}
diff --git a/pkg/validator/controller.go b/pkg/validator/controller.go
index f92f7521..d3ac89c1 100644
--- a/pkg/validator/controller.go
+++ b/pkg/validator/controller.go
@@ -31,11 +31,12 @@ func ValidateController(conf conf.Configuration, controller controller.Interface
 	controllerType := controller.GetType()
 	pod := controller.GetPodSpec()
 	podResult := ValidatePod(conf, pod, controller.GetName(), controllerType)
-	return ControllerResult{
+	result := ControllerResult{
 		Type:      controllerType.String(),
 		Name:      controller.GetName(),
 		PodResult: podResult,
 	}
+	return result
 }
 
 // ValidateControllers validates that each deployment conforms to the Polaris config,
diff --git a/pkg/validator/pod.go b/pkg/validator/pod.go
index 52ed9e9f..c9130a9c 100644
--- a/pkg/validator/pod.go
+++ b/pkg/validator/pod.go
@@ -16,7 +16,6 @@ package validator
 
 import (
 	"github.com/fairwindsops/polaris/pkg/config"
-	"github.com/fairwindsops/polaris/pkg/validator/messages"
 	corev1 "k8s.io/api/core/v1"
 )
 
@@ -33,8 +32,11 @@ func ValidatePod(conf config.Configuration, pod *corev1.PodSpec, controllerName
 		ResourceValidation: &ResourceValidation{},
 	}
 
-	pv.validateSecurity(&conf, controllerName)
-	pv.validateNetworking(&conf, controllerName)
+	err := applyPodSchemaChecks(&conf, pod, controllerName, controllerType, &pv)
+	// FIXME: don't panic
+	if err != nil {
+		panic(err)
+	}
 
 	pRes := PodResult{
 		Messages:         pv.messages(),
@@ -59,41 +61,3 @@ func (pv *PodValidation) validateContainers(containers []corev1.Container, pRes
 		pRes.ContainerResults = append(pRes.ContainerResults, cRes)
 	}
 }
-
-func (pv *PodValidation) validateSecurity(conf *config.Configuration, controllerName string) {
-	category := messages.CategorySecurity
-
-	name := "HostIPCSet"
-	if conf.IsActionable(conf.Security, name, controllerName) {
-		id := config.GetIDFromField(conf.Security, name)
-		if pv.Pod.HostIPC {
-			pv.addFailure(messages.HostIPCFailure, conf.Security.HostIPCSet, category, id)
-		} else {
-			pv.addSuccess(messages.HostIPCSuccess, category, id)
-		}
-	}
-
-	name = "HostPIDSet"
-	if conf.IsActionable(conf.Security, name, controllerName) {
-		id := config.GetIDFromField(conf.Security, name)
-		if pv.Pod.HostPID {
-			pv.addFailure(messages.HostPIDFailure, conf.Security.HostPIDSet, category, id)
-		} else {
-			pv.addSuccess(messages.HostPIDSuccess, category, id)
-		}
-	}
-}
-
-func (pv *PodValidation) validateNetworking(conf *config.Configuration, controllerName string) {
-	category := messages.CategoryNetworking
-
-	name := "HostNetworkSet"
-	if conf.IsActionable(conf.Networking, name, controllerName) {
-		id := config.GetIDFromField(conf.Networking, name)
-		if pv.Pod.HostNetwork {
-			pv.addFailure(messages.HostNetworkFailure, conf.Networking.HostNetworkSet, category, id)
-		} else {
-			pv.addSuccess(messages.HostNetworkSuccess, category, id)
-		}
-	}
-}
diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go
new file mode 100644
index 00000000..f6762381
--- /dev/null
+++ b/pkg/validator/schema.go
@@ -0,0 +1,204 @@
+package validator
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+
+	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{},
+	}
+	// We explicitly set the order to avoid thrash in the
+	// tests as we migrate toward JSON schema
+	checkOrder = []string{
+		// Pod checks
+		"hostIPC",
+		"hostPID",
+		"hostNetwork",
+		// Container checks
+		"readinessProbe",
+		"livenessProbe",
+		"pullPolicyNotAlways",
+		"tagNotSpecified",
+		"hostPortSet",
+		"runAsRootAllowed",
+		"runAsPrivileged",
+		"notReadOnlyRootFileSystem",
+		"privilegeEscalationAllowed",
+	}
+)
+
+func init() {
+	schemaBox = packr.New("Schemas", "../../checks")
+	for _, file := range checkOrder {
+		contents, err := schemaBox.Find(file + ".yaml")
+		if err != nil {
+			panic(err)
+		}
+		check, err := parseCheck(contents)
+		if err != nil {
+			panic(err)
+		}
+		checks[check.Target] = append(checks[check.Target], check)
+	}
+}
+
+func parseCheck(rawBytes []byte) (SchemaCheck, error) {
+	reader := bytes.NewReader(rawBytes)
+	check := SchemaCheck{}
+	d := yaml.NewYAMLOrJSONDecoder(reader, 4096)
+	for {
+		if err := d.Decode(&check); err != nil {
+			if err == io.EOF {
+				return check, nil
+			}
+			return check, fmt.Errorf("Decoding schema check failed: %v", err)
+		}
+	}
+}
+
+func (check SchemaCheck) checkPod(pod *corev1.PodSpec) (bool, error) {
+	return check.checkObject(pod)
+}
+
+func (check SchemaCheck) checkContainer(container *corev1.Container) (bool, error) {
+	return check.checkObject(container)
+}
+
+func (check SchemaCheck) checkObject(obj interface{}) (bool, error) {
+	bytes, err := json.Marshal(obj)
+	if err != nil {
+		return false, err
+	}
+	errors, err := check.Schema.ValidateBytes(bytes)
+	return len(errors) == 0, err
+}
+
+func (check SchemaCheck) isActionable(target target, controllerType config.SupportedController, isInit bool) bool {
+	if check.Target != target {
+		return false
+	}
+	isIncluded := len(check.Controllers.Include) == 0
+	for _, inclusion := range check.Controllers.Include {
+		if config.GetSupportedControllerFromString(inclusion) == controllerType {
+			isIncluded = true
+			break
+		}
+	}
+	if !isIncluded {
+		return false
+	}
+	for _, exclusion := range check.Controllers.Exclude {
+		if config.GetSupportedControllerFromString(exclusion) == controllerType {
+			return false
+		}
+	}
+	if check.Target == targetContainer {
+		isIncluded := len(check.Containers.Include) == 0
+		for _, inclusion := range check.Containers.Include {
+			if (inclusion == "initContainer" && isInit) || (inclusion == "container" && !isInit) {
+				isIncluded = true
+				break
+			}
+		}
+		if !isIncluded {
+			return false
+		}
+		for _, exclusion := range check.Containers.Exclude {
+			if (exclusion == "initContainer" && isInit) || (exclusion == "container" && !isInit) {
+				return false
+			}
+		}
+	}
+	return true
+}
+
+func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController, pv *PodValidation) error {
+	for _, check := range checks[targetPod] {
+		if !conf.IsActionable(check.Category, check.Name, controllerName) {
+			continue
+		}
+		if !check.isActionable(targetPod, controllerType, false) {
+			continue
+		}
+		severity := conf.GetSeverity(check.Category, check.Name)
+		passes, err := check.checkPod(pod)
+		if err != nil {
+			return err
+		}
+		if passes {
+			pv.addSuccess(check.SuccessMessage, check.Category, check.ID)
+		} else {
+			pv.addFailure(check.FailureMessage, severity, check.Category, check.ID)
+		}
+	}
+	return nil
+}
+
+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) {
+			continue
+		}
+		if !check.isActionable(targetContainer, controllerType, cv.IsInitContainer) {
+			continue
+		}
+		severity := conf.GetSeverity(check.Category, check.Name)
+		var passes bool
+		var err error
+		if check.SchemaTarget == targetPod {
+			cv.parentPodSpec.Containers = []corev1.Container{*cv.Container}
+			passes, err = check.checkPod(&cv.parentPodSpec)
+			cv.parentPodSpec.Containers = []corev1.Container{}
+		} else {
+			passes, err = check.checkContainer(cv.Container)
+		}
+		if err != nil {
+			return err
+		}
+		if passes {
+			cv.addSuccess(check.SuccessMessage, check.Category, check.ID)
+		} else {
+			cv.addFailure(check.FailureMessage, severity, check.Category, check.ID)
+		}
+	}
+	return nil
+}
diff --git a/pkg/webhook/validator.go b/pkg/webhook/validator.go
index b37d73af..60e7f842 100644
--- a/pkg/webhook/validator.go
+++ b/pkg/webhook/validator.go
@@ -103,11 +103,11 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
 		}
 
 		// We should never hit this case unless something is misconfiured in CheckIfKindIsConfiguredForValidation
-		controllerType, err := config.GetSupportedControllerFromString(req.AdmissionRequest.Kind.Kind)
-		if err != nil {
-			msg := fmt.Errorf("Unexpected error occurred. Expected Kind to be a supported type (%s)", req.AdmissionRequest.Kind.Kind)
+		controllerType := config.GetSupportedControllerFromString(req.AdmissionRequest.Kind.Kind)
+		if controllerType == config.Unsupported {
+			msg := fmt.Errorf("Expected Kind (%s) to be a supported type", req.AdmissionRequest.Kind.Kind)
 			logrus.Error(msg)
-			return admission.ErrorResponse(http.StatusInternalServerError, err)
+			return admission.ErrorResponse(http.StatusInternalServerError, msg)
 		}
 
 		// For each type, perform the scan


Test patch:
diff --git a/pkg/config/supportedcontrollers_test.go b/pkg/config/supportedcontrollers_test.go
index 0aeeec2f..694f4ccf 100644
--- a/pkg/config/supportedcontrollers_test.go
+++ b/pkg/config/supportedcontrollers_test.go
@@ -51,19 +51,17 @@ func TestUnmarshalSupportedControllers(t *testing.T) {
 
 func TestMarshalSupportedControllers(t *testing.T) {
 	for idx, controllerString := range ControllerStrings {
-		controllerType, err := GetSupportedControllerFromString(controllerString)
+		controllerType := GetSupportedControllerFromString(controllerString)
 		if idx == 0 {
-			if err == nil {
-				t.Errorf("Expected first element (%s) to fail as a non-valid supported controller. Reserved for 'Unsupported'", controllerString)
-			}
-		} else if err != nil {
-			t.Errorf("Unable to take the configured string (%s) and convert into Enum; Error: (%s)", controllerString, err)
+			assert.Equal(t, SupportedController(0), controllerType)
+		} else {
+			assert.NotEqual(t, SupportedController(0), controllerType)
 		}
 
 		object := checkMarshal{
 			Controllers: []SupportedController{controllerType},
 		}
-		_, err = json.Marshal(object)
+		_, err := json.Marshal(object)
 		if idx == 0 {
 			if err == nil {
 				t.Errorf("Expected (%s) to throw an error. Reserving the first element in the enum to be an invalid config", controllerString)
@@ -77,10 +75,8 @@ func TestMarshalSupportedControllers(t *testing.T) {
 func TestCheckIfControllerKindIsConfiguredForValidation(t *testing.T) {
 	config := Configuration{}
 	for _, controllerString := range ControllerStrings[1:] {
-		controllerEnum, err := GetSupportedControllerFromString(controllerString)
-		if err != nil {
-			t.Errorf("Expected controller string (%s) to be convertable into enum: (%s)", controllerString, err)
-		}
+		controllerEnum := GetSupportedControllerFromString(controllerString)
+		assert.NotEqual(t, SupportedController(0), controllerEnum)
 		config.ControllersToScan = append(config.ControllersToScan, controllerEnum)
 	}
 
@@ -116,10 +112,7 @@ func TestGetSupportedControllerFromString(t *testing.T) {
 	}
 
 	for inputString, expectedType := range fixture {
-		resolvedType, err := GetSupportedControllerFromString(inputString)
-		if expectedType == Unsupported && err == nil {
-			t.Errorf("Expected (%s) to resolve to an unsupported type and throw an error.", inputString)
-		}
+		resolvedType := GetSupportedControllerFromString(inputString)
 		assert.Equal(t, expectedType, resolvedType, fmt.Sprintf("Expected (%s) to return (%s) controller type.", inputString, expectedType))
 	}
 }
diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go
index 7d5da0c8..20ff2c17 100644
--- a/pkg/validator/container_test.go
+++ b/pkg/validator/container_test.go
@@ -409,17 +409,21 @@ func TestValidateHealthChecks(t *testing.T) {
 		{name: "probes configured, but not required", probes: p2, cv: goodCV, errors: &f1},
 	}
 
-	for _, tt := range testCases {
+	for idx, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
-			tt.cv.validateHealthChecks(&conf.Configuration{HealthChecks: tt.probes}, "")
+			err := applyContainerSchemaChecks(&conf.Configuration{HealthChecks: tt.probes}, "", conf.Deployments, &tt.cv)
+			if err != nil {
+				panic(err)
+			}
+			message := fmt.Sprintf("test case %d", idx)
 
 			if tt.warnings != nil {
-				assert.Len(t, tt.cv.Warnings, len(*tt.warnings))
-				assert.ElementsMatch(t, tt.cv.Warnings, *tt.warnings)
+				assert.Len(t, tt.cv.Warnings, len(*tt.warnings), message)
+				assert.ElementsMatch(t, tt.cv.Warnings, *tt.warnings, message)
 			}
 
-			assert.Len(t, tt.cv.Errors, len(*tt.errors))
-			assert.ElementsMatch(t, tt.cv.Errors, *tt.errors)
+			assert.Len(t, tt.cv.Errors, len(*tt.errors), message)
+			assert.ElementsMatch(t, tt.cv.Errors, *tt.errors, message)
 		})
 	}
 }
@@ -524,7 +528,10 @@ func TestValidateImage(t *testing.T) {
 	for _, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
 			tt.cv = resetCV(tt.cv)
-			tt.cv.validateImage(&conf.Configuration{Images: tt.image}, "")
+			err := applyContainerSchemaChecks(&conf.Configuration{Images: tt.image}, "", conf.Deployments, &tt.cv)
+			if err != nil {
+				panic(err)
+			}
 			assert.Len(t, tt.cv.Errors, len(tt.expected))
 			assert.ElementsMatch(t, tt.cv.Errors, tt.expected)
 		})
@@ -643,7 +650,10 @@ func TestValidateNetworking(t *testing.T) {
 	for _, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
 			tt.cv = resetCV(tt.cv)
-			tt.cv.validateNetworking(&conf.Configuration{Networking: tt.networkConf}, "")
+			err := applyContainerSchemaChecks(&conf.Configuration{Networking: tt.networkConf}, "", conf.Deployments, &tt.cv)
+			if err != nil {
+				panic(err)
+			}
 			assert.Len(t, tt.cv.messages(), len(tt.expectedMessages))
 			assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages)
 		})
@@ -1133,6 +1143,10 @@ 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)
+			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)
@@ -1151,10 +1165,12 @@ func TestValidateRunAsRoot(t *testing.T) {
 		},
 	}
 	testCases := []struct {
+		name    string
 		cv      ContainerValidation
 		message ResultMessage
 	}{
 		{
+			name: "pod=false,container=nil",
 			cv: ContainerValidation{
 				ResourceValidation: &ResourceValidation{},
 				Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
@@ -1174,6 +1190,7 @@ func TestValidateRunAsRoot(t *testing.T) {
 			},
 		},
 		{
+			name: "pod=false,container=true",
 			cv: ContainerValidation{
 				ResourceValidation: &ResourceValidation{},
 				Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
@@ -1193,6 +1210,7 @@ func TestValidateRunAsRoot(t *testing.T) {
 			},
 		},
 		{
+			name: "pod=nil,container=runAsUser",
 			cv: ContainerValidation{
 				ResourceValidation: &ResourceValidation{},
 				Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
@@ -1207,6 +1225,7 @@ func TestValidateRunAsRoot(t *testing.T) {
 			},
 		},
 		{
+			name: "pod=runAsUser,container=nil",
 			cv: ContainerValidation{
 				ResourceValidation: &ResourceValidation{},
 				Container:          &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{}},
@@ -1224,6 +1243,7 @@ func TestValidateRunAsRoot(t *testing.T) {
 			},
 		},
 		{
+			name: "pod=runAsUser,container=runAsUser0",
 			cv: ContainerValidation{
 				ResourceValidation: &ResourceValidation{},
 				Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
@@ -1243,6 +1263,7 @@ func TestValidateRunAsRoot(t *testing.T) {
 			},
 		},
 		{
+			name: "pod=false,container=runAsUser",
 			cv: ContainerValidation{
 				ResourceValidation: &ResourceValidation{},
 				Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
@@ -1263,9 +1284,16 @@ func TestValidateRunAsRoot(t *testing.T) {
 		},
 	}
 	for idx, tt := range testCases {
-		tt.cv.validateSecurity(&config, "")
-		assert.Len(t, tt.cv.messages(), 1)
-		assert.Equal(t, &tt.message, tt.cv.messages()[0], fmt.Sprintf("Test case %d failed", idx))
+		t.Run(tt.name, func(t *testing.T) {
+			err := applyContainerSchemaChecks(&config, "", conf.Deployments, &tt.cv)
+			if err != nil {
+				panic(err)
+			}
+			assert.Len(t, tt.cv.messages(), 1)
+			if len(tt.cv.messages()) > 0 {
+				assert.Equal(t, &tt.message, tt.cv.messages()[0], fmt.Sprintf("Test case %d failed", idx))
+			}
+		})
 	}
 }