Repository: FairwindsOps/polaris
Pull request #236 (2020-01-03 20:12:14)

Major Refactor
This is a PR that uses #235 as a base.

Some improvements here:
* stop doing error-prone count tracking (where counter state changes with each check), and create helper functions instead
* Change results from an array to a map from checkID -> result
* Instead of passing around ControllerName and ControllerType, pass the controller itself
* Rename some things (e.g. ClusterSummary -> ClusterInfo, controller.Type -> controller.Kind)


codecov[bot]: # [Codecov](https://codecov.io/gh/FairwindsOps/polaris/pull/236?src=pr&el=h1) Report
> Merging [#236](https://codecov.io/gh/FairwindsOps/polaris/pull/236?src=pr&el=desc) into [rb/custom-checks-2](https://codecov.io/gh/FairwindsOps/polaris/commit/e7baeac2e5c6e55924bdd750778ebefc0ed57aa7?src=pr&el=desc) will **decrease** coverage by `9.43%`.
> The diff coverage is `54.75%`.

[![Impacted file tree graph](https://codecov.io/gh/FairwindsOps/polaris/pull/236/graphs/tree.svg?width=650&token=RR1ySHa61e&height=150&src=pr)](https://codecov.io/gh/FairwindsOps/polaris/pull/236?src=pr&el=tree)

```diff
@@                  Coverage Diff                   @@
##           rb/custom-checks-2     #236      +/-   ##
======================================================
- Coverage               64.58%   55.15%   -9.44%     
======================================================
  Files                      14       13       -1     
  Lines                     658      640      -18     
======================================================
- Hits                      425      353      -72     
- Misses                    203      251      +48     
- Partials                   30       36       +6
```


| [Impacted Files](https://codecov.io/gh/FairwindsOps/polaris/pull/236?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/config/supportedcontrollers.go](https://codecov.io/gh/FairwindsOps/polaris/pull/236/diff?src=pr&el=tree#diff-cGtnL2NvbmZpZy9zdXBwb3J0ZWRjb250cm9sbGVycy5nbw==) | `55.35% <0%> (-3.14%)` | :arrow_down: |
| [pkg/validator/fullaudit.go](https://codecov.io/gh/FairwindsOps/polaris/pull/236/diff?src=pr&el=tree#diff-cGtnL3ZhbGlkYXRvci9mdWxsYXVkaXQuZ28=) | `53.19% <12%> (-46.81%)` | :arrow_down: |
| [pkg/validator/summary.go](https://codecov.io/gh/FairwindsOps/polaris/pull/236/diff?src=pr&el=tree#diff-cGtnL3ZhbGlkYXRvci9zdW1tYXJ5Lmdv) | `49% <49%> (ø)` | |
| [pkg/validator/pod.go](https://codecov.io/gh/FairwindsOps/polaris/pull/236/diff?src=pr&el=tree#diff-cGtnL3ZhbGlkYXRvci9wb2QuZ28=) | `63.63% <62.5%> (-26.84%)` | :arrow_down: |
| [pkg/validator/container.go](https://codecov.io/gh/FairwindsOps/polaris/pull/236/diff?src=pr&el=tree#diff-cGtnL3ZhbGlkYXRvci9jb250YWluZXIuZ28=) | `72.72% <75%> (+2.13%)` | :arrow_up: |
| [pkg/validator/schema.go](https://codecov.io/gh/FairwindsOps/polaris/pull/236/diff?src=pr&el=tree#diff-cGtnL3ZhbGlkYXRvci9zY2hlbWEuZ28=) | `77.64% <75%> (-5.02%)` | :arrow_down: |
| [pkg/validator/controller.go](https://codecov.io/gh/FairwindsOps/polaris/pull/236/diff?src=pr&el=tree#diff-cGtnL3ZhbGlkYXRvci9jb250cm9sbGVyLmdv) | `86.2% <76.47%> (-6.11%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/FairwindsOps/polaris/pull/236?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/FairwindsOps/polaris/pull/236?src=pr&el=footer). Last update [e7baeac...ff2a967](https://codecov.io/gh/FairwindsOps/polaris/pull/236?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/examples/output.json b/examples/output.json
new file mode 100644
index 00000000..1765e1d4
--- /dev/null
+++ b/examples/output.json
@@ -0,0 +1,281 @@
+{
+  "PolarisOutputVersion": "1.0",
+  "AuditTime": "0001-01-01T00:00:00Z",
+  "SourceType": "Path",
+  "SourceName": "deploy/",
+  "DisplayName": "deploy/",
+  "ClusterInfo": {
+    "Version": "unknown",
+    "Nodes": 0,
+    "Pods": 0,
+    "Namespaces": 2,
+    "Deployments": 2,
+    "StatefulSets": 0,
+    "DaemonSets": 0,
+    "Jobs": 0,
+    "CronJobs": 0,
+    "ReplicationControllers": 0
+  },
+  "Results": [
+    {
+      "Name": "polaris-dashboard",
+      "Namespace": "polaris",
+      "Kind": "Deployment",
+      "Messages": {},
+      "PodResult": {
+        "Name": "",
+        "Messages": {
+          "hostIPCSet": {
+            "ID": "hostIPCSet",
+            "Message": "Host IPC is not configured",
+            "Success": true,
+            "Severity": "error",
+            "Category": "Security"
+          },
+          "hostNetworkSet": {
+            "ID": "hostNetworkSet",
+            "Message": "Host network is not configured",
+            "Success": true,
+            "Severity": "warning",
+            "Category": "Networking"
+          },
+          "hostPIDSet": {
+            "ID": "hostPIDSet",
+            "Message": "Host PID is not configured",
+            "Success": true,
+            "Severity": "error",
+            "Category": "Security"
+          }
+        },
+        "ContainerResults": [
+          {
+            "Name": "dashboard",
+            "Messages": {
+              "cpuLimitsMissing": {
+                "ID": "cpuLimitsMissing",
+                "Message": "CPU limits are set",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Resources"
+              },
+              "cpuRequestsMissing": {
+                "ID": "cpuRequestsMissing",
+                "Message": "CPU requests are set",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Resources"
+              },
+              "dangerousCapabilities": {
+                "ID": "dangerousCapabilities",
+                "Message": "Container does not have any dangerous capabilities",
+                "Success": true,
+                "Severity": "error",
+                "Category": "Security"
+              },
+              "hostPortSet": {
+                "ID": "hostPortSet",
+                "Message": "Host port is not configured",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Networking"
+              },
+              "insecureCapabilities": {
+                "ID": "insecureCapabilities",
+                "Message": "Container does not have any insecure capabilities",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Security"
+              },
+              "livenessProbeMissing": {
+                "ID": "livenessProbeMissing",
+                "Message": "Liveness probe is configured",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Health Checks"
+              },
+              "memoryLimitsMissing": {
+                "ID": "memoryLimitsMissing",
+                "Message": "Memory limits are set",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Resources"
+              },
+              "memoryRequestsMissing": {
+                "ID": "memoryRequestsMissing",
+                "Message": "Memory requests are set",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Resources"
+              },
+              "privilegeEscalationAllowed": {
+                "ID": "privilegeEscalationAllowed",
+                "Message": "Privilege escalation not allowed",
+                "Success": true,
+                "Severity": "error",
+                "Category": "Security"
+              },
+              "readinessProbeMissing": {
+                "ID": "readinessProbeMissing",
+                "Message": "Readiness probe is configured",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Health Checks"
+              },
+              "runAsPrivileged": {
+                "ID": "runAsPrivileged",
+                "Message": "Not running as privileged",
+                "Success": true,
+                "Severity": "error",
+                "Category": "Security"
+              },
+              "runAsRootAllowed": {
+                "ID": "runAsRootAllowed",
+                "Message": "Is not allowed to run as root",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Security"
+              },
+              "tagNotSpecified": {
+                "ID": "tagNotSpecified",
+                "Message": "Image tag is specified",
+                "Success": true,
+                "Severity": "error",
+                "Category": "Images"
+              }
+            }
+          }
+        ]
+      }
+    },
+    {
+      "Name": "polaris-webhook",
+      "Namespace": "polaris",
+      "Kind": "Deployment",
+      "Messages": {},
+      "PodResult": {
+        "Name": "",
+        "Messages": {
+          "hostIPCSet": {
+            "ID": "hostIPCSet",
+            "Message": "Host IPC is not configured",
+            "Success": true,
+            "Severity": "error",
+            "Category": "Security"
+          },
+          "hostNetworkSet": {
+            "ID": "hostNetworkSet",
+            "Message": "Host network is not configured",
+            "Success": true,
+            "Severity": "warning",
+            "Category": "Networking"
+          },
+          "hostPIDSet": {
+            "ID": "hostPIDSet",
+            "Message": "Host PID is not configured",
+            "Success": true,
+            "Severity": "error",
+            "Category": "Security"
+          }
+        },
+        "ContainerResults": [
+          {
+            "Name": "webhook",
+            "Messages": {
+              "cpuLimitsMissing": {
+                "ID": "cpuLimitsMissing",
+                "Message": "CPU limits are set",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Resources"
+              },
+              "cpuRequestsMissing": {
+                "ID": "cpuRequestsMissing",
+                "Message": "CPU requests are set",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Resources"
+              },
+              "dangerousCapabilities": {
+                "ID": "dangerousCapabilities",
+                "Message": "Container does not have any dangerous capabilities",
+                "Success": true,
+                "Severity": "error",
+                "Category": "Security"
+              },
+              "hostPortSet": {
+                "ID": "hostPortSet",
+                "Message": "Host port is not configured",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Networking"
+              },
+              "insecureCapabilities": {
+                "ID": "insecureCapabilities",
+                "Message": "Container does not have any insecure capabilities",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Security"
+              },
+              "livenessProbeMissing": {
+                "ID": "livenessProbeMissing",
+                "Message": "Liveness probe is configured",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Health Checks"
+              },
+              "memoryLimitsMissing": {
+                "ID": "memoryLimitsMissing",
+                "Message": "Memory limits are set",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Resources"
+              },
+              "memoryRequestsMissing": {
+                "ID": "memoryRequestsMissing",
+                "Message": "Memory requests are set",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Resources"
+              },
+              "privilegeEscalationAllowed": {
+                "ID": "privilegeEscalationAllowed",
+                "Message": "Privilege escalation not allowed",
+                "Success": true,
+                "Severity": "error",
+                "Category": "Security"
+              },
+              "readinessProbeMissing": {
+                "ID": "readinessProbeMissing",
+                "Message": "Readiness probe is configured",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Health Checks"
+              },
+              "runAsPrivileged": {
+                "ID": "runAsPrivileged",
+                "Message": "Not running as privileged",
+                "Success": true,
+                "Severity": "error",
+                "Category": "Security"
+              },
+              "runAsRootAllowed": {
+                "ID": "runAsRootAllowed",
+                "Message": "Is not allowed to run as root",
+                "Success": true,
+                "Severity": "warning",
+                "Category": "Security"
+              },
+              "tagNotSpecified": {
+                "ID": "tagNotSpecified",
+                "Message": "Image tag is specified",
+                "Success": true,
+                "Severity": "error",
+                "Category": "Images"
+              }
+            }
+          }
+        ]
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/go.sum b/go.sum
index a46c4ae4..59ea8831 100644
--- a/go.sum
+++ b/go.sum
@@ -458,6 +458,7 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 k8s.io/api v0.0.0-20181213150558-05914d821849 h1:WZFcFPXmLR7g5CxQNmjWv0mg8qulJLxDghbzS4pQtzY=
 k8s.io/api v0.0.0-20181213150558-05914d821849/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
+k8s.io/api v0.17.0 h1:H9d/lw+VkZKEVIUc8F3wgiQ+FUXTTr21M87jXLU7yqM=
 k8s.io/apimachinery v0.0.0-20181127025237-2b1284ed4c93 h1:tT6oQBi0qwLbbZSfDkdIsb23EwaLY85hoAV4SpXfdao=
 k8s.io/apimachinery v0.0.0-20181127025237-2b1284ed4c93/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
 k8s.io/client-go v0.0.0-20181213151034-8d9ed539ba31 h1:OH3z6khCtxnJBAc0C5CMYWLl1CoK5R5fngX7wrwdN5c=
diff --git a/main.go b/main.go
index a2d9ada4..124847ba 100644
--- a/main.go
+++ b/main.go
@@ -107,11 +107,13 @@ func main() {
 	} else if *audit {
 		auditData := runAndReportAudit(c, *auditPath, *auditOutputFile, *auditOutputURL, *auditOutputFormat)
 
-		if *setExitCode && auditData.ClusterSummary.Results.Totals.Errors > 0 {
-			logrus.Infof("%d errors found in audit", auditData.ClusterSummary.Results.Totals.Errors)
+		summary := auditData.GetSummary()
+		score := summary.GetScore()
+		if *setExitCode && summary.Errors > 0 {
+			logrus.Infof("%d errors found in audit", summary.Errors)
 			os.Exit(3)
-		} else if *minScore != 0 && auditData.ClusterSummary.Score < uint(*minScore) {
-			logrus.Infof("Audit score of %d is less than the provided minimum of %d", auditData.ClusterSummary.Score, *minScore)
+		} else if *minScore != 0 && score < uint(*minScore) {
+			logrus.Infof("Audit score of %d is less than the provided minimum of %d", score, *minScore)
 			os.Exit(4)
 		}
 	}
@@ -228,7 +230,7 @@ func runAndReportAudit(c conf.Configuration, auditPath string, outputFile string
 
 	var outputBytes []byte
 	if outputFormat == "score" {
-		outputBytes = []byte(fmt.Sprintf("%d\n", auditData.ClusterSummary.Score))
+		outputBytes = []byte(fmt.Sprintf("%d\n", auditData.GetSummary().GetScore()))
 	} else if outputFormat == "yaml" {
 		jsonBytes, err := json.Marshal(auditData)
 		if err == nil {
diff --git a/pkg/config/ids.go b/pkg/config/ids.go
deleted file mode 100644
index 5446bd81..00000000
--- a/pkg/config/ids.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package config
-
-import (
-	"reflect"
-)
-
-// GetIDFromField returns the JSON key associated with a particular field, which serves as the check ID.
-func GetIDFromField(config interface{}, name string) string {
-	t := reflect.TypeOf(config)
-	field, ok := t.FieldByName(name)
-	if !ok {
-		panic("No JSON annotation for field " + name)
-	}
-	id, ok := field.Tag.Lookup("json")
-	if !ok {
-		panic("No JSON tag for field " + name)
-	}
-	return id
-}
diff --git a/pkg/config/supportedcontrollers.go b/pkg/config/supportedcontrollers.go
index 808a9538..1bca5c86 100644
--- a/pkg/config/supportedcontrollers.go
+++ b/pkg/config/supportedcontrollers.go
@@ -28,17 +28,20 @@ const (
 	CronJobs
 	// ReplicationControllers are supported controllers for scanning pod specs
 	ReplicationControllers
+	// NakedPods are a pseudo-controller for scanning pod specs
+	NakedPods
 )
 
 // ControllerStrings are strongly ordered to match the SupportedController enum
 var ControllerStrings = []string{
 	"Unsupported",
-	"Deployments",
-	"StatefulSets",
-	"DaemonSets",
-	"Jobs",
-	"CronJobs",
+	"Deployment",
+	"StatefulSet",
+	"DaemonSet",
+	"Job",
+	"CronJob",
 	"ReplicationController",
+	"NakedPod",
 }
 
 // stringLookupForSupportedControllers is the list of lowercase singular and plural strings for string to enum lookup
@@ -55,6 +58,8 @@ var stringLookupForSupportedControllers = map[string]SupportedController{
 	"cronjobs":               CronJobs,
 	"replicationcontroller":  ReplicationControllers,
 	"replicationcontrollers": ReplicationControllers,
+	"nakedpod":               NakedPods,
+	"nakedpods":              NakedPods,
 }
 
 // SupportedController is a constant item of a controller that is supported for scanning pod specs
@@ -119,6 +124,10 @@ func (s SupportedController) ListSupportedAPIVersions() []runtime.Object {
 		supportedVersions = []runtime.Object{
 			&corev1.ReplicationController{},
 		}
+	case NakedPods:
+		supportedVersions = []runtime.Object{
+			&corev1.Pod{},
+		}
 	}
 	return supportedVersions
 }
diff --git a/pkg/dashboard/assets/css/dashboard.css b/pkg/dashboard/assets/css/dashboard.css
index e915e831..6c930e8c 100644
--- a/pkg/dashboard/assets/css/dashboard.css
+++ b/pkg/dashboard/assets/css/dashboard.css
@@ -257,11 +257,11 @@ ul.message-list li i.message-icon {
   color: #8BD2DC;
 }
 
-.result-messages .warning i.message-icon {
+.result-messages .failure.warning i.message-icon {
   color: #f26c21;
 }
 
-.result-messages .error i.message-icon {
+.result-messages .failure.error i.message-icon {
   color: #a11f4c;
 }
 
diff --git a/pkg/dashboard/assets/js/charts.js b/pkg/dashboard/assets/js/charts.js
index f7cba310..b91fa5c7 100644
--- a/pkg/dashboard/assets/js/charts.js
+++ b/pkg/dashboard/assets/js/charts.js
@@ -5,9 +5,9 @@ $(function () {
       labels: ["Passing", "Warning", "Error"],
       datasets: [{
         data: [
-          polarisAuditData.ClusterSummary.Results.Totals.Successes,
-          polarisAuditData.ClusterSummary.Results.Totals.Warnings,
-          polarisAuditData.ClusterSummary.Results.Totals.Errors,
+          polarisSummary.Successes,
+          polarisSummary.Warnings,
+          polarisSummary.Errors,
         ],
         backgroundColor: ['#8BD2DC', '#f26c21', '#a11f4c'],
       }]
diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go
index 54b574f8..95fa5d69 100644
--- a/pkg/dashboard/dashboard.go
+++ b/pkg/dashboard/dashboard.go
@@ -90,15 +90,15 @@ type templateData struct {
 // GetBaseTemplate puts together the dashboard template. Individual pieces can be overridden before rendering.
 func GetBaseTemplate(name string) (*template.Template, error) {
 	tmpl := template.New(name).Funcs(template.FuncMap{
-		"getWarningWidth":         getWarningWidth,
-		"getSuccessWidth":         getSuccessWidth,
-		"getWeatherIcon":          getWeatherIcon,
-		"getWeatherText":          getWeatherText,
-		"getGrade":                getGrade,
-		"getIcon":                 getIcon,
-		"getCategoryLink":         getCategoryLink,
-		"getCategoryInfo":         getCategoryInfo,
-		"getAllControllerResults": getAllControllerResults,
+		"getWarningWidth": getWarningWidth,
+		"getSuccessWidth": getSuccessWidth,
+		"getWeatherIcon":  getWeatherIcon,
+		"getWeatherText":  getWeatherText,
+		"getGrade":        getGrade,
+		"getIcon":         getIcon,
+		"getResultClass":  getResultClass,
+		"getCategoryLink": getCategoryLink,
+		"getCategoryInfo": getCategoryInfo,
 	})
 
 	templateFileNames := []string{
@@ -230,7 +230,7 @@ func GetRouter(c config.Configuration, auditPath string, port int, basePath stri
 
 // MainHandler gets template data and renders the dashboard with it.
 func MainHandler(w http.ResponseWriter, r *http.Request, c config.Configuration, auditData validator.AuditData, basePath string) {
-	jsonData, err := json.Marshal(auditData)
+	jsonData, err := json.Marshal(auditData.GetSummary())
 
 	if err != nil {
 		http.Error(w, "Error serializing audit data", 500)
diff --git a/pkg/dashboard/helpers.go b/pkg/dashboard/helpers.go
index 961bf2c1..1a041f55 100644
--- a/pkg/dashboard/helpers.go
+++ b/pkg/dashboard/helpers.go
@@ -18,13 +18,10 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/fairwindsops/polaris/pkg/config"
 	"github.com/fairwindsops/polaris/pkg/validator"
 )
 
-func getAllControllerResults(nr validator.NamespaceResult) []validator.ControllerResult {
-	return nr.GetAllControllerResults()
-}
-
 func getWarningWidth(counts validator.CountSummary, fullWidth int) uint {
 	return uint(float64(counts.Successes+counts.Warnings) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth))
 }
@@ -79,6 +76,16 @@ func getWeatherIcon(counts validator.CountSummary) string {
 	}
 }
 
+func getResultClass(result validator.ResultMessage) string {
+	cls := string(result.Severity)
+	if result.Success {
+		cls += " success"
+	} else {
+		cls += " failure"
+	}
+	return cls
+}
+
 func getWeatherText(counts validator.CountSummary) string {
 	score := counts.GetScore()
 	if score >= 90 {
@@ -95,12 +102,11 @@ func getWeatherText(counts validator.CountSummary) string {
 }
 
 func getIcon(rm validator.ResultMessage) string {
-	switch rm.Type {
-	case "success":
+	if rm.Success {
 		return "fas fa-check"
-	case "warning":
+	} else if rm.Severity == config.SeverityWarning {
 		return "fas fa-exclamation"
-	default:
+	} else {
 		return "fas fa-times"
 	}
 }
diff --git a/pkg/dashboard/templates/dashboard.gohtml b/pkg/dashboard/templates/dashboard.gohtml
index df87c3a2..7b3e85cf 100644
--- a/pkg/dashboard/templates/dashboard.gohtml
+++ b/pkg/dashboard/templates/dashboard.gohtml
@@ -9,10 +9,10 @@
     <div class="cluster-overview">
       <div class="cluster-score">
         <div class="score-details">
-          <div class="weather"><i class="fas {{ getWeatherIcon .AuditData.ClusterSummary.Results.Totals }}"></i></div>
-          <div class="sailing">{{ getWeatherText .AuditData.ClusterSummary.Results.Totals }}</div>
-          <div class="scores"><span>Grade: </span><strong>{{ getGrade .AuditData.ClusterSummary.Results.Totals }}</strong></div>
-          <div class="scores"><span>Score: </span><strong>{{ .AuditData.ClusterSummary.Results.Totals.GetScore }}%</strong></div>
+          <div class="weather"><i class="fas {{ getWeatherIcon .AuditData.GetSummary }}"></i></div>
+          <div class="sailing">{{ getWeatherText .AuditData.GetSummary }}</div>
+          <div class="scores"><span>Grade: </span><strong>{{ getGrade .AuditData.GetSummary }}</strong></div>
+          <div class="scores"><span>Score: </span><strong>{{ .AuditData.GetSummary.GetScore }}%</strong></div>
         </div>
       </div>
       <div class="graph">
@@ -20,9 +20,9 @@
       </div>
       <div class="result-messages">
         <ul class="message-list">
-          <li class="success"><i class="message-icon fas fa-check"></i> {{ .AuditData.ClusterSummary.Results.Totals.Successes }} checks passed</li>
-          <li class="warning"><i class="message-icon fas fa-exclamation"></i> {{ .AuditData.ClusterSummary.Results.Totals.Warnings }} checks had warnings</li>
-          <li class="error"><i class="message-icon fas fa-times"></i> {{ .AuditData.ClusterSummary.Results.Totals.Errors }} checks had errors</li>
+          <li class="success"><i class="message-icon fas fa-check"></i> {{ .AuditData.GetSummary.Successes }} checks passed</li>
+          <li class="failure warning"><i class="message-icon fas fa-exclamation"></i> {{ .AuditData.GetSummary.Warnings }} checks had warnings</li>
+          <li class="failure error"><i class="message-icon fas fa-times"></i> {{ .AuditData.GetSummary.Errors }} checks had errors</li>
         </ul>
       </div>
     </div>
@@ -33,19 +33,19 @@
       <p class="kubernetes-overview">
           <span class="kubernetes-stat">
             <span>Kubernetes Version:</span>
-            <strong>{{.AuditData.ClusterSummary.Version}}</strong>
+            <strong>{{.AuditData.ClusterInfo.Version}}</strong>
           </span>
           <span class="kubernetes-stat">
             <span>Nodes:</span>
-            <strong>{{.AuditData.ClusterSummary.Nodes}}</strong>
+            <strong>{{.AuditData.ClusterInfo.Nodes}}</strong>
           </span>
           <span class="kubernetes-stat">
             <span>Pods:</span>
-            <strong>{{.AuditData.ClusterSummary.Pods}}</strong>
+            <strong>{{.AuditData.ClusterInfo.Pods}}</strong>
           </span>
           <span class="kubernetes-stat">
             <span>Namespaces:</span>
-            <strong>{{.AuditData.ClusterSummary.Namespaces}}</strong>
+            <strong>{{.AuditData.ClusterInfo.Namespaces}}</strong>
           </span>
       </p>
     </div>
@@ -54,7 +54,7 @@
   <div class="card category">
     <h3>Results by Category</h3>
     <div class="expandable-table">
-      {{ range $category, $summary := .AuditData.ClusterSummary.Results.ByCategory }}
+      {{ range $category, $summary := .AuditData.GetSummaryByCategory }}
         <div class="resource-info">
           <div class="status-bar">
             <div class="status">
@@ -74,30 +74,30 @@
     </div>
   </div>
 
-  {{ range $namespace, $nsResult := .AuditData.NamespacedResults }}
+  {{ range $namespace, $ctrlResults := .AuditData.GetResultsByNamespace }}
     <div class="card namespace">
       <h3>Namespace: <strong>{{ $namespace }}</strong></h3>
       <div class="expandable-table">
-        {{ range getAllControllerResults $nsResult }}
+        {{ range $ctrlResults }}
           <div class="resource-info">
             <div class="status-bar">
               <div class="status">
                 <div class="failing">
-                  <div class="warning" style="width: {{ getWarningWidth .PodResult.Summary.Totals 200 }}px;">
-                    <div class="passing" style="width: {{ getSuccessWidth .PodResult.Summary.Totals 200 }}px;"></div>
+                  <div class="warning" style="width: {{ getWarningWidth .PodResult.GetSummary 200 }}px;">
+                    <div class="passing" style="width: {{ getSuccessWidth .PodResult.GetSummary 200 }}px;"></div>
                   </div>
                 </div>
               </div>
             </div>
 
             <div class="name"><span class="caret-expander"></span>
-              <span class="controller-type">{{ .Type }}:</span>
+              <span class="controller-type">{{ .Kind }}:</span>
               <strong>{{ .Name }}</strong></div>
               <div class="result-messages expandable-content">
                 <h4>Pod Spec:</h4>
                 <ul class="message-list">
-                  {{ range $message := .PodResult.Messages}}
-                    <li class="{{ .Type }}">
+                  {{ range $message := .PodResult.Results.GetSortedResults }}
+                    <li class="{{ getResultClass . }}">
                       <i class="message-icon {{ getIcon $message }}"></i>
                       <span class="message">{{ .Message }}</span>
                       <a class="more-info" href="details/{{ getCategoryLink .Category }}">
@@ -107,12 +107,12 @@
                   {{ end }}
                 </ul>
               </div>
-              {{ range .PodResult.ContainerResults}}
+              {{ range .PodResult.ContainerResults }}
                 <div class="result-messages expandable-content">
                   <h4>Container: {{ .Name }}</h4>
                   <ul class="message-list">
-                    {{ range $message := .Messages}}
-                      <li class="{{ .Type }}">
+                    {{ range $message := .Results.GetSortedResults }}
+                      <li class="{{ getResultClass . }}">
                         <i class="message-icon {{ getIcon $message }}"></i>
                         <span class="message">{{ .Message }}</span>
                         <a class="more-info" href="details/{{ getCategoryLink .Category }}">
@@ -124,10 +124,10 @@
                 </div>
               {{ end }} {{/* end range .PodResult.ContainerResults */}}
             </div>
-        {{ end }} {{/* end range .DeploymentResults */}}
+        {{ end }} {{/* end range .Results.GetSortedResults */}}
       </div>
     </div>
-  {{ end }} {{/* end range .AuditData.NamespacedResults */}}
+  {{ end }} {{/* end range .AuditData.GetResultsByNamespace */}}
   <script src="static/js/charts.js">
   </script>
 {{end}}
diff --git a/pkg/dashboard/templates/main.gohtml b/pkg/dashboard/templates/main.gohtml
index 92b722bc..7c999931 100644
--- a/pkg/dashboard/templates/main.gohtml
+++ b/pkg/dashboard/templates/main.gohtml
@@ -3,7 +3,7 @@
 
 <head>
   <script>
-    window.polarisAuditData = {{ .JSON }};
+    window.polarisSummary = {{ .JSON }};
   </script>
   {{ template "head" . }}
 </head>
diff --git a/pkg/validator/container.go b/pkg/validator/container.go
index 0926cf71..66f7f5a2 100644
--- a/pkg/validator/container.go
+++ b/pkg/validator/container.go
@@ -16,53 +16,43 @@ package validator
 
 import (
 	"github.com/fairwindsops/polaris/pkg/config"
+	"github.com/fairwindsops/polaris/pkg/validator/controllers"
+
 	corev1 "k8s.io/api/core/v1"
 )
 
-// ContainerValidation tracks validation failures associated with a Container.
-type ContainerValidation struct {
-	*ResourceValidation
-	Container       *corev1.Container
-	IsInitContainer bool
-	parentPodSpec   corev1.PodSpec
-}
-
-// ValidateContainer validates that each pod conforms to the Polaris config, returns a ResourceResult.
-// FIXME When validating a container, there are some things in a container spec
-//       that can be affected by the podSpec. This means we need a copy of the
-//       relevant podSpec in order to check certain aspects of a containerSpec.
-//       Perhaps there is a more ideal solution instead of attaching a parent
-//       podSpec to every container Validation struct...
-func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, conf *config.Configuration, controllerName string, controllerType config.SupportedController, isInit bool) ContainerResult {
-	cv := ContainerValidation{
-		Container:          container,
-		ResourceValidation: &ResourceValidation{},
-		IsInitContainer:    isInit,
-	}
-
-	// Support initializing
-	// FIXME This is a product of pulling in the podSpec, ideally we'd never
-	//       expect this be nil but our tests have conditions in which the
-	//       parent podResult isn't initialized in this ContainerValidation
-	//       struct.
-	if parentPodResult == nil {
-		// initialize a blank pod spec
-		cv.parentPodSpec = corev1.PodSpec{}
-	} else {
-		cv.parentPodSpec = parentPodResult.podSpec
-	}
-
-	err := applyContainerSchemaChecks(conf, controllerName, controllerType, &cv)
-	// FIXME: don't panic
+// ValidateContainer validates a single container from a given controller
+func ValidateContainer(conf *config.Configuration, controller controllers.Interface, container *corev1.Container, isInit bool) (ContainerResult, error) {
+	results, err := applyContainerSchemaChecks(conf, controller, container, isInit)
 	if err != nil {
-		panic(err)
+		return ContainerResult{}, err
 	}
 
 	cRes := ContainerResult{
-		Name:     container.Name,
-		Messages: cv.messages(),
-		Summary:  cv.summary(),
+		Name:    container.Name,
+		Results: results,
 	}
 
-	return cRes
+	return cRes, nil
+}
+
+// ValidateAllContainers validates both init and regular containers
+func ValidateAllContainers(conf *config.Configuration, controller controllers.Interface) ([]ContainerResult, error) {
+	results := []ContainerResult{}
+	pod := controller.GetPodSpec()
+	for _, container := range pod.InitContainers {
+		result, err := ValidateContainer(conf, controller, &container, true)
+		if err != nil {
+			return nil, err
+		}
+		results = append(results, result)
+	}
+	for _, container := range pod.Containers {
+		result, err := ValidateContainer(conf, controller, &container, false)
+		if err != nil {
+			return nil, err
+		}
+		results = append(results, result)
+	}
+	return results, nil
 }
diff --git a/pkg/validator/controller.go b/pkg/validator/controller.go
index d3ac89c1..9aa01ba6 100644
--- a/pkg/validator/controller.go
+++ b/pkg/validator/controller.go
@@ -21,48 +21,51 @@ import (
 	"github.com/fairwindsops/polaris/pkg/kube"
 	"github.com/fairwindsops/polaris/pkg/validator/controllers"
 	controller "github.com/fairwindsops/polaris/pkg/validator/controllers"
-	"github.com/sirupsen/logrus"
 )
 
 const exemptionAnnotationKey = "polaris.fairwinds.com/exempt"
 
 // ValidateController validates a single controller, returns a ControllerResult.
-func ValidateController(conf conf.Configuration, controller controller.Interface) ControllerResult {
-	controllerType := controller.GetType()
-	pod := controller.GetPodSpec()
-	podResult := ValidatePod(conf, pod, controller.GetName(), controllerType)
+func ValidateController(conf *conf.Configuration, controller controller.Interface) (ControllerResult, error) {
+	podResult, err := ValidatePod(conf, controller)
+	if err != nil {
+		return ControllerResult{}, err
+	}
 	result := ControllerResult{
-		Type:      controllerType.String(),
+		Kind:      controller.GetKind().String(),
 		Name:      controller.GetName(),
+		Namespace: controller.GetObjectMeta().Namespace,
+		Results:   ResultSet{},
 		PodResult: podResult,
 	}
-	return result
+	return result, nil
 }
 
 // ValidateControllers validates that each deployment conforms to the Polaris config,
 // builds a list of ResourceResults organized by namespace.
-func ValidateControllers(config conf.Configuration, kubeResources *kube.ResourceProvider, nsResults *NamespacedResults) {
+func ValidateControllers(config *conf.Configuration, kubeResources *kube.ResourceProvider) ([]ControllerResult, error) {
 	var controllersToAudit []controller.Interface
 	for _, supportedControllers := range config.ControllersToScan {
-		loadedControllers, _ := controllers.LoadControllersByType(supportedControllers, kubeResources)
+		loadedControllers, _ := controllers.LoadControllersByKind(supportedControllers, kubeResources)
 		controllersToAudit = append(controllersToAudit, loadedControllers...)
 	}
 
+	results := []ControllerResult{}
 	for _, controller := range controllersToAudit {
 		if !config.DisallowExemptions && hasExemptionAnnotation(controller) {
 			continue
 		}
-		controllerResult := ValidateController(config, controller)
-		nsResult := nsResults.getNamespaceResult(controller.GetNamespace())
-		nsResult.Summary.appendResults(*controllerResult.PodResult.Summary)
-		if err := nsResult.AddResult(controller.GetType(), controllerResult); err != nil {
-			logrus.Errorf("Internal Error: Failed to add a grouped result: %s", err)
+		result, err := ValidateController(config, controller)
+		if err != nil {
+			return nil, err
 		}
+		results = append(results, result)
 	}
+	return results, nil
 }
 
 func hasExemptionAnnotation(ctrl controller.Interface) bool {
-	annot := ctrl.GetAnnotations()
+	annot := ctrl.GetObjectMeta().Annotations
 	val := annot[exemptionAnnotationKey]
 	return strings.ToLower(val) == "true"
 }
diff --git a/pkg/validator/controllers/cronjob.go b/pkg/validator/controllers/cronjob.go
index 2e94ba8a..e55fc485 100644
--- a/pkg/validator/controllers/cronjob.go
+++ b/pkg/validator/controllers/cronjob.go
@@ -4,6 +4,7 @@ import (
 	"github.com/fairwindsops/polaris/pkg/config"
 	kubeAPIBatchV1beta1 "k8s.io/api/batch/v1beta1"
 	kubeAPICoreV1 "k8s.io/api/core/v1"
+	kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 // CronJobController is an implementation of controller for deployments
@@ -22,14 +23,14 @@ func (c CronJobController) GetPodSpec() *kubeAPICoreV1.PodSpec {
 	return &c.K8SResource.Spec.JobTemplate.Spec.Template.Spec
 }
 
-// GetType returns the supportedcontroller enum type
-func (c CronJobController) GetType() config.SupportedController {
+// GetKind returns the supportedcontroller enum type
+func (c CronJobController) GetKind() config.SupportedController {
 	return config.CronJobs
 }
 
-// GetAnnotations returns the controller's annotations
-func (c CronJobController) GetAnnotations() map[string]string {
-	return c.K8SResource.ObjectMeta.Annotations
+// GetObjectMeta returns the metadata
+func (c CronJobController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
+	return c.K8SResource.ObjectMeta
 }
 
 // NewCronJobController builds a new controller interface for Deployments
diff --git a/pkg/validator/controllers/daemonset.go b/pkg/validator/controllers/daemonset.go
index 4a6ea455..a3c67dad 100644
--- a/pkg/validator/controllers/daemonset.go
+++ b/pkg/validator/controllers/daemonset.go
@@ -4,6 +4,7 @@ import (
 	"github.com/fairwindsops/polaris/pkg/config"
 	kubeAPIAppsV1 "k8s.io/api/apps/v1"
 	kubeAPICoreV1 "k8s.io/api/core/v1"
+	kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 // DaemonSetController is an implementation of controller for deployments
@@ -22,13 +23,13 @@ func (d DaemonSetController) GetPodSpec() *kubeAPICoreV1.PodSpec {
 	return &d.K8SResource.Spec.Template.Spec
 }
 
-// GetAnnotations returns the controller's annotations
-func (d DaemonSetController) GetAnnotations() map[string]string {
-	return d.K8SResource.ObjectMeta.Annotations
+// GetObjectMeta returns the metadata
+func (d DaemonSetController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
+	return d.K8SResource.ObjectMeta
 }
 
-// GetType returns the supportedcontroller enum type
-func (d DaemonSetController) GetType() config.SupportedController {
+// GetKind returns the supportedcontroller enum type
+func (d DaemonSetController) GetKind() config.SupportedController {
 	return config.DaemonSets
 }
 
diff --git a/pkg/validator/controllers/deployment.go b/pkg/validator/controllers/deployment.go
index ddf76a58..e02a60bd 100644
--- a/pkg/validator/controllers/deployment.go
+++ b/pkg/validator/controllers/deployment.go
@@ -4,6 +4,7 @@ import (
 	"github.com/fairwindsops/polaris/pkg/config"
 	kubeAPIAppsV1 "k8s.io/api/apps/v1"
 	kubeAPICoreV1 "k8s.io/api/core/v1"
+	kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 // DeploymentController is an implementation of controller for deployments
@@ -22,13 +23,13 @@ func (d DeploymentController) GetPodSpec() *kubeAPICoreV1.PodSpec {
 	return &d.K8SResource.Spec.Template.Spec
 }
 
-// GetAnnotations returns the controller's annotations
-func (d DeploymentController) GetAnnotations() map[string]string {
-	return d.K8SResource.ObjectMeta.Annotations
+// GetObjectMeta returns the metadata
+func (d DeploymentController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
+	return d.K8SResource.ObjectMeta
 }
 
-// GetType returns the supportedcontroller enum type
-func (d DeploymentController) GetType() config.SupportedController {
+// GetKind returns the supportedcontroller enum type
+func (d DeploymentController) GetKind() config.SupportedController {
 	return config.Deployments
 }
 
diff --git a/pkg/validator/controllers/interface.go b/pkg/validator/controllers/interface.go
index f69dd422..fbcca9aa 100644
--- a/pkg/validator/controllers/interface.go
+++ b/pkg/validator/controllers/interface.go
@@ -6,6 +6,7 @@ import (
 	"github.com/fairwindsops/polaris/pkg/config"
 	"github.com/fairwindsops/polaris/pkg/kube"
 	kubeAPICoreV1 "k8s.io/api/core/v1"
+	kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 // Interface is an interface for k8s controllers (e.g. Deployments and StatefulSets)
@@ -14,8 +15,8 @@ type Interface interface {
 	GetNamespace() string
 	GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec
 	GetPodSpec() *kubeAPICoreV1.PodSpec
-	GetType() config.SupportedController
-	GetAnnotations() map[string]string
+	GetKind() config.SupportedController
+	GetObjectMeta() kubeAPIMetaV1.ObjectMeta
 }
 
 // GenericController is a base implementation with some free methods for inherited structs
@@ -34,10 +35,10 @@ func (g GenericController) GetNamespace() string {
 	return g.Namespace
 }
 
-// LoadControllersByType loads a list of controllers from the kubeResources by detecting their type
-func LoadControllersByType(controllerType config.SupportedController, kubeResources *kube.ResourceProvider) ([]Interface, error) {
+// LoadControllersByKind loads a list of controllers from the kubeResources by detecting their type
+func LoadControllersByKind(controllerKind config.SupportedController, kubeResources *kube.ResourceProvider) ([]Interface, error) {
 	interfaces := []Interface{}
-	switch controllerType {
+	switch controllerKind {
 	case config.Deployments:
 		for _, deploy := range kubeResources.Deployments {
 			interfaces = append(interfaces, NewDeploymentController(deploy))
@@ -66,5 +67,5 @@ func LoadControllersByType(controllerType config.SupportedController, kubeResour
 	if len(interfaces) > 0 {
 		return interfaces, nil
 	}
-	return nil, fmt.Errorf("Controller type (%s) does not have a generator", controllerType)
+	return nil, fmt.Errorf("Controller type (%s) does not have a generator", controllerKind)
 }
diff --git a/pkg/validator/controllers/job.go b/pkg/validator/controllers/job.go
index f48fa03b..3efe181c 100644
--- a/pkg/validator/controllers/job.go
+++ b/pkg/validator/controllers/job.go
@@ -4,6 +4,7 @@ import (
 	"github.com/fairwindsops/polaris/pkg/config"
 	kubeAPIBatchV1 "k8s.io/api/batch/v1"
 	kubeAPICoreV1 "k8s.io/api/core/v1"
+	kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 // JobController is an implementation of controller for deployments
@@ -22,13 +23,13 @@ func (j JobController) GetPodSpec() *kubeAPICoreV1.PodSpec {
 	return &j.K8SResource.Spec.Template.Spec
 }
 
-// GetAnnotations returns the controller's annotations
-func (j JobController) GetAnnotations() map[string]string {
-	return j.K8SResource.ObjectMeta.Annotations
+// GetObjectMeta returns the metadata
+func (j JobController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
+	return j.K8SResource.ObjectMeta
 }
 
-// GetType returns the supportedcontroller enum type
-func (j JobController) GetType() config.SupportedController {
+// GetKind returns the supportedcontroller enum type
+func (j JobController) GetKind() config.SupportedController {
 	return config.Jobs
 }
 
diff --git a/pkg/validator/controllers/naked-pod.go b/pkg/validator/controllers/naked-pod.go
new file mode 100644
index 00000000..c5ab8e66
--- /dev/null
+++ b/pkg/validator/controllers/naked-pod.go
@@ -0,0 +1,42 @@
+package controllers
+
+import (
+	"github.com/fairwindsops/polaris/pkg/config"
+	kubeAPICoreV1 "k8s.io/api/core/v1"
+	kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// NakedPodController is an implementation of controller for deployments
+type NakedPodController struct {
+	GenericController
+	K8SResource kubeAPICoreV1.Pod
+}
+
+// GetPodTemplate returns the original template spec
+func (n NakedPodController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
+	return nil
+}
+
+// GetPodSpec returns the original kubernetes template pod spec
+func (n NakedPodController) GetPodSpec() *kubeAPICoreV1.PodSpec {
+	return &n.K8SResource.Spec
+}
+
+// GetObjectMeta returns the metadata
+func (n NakedPodController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
+	return n.K8SResource.ObjectMeta
+}
+
+// GetKind returns the supportedcontroller enum type
+func (n NakedPodController) GetKind() config.SupportedController {
+	return config.NakedPods
+}
+
+// NewNakedPodController builds a new controller interface for NakedPods
+func NewNakedPodController(originalNakedPodResource kubeAPICoreV1.Pod) Interface {
+	controller := NakedPodController{}
+	controller.Name = originalNakedPodResource.Name
+	controller.Namespace = originalNakedPodResource.Namespace
+	controller.K8SResource = originalNakedPodResource
+	return controller
+}
diff --git a/pkg/validator/controllers/replicationcontroller.go b/pkg/validator/controllers/replicationcontroller.go
index d03f6e30..cb510f88 100644
--- a/pkg/validator/controllers/replicationcontroller.go
+++ b/pkg/validator/controllers/replicationcontroller.go
@@ -3,6 +3,7 @@ package controllers
 import (
 	"github.com/fairwindsops/polaris/pkg/config"
 	kubeAPICoreV1 "k8s.io/api/core/v1"
+	kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 // NOTE: Maybe this name of ReplicationController is duplicative but it's more explicit since
@@ -24,13 +25,13 @@ func (r ReplicationControllerController) GetPodSpec() *kubeAPICoreV1.PodSpec {
 	return &r.K8SResource.Spec.Template.Spec
 }
 
-// GetAnnotations returns the controller's annotations
-func (r ReplicationControllerController) GetAnnotations() map[string]string {
-	return r.K8SResource.ObjectMeta.Annotations
+// GetObjectMeta returns the metadata
+func (r ReplicationControllerController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
+	return r.K8SResource.ObjectMeta
 }
 
-// GetType returns the supportedcontroller enum type
-func (r ReplicationControllerController) GetType() config.SupportedController {
+// GetKind returns the supportedcontroller enum type
+func (r ReplicationControllerController) GetKind() config.SupportedController {
 	return config.ReplicationControllers
 }
 
diff --git a/pkg/validator/controllers/statefulsets.go b/pkg/validator/controllers/statefulsets.go
index 16c8f1bb..4ac1f62b 100644
--- a/pkg/validator/controllers/statefulsets.go
+++ b/pkg/validator/controllers/statefulsets.go
@@ -4,6 +4,7 @@ import (
 	"github.com/fairwindsops/polaris/pkg/config"
 	kubeAPIAppsV1 "k8s.io/api/apps/v1"
 	kubeAPICoreV1 "k8s.io/api/core/v1"
+	kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 // StatefulSetController is an implementation of controller for deployments
@@ -22,13 +23,13 @@ func (s StatefulSetController) GetPodSpec() *kubeAPICoreV1.PodSpec {
 	return &s.K8SResource.Spec.Template.Spec
 }
 
-// GetAnnotations returns the controller's annotations
-func (s StatefulSetController) GetAnnotations() map[string]string {
-	return s.K8SResource.ObjectMeta.Annotations
+// GetObjectMeta returns the metadata
+func (s StatefulSetController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
+	return s.K8SResource.ObjectMeta
 }
 
-// GetType returns the supportedcontroller enum type
-func (s StatefulSetController) GetType() config.SupportedController {
+// GetKind returns the supportedcontroller enum type
+func (s StatefulSetController) GetKind() config.SupportedController {
 	return config.StatefulSets
 }
 
diff --git a/pkg/validator/fullaudit.go b/pkg/validator/fullaudit.go
index 3b07f242..06eb6e93 100644
--- a/pkg/validator/fullaudit.go
+++ b/pkg/validator/fullaudit.go
@@ -1,36 +1,39 @@
 package validator
 
 import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
 	"time"
 
 	conf "github.com/fairwindsops/polaris/pkg/config"
 	"github.com/fairwindsops/polaris/pkg/kube"
+
+	"github.com/sirupsen/logrus"
+	apiMachineryYAML "k8s.io/apimachinery/pkg/util/yaml"
 )
 
 // RunAudit runs a full Polaris audit and returns an AuditData object
 func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (AuditData, error) {
-	nsResults := NamespacedResults{}
-	ValidateControllers(config, kubeResources, &nsResults)
-
-	clusterResults := ResultSummary{}
-
-	// Aggregate all summary counts to get a clusterwide count.
-	for _, result := range nsResults.GetAllControllerResults() {
-		clusterResults.appendResults(*result.PodResult.Summary)
-	}
-
 	displayName := config.DisplayName
 	if displayName == "" {
 		displayName = kubeResources.SourceName
 	}
 
+	results, err := ValidateControllers(&config, kubeResources)
+	if err != nil {
+		return AuditData{}, err
+	}
+
 	auditData := AuditData{
 		PolarisOutputVersion: PolarisOutputVersion,
 		AuditTime:            kubeResources.CreationTime.Format(time.RFC3339),
 		SourceType:           kubeResources.SourceType,
 		SourceName:           kubeResources.SourceName,
 		DisplayName:          displayName,
-		ClusterSummary: ClusterSummary{
+		ClusterInfo: ClusterInfo{
 			Version:                kubeResources.ServerVersion,
 			Nodes:                  len(kubeResources.Nodes),
 			Pods:                   len(kubeResources.Pods),
@@ -41,10 +44,39 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
 			Jobs:                   len(kubeResources.Jobs),
 			CronJobs:               len(kubeResources.CronJobs),
 			ReplicationControllers: len(kubeResources.ReplicationControllers),
-			Results:                clusterResults,
-			Score:                  clusterResults.Totals.GetScore(),
 		},
-		NamespacedResults: nsResults,
+		Results: results,
 	}
 	return auditData, nil
 }
+
+// ReadAuditFromFile reads the data from a past audit stored in a JSON or YAML file.
+func ReadAuditFromFile(fileName string) AuditData {
+	auditData := AuditData{}
+	oldFileBytes, err := ioutil.ReadFile(fileName)
+	if err != nil {
+		logrus.Errorf("Unable to read contents of loaded file: %v", err)
+		os.Exit(1)
+	}
+	auditData, err = ParseAudit(oldFileBytes)
+	if err != nil {
+		logrus.Errorf("Error parsing file contents into auditData: %v", err)
+		os.Exit(1)
+	}
+	return auditData
+}
+
+// ParseAudit decodes either a YAML or JSON file and returns AuditData.
+func ParseAudit(oldFileBytes []byte) (AuditData, error) {
+	reader := bytes.NewReader(oldFileBytes)
+	conf := AuditData{}
+	d := apiMachineryYAML.NewYAMLOrJSONDecoder(reader, 4096)
+	for {
+		if err := d.Decode(&conf); err != nil {
+			if err == io.EOF {
+				return conf, nil
+			}
+			return conf, fmt.Errorf("Decoding config failed: %v", err)
+		}
+	}
+}
diff --git a/pkg/validator/output.go b/pkg/validator/output.go
new file mode 100644
index 00000000..49fc7626
--- /dev/null
+++ b/pkg/validator/output.go
@@ -0,0 +1,83 @@
+// Copyright 2019 FairwindsOps Inc
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package validator
+
+import (
+	"github.com/fairwindsops/polaris/pkg/config"
+)
+
+const (
+	// PolarisOutputVersion is the version of the current output structure
+	PolarisOutputVersion = "1.0"
+)
+
+// AuditData contains all the data from a full Polaris audit
+type AuditData struct {
+	PolarisOutputVersion string
+	AuditTime            string
+	SourceType           string
+	SourceName           string
+	DisplayName          string
+	ClusterInfo          ClusterInfo
+	Results              []ControllerResult
+}
+
+// ClusterInfo contains Polaris results as well as some high-level stats
+type ClusterInfo struct {
+	Version                string
+	Nodes                  int
+	Pods                   int
+	Namespaces             int
+	Deployments            int
+	StatefulSets           int
+	DaemonSets             int
+	Jobs                   int
+	CronJobs               int
+	ReplicationControllers int
+}
+
+// ResultMessage is the result of a given check
+type ResultMessage struct {
+	ID       string
+	Message  string
+	Success  bool
+	Severity config.Severity
+	Category string
+}
+
+// ResultSet contiains the results for a set of checks
+type ResultSet map[string]ResultMessage
+
+// ControllerResult provides results for a controller
+type ControllerResult struct {
+	Name      string
+	Namespace string
+	Kind      string
+	Results   ResultSet
+	PodResult PodResult
+}
+
+// PodResult provides a list of validation messages for each pod.
+type PodResult struct {
+	Name             string
+	Results          ResultSet
+	ContainerResults []ContainerResult
+}
+
+// ContainerResult provides a list of validation messages for each container.
+type ContainerResult struct {
+	Name    string
+	Results ResultSet
+}
diff --git a/pkg/validator/pod.go b/pkg/validator/pod.go
index c9130a9c..420ef39f 100644
--- a/pkg/validator/pod.go
+++ b/pkg/validator/pod.go
@@ -16,48 +16,25 @@ package validator
 
 import (
 	"github.com/fairwindsops/polaris/pkg/config"
-	corev1 "k8s.io/api/core/v1"
+	"github.com/fairwindsops/polaris/pkg/validator/controllers"
 )
 
-// PodValidation tracks validation failures associated with a Pod.
-type PodValidation struct {
-	*ResourceValidation
-	Pod *corev1.PodSpec
-}
-
 // ValidatePod validates that each pod conforms to the Polaris config, returns a ResourceResult.
-func ValidatePod(conf config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController) PodResult {
-	pv := PodValidation{
-		Pod:                pod,
-		ResourceValidation: &ResourceValidation{},
-	}
-
-	err := applyPodSchemaChecks(&conf, pod, controllerName, controllerType, &pv)
-	// FIXME: don't panic
+func ValidatePod(conf *config.Configuration, controller controllers.Interface) (PodResult, error) {
+	podResults, err := applyPodSchemaChecks(conf, controller)
 	if err != nil {
-		panic(err)
+		return PodResult{}, err
 	}
 
 	pRes := PodResult{
-		Messages:         pv.messages(),
+		Results:          podResults,
 		ContainerResults: []ContainerResult{},
-		Summary:          pv.summary(),
-		podSpec:          *pod,
 	}
 
-	pv.validateContainers(pod.InitContainers, &pRes, &conf, controllerName, controllerType, true)
-	pv.validateContainers(pod.Containers, &pRes, &conf, controllerName, controllerType, false)
-
-	for _, cRes := range pRes.ContainerResults {
-		pRes.Summary.appendResults(*cRes.Summary)
+	pRes.ContainerResults, err = ValidateAllContainers(conf, controller)
+	if err != nil {
+		return pRes, err
 	}
 
-	return pRes
-}
-
-func (pv *PodValidation) validateContainers(containers []corev1.Container, pRes *PodResult, conf *config.Configuration, controllerName string, controllerType config.SupportedController, isInit bool) {
-	for _, container := range containers {
-		cRes := ValidateContainer(&container, pRes, conf, controllerName, controllerType, isInit)
-		pRes.ContainerResults = append(pRes.ContainerResults, cRes)
-	}
+	return pRes, nil
 }
diff --git a/pkg/validator/resource.go b/pkg/validator/resource.go
deleted file mode 100644
index 8e7c38e2..00000000
--- a/pkg/validator/resource.go
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright 2019 FairwindsOps Inc
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package validator
-
-import (
-	conf "github.com/fairwindsops/polaris/pkg/config"
-	"github.com/sirupsen/logrus"
-)
-
-// ResourceValidation contains methods shared by PodValidation and ContainerValidation
-type ResourceValidation struct {
-	Errors    []*ResultMessage
-	Warnings  []*ResultMessage
-	Successes []*ResultMessage
-}
-
-func (rv *ResourceValidation) messages() []*ResultMessage {
-	messages := []*ResultMessage{}
-	messages = append(messages, rv.Errors...)
-	messages = append(messages, rv.Warnings...)
-	messages = append(messages, rv.Successes...)
-	return messages
-}
-
-func (rv *ResourceValidation) summary() *ResultSummary {
-	counts := CountSummary{
-		Errors:    uint(len(rv.Errors)),
-		Warnings:  uint(len(rv.Warnings)),
-		Successes: uint(len(rv.Successes)),
-	}
-	byCategory := CategorySummary{}
-	for _, msg := range rv.messages() {
-		if _, ok := byCategory[msg.Category]; !ok {
-			byCategory[msg.Category] = &CountSummary{}
-		}
-		if msg.Type == MessageTypeError {
-			byCategory[msg.Category].Errors++
-		} else if msg.Type == MessageTypeWarning {
-			byCategory[msg.Category].Warnings++
-		} else if msg.Type == MessageTypeSuccess {
-			byCategory[msg.Category].Successes++
-		}
-	}
-	return &ResultSummary{
-		Totals:     counts,
-		ByCategory: byCategory,
-	}
-}
-
-func (rv *ResourceValidation) addMessage(message ResultMessage) {
-	if message.Type == MessageTypeError {
-		rv.Errors = append(rv.Errors, &message)
-	} else if message.Type == MessageTypeWarning {
-		rv.Warnings = append(rv.Warnings, &message)
-	} else if message.Type == MessageTypeSuccess {
-		rv.Successes = append(rv.Successes, &message)
-	} else {
-		panic("Bad message type")
-	}
-}
-
-func (rv *ResourceValidation) addFailure(message string, severity conf.Severity, category string, id string) {
-	if severity == conf.SeverityError {
-		rv.addError(message, category, id)
-	} else if severity == conf.SeverityWarning {
-		rv.addWarning(message, category, id)
-	} else {
-		logrus.Errorf("Invalid severity: %s", severity)
-	}
-}
-
-func (rv *ResourceValidation) addError(message string, category string, id string) {
-	rv.Errors = append(rv.Errors, &ResultMessage{
-		ID:       id,
-		Message:  message,
-		Type:     MessageTypeError,
-		Category: category,
-	})
-}
-
-func (rv *ResourceValidation) addWarning(message string, category string, id string) {
-	rv.Warnings = append(rv.Warnings, &ResultMessage{
-		ID:       id,
-		Message:  message,
-		Type:     MessageTypeWarning,
-		Category: category,
-	})
-}
-
-func (rv *ResourceValidation) addSuccess(message string, category string, id string) {
-	rv.Successes = append(rv.Successes, &ResultMessage{
-		ID:       id,
-		Message:  message,
-		Type:     MessageTypeSuccess,
-		Category: category,
-	})
-}
diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go
index 2060991d..a18f19df 100644
--- a/pkg/validator/schema.go
+++ b/pkg/validator/schema.go
@@ -11,6 +11,7 @@ import (
 	"k8s.io/apimachinery/pkg/util/yaml"
 
 	"github.com/fairwindsops/polaris/pkg/config"
+	"github.com/fairwindsops/polaris/pkg/validator/controllers"
 )
 
 var (
@@ -72,72 +73,85 @@ func parseCheck(rawBytes []byte) (config.SchemaCheck, error) {
 	}
 }
 
-func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController, pv *PodValidation) error {
+func resolveCheck(conf *config.Configuration, checkID string, controller controllers.Interface, target config.TargetKind, isInitContainer bool) (*config.SchemaCheck, error) {
+	check, ok := conf.CustomChecks[checkID]
+	if !ok {
+		check, ok = builtInChecks[checkID]
+	}
+	if !ok {
+		return nil, fmt.Errorf("Check %s not found", checkID)
+	}
+	if !conf.IsActionable(check.ID, controller.GetName()) {
+		return nil, nil
+	}
+	if !check.IsActionable(target, controller.GetKind(), isInitContainer) {
+		return nil, nil
+	}
+	return &check, nil
+}
+
+func makeResult(conf *config.Configuration, check *config.SchemaCheck, passes bool) ResultMessage {
+	result := ResultMessage{
+		ID:       check.ID,
+		Severity: conf.Checks[check.ID],
+		Category: check.Category,
+		Success:  passes,
+	}
+	if passes {
+		result.Message = check.SuccessMessage
+	} else {
+		result.Message = check.FailureMessage
+	}
+	return result
+}
+
+func applyPodSchemaChecks(conf *config.Configuration, controller controllers.Interface) (ResultSet, error) {
+	results := ResultSet{}
 	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
+		check, err := resolveCheck(conf, checkID, controller, config.TargetPod, false)
+		if err != nil {
+			return nil, err
 		}
-		if !check.IsActionable(config.TargetPod, controllerType, false) {
+		if err != nil {
+			return nil, err
+		} else if check == nil {
 			continue
 		}
-		passes, err := check.CheckPod(pod)
+		passes, err := check.CheckPod(controller.GetPodSpec())
 		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)
+			return nil, err
 		}
+		results[check.ID] = makeResult(conf, check, passes)
 	}
-	return nil
+	return results, nil
 }
 
-func applyContainerSchemaChecks(conf *config.Configuration, controllerName string, controllerType config.SupportedController, cv *ContainerValidation) error {
+func applyContainerSchemaChecks(conf *config.Configuration, controller controllers.Interface, container *corev1.Container, isInit bool) (ResultSet, error) {
+	results := ResultSet{}
 	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(config.TargetContainer, controllerType, cv.IsInitContainer) {
+		check, err := resolveCheck(conf, checkID, controller, config.TargetContainer, isInit)
+		if err != nil {
+			return nil, err
+		} else if check == nil {
 			continue
 		}
 		var passes bool
-		var err error
 		if check.SchemaTarget == config.TargetPod {
-			cv.parentPodSpec.Containers = []corev1.Container{*cv.Container}
-			passes, err = check.CheckPod(&cv.parentPodSpec)
-			cv.parentPodSpec.Containers = []corev1.Container{}
+			podCopy := *controller.GetPodSpec()
+			podCopy.InitContainers = []corev1.Container{}
+			podCopy.Containers = []corev1.Container{*container}
+			passes, err = check.CheckPod(&podCopy)
 		} else {
-			passes, err = check.CheckContainer(cv.Container)
+			passes, err = check.CheckContainer(container)
 		}
 		if err != nil {
-			return err
-		}
-		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, err
 		}
+		results[check.ID] = makeResult(conf, check, passes)
 	}
-	return nil
+	return results, nil
 }
 
 func getSortedKeys(m map[string]config.Severity) []string {
diff --git a/pkg/validator/summary.go b/pkg/validator/summary.go
new file mode 100644
index 00000000..6801de8a
--- /dev/null
+++ b/pkg/validator/summary.go
@@ -0,0 +1,186 @@
+package validator
+
+import (
+	"github.com/fairwindsops/polaris/pkg/config"
+)
+
+// CountSummary provides a high level overview of success, warnings, and errors.
+type CountSummary struct {
+	Successes uint
+	Warnings  uint
+	Errors    uint
+}
+
+// CountSummaryByCategory is a map from category to CountSummary
+type CountSummaryByCategory map[string]CountSummary
+
+// GetScore returns an overall score in [0, 100] for the CountSummary
+func (cs CountSummary) GetScore() uint {
+	total := (cs.Successes * 2) + cs.Warnings + (cs.Errors * 2)
+	return uint((float64(cs.Successes*2) / float64(total)) * 100)
+}
+
+// AddSummary adds two CountSummaries together
+func (cs *CountSummary) AddSummary(other CountSummary) {
+	cs.Successes += other.Successes
+	cs.Warnings += other.Warnings
+	cs.Errors += other.Errors
+}
+
+// AddResult adds a single result to the summary
+func (cs *CountSummary) AddResult(result ResultMessage) {
+	if result.Success == false {
+		if result.Severity == config.SeverityWarning {
+			cs.Warnings++
+		} else {
+			cs.Errors++
+		}
+	} else {
+		cs.Successes++
+	}
+}
+
+// AddSummary adds two CountSummaryByCategories together
+func (csc CountSummaryByCategory) AddSummary(other CountSummaryByCategory) {
+	categories := []string{}
+	for cat := range csc {
+		categories = append(categories, cat)
+	}
+	for cat := range other {
+		categories = append(categories, cat)
+	}
+	for _, cat := range categories {
+		cur := csc[cat]
+		cur.AddSummary(other[cat])
+		csc[cat] = cur
+	}
+}
+
+// GetSummary summarizes a ResultSet
+func (rs ResultSet) GetSummary() CountSummary {
+	cs := CountSummary{}
+	for _, result := range rs {
+		cs.AddResult(result)
+	}
+	return cs
+}
+
+// GetSummaryByCategory summarizes a ResultSet
+func (rs ResultSet) GetSummaryByCategory() CountSummaryByCategory {
+	summaries := CountSummaryByCategory{}
+	for _, result := range rs {
+		cs, ok := summaries[result.Category]
+		if !ok {
+			cs = CountSummary{}
+		}
+		cs.AddResult(result)
+		summaries[result.Category] = cs
+	}
+	return summaries
+}
+
+// GetSummary summarizes a PodResult
+func (p PodResult) GetSummary() CountSummary {
+	summary := p.Results.GetSummary()
+	for _, containerResult := range p.ContainerResults {
+		summary.AddSummary(containerResult.Results.GetSummary())
+	}
+	return summary
+}
+
+// GetSummaryByCategory summarizes a PodResult
+func (p PodResult) GetSummaryByCategory() CountSummaryByCategory {
+	summaries := p.Results.GetSummaryByCategory()
+	for _, containerResult := range p.ContainerResults {
+		summaries.AddSummary(containerResult.Results.GetSummaryByCategory())
+	}
+	return summaries
+}
+
+// GetSummary summarizes a ControllerResult
+func (c ControllerResult) GetSummary() CountSummary {
+	summary := c.Results.GetSummary()
+	summary.AddSummary(c.PodResult.GetSummary())
+	return summary
+}
+
+// GetSummaryByCategory summarizes a ControllerResult
+func (c ControllerResult) GetSummaryByCategory() CountSummaryByCategory {
+	summary := c.Results.GetSummaryByCategory()
+	summary.AddSummary(c.PodResult.GetSummaryByCategory())
+	return summary
+}
+
+// GetSummary summarizes AuditData
+func (a AuditData) GetSummary() CountSummary {
+	summary := CountSummary{}
+	for _, ctrlResult := range a.Results {
+		summary.AddSummary(ctrlResult.GetSummary())
+	}
+	return summary
+}
+
+// GetSummaryByCategory summarizes AuditData
+func (a AuditData) GetSummaryByCategory() CountSummaryByCategory {
+	summaries := CountSummaryByCategory{}
+	for _, ctrlResult := range a.Results {
+		summaries.AddSummary(ctrlResult.GetSummaryByCategory())
+	}
+	return summaries
+}
+
+// GetResultsByNamespace organizes results by namespace
+func (a AuditData) GetResultsByNamespace() map[string][]*ControllerResult {
+	allResults := map[string][]*ControllerResult{}
+	for idx, ctrlResult := range a.Results {
+		nsResults, ok := allResults[ctrlResult.Namespace]
+		if !ok {
+			nsResults = []*ControllerResult{}
+		}
+		nsResults = append(nsResults, &a.Results[idx])
+		allResults[ctrlResult.Namespace] = nsResults
+	}
+	return allResults
+}
+
+// GetSuccesses returns the success messages in a result set
+func (rs ResultSet) GetSuccesses() []ResultMessage {
+	successes := []ResultMessage{}
+	for _, msg := range rs {
+		if msg.Success {
+			successes = append(successes, msg)
+		}
+	}
+	return successes
+}
+
+// GetWarnings returns the warning messages in a result set
+func (rs ResultSet) GetWarnings() []ResultMessage {
+	warnings := []ResultMessage{}
+	for _, msg := range rs {
+		if msg.Success == false && msg.Severity == config.SeverityWarning {
+			warnings = append(warnings, msg)
+		}
+	}
+	return warnings
+}
+
+// GetErrors returns the error messages in a result set
+func (rs ResultSet) GetErrors() []ResultMessage {
+	errors := []ResultMessage{}
+	for _, msg := range rs {
+		if msg.Success == false && msg.Severity == config.SeverityError {
+			errors = append(errors, msg)
+		}
+	}
+	return errors
+}
+
+// GetSortedResults returns messages sorted as errors, then warnings, then successes
+func (rs ResultSet) GetSortedResults() []ResultMessage {
+	messages := []ResultMessage{}
+	messages = append(messages, rs.GetErrors()...)
+	messages = append(messages, rs.GetWarnings()...)
+	messages = append(messages, rs.GetSuccesses()...)
+	return messages
+}
diff --git a/pkg/validator/types.go b/pkg/validator/types.go
deleted file mode 100644
index ffe86474..00000000
--- a/pkg/validator/types.go
+++ /dev/null
@@ -1,270 +0,0 @@
-// Copyright 2019 FairwindsOps Inc
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package validator
-
-import (
-	"bytes"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"os"
-
-	"github.com/sirupsen/logrus"
-
-	"github.com/fairwindsops/polaris/pkg/config"
-	conf "github.com/fairwindsops/polaris/pkg/config"
-	corev1 "k8s.io/api/core/v1"
-	apiMachineryYAML "k8s.io/apimachinery/pkg/util/yaml"
-)
-
-const (
-	// PolarisOutputVersion is the version of the current output structure
-	PolarisOutputVersion = "0.3"
-)
-
-// AuditData contains all the data from a full Polaris audit
-type AuditData struct {
-	PolarisOutputVersion string
-	AuditTime            string
-	SourceType           string
-	SourceName           string
-	DisplayName          string
-	ClusterSummary       ClusterSummary
-	NamespacedResults    NamespacedResults
-}
-
-// ClusterSummary contains Polaris results as well as some high-level stats
-type ClusterSummary struct {
-	Results                ResultSummary
-	Version                string
-	Nodes                  int
-	Pods                   int
-	Namespaces             int
-	Deployments            int
-	StatefulSets           int
-	DaemonSets             int
-	Jobs                   int
-	CronJobs               int
-	ReplicationControllers int
-	Score                  uint
-}
-
-// MessageType represents the type of Message
-type MessageType string
-
-const (
-	// MessageTypeSuccess indicates a validation success
-	MessageTypeSuccess MessageType = "success"
-
-	// MessageTypeWarning indicates a validation warning
-	MessageTypeWarning MessageType = "warning"
-
-	// MessageTypeError indicates a validation error
-	MessageTypeError MessageType = "error"
-)
-
-// NamespaceResult groups container results by parent resource.
-type NamespaceResult struct {
-	Name    string
-	Summary *ResultSummary
-
-	// TODO: This struct could use some love to reorganize it as just having "results"
-	//       and then having methods to return filtered results by type
-	//       (deploy, daemonset, etc)
-	//       The way this is structured right now makes it difficult to add
-	//       additional result types and potentially miss things in the metrics
-	//       summary.
-	DeploymentResults            []ControllerResult
-	StatefulSetResults           []ControllerResult
-	DaemonSetResults             []ControllerResult
-	JobResults                   []ControllerResult
-	CronJobResults               []ControllerResult
-	ReplicationControllerResults []ControllerResult
-}
-
-// AddResult adds a result to the result sets by leveraging the types supported by NamespaceResult
-func (n *NamespaceResult) AddResult(resourceType config.SupportedController, result ControllerResult) error {
-	// Iterate all the resource types supported in this struct
-	var results *[]ControllerResult
-	switch resourceType {
-	case conf.Deployments:
-		results = &n.DeploymentResults
-	case conf.StatefulSets:
-		results = &n.StatefulSetResults
-	case conf.DaemonSets:
-		results = &n.DaemonSetResults
-	case conf.Jobs:
-		results = &n.JobResults
-	case conf.CronJobs:
-		results = &n.CronJobResults
-	case conf.ReplicationControllers:
-		results = &n.ReplicationControllerResults
-	default:
-		return fmt.Errorf("Unknown Resource Type: (%s) Missing Implementation in NamespacedResult", resourceType)
-	}
-
-	// Append the new result to the results pointer loaded from the supported values
-	*results = append(*results, result)
-
-	return nil
-}
-
-// GetAllControllerResults grabs all the different types of controller results from the namespaced result as a single list for easier iteration
-func (n NamespaceResult) GetAllControllerResults() []ControllerResult {
-	all := []ControllerResult{}
-	all = append(all, n.DeploymentResults...)
-	all = append(all, n.StatefulSetResults...)
-	all = append(all, n.DaemonSetResults...)
-	all = append(all, n.JobResults...)
-	all = append(all, n.CronJobResults...)
-	all = append(all, n.ReplicationControllerResults...)
-
-	return all
-}
-
-// NamespacedResults is a mapping of namespace name to the validation results.
-type NamespacedResults map[string]*NamespaceResult
-
-// GetAllControllerResults aggregates all the namespaced results in the set together
-func (nsResults NamespacedResults) GetAllControllerResults() []ControllerResult {
-	all := []ControllerResult{}
-	for _, nsResult := range nsResults {
-		all = append(all, nsResult.GetAllControllerResults()...)
-	}
-	return all
-}
-
-func (nsResults NamespacedResults) getNamespaceResult(nsName string) *NamespaceResult {
-	nsResult := &NamespaceResult{}
-	switch nsResults[nsName] {
-	case nil:
-		nsResult = &NamespaceResult{
-			Summary:                      &ResultSummary{},
-			DeploymentResults:            []ControllerResult{},
-			StatefulSetResults:           []ControllerResult{},
-			DaemonSetResults:             []ControllerResult{},
-			JobResults:                   []ControllerResult{},
-			CronJobResults:               []ControllerResult{},
-			ReplicationControllerResults: []ControllerResult{},
-		}
-		nsResults[nsName] = nsResult
-	default:
-		nsResult = nsResults[nsName]
-	}
-	return nsResult
-}
-
-// CountSummary provides a high level overview of success, warnings, and errors.
-type CountSummary struct {
-	Successes uint
-	Warnings  uint
-	Errors    uint
-}
-
-// GetScore returns an overall score in [0, 100] for the CountSummary
-func (cs *CountSummary) GetScore() uint {
-	total := (cs.Successes * 2) + cs.Warnings + (cs.Errors * 2)
-	return uint((float64(cs.Successes*2) / float64(total)) * 100)
-}
-
-func (cs *CountSummary) appendCounts(toAppend CountSummary) {
-	cs.Errors += toAppend.Errors
-	cs.Warnings += toAppend.Warnings
-	cs.Successes += toAppend.Successes
-}
-
-// CategorySummary provides a map from category name to a CountSummary
-type CategorySummary map[string]*CountSummary
-
-// ResultSummary provides a high level overview of success, warnings, and errors.
-type ResultSummary struct {
-	Totals     CountSummary
-	ByCategory CategorySummary
-}
-
-func (rs *ResultSummary) appendResults(toAppend ResultSummary) {
-	rs.Totals.appendCounts(toAppend.Totals)
-	for category, summary := range toAppend.ByCategory {
-		if rs.ByCategory == nil {
-			rs.ByCategory = CategorySummary{}
-		}
-		if _, exists := rs.ByCategory[category]; !exists {
-			rs.ByCategory[category] = &CountSummary{}
-		}
-		rs.ByCategory[category].appendCounts(*summary)
-	}
-}
-
-// ControllerResult provides a wrapper around a PodResult
-type ControllerResult struct {
-	Name      string
-	Type      string
-	PodResult PodResult
-}
-
-// ContainerResult provides a list of validation messages for each container.
-type ContainerResult struct {
-	Name     string
-	Messages []*ResultMessage
-	Summary  *ResultSummary
-}
-
-// PodResult provides a list of validation messages for each pod.
-type PodResult struct {
-	Name             string
-	Summary          *ResultSummary
-	Messages         []*ResultMessage
-	ContainerResults []ContainerResult
-	podSpec          corev1.PodSpec
-}
-
-// ResultMessage contains a message and a type indicator (success, warning, or error).
-type ResultMessage struct {
-	ID       string
-	Message  string
-	Type     MessageType
-	Category string
-}
-
-// ReadAuditFromFile reads the data from a past audit stored in a JSON or YAML file.
-func ReadAuditFromFile(fileName string) AuditData {
-	auditData := AuditData{}
-	oldFileBytes, err := ioutil.ReadFile(fileName)
-	if err != nil {
-		logrus.Errorf("Unable to read contents of loaded file: %v", err)
-		os.Exit(1)
-	}
-	auditData, err = ParseAudit(oldFileBytes)
-	if err != nil {
-		logrus.Errorf("Error parsing file contents into auditData: %v", err)
-		os.Exit(1)
-	}
-	return auditData
-}
-
-// ParseAudit decodes either a YAML or JSON file and returns AuditData.
-func ParseAudit(oldFileBytes []byte) (AuditData, error) {
-	reader := bytes.NewReader(oldFileBytes)
-	conf := AuditData{}
-	d := apiMachineryYAML.NewYAMLOrJSONDecoder(reader, 4096)
-	for {
-		if err := d.Decode(&conf); err != nil {
-			if err == io.EOF {
-				return conf, nil
-			}
-			return conf, fmt.Errorf("Decoding config failed: %v", err)
-		}
-	}
-}
diff --git a/pkg/webhook/validator.go b/pkg/webhook/validator.go
index 60e7f842..ba65d34f 100644
--- a/pkg/webhook/validator.go
+++ b/pkg/webhook/validator.go
@@ -23,6 +23,7 @@ import (
 	"github.com/fairwindsops/polaris/pkg/config"
 	validator "github.com/fairwindsops/polaris/pkg/validator"
 	"github.com/fairwindsops/polaris/pkg/validator/controllers"
+
 	"github.com/sirupsen/logrus"
 	admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
 	appsv1 "k8s.io/api/apps/v1"
@@ -93,8 +94,11 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
 
 	if req.AdmissionRequest.Kind.Kind == "Pod" {
 		pod := corev1.Pod{}
-		err = v.decoder.Decode(req, &pod)
-		podResult = validator.ValidatePod(v.Config, &pod.Spec, "", config.Unsupported)
+		err = v.decoder.Decode(req, &pod) // err is handled below
+		nakedPod := controllers.NewNakedPodController(pod)
+		if err == nil {
+			podResult, err = validator.ValidatePod(&v.Config, nakedPod)
+		}
 	} else {
 		var controller controllers.Interface
 		if yes := v.Config.CheckIfKindIsConfiguredForValidation(req.AdmissionRequest.Kind.Kind); !yes {
@@ -138,8 +142,11 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
 			err = v.decoder.Decode(req, &replicationController)
 			controller = controllers.NewReplicationControllerController(replicationController)
 		}
-		controllerResult := validator.ValidateController(v.Config, controller)
-		podResult = controllerResult.PodResult
+		if err == nil {
+			var controllerResult validator.ControllerResult
+			controllerResult, err = validator.ValidateController(&v.Config, controller)
+			podResult = controllerResult.PodResult
+		}
 	}
 
 	if err != nil {
@@ -149,26 +156,27 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
 
 	allowed := true
 	reason := ""
-	if podResult.Summary.Totals.Errors > 0 {
+	numErrors := podResult.GetSummary().Errors
+	if numErrors > 0 {
 		allowed = false
 		reason = getFailureReason(podResult)
 	}
-	logrus.Infof("%d validation errors found when validating %s", podResult.Summary.Totals.Errors, podResult.Name)
+	logrus.Infof("%d validation errors found when validating %s", numErrors, podResult.Name)
 	return admission.ValidationResponse(allowed, reason)
 }
 
 func getFailureReason(podResult validator.PodResult) string {
 	reason := "\nPolaris prevented this deployment due to configuration problems:\n"
 
-	for _, message := range podResult.Messages {
-		if message.Type == validator.MessageTypeError {
+	for _, message := range podResult.Results {
+		if !message.Success && message.Severity == config.SeverityError {
 			reason += fmt.Sprintf("- Pod: %s\n", message.Message)
 		}
 	}
 
 	for _, containerResult := range podResult.ContainerResults {
-		for _, message := range containerResult.Messages {
-			if message.Type == validator.MessageTypeError {
+		for _, message := range containerResult.Results {
+			if !message.Success && message.Severity == config.SeverityError {
 				reason += fmt.Sprintf("- Container %s: %s\n", containerResult.Name, message.Message)
 			}
 		}


Test patch:
diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go
index 66d20865..60997cb1 100644
--- a/pkg/validator/container_test.go
+++ b/pkg/validator/container_test.go
@@ -19,8 +19,12 @@ import (
 	"testing"
 
 	conf "github.com/fairwindsops/polaris/pkg/config"
+	"github.com/fairwindsops/polaris/pkg/validator/controllers"
+
 	"github.com/stretchr/testify/assert"
+	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 var resourceConfMinimal = `---
@@ -47,45 +51,47 @@ exemptions:
     - foo
 `
 
-func testValidate(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []*ResultMessage, expectedWarnings []*ResultMessage, expectedSuccesses []*ResultMessage) {
-	cv := ContainerValidation{
-		Container:          container,
-		ResourceValidation: &ResourceValidation{},
-	}
+func getEmptyController(name string) controllers.Interface {
+	return controllers.NewDeploymentController(appsv1.Deployment{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: name,
+		},
+		Spec: appsv1.DeploymentSpec{
+			Template: corev1.PodTemplateSpec{},
+		},
+	})
+}
 
+func testValidate(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) {
 	parsedConf, err := conf.Parse([]byte(*resourceConf))
 	assert.NoError(t, err, "Expected no error when parsing config")
 
-	err = applyContainerSchemaChecks(&parsedConf, controllerName, conf.Deployments, &cv)
+	results, err := applyContainerSchemaChecks(&parsedConf, getEmptyController(controllerName), container, false)
 	if err != nil {
 		panic(err)
 	}
+	summary := results.GetSummary()
 
-	assert.Len(t, cv.Warnings, len(expectedWarnings))
-	assert.ElementsMatch(t, expectedWarnings, cv.Warnings)
+	assert.Equal(t, uint(len(expectedWarnings)), summary.Warnings)
+	assert.ElementsMatch(t, expectedWarnings, results.GetWarnings())
 
-	assert.Len(t, cv.Errors, len(expectedErrors))
-	assert.ElementsMatch(t, expectedErrors, cv.Errors)
+	assert.Equal(t, uint(len(expectedErrors)), summary.Errors)
+	assert.ElementsMatch(t, expectedErrors, results.GetErrors())
 
-	assert.Len(t, cv.Successes, len(expectedSuccesses))
-	assert.ElementsMatch(t, expectedSuccesses, cv.Successes)
+	assert.Equal(t, uint(len(expectedSuccesses)), summary.Successes)
+	assert.ElementsMatch(t, expectedSuccesses, results.GetSuccesses())
 }
 
 func TestValidateResourcesEmptyConfig(t *testing.T) {
-	container := corev1.Container{
+	container := &corev1.Container{
 		Name: "Empty",
 	}
 
-	cv := ContainerValidation{
-		Container:          &container,
-		ResourceValidation: &ResourceValidation{},
-	}
-
-	err := applyContainerSchemaChecks(&conf.Configuration{}, "", conf.Deployments, &cv)
+	results, err := applyContainerSchemaChecks(&conf.Configuration{}, getEmptyController(""), container, false)
 	if err != nil {
 		panic(err)
 	}
-	assert.Len(t, cv.Errors, 0)
+	assert.Equal(t, uint(0), results.GetSummary().Errors)
 }
 
 func TestValidateResourcesEmptyContainer(t *testing.T) {
@@ -93,37 +99,41 @@ func TestValidateResourcesEmptyContainer(t *testing.T) {
 		Name: "Empty",
 	}
 
-	expectedWarnings := []*ResultMessage{
+	expectedWarnings := []ResultMessage{
 		{
 			ID:       "cpuRequestsMissing",
-			Type:     "warning",
+			Success:  false,
+			Severity: "warning",
 			Message:  "CPU requests should be set",
 			Category: "Resources",
 		},
 		{
 			ID:       "memoryRequestsMissing",
-			Type:     "warning",
+			Success:  false,
+			Severity: "warning",
 			Message:  "Memory requests should be set",
 			Category: "Resources",
 		},
 	}
 
-	expectedErrors := []*ResultMessage{
+	expectedErrors := []ResultMessage{
 		{
 			ID:       "cpuLimitsMissing",
-			Type:     "error",
+			Success:  false,
+			Severity: "error",
 			Message:  "CPU limits should be set",
 			Category: "Resources",
 		},
 		{
 			ID:       "memoryLimitsMissing",
-			Type:     "error",
+			Success:  false,
+			Severity: "error",
 			Message:  "Memory limits should be set",
 			Category: "Resources",
 		},
 	}
 
-	expectedSuccesses := []*ResultMessage{}
+	expectedSuccesses := []ResultMessage{}
 
 	testValidate(t, &container, &resourceConfMinimal, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
 }
@@ -142,60 +152,55 @@ func TestValidateHealthChecks(t *testing.T) {
 	}
 
 	probe := corev1.Probe{}
-	emptyCV := ContainerValidation{
-		Container:          &corev1.Container{Name: ""},
-		ResourceValidation: &ResourceValidation{},
-	}
-	emptyCVInit := ContainerValidation{
-		Container:          &corev1.Container{Name: ""},
-		ResourceValidation: &ResourceValidation{},
-		IsInitContainer:    true,
-	}
-	goodCV := ContainerValidation{
-		Container: &corev1.Container{
-			Name:           "",
-			LivenessProbe:  &probe,
-			ReadinessProbe: &probe,
-		},
-		ResourceValidation: &ResourceValidation{},
+	emptyContainer := &corev1.Container{Name: ""}
+	goodContainer := &corev1.Container{
+		Name:           "",
+		LivenessProbe:  &probe,
+		ReadinessProbe: &probe,
 	}
 
-	l := &ResultMessage{ID: "livenessProbeMissing", Type: "warning", Message: "Liveness probe should be configured", Category: "Health Checks"}
-	r := &ResultMessage{ID: "readinessProbeMissing", Type: "error", Message: "Readiness probe should be configured", Category: "Health Checks"}
-	f1 := []*ResultMessage{}
-	f2 := []*ResultMessage{r}
-	w1 := []*ResultMessage{l}
+	l := ResultMessage{ID: "livenessProbeMissing", Success: false, Severity: "warning", Message: "Liveness probe should be configured", Category: "Health Checks"}
+	r := ResultMessage{ID: "readinessProbeMissing", Success: false, Severity: "error", Message: "Readiness probe should be configured", Category: "Health Checks"}
+	f1 := []ResultMessage{}
+	f2 := []ResultMessage{r}
+	w1 := []ResultMessage{l}
 
 	var testCases = []struct {
-		name     string
-		probes   map[string]conf.Severity
-		cv       ContainerValidation
-		errors   *[]*ResultMessage
-		warnings *[]*ResultMessage
+		name      string
+		probes    map[string]conf.Severity
+		container *corev1.Container
+		isInit    bool
+		errors    *[]ResultMessage
+		warnings  *[]ResultMessage
 	}{
-		{name: "probes not configured", probes: p1, cv: emptyCV, errors: &f1},
-		{name: "probes not required", probes: p2, cv: emptyCV, errors: &f1},
-		{name: "probes required & configured", probes: p3, cv: goodCV, errors: &f1},
-		{name: "probes required, not configured, but init", probes: p3, cv: emptyCVInit, errors: &f1},
-		{name: "probes required & not configured", probes: p3, cv: emptyCV, errors: &f2, warnings: &w1},
-		{name: "probes configured, but not required", probes: p2, cv: goodCV, errors: &f1},
+		{name: "probes not configured", probes: p1, container: emptyContainer, errors: &f1},
+		{name: "probes not required", probes: p2, container: emptyContainer, errors: &f1},
+		{name: "probes required & configured", probes: p3, container: goodContainer, errors: &f1},
+		{name: "probes required, not configured, but init", probes: p3, container: emptyContainer, isInit: true, errors: &f1},
+		{name: "probes required & not configured", probes: p3, container: emptyContainer, errors: &f2, warnings: &w1},
+		{name: "probes configured, but not required", probes: p2, container: goodContainer, errors: &f1},
 	}
 
 	for idx, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
-			err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.probes}, "", conf.Deployments, &tt.cv)
+			controller := getEmptyController("")
+			results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.probes}, controller, tt.container, tt.isInit)
 			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), message)
-				assert.ElementsMatch(t, tt.cv.Warnings, *tt.warnings, message)
+				warnings := results.GetWarnings()
+				assert.Len(t, warnings, len(*tt.warnings), message)
+				assert.ElementsMatch(t, warnings, *tt.warnings, message)
 			}
 
-			assert.Len(t, tt.cv.Errors, len(*tt.errors), message)
-			assert.ElementsMatch(t, tt.cv.Errors, *tt.errors, message)
+			if tt.errors != nil {
+				errors := results.GetErrors()
+				assert.Len(t, errors, len(*tt.errors), message)
+				assert.ElementsMatch(t, errors, *tt.errors, message)
+			}
 		})
 	}
 }
@@ -211,101 +216,95 @@ func TestValidateImage(t *testing.T) {
 		"pullPolicyNotAlways": conf.SeverityError,
 	}
 
-	emptyCV := ContainerValidation{
-		Container:          &corev1.Container{},
-		ResourceValidation: &ResourceValidation{},
-	}
-	badCV := ContainerValidation{
-		Container:          &corev1.Container{Image: "test"},
-		ResourceValidation: &ResourceValidation{},
-	}
-	lessBadCV := ContainerValidation{
-		Container:          &corev1.Container{Image: "test:latest", ImagePullPolicy: ""},
-		ResourceValidation: &ResourceValidation{},
-	}
-	goodCV := ContainerValidation{
-		Container:          &corev1.Container{Image: "test:0.1.0", ImagePullPolicy: "Always"},
-		ResourceValidation: &ResourceValidation{},
-	}
+	emptyContainer := &corev1.Container{}
+	badContainer := &corev1.Container{Image: "test"}
+	lessBadContainer := &corev1.Container{Image: "test:latest", ImagePullPolicy: ""}
+	goodContainer := &corev1.Container{Image: "test:0.1.0", ImagePullPolicy: "Always"}
 
 	var testCases = []struct {
-		name     string
-		image    map[string]conf.Severity
-		cv       ContainerValidation
-		expected []*ResultMessage
+		name      string
+		image     map[string]conf.Severity
+		container *corev1.Container
+		expected  []ResultMessage
 	}{
 		{
-			name:     "emptyConf + emptyCV",
-			image:    emptyConf,
-			cv:       emptyCV,
-			expected: []*ResultMessage{},
+			name:      "emptyConf + emptyCV",
+			image:     emptyConf,
+			container: emptyContainer,
+			expected:  []ResultMessage{},
 		},
 		{
-			name:  "standardConf + emptyCV",
-			image: standardConf,
-			cv:    emptyCV,
-			expected: []*ResultMessage{{
+			name:      "standardConf + emptyCV",
+			image:     standardConf,
+			container: emptyContainer,
+			expected: []ResultMessage{{
 				ID:       "tagNotSpecified",
 				Message:  "Image tag should be specified",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Images",
 			}},
 		},
 		{
-			name:  "standardConf + badCV",
-			image: standardConf,
-			cv:    badCV,
-			expected: []*ResultMessage{{
+			name:      "standardConf + badCV",
+			image:     standardConf,
+			container: badContainer,
+			expected: []ResultMessage{{
 				ID:       "tagNotSpecified",
 				Message:  "Image tag should be specified",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Images",
 			}},
 		},
 		{
-			name:  "standardConf + lessBadCV",
-			image: standardConf,
-			cv:    lessBadCV,
-			expected: []*ResultMessage{{
+			name:      "standardConf + lessBadCV",
+			image:     standardConf,
+			container: lessBadContainer,
+			expected: []ResultMessage{{
 				ID:       "tagNotSpecified",
 				Message:  "Image tag should be specified",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Images",
 			}},
 		},
 		{
-			name:  "strongConf + badCV",
-			image: strongConf,
-			cv:    badCV,
-			expected: []*ResultMessage{{
+			name:      "strongConf + badCV",
+			image:     strongConf,
+			container: badContainer,
+			expected: []ResultMessage{{
 				ID:       "pullPolicyNotAlways",
 				Message:  "Image pull policy should be \"Always\"",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Images",
 			}, {
 				ID:       "tagNotSpecified",
 				Message:  "Image tag should be specified",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Images",
 			}},
 		},
 		{
-			name:     "strongConf + goodCV",
-			image:    strongConf,
-			cv:       goodCV,
-			expected: []*ResultMessage{},
+			name:      "strongConf + goodCV",
+			image:     strongConf,
+			container: goodContainer,
+			expected:  []ResultMessage{},
 		},
 	}
 
 	for _, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
-			tt.cv = resetCV(tt.cv)
-			err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.image}, "", conf.Deployments, &tt.cv)
+			controller := getEmptyController("")
+			results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.image}, controller, tt.container, false)
 			if err != nil {
 				panic(err)
 			}
-			assert.Len(t, tt.cv.Errors, len(tt.expected))
-			assert.ElementsMatch(t, tt.cv.Errors, tt.expected)
+			errors := results.GetErrors()
+			assert.Len(t, errors, len(tt.expected))
+			assert.ElementsMatch(t, errors, tt.expected)
 		})
 	}
 }
@@ -320,100 +319,94 @@ func TestValidateNetworking(t *testing.T) {
 		"hostPortSet": conf.SeverityError,
 	}
 
-	emptyCV := ContainerValidation{
-		Container:          &corev1.Container{Name: ""},
-		ResourceValidation: &ResourceValidation{},
-	}
-
-	badCV := ContainerValidation{
-		Container: &corev1.Container{
-			Ports: []corev1.ContainerPort{{
-				ContainerPort: 3000,
-				HostPort:      443,
-			}},
-		},
-		ResourceValidation: &ResourceValidation{},
+	emptyContainer := &corev1.Container{Name: ""}
+	badContainer := &corev1.Container{
+		Ports: []corev1.ContainerPort{{
+			ContainerPort: 3000,
+			HostPort:      443,
+		}},
 	}
-
-	goodCV := ContainerValidation{
-		Container: &corev1.Container{
-			Ports: []corev1.ContainerPort{{
-				ContainerPort: 3000,
-			}},
-		},
-		ResourceValidation: &ResourceValidation{},
+	goodContainer := &corev1.Container{
+		Ports: []corev1.ContainerPort{{
+			ContainerPort: 3000,
+		}},
 	}
 
 	var testCases = []struct {
-		name             string
-		networkConf      map[string]conf.Severity
-		cv               ContainerValidation
-		expectedMessages []*ResultMessage
+		name            string
+		networkConf     map[string]conf.Severity
+		container       *corev1.Container
+		expectedResults []ResultMessage
 	}{
 		{
-			name:             "empty ports + empty validation config",
-			networkConf:      emptyConf,
-			cv:               emptyCV,
-			expectedMessages: []*ResultMessage{},
+			name:            "empty ports + empty validation config",
+			networkConf:     emptyConf,
+			container:       emptyContainer,
+			expectedResults: []ResultMessage{},
 		},
 		{
 			name:        "empty ports + standard validation config",
 			networkConf: standardConf,
-			cv:          emptyCV,
-			expectedMessages: []*ResultMessage{{
+			container:   emptyContainer,
+			expectedResults: []ResultMessage{{
 				ID:       "hostPortSet",
 				Message:  "Host port is not configured",
-				Type:     "success",
+				Success:  true,
+				Severity: "warning",
 				Category: "Networking",
 			}},
 		},
 		{
 			name:        "empty ports + strong validation config",
 			networkConf: standardConf,
-			cv:          emptyCV,
-			expectedMessages: []*ResultMessage{{
+			container:   emptyContainer,
+			expectedResults: []ResultMessage{{
 				ID:       "hostPortSet",
 				Message:  "Host port is not configured",
-				Type:     "success",
+				Success:  true,
+				Severity: "warning",
 				Category: "Networking",
 			}},
 		},
 		{
-			name:             "host ports + empty validation config",
-			networkConf:      emptyConf,
-			cv:               badCV,
-			expectedMessages: []*ResultMessage{},
+			name:            "host ports + empty validation config",
+			networkConf:     emptyConf,
+			container:       badContainer,
+			expectedResults: []ResultMessage{},
 		},
 		{
 			name:        "host ports + standard validation config",
 			networkConf: standardConf,
-			cv:          badCV,
-			expectedMessages: []*ResultMessage{{
+			container:   badContainer,
+			expectedResults: []ResultMessage{{
 				ID:       "hostPortSet",
 				Message:  "Host port should not be configured",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Networking",
 			}},
 		},
 		{
 			name:        "no host ports + standard validation config",
 			networkConf: standardConf,
-			cv:          goodCV,
-			expectedMessages: []*ResultMessage{{
+			container:   goodContainer,
+			expectedResults: []ResultMessage{{
 				ID:       "hostPortSet",
 				Message:  "Host port is not configured",
-				Type:     "success",
+				Success:  true,
+				Severity: "warning",
 				Category: "Networking",
 			}},
 		},
 		{
 			name:        "host ports + strong validation config",
 			networkConf: strongConf,
-			cv:          badCV,
-			expectedMessages: []*ResultMessage{{
+			container:   badContainer,
+			expectedResults: []ResultMessage{{
 				ID:       "hostPortSet",
 				Message:  "Host port should not be configured",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Networking",
 			}},
 		},
@@ -421,13 +414,17 @@ 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{Checks: tt.networkConf}, "", conf.Deployments, &tt.cv)
+			controller := getEmptyController("")
+			results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.networkConf}, controller, tt.container, false)
 			if err != nil {
 				panic(err)
 			}
-			assert.Len(t, tt.cv.messages(), len(tt.expectedMessages))
-			assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages)
+			messages := []ResultMessage{}
+			for _, msg := range results {
+				messages = append(messages, msg)
+			}
+			assert.Len(t, messages, len(tt.expectedResults))
+			assert.ElementsMatch(t, messages, tt.expectedResults)
 		})
 	}
 }
@@ -455,13 +452,10 @@ func TestValidateSecurity(t *testing.T) {
 		"insecureCapabilities":       conf.SeverityError,
 	}
 
-	emptyCV := ContainerValidation{
-		Container:          &corev1.Container{Name: ""},
-		ResourceValidation: &ResourceValidation{},
-	}
-
-	badCV := ContainerValidation{
-		Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
+	emptyContainer := &corev1.Container{Name: ""}
+	badContainer := &corev1.Container{
+		Name: "",
+		SecurityContext: &corev1.SecurityContext{
 			RunAsNonRoot:             &falseVar,
 			ReadOnlyRootFilesystem:   &falseVar,
 			Privileged:               &trueVar,
@@ -469,441 +463,454 @@ func TestValidateSecurity(t *testing.T) {
 			Capabilities: &corev1.Capabilities{
 				Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"},
 			},
-		}},
-		ResourceValidation: &ResourceValidation{},
-	}
-
-	badCVWithGoodPodSpec := ContainerValidation{
-		Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
-			RunAsNonRoot:             &falseVar,
-			ReadOnlyRootFilesystem:   &falseVar,
-			Privileged:               &trueVar,
-			AllowPrivilegeEscalation: &trueVar,
-			Capabilities: &corev1.Capabilities{
-				Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"},
-			},
-		}},
-		ResourceValidation: &ResourceValidation{},
-		parentPodSpec: corev1.PodSpec{
-			SecurityContext: &corev1.PodSecurityContext{
-				RunAsNonRoot: &trueVar,
-			},
 		},
 	}
-
-	badCVWithBadPodSpec := ContainerValidation{
-		Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
-			RunAsNonRoot:             nil, // this will use the default from the podspec
-			ReadOnlyRootFilesystem:   &falseVar,
-			Privileged:               &trueVar,
-			AllowPrivilegeEscalation: &trueVar,
-			Capabilities: &corev1.Capabilities{
-				Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"},
-			},
-		}},
-		ResourceValidation: &ResourceValidation{},
-		parentPodSpec: corev1.PodSpec{
-			SecurityContext: &corev1.PodSecurityContext{
-				RunAsNonRoot: &falseVar,
-			},
+	emptyPodSpec := &corev1.PodSpec{}
+	goodPodSpec := &corev1.PodSpec{
+		SecurityContext: &corev1.PodSecurityContext{
+			RunAsNonRoot: &trueVar,
 		},
 	}
-
-	goodCV := ContainerValidation{
-		Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
-			RunAsNonRoot:             &trueVar,
-			ReadOnlyRootFilesystem:   &trueVar,
-			Privileged:               &falseVar,
-			AllowPrivilegeEscalation: &falseVar,
-			Capabilities: &corev1.Capabilities{
-				Drop: []corev1.Capability{"NET_BIND_SERVICE", "FOWNER"},
-			},
-		}},
-		ResourceValidation: &ResourceValidation{},
+	badPodSpec := &corev1.PodSpec{
+		SecurityContext: &corev1.PodSecurityContext{
+			RunAsNonRoot: &falseVar,
+		},
 	}
-
-	strongCV := ContainerValidation{
-		Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
-			RunAsNonRoot:             &trueVar,
+	inheritContainer := &corev1.Container{
+		Name: "",
+		SecurityContext: &corev1.SecurityContext{
+			RunAsNonRoot:             nil, // this will use the default from the podspec
 			ReadOnlyRootFilesystem:   &trueVar,
 			Privileged:               &falseVar,
 			AllowPrivilegeEscalation: &falseVar,
 			Capabilities: &corev1.Capabilities{
 				Drop: []corev1.Capability{"ALL"},
 			},
-		}},
-		ResourceValidation: &ResourceValidation{},
+		},
 	}
-
-	strongCVWithPodSpecSecurityContext := ContainerValidation{
-		Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
-			RunAsNonRoot:             nil, // not set but overridden via podSpec
+	goodContainer := &corev1.Container{
+		Name: "",
+		SecurityContext: &corev1.SecurityContext{
+			RunAsNonRoot:             &trueVar,
 			ReadOnlyRootFilesystem:   &trueVar,
 			Privileged:               &falseVar,
 			AllowPrivilegeEscalation: &falseVar,
 			Capabilities: &corev1.Capabilities{
-				Drop: []corev1.Capability{"ALL"},
-			},
-		}},
-		ResourceValidation: &ResourceValidation{},
-		parentPodSpec: corev1.PodSpec{
-			SecurityContext: &corev1.PodSecurityContext{
-				RunAsNonRoot: &trueVar,
+				Drop: []corev1.Capability{"NET_BIND_SERVICE", "FOWNER"},
 			},
 		},
 	}
-
-	strongCVWithBadPodSpecSecurityContext := ContainerValidation{
-		Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
-			RunAsNonRoot:             &trueVar, // will override the bad setting in PodSpec
+	strongContainer := &corev1.Container{
+		Name: "",
+		SecurityContext: &corev1.SecurityContext{
+			RunAsNonRoot:             &trueVar,
 			ReadOnlyRootFilesystem:   &trueVar,
 			Privileged:               &falseVar,
 			AllowPrivilegeEscalation: &falseVar,
 			Capabilities: &corev1.Capabilities{
 				Drop: []corev1.Capability{"ALL"},
 			},
-		}},
-		ResourceValidation: &ResourceValidation{},
-		parentPodSpec: corev1.PodSpec{
-			SecurityContext: &corev1.PodSecurityContext{
-				RunAsNonRoot: &falseVar, // is overridden at container level with RunAsNonRoot:true
-			},
 		},
 	}
 
 	var testCases = []struct {
-		name             string
-		securityConf     map[string]conf.Severity
-		cv               ContainerValidation
-		expectedMessages []*ResultMessage
+		name            string
+		securityConf    map[string]conf.Severity
+		container       *corev1.Container
+		pod             *corev1.PodSpec
+		expectedResults []ResultMessage
 	}{
 		{
-			name:             "empty security context + empty validation config",
-			securityConf:     emptyConf,
-			cv:               emptyCV,
-			expectedMessages: []*ResultMessage{},
+			name:            "empty security context + empty validation config",
+			securityConf:    emptyConf,
+			container:       emptyContainer,
+			pod:             emptyPodSpec,
+			expectedResults: []ResultMessage{},
 		},
 		{
 			name:         "empty security context + standard validation config",
 			securityConf: standardConf,
-			cv:           emptyCV,
-			expectedMessages: []*ResultMessage{{
+			container:    emptyContainer,
+			pod:          emptyPodSpec,
+			expectedResults: []ResultMessage{{
 				ID:       "runAsRootAllowed",
 				Message:  "Should not be allowed to run as root",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			}, {
 				ID:       "notReadOnlyRootFileSystem",
 				Message:  "Filesystem should be read only",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			}, {
 				ID:       "runAsPrivileged",
 				Message:  "Not running as privileged",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "privilegeEscalationAllowed",
 				Message:  "Privilege escalation not allowed",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "insecureCapabilities",
 				Message:  "Container does not have any insecure capabilities",
-				Type:     "success",
+				Success:  true,
+				Severity: "warning",
 				Category: "Security",
 			}, {
 				ID:       "dangerousCapabilities",
 				Message:  "Container does not have any dangerous capabilities",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}},
 		},
 		{
 			name:         "bad security context + standard validation config",
 			securityConf: standardConf,
-			cv:           badCV,
-			expectedMessages: []*ResultMessage{{
+			container:    badContainer,
+			pod:          emptyPodSpec,
+			expectedResults: []ResultMessage{{
 				ID:       "dangerousCapabilities",
 				Message:  "Container should not have dangerous capabilities",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "privilegeEscalationAllowed",
 				Message:  "Privilege escalation should not be allowed",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "runAsPrivileged",
 				Message:  "Should not be running as privileged",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "insecureCapabilities",
 				Message:  "Container should not have insecure capabilities",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			}, {
 				ID:       "runAsRootAllowed",
 				Message:  "Should not be allowed to run as root",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			}, {
 				ID:       "notReadOnlyRootFileSystem",
 				Message:  "Filesystem should be read only",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			}},
 		},
 		{
 			name:         "bad security context + standard validation config with good settings in podspec",
 			securityConf: standardConf,
-			cv:           badCVWithGoodPodSpec,
-			expectedMessages: []*ResultMessage{{
+			container:    badContainer,
+			pod:          goodPodSpec,
+			expectedResults: []ResultMessage{{
 				ID:       "dangerousCapabilities",
 				Message:  "Container should not have dangerous capabilities",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "privilegeEscalationAllowed",
 				Message:  "Privilege escalation should not be allowed",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "runAsPrivileged",
 				Message:  "Should not be running as privileged",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "insecureCapabilities",
 				Message:  "Container should not have insecure capabilities",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			}, {
 				ID:       "runAsRootAllowed",
 				Message:  "Should not be allowed to run as root",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			}, {
 				ID:       "notReadOnlyRootFileSystem",
 				Message:  "Filesystem should be read only",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			}},
 		},
 		{
 			name:         "bad security context + standard validation config from default set in podspec",
 			securityConf: standardConf,
-			cv:           badCVWithBadPodSpec,
-			expectedMessages: []*ResultMessage{{
+			container:    badContainer,
+			pod:          badPodSpec,
+			expectedResults: []ResultMessage{{
 				ID:       "dangerousCapabilities",
 				Message:  "Container should not have dangerous capabilities",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "insecureCapabilities",
 				Message:  "Container should not have insecure capabilities",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			}, {
 				ID:       "privilegeEscalationAllowed",
 				Message:  "Privilege escalation should not be allowed",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "runAsPrivileged",
 				Message:  "Should not be running as privileged",
-				Type:     "error",
+				Success:  false,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "runAsRootAllowed",
 				Message:  "Should not be allowed to run as root",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			}, {
 				ID:       "notReadOnlyRootFileSystem",
 				Message:  "Filesystem should be read only",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			}},
 		},
 		{
 			name:         "good security context + standard validation config",
 			securityConf: standardConf,
-			cv:           goodCV,
-			expectedMessages: []*ResultMessage{{
+			container:    goodContainer,
+			pod:          emptyPodSpec,
+			expectedResults: []ResultMessage{{
 				ID:       "runAsRootAllowed",
 				Message:  "Is not allowed to run as root",
-				Type:     "success",
+				Success:  true,
+				Severity: "warning",
 				Category: "Security",
 			}, {
 				ID:       "notReadOnlyRootFileSystem",
 				Message:  "Filesystem is read only",
-				Type:     "success",
+				Success:  true,
+				Severity: "warning",
 				Category: "Security",
 			}, {
 				ID:       "runAsPrivileged",
 				Message:  "Not running as privileged",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "privilegeEscalationAllowed",
 				Message:  "Privilege escalation not allowed",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "dangerousCapabilities",
 				Message:  "Container does not have any dangerous capabilities",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "insecureCapabilities",
 				Message:  "Container does not have any insecure capabilities",
-				Type:     "success",
+				Success:  true,
+				Severity: "warning",
 				Category: "Security",
 			}},
 		},
 		{
 			name:         "good security context + strong validation config",
 			securityConf: strongConf,
-			cv:           goodCV,
-			expectedMessages: []*ResultMessage{{
+			container:    goodContainer,
+			pod:          emptyPodSpec,
+			expectedResults: []ResultMessage{{
 				ID:       "dangerousCapabilities",
 				Message:  "Container does not have any dangerous capabilities",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "insecureCapabilities",
 				Message:  "Container does not have any insecure capabilities",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "runAsRootAllowed",
 				Message:  "Is not allowed to run as root",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "notReadOnlyRootFileSystem",
 				Message:  "Filesystem is read only",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "runAsPrivileged",
 				Message:  "Not running as privileged",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "privilegeEscalationAllowed",
 				Message:  "Privilege escalation not allowed",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}},
 		},
 		{
 			name:         "strong security context + strong validation config",
 			securityConf: strongConf,
-			cv:           strongCV,
-			expectedMessages: []*ResultMessage{{
+			container:    strongContainer,
+			pod:          emptyPodSpec,
+			expectedResults: []ResultMessage{{
 				ID:       "runAsRootAllowed",
 				Message:  "Is not allowed to run as root",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "notReadOnlyRootFileSystem",
 				Message:  "Filesystem is read only",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "runAsPrivileged",
 				Message:  "Not running as privileged",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "privilegeEscalationAllowed",
 				Message:  "Privilege escalation not allowed",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "dangerousCapabilities",
 				Message:  "Container does not have any dangerous capabilities",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "insecureCapabilities",
 				Message:  "Container does not have any insecure capabilities",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}},
 		},
 		{
 			name:         "strong security context + strong validation config via podspec default",
 			securityConf: strongConf,
-			cv:           strongCVWithPodSpecSecurityContext,
-			expectedMessages: []*ResultMessage{{
+			container:    inheritContainer,
+			pod:          goodPodSpec,
+			expectedResults: []ResultMessage{{
 				ID:       "runAsRootAllowed",
 				Message:  "Is not allowed to run as root",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "notReadOnlyRootFileSystem",
 				Message:  "Filesystem is read only",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "runAsPrivileged",
 				Message:  "Not running as privileged",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "privilegeEscalationAllowed",
 				Message:  "Privilege escalation not allowed",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "dangerousCapabilities",
 				Message:  "Container does not have any dangerous capabilities",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "insecureCapabilities",
 				Message:  "Container does not have any insecure capabilities",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}},
 		},
 		{
 			name:         "strong security context + strong validation config with bad setting in podspec default",
 			securityConf: strongConf,
-			cv:           strongCVWithBadPodSpecSecurityContext,
-			expectedMessages: []*ResultMessage{{
+			container:    strongContainer,
+			pod:          badPodSpec,
+			expectedResults: []ResultMessage{{
 				ID:       "runAsRootAllowed",
 				Message:  "Is not allowed to run as root",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "notReadOnlyRootFileSystem",
 				Message:  "Filesystem is read only",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "runAsPrivileged",
 				Message:  "Not running as privileged",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "privilegeEscalationAllowed",
 				Message:  "Privilege escalation not allowed",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "dangerousCapabilities",
 				Message:  "Container does not have any dangerous capabilities",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}, {
 				ID:       "insecureCapabilities",
 				Message:  "Container does not have any insecure capabilities",
-				Type:     "success",
+				Success:  true,
+				Severity: "error",
 				Category: "Security",
 			}},
 		},
@@ -911,13 +918,26 @@ 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{Checks: tt.securityConf}, "", conf.Deployments, &tt.cv)
+			controller := controllers.NewDeploymentController(appsv1.Deployment{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "",
+				},
+				Spec: appsv1.DeploymentSpec{
+					Template: corev1.PodTemplateSpec{
+						Spec: *tt.pod,
+					},
+				},
+			})
+			results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.securityConf}, controller, tt.container, false)
 			if err != nil {
 				panic(err)
 			}
-			assert.Len(t, tt.cv.messages(), len(tt.expectedMessages))
-			assert.ElementsMatch(t, tt.expectedMessages, tt.cv.messages())
+			messages := []ResultMessage{}
+			for _, msg := range results {
+				messages = append(messages, msg)
+			}
+			assert.Len(t, messages, len(tt.expectedResults))
+			assert.ElementsMatch(t, tt.expectedResults, messages)
 		})
 	}
 }
@@ -932,134 +952,146 @@ func TestValidateRunAsRoot(t *testing.T) {
 			"runAsRootAllowed": conf.SeverityWarning,
 		},
 	}
+
+	goodContainer := &corev1.Container{
+		SecurityContext: &corev1.SecurityContext{
+			RunAsNonRoot: &trueVar,
+		},
+	}
+	badContainer := &corev1.Container{
+		SecurityContext: &corev1.SecurityContext{
+			RunAsNonRoot: &falseVar,
+		},
+	}
+	inheritContainer := &corev1.Container{
+		SecurityContext: &corev1.SecurityContext{
+			RunAsNonRoot: nil,
+		},
+	}
+	runAsUserContainer := &corev1.Container{
+		SecurityContext: &corev1.SecurityContext{
+			RunAsUser: &nonRootUser,
+		},
+	}
+	runAsUser0Container := &corev1.Container{
+		SecurityContext: &corev1.SecurityContext{
+			RunAsUser: &rootUser,
+		},
+	}
+	badPod := &corev1.PodSpec{
+		SecurityContext: &corev1.PodSecurityContext{
+			RunAsNonRoot: &falseVar,
+		},
+	}
+	runAsUserPod := &corev1.PodSpec{
+		SecurityContext: &corev1.PodSecurityContext{
+			RunAsUser: &nonRootUser,
+		},
+	}
+	emptyPod := &corev1.PodSpec{}
+
 	testCases := []struct {
-		name    string
-		cv      ContainerValidation
-		message ResultMessage
+		name      string
+		container *corev1.Container
+		pod       *corev1.PodSpec
+		message   ResultMessage
 	}{
 		{
-			name: "pod=false,container=nil",
-			cv: ContainerValidation{
-				ResourceValidation: &ResourceValidation{},
-				Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
-					RunAsNonRoot: nil,
-				}},
-				parentPodSpec: corev1.PodSpec{
-					SecurityContext: &corev1.PodSecurityContext{
-						RunAsNonRoot: &falseVar,
-					},
-				},
-			},
+			name:      "pod=false,container=nil",
+			container: inheritContainer,
+			pod:       badPod,
 			message: ResultMessage{
 				ID:       "runAsRootAllowed",
 				Message:  "Should not be allowed to run as root",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			},
 		},
 		{
-			name: "pod=false,container=true",
-			cv: ContainerValidation{
-				ResourceValidation: &ResourceValidation{},
-				Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
-					RunAsNonRoot: &trueVar,
-				}},
-				parentPodSpec: corev1.PodSpec{
-					SecurityContext: &corev1.PodSecurityContext{
-						RunAsNonRoot: &falseVar,
-					},
-				},
-			},
+			name:      "pod=false,container=true",
+			container: goodContainer,
+			pod:       badPod,
 			message: ResultMessage{
 				ID:       "runAsRootAllowed",
 				Message:  "Is not allowed to run as root",
-				Type:     "success",
+				Success:  true,
+				Severity: "warning",
 				Category: "Security",
 			},
 		},
 		{
-			name: "pod=nil,container=runAsUser",
-			cv: ContainerValidation{
-				ResourceValidation: &ResourceValidation{},
-				Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
-					RunAsUser: &nonRootUser,
-				}},
-			},
+			name:      "pod=nil,container=runAsUser",
+			container: runAsUserContainer,
+			pod:       emptyPod,
 			message: ResultMessage{
 				ID:       "runAsRootAllowed",
 				Message:  "Is not allowed to run as root",
-				Type:     "success",
+				Success:  true,
+				Severity: "warning",
 				Category: "Security",
 			},
 		},
 		{
-			name: "pod=runAsUser,container=nil",
-			cv: ContainerValidation{
-				ResourceValidation: &ResourceValidation{},
-				Container:          &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{}},
-				parentPodSpec: corev1.PodSpec{
-					SecurityContext: &corev1.PodSecurityContext{
-						RunAsUser: &nonRootUser,
-					},
-				},
-			},
+			name:      "pod=runAsUser,container=nil",
+			container: inheritContainer,
+			pod:       runAsUserPod,
 			message: ResultMessage{
 				ID:       "runAsRootAllowed",
 				Message:  "Is not allowed to run as root",
-				Type:     "success",
+				Success:  true,
+				Severity: "warning",
 				Category: "Security",
 			},
 		},
 		{
-			name: "pod=runAsUser,container=runAsUser0",
-			cv: ContainerValidation{
-				ResourceValidation: &ResourceValidation{},
-				Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
-					RunAsUser: &rootUser,
-				}},
-				parentPodSpec: corev1.PodSpec{
-					SecurityContext: &corev1.PodSecurityContext{
-						RunAsUser: &nonRootUser,
-					},
-				},
-			},
+			name:      "pod=runAsUser,container=runAsUser0",
+			container: runAsUser0Container,
+			pod:       runAsUserPod,
 			message: ResultMessage{
 				ID:       "runAsRootAllowed",
 				Message:  "Should not be allowed to run as root",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			},
 		},
 		{
-			name: "pod=false,container=runAsUser",
-			cv: ContainerValidation{
-				ResourceValidation: &ResourceValidation{},
-				Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
-					RunAsNonRoot: &falseVar,
-				}},
-				parentPodSpec: corev1.PodSpec{
-					SecurityContext: &corev1.PodSecurityContext{
-						RunAsUser: &nonRootUser,
-					},
-				},
-			},
+			name:      "pod=runAsUser,container=false",
+			pod:       runAsUserPod,
+			container: badContainer,
 			message: ResultMessage{
 				ID:       "runAsRootAllowed",
 				Message:  "Should not be allowed to run as root",
-				Type:     "warning",
+				Success:  false,
+				Severity: "warning",
 				Category: "Security",
 			},
 		},
 	}
 	for idx, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {
-			err := applyContainerSchemaChecks(&config, "", conf.Deployments, &tt.cv)
+			controller := controllers.NewDeploymentController(appsv1.Deployment{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "",
+				},
+				Spec: appsv1.DeploymentSpec{
+					Template: corev1.PodTemplateSpec{
+						Spec: *tt.pod,
+					},
+				},
+			})
+			results, err := applyContainerSchemaChecks(&config, controller, tt.container, false)
 			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))
+			messages := []ResultMessage{}
+			for _, msg := range results {
+				messages = append(messages, msg)
+			}
+			assert.Len(t, messages, 1)
+			if len(messages) > 0 {
+				assert.Equal(t, tt.message, messages[0], fmt.Sprintf("Test case %d failed", idx))
 			}
 		})
 	}
@@ -1070,37 +1102,41 @@ func TestValidateResourcesExemption(t *testing.T) {
 		Name: "Empty",
 	}
 
-	expectedWarnings := []*ResultMessage{}
-	expectedErrors := []*ResultMessage{}
-	expectedSuccesses := []*ResultMessage{}
+	expectedWarnings := []ResultMessage{}
+	expectedErrors := []ResultMessage{}
+	expectedSuccesses := []ResultMessage{}
 
 	testValidate(t, &container, &resourceConfExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
 
-	expectedWarnings = []*ResultMessage{
+	expectedWarnings = []ResultMessage{
 		{
 			ID:       "cpuRequestsMissing",
-			Type:     "warning",
+			Success:  false,
+			Severity: "warning",
 			Message:  "CPU requests should be set",
 			Category: "Resources",
 		},
 		{
 			ID:       "memoryRequestsMissing",
-			Type:     "warning",
+			Success:  false,
+			Severity: "warning",
 			Message:  "Memory requests should be set",
 			Category: "Resources",
 		},
 	}
 
-	expectedErrors = []*ResultMessage{
+	expectedErrors = []ResultMessage{
 		{
 			ID:       "cpuLimitsMissing",
-			Type:     "error",
+			Success:  false,
+			Severity: "error",
 			Message:  "CPU limits should be set",
 			Category: "Resources",
 		},
 		{
 			ID:       "memoryLimitsMissing",
-			Type:     "error",
+			Success:  false,
+			Severity: "error",
 			Message:  "Memory limits should be set",
 			Category: "Resources",
 		},
@@ -1110,10 +1146,3 @@ func TestValidateResourcesExemption(t *testing.T) {
 
 	testValidate(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
 }
-
-func resetCV(cv ContainerValidation) ContainerValidation {
-	cv.Errors = []*ResultMessage{}
-	cv.Successes = []*ResultMessage{}
-	cv.Warnings = []*ResultMessage{}
-	return cv
-}
diff --git a/pkg/validator/controller_test.go b/pkg/validator/controller_test.go
index 90a67486..e8e832fd 100644
--- a/pkg/validator/controller_test.go
+++ b/pkg/validator/controller_test.go
@@ -35,31 +35,26 @@ func TestValidateController(t *testing.T) {
 		},
 	}
 	deployment := controller.NewDeploymentController(test.MockDeploy())
-	expectedSum := ResultSummary{
-		Totals: CountSummary{
-			Successes: uint(2),
-			Warnings:  uint(0),
-			Errors:    uint(0),
-		},
-		ByCategory: make(map[string]*CountSummary),
-	}
-	expectedSum.ByCategory["Security"] = &CountSummary{
+	expectedSum := CountSummary{
 		Successes: uint(2),
 		Warnings:  uint(0),
 		Errors:    uint(0),
 	}
 
-	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"},
+	expectedResults := ResultSet{
+		"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "error", Category: "Security"},
+		"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"},
 	}
 
-	actualResult := ValidateController(c, deployment)
+	actualResult, err := ValidateController(&c, deployment)
+	if err != nil {
+		panic(err)
+	}
 
-	assert.Equal(t, "Deployments", actualResult.Type)
+	assert.Equal(t, "Deployment", actualResult.Kind)
 	assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal")
-	assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
-	assert.EqualValues(t, expectedMessages, actualResult.PodResult.Messages)
+	assert.EqualValues(t, expectedSum, actualResult.GetSummary())
+	assert.EqualValues(t, expectedResults, actualResult.PodResult.Results)
 }
 
 func TestSkipHealthChecks(t *testing.T) {
@@ -80,61 +75,56 @@ func TestSkipHealthChecks(t *testing.T) {
 	deploymentBase := test.MockDeploy()
 	deploymentBase.Spec.Template.Spec.InitContainers = []corev1.Container{test.MockContainer("test")}
 	deployment := controller.NewDeploymentController(deploymentBase)
-	expectedSum := ResultSummary{
-		Totals: CountSummary{
-			Successes: uint(0),
-			Warnings:  uint(1),
-			Errors:    uint(1),
-		},
-		ByCategory: make(map[string]*CountSummary),
-	}
-	expectedSum.ByCategory["Health Checks"] = &CountSummary{
+	expectedSum := CountSummary{
 		Successes: uint(0),
 		Warnings:  uint(1),
 		Errors:    uint(1),
 	}
-	expectedMessages := []*ResultMessage{
-		{ID: "readinessProbeMissing", Message: "Readiness probe should be configured", Type: "error", Category: "Health Checks"},
-		{ID: "livenessProbeMissing", Message: "Liveness probe should be configured", Type: "warning", Category: "Health Checks"},
+	expectedResults := ResultSet{
+		"readinessProbeMissing": {ID: "readinessProbeMissing", Message: "Readiness probe should be configured", Success: false, Severity: "error", Category: "Health Checks"},
+		"livenessProbeMissing":  {ID: "livenessProbeMissing", Message: "Liveness probe should be configured", Success: false, Severity: "warning", Category: "Health Checks"},
 	}
-	actualResult := ValidateController(c, deployment)
-	assert.Equal(t, "Deployments", actualResult.Type)
+	actualResult, err := ValidateController(&c, deployment)
+	if err != nil {
+		panic(err)
+	}
+	assert.Equal(t, "Deployment", actualResult.Kind)
 	assert.Equal(t, 2, len(actualResult.PodResult.ContainerResults), "should be equal")
-	assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
-	assert.EqualValues(t, []*ResultMessage{}, actualResult.PodResult.ContainerResults[0].Messages)
-	assert.EqualValues(t, expectedMessages, actualResult.PodResult.ContainerResults[1].Messages)
+	assert.EqualValues(t, expectedSum, actualResult.GetSummary())
+	assert.EqualValues(t, ResultSet{}, actualResult.PodResult.ContainerResults[0].Results)
+	assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[1].Results)
 
 	job := controller.NewJobController(test.MockJob())
-	expectedSum = ResultSummary{
-		Totals: CountSummary{
-			Successes: uint(0),
-			Warnings:  uint(0),
-			Errors:    uint(0),
-		},
-		ByCategory: make(map[string]*CountSummary),
+	expectedSum = CountSummary{
+		Successes: uint(0),
+		Warnings:  uint(0),
+		Errors:    uint(0),
 	}
-	expectedMessages = []*ResultMessage{}
-	actualResult = ValidateController(c, job)
-	assert.Equal(t, "Jobs", actualResult.Type)
+	expectedResults = ResultSet{}
+	actualResult, err = ValidateController(&c, job)
+	if err != nil {
+		panic(err)
+	}
+	assert.Equal(t, "Job", actualResult.Kind)
 	assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal")
-	assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
-	assert.EqualValues(t, expectedMessages, actualResult.PodResult.ContainerResults[0].Messages)
+	assert.EqualValues(t, expectedSum, actualResult.GetSummary())
+	assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[0].Results)
 
 	cronjob := controller.NewCronJobController(test.MockCronJob())
-	expectedSum = ResultSummary{
-		Totals: CountSummary{
-			Successes: uint(0),
-			Warnings:  uint(0),
-			Errors:    uint(0),
-		},
-		ByCategory: make(map[string]*CountSummary),
+	expectedSum = CountSummary{
+		Successes: uint(0),
+		Warnings:  uint(0),
+		Errors:    uint(0),
 	}
-	expectedMessages = []*ResultMessage{}
-	actualResult = ValidateController(c, cronjob)
-	assert.Equal(t, "CronJobs", actualResult.Type)
+	expectedResults = ResultSet{}
+	actualResult, err = ValidateController(&c, cronjob)
+	if err != nil {
+		panic(err)
+	}
+	assert.Equal(t, "CronJob", actualResult.Kind)
 	assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal")
-	assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
-	assert.EqualValues(t, expectedMessages, actualResult.PodResult.ContainerResults[0].Messages)
+	assert.EqualValues(t, expectedSum, actualResult.GetSummary())
+	assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[0].Results)
 }
 
 func TestControllerExemptions(t *testing.T) {
@@ -151,29 +141,25 @@ func TestControllerExemptions(t *testing.T) {
 		Deployments: []appsv1.Deployment{test.MockDeploy()},
 	}
 
-	expectedSum := ResultSummary{
-		Totals: CountSummary{
-			Successes: uint(0),
-			Warnings:  uint(1),
-			Errors:    uint(1),
-		},
-		ByCategory: make(map[string]*CountSummary),
-	}
-	expectedSum.ByCategory["Health Checks"] = &CountSummary{
+	expectedSum := CountSummary{
 		Successes: uint(0),
 		Warnings:  uint(1),
 		Errors:    uint(1),
 	}
-	nsResults := NamespacedResults{}
-	ValidateControllers(c, resources, &nsResults)
-	actualResult := nsResults[""].DeploymentResults[0]
-	assert.Equal(t, "Deployments", actualResult.Type)
-	assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
+	actualResults, err := ValidateControllers(&c, resources)
+	if err != nil {
+		panic(err)
+	}
+	assert.Equal(t, 1, len(actualResults))
+	assert.Equal(t, "Deployment", actualResults[0].Kind)
+	assert.EqualValues(t, expectedSum, actualResults[0].GetSummary())
 
 	resources.Deployments[0].ObjectMeta.Annotations = map[string]string{
 		exemptionAnnotationKey: "true",
 	}
-	nsResults = NamespacedResults{}
-	ValidateControllers(c, resources, &nsResults)
-	assert.Equal(t, (*NamespaceResult)(nil), nsResults[""])
+	actualResults, err = ValidateControllers(&c, resources)
+	if err != nil {
+		panic(err)
+	}
+	assert.Equal(t, 0, len(actualResults))
 }
diff --git a/pkg/validator/fullaudit_test.go b/pkg/validator/fullaudit_test.go
index af9411c0..b51bf595 100644
--- a/pkg/validator/fullaudit_test.go
+++ b/pkg/validator/fullaudit_test.go
@@ -30,16 +30,7 @@ func TestGetTemplateData(t *testing.T) {
 		},
 	}
 
-	// TODO: split out the logic for calculating summaries into another set of tests
-	sum := ResultSummary{
-		Totals: CountSummary{
-			Successes: uint(0),
-			Warnings:  uint(4),
-			Errors:    uint(4),
-		},
-		ByCategory: CategorySummary{},
-	}
-	sum.ByCategory["Health Checks"] = &CountSummary{
+	sum := CountSummary{
 		Successes: uint(0),
 		Warnings:  uint(4),
 		Errors:    uint(4),
@@ -48,17 +39,33 @@ func TestGetTemplateData(t *testing.T) {
 	actualAudit, err := RunAudit(c, resources)
 	assert.Equal(t, err, nil, "error should be nil")
 
-	assert.EqualValues(t, sum, actualAudit.ClusterSummary.Results)
+	assert.EqualValues(t, sum, actualAudit.GetSummary())
 	assert.Equal(t, actualAudit.SourceType, "Cluster", "should be from a cluster")
 	assert.Equal(t, actualAudit.SourceName, "test", "should be from a cluster")
 
-	assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults), "should be equal")
-	assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults), "should be equal")
-	assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults), "should be equal")
-	assert.Equal(t, 2, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
+	assert.Equal(t, 6, len(actualAudit.Results))
+
+	assert.Equal(t, "Deployment", actualAudit.Results[0].Kind)
+	assert.Equal(t, 1, len(actualAudit.Results[0].PodResult.ContainerResults))
+	assert.Equal(t, 2, len(actualAudit.Results[0].PodResult.ContainerResults[0].Results))
+
+	assert.Equal(t, "StatefulSet", actualAudit.Results[1].Kind)
+	assert.Equal(t, 1, len(actualAudit.Results[1].PodResult.ContainerResults))
+	assert.Equal(t, 2, len(actualAudit.Results[1].PodResult.ContainerResults[0].Results))
+
+	assert.Equal(t, "DaemonSet", actualAudit.Results[2].Kind)
+	assert.Equal(t, 1, len(actualAudit.Results[2].PodResult.ContainerResults))
+	assert.Equal(t, 2, len(actualAudit.Results[2].PodResult.ContainerResults[0].Results))
+
+	assert.Equal(t, "Job", actualAudit.Results[3].Kind)
+	assert.Equal(t, 1, len(actualAudit.Results[3].PodResult.ContainerResults))
+	assert.Equal(t, 0, len(actualAudit.Results[3].PodResult.ContainerResults[0].Results))
+
+	assert.Equal(t, "CronJob", actualAudit.Results[4].Kind)
+	assert.Equal(t, 1, len(actualAudit.Results[4].PodResult.ContainerResults))
+	assert.Equal(t, 0, len(actualAudit.Results[4].PodResult.ContainerResults[0].Results))
 
-	assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal")
-	assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal")
-	assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults), "should be equal")
-	assert.Equal(t, 2, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
+	assert.Equal(t, "ReplicationController", actualAudit.Results[5].Kind)
+	assert.Equal(t, 1, len(actualAudit.Results[5].PodResult.ContainerResults))
+	assert.Equal(t, 2, len(actualAudit.Results[5].PodResult.ContainerResults[0].Results))
 }
diff --git a/pkg/validator/pod_test.go b/pkg/validator/pod_test.go
index fd9df744..a0e72b12 100644
--- a/pkg/validator/pod_test.go
+++ b/pkg/validator/pod_test.go
@@ -18,8 +18,12 @@ import (
 	"testing"
 
 	conf "github.com/fairwindsops/polaris/pkg/config"
+	"github.com/fairwindsops/polaris/pkg/validator/controllers"
 	"github.com/fairwindsops/polaris/test"
+
 	"github.com/stretchr/testify/assert"
+	appsv1 "k8s.io/api/apps/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 func TestValidatePod(t *testing.T) {
@@ -34,38 +38,29 @@ func TestValidatePod(t *testing.T) {
 
 	k8s := test.SetupTestAPI()
 	k8s = test.SetupAddControllers(k8s, "test")
-	pod := test.MockPod()
+	p := test.MockPod()
+	deployment := controllers.NewDeploymentController(appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: p}})
 
-	expectedSum := ResultSummary{
-		Totals: CountSummary{
-			Successes: uint(4),
-			Warnings:  uint(0),
-			Errors:    uint(0),
-		},
-		ByCategory: make(map[string]*CountSummary),
-	}
-	expectedSum.ByCategory["Networking"] = &CountSummary{
-		Successes: uint(2),
-		Warnings:  uint(0),
-		Errors:    uint(0),
-	}
-	expectedSum.ByCategory["Security"] = &CountSummary{
-		Successes: uint(2),
+	expectedSum := CountSummary{
+		Successes: uint(4),
 		Warnings:  uint(0),
 		Errors:    uint(0),
 	}
 
-	expectedMessages := []*ResultMessage{
-		{ID: "hostIPCSet", Message: "Host IPC 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"},
+	expectedResults := ResultSet{
+		"hostIPCSet":     {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "error", Category: "Security"},
+		"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"},
+		"hostPIDSet":     {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"},
 	}
 
-	actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
+	actualPodResult, err := ValidatePod(&c, deployment)
+	if err != nil {
+		panic(err)
+	}
 
 	assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
-	assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
-	assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
+	assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
+	assert.EqualValues(t, expectedResults, actualPodResult.Results)
 }
 
 func TestInvalidIPCPod(t *testing.T) {
@@ -80,38 +75,29 @@ func TestInvalidIPCPod(t *testing.T) {
 
 	k8s := test.SetupTestAPI()
 	k8s = test.SetupAddControllers(k8s, "test")
-	pod := test.MockPod()
-	pod.Spec.HostIPC = true
-
-	expectedSum := ResultSummary{
-		Totals: CountSummary{
-			Successes: uint(3),
-			Warnings:  uint(0),
-			Errors:    uint(1),
-		},
-		ByCategory: make(map[string]*CountSummary),
-	}
-	expectedSum.ByCategory["Networking"] = &CountSummary{
-		Successes: uint(2),
-		Warnings:  uint(0),
-		Errors:    uint(0),
-	}
-	expectedSum.ByCategory["Security"] = &CountSummary{
-		Successes: uint(1),
+	p := test.MockPod()
+	p.Spec.HostIPC = true
+	deployment := controllers.NewDeploymentController(appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: p}})
+
+	expectedSum := CountSummary{
+		Successes: uint(3),
 		Warnings:  uint(0),
 		Errors:    uint(1),
 	}
-	expectedMessages := []*ResultMessage{
-		{ID: "hostIPCSet", Message: "Host IPC should not be configured", Type: "error", 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"},
+	expectedResults := ResultSet{
+		"hostIPCSet":     {ID: "hostIPCSet", Message: "Host IPC should not be configured", Success: false, Severity: "error", Category: "Security"},
+		"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"},
+		"hostPIDSet":     {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"},
 	}
 
-	actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
+	actualPodResult, err := ValidatePod(&c, deployment)
+	if err != nil {
+		panic(err)
+	}
 
 	assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
-	assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
-	assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
+	assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
+	assert.EqualValues(t, expectedResults, actualPodResult.Results)
 }
 
 func TestInvalidNeworkPod(t *testing.T) {
@@ -126,40 +112,30 @@ func TestInvalidNeworkPod(t *testing.T) {
 
 	k8s := test.SetupTestAPI()
 	k8s = test.SetupAddControllers(k8s, "test")
-	pod := test.MockPod()
-	pod.Spec.HostNetwork = true
-
-	expectedSum := ResultSummary{
-		Totals: CountSummary{
-			Successes: uint(3),
-			Warnings:  uint(1),
-			Errors:    uint(0),
-		},
-		ByCategory: make(map[string]*CountSummary),
-	}
-	expectedSum.ByCategory["Networking"] = &CountSummary{
-		Successes: uint(1),
+	p := test.MockPod()
+	p.Spec.HostNetwork = true
+	deployment := controllers.NewDeploymentController(appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: p}})
+
+	expectedSum := CountSummary{
+		Successes: uint(3),
 		Warnings:  uint(1),
 		Errors:    uint(0),
 	}
 
-	expectedSum.ByCategory["Security"] = &CountSummary{
-		Successes: uint(2),
-		Warnings:  uint(0),
-		Errors:    uint(0),
+	expectedResults := ResultSet{
+		"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network should not be configured", Success: false, Severity: "warning", Category: "Networking"},
+		"hostIPCSet":     {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "error", Category: "Security"},
+		"hostPIDSet":     {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"},
 	}
 
-	expectedMessages := []*ResultMessage{
-		{ID: "hostNetworkSet", Message: "Host network should not be configured", Type: "warning", Category: "Networking"},
-		{ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"},
-		{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
+	actualPodResult, err := ValidatePod(&c, deployment)
+	if err != nil {
+		panic(err)
 	}
 
-	actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
-
 	assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
-	assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
-	assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
+	assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
+	assert.EqualValues(t, expectedResults, actualPodResult.Results)
 }
 
 func TestInvalidPIDPod(t *testing.T) {
@@ -174,39 +150,30 @@ func TestInvalidPIDPod(t *testing.T) {
 
 	k8s := test.SetupTestAPI()
 	k8s = test.SetupAddControllers(k8s, "test")
-	pod := test.MockPod()
-	pod.Spec.HostPID = true
-
-	expectedSum := ResultSummary{
-		Totals: CountSummary{
-			Successes: uint(3),
-			Warnings:  uint(0),
-			Errors:    uint(1),
-		},
-		ByCategory: make(map[string]*CountSummary),
-	}
-	expectedSum.ByCategory["Networking"] = &CountSummary{
-		Successes: uint(2),
-		Warnings:  uint(0),
-		Errors:    uint(0),
-	}
-	expectedSum.ByCategory["Security"] = &CountSummary{
-		Successes: uint(1),
+	p := test.MockPod()
+	p.Spec.HostPID = true
+	deployment := controllers.NewDeploymentController(appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: p}})
+
+	expectedSum := CountSummary{
+		Successes: uint(3),
 		Warnings:  uint(0),
 		Errors:    uint(1),
 	}
 
-	expectedMessages := []*ResultMessage{
-		{ID: "hostPIDSet", Message: "Host PID should not be configured", Type: "error", Category: "Security"},
-		{ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"},
-		{ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"},
+	expectedResults := ResultSet{
+		"hostPIDSet":     {ID: "hostPIDSet", Message: "Host PID should not be configured", Success: false, Severity: "error", Category: "Security"},
+		"hostIPCSet":     {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "error", Category: "Security"},
+		"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"},
 	}
 
-	actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
+	actualPodResult, err := ValidatePod(&c, deployment)
+	if err != nil {
+		panic(err)
+	}
 
 	assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
-	assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
-	assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
+	assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
+	assert.EqualValues(t, expectedResults, actualPodResult.Results)
 }
 
 func TestExemption(t *testing.T) {
@@ -227,35 +194,32 @@ func TestExemption(t *testing.T) {
 
 	k8s := test.SetupTestAPI()
 	k8s = test.SetupAddControllers(k8s, "test")
-	pod := test.MockPod()
-	pod.Spec.HostIPC = true
-
-	expectedSum := ResultSummary{
-		Totals: CountSummary{
-			Successes: uint(3),
-			Warnings:  uint(0),
-			Errors:    uint(0),
-		},
-		ByCategory: make(map[string]*CountSummary),
+	p := test.MockPod()
+	p.Spec.HostIPC = true
+	meta := metav1.ObjectMeta{
+		Name: "foo",
 	}
-	expectedSum.ByCategory["Networking"] = &CountSummary{
-		Successes: uint(2),
-		Warnings:  uint(0),
-		Errors:    uint(0),
+	deploySpec := appsv1.DeploymentSpec{
+		Template: p,
 	}
-	expectedSum.ByCategory["Security"] = &CountSummary{
-		Successes: uint(1),
+	deployment := controllers.NewDeploymentController(appsv1.Deployment{ObjectMeta: meta, Spec: deploySpec})
+
+	expectedSum := CountSummary{
+		Successes: uint(3),
 		Warnings:  uint(0),
 		Errors:    uint(0),
 	}
-	expectedMessages := []*ResultMessage{
-		{ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"},
-		{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
+	expectedResults := ResultSet{
+		"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"},
+		"hostPIDSet":     {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"},
 	}
 
-	actualPodResult := ValidatePod(c, &pod.Spec, "foo", conf.Deployments)
+	actualPodResult, err := ValidatePod(&c, deployment)
+	if err != nil {
+		panic(err)
+	}
 
 	assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
-	assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
-	assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
+	assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
+	assert.EqualValues(t, expectedResults, actualPodResult.Results)
 }
diff --git a/pkg/validator/schema_test.go b/pkg/validator/schema_test.go
index a6d8737b..0db08d2b 100644
--- a/pkg/validator/schema_test.go
+++ b/pkg/validator/schema_test.go
@@ -4,6 +4,7 @@ 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"
@@ -110,55 +111,51 @@ func TestValidateResourcesPartiallyValid(t *testing.T) {
 		},
 	}
 
-	expectedWarnings := []*ResultMessage{
+	expectedWarnings := []ResultMessage{
 		{
 			ID:       "memoryLimitsRange",
-			Type:     "warning",
+			Success:  false,
+			Severity: "warning",
 			Message:  "Memory limits should be within the required range",
 			Category: "Resources",
 		},
 	}
 
-	expectedErrors := []*ResultMessage{
+	expectedErrors := []ResultMessage{
 		{
 			ID:       "memoryRequestsRange",
-			Type:     "error",
+			Success:  false,
+			Severity: "error",
 			Message:  "Memory requests should be within the required range",
 			Category: "Resources",
 		},
 	}
 
-	expectedSuccesses := []*ResultMessage{}
+	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,
-	}
+	emptyContainer := &corev1.Container{}
+	controller := getEmptyController("")
 
 	parsedConf, err := conf.Parse([]byte(resourceConfRanges))
 	assert.NoError(t, err, "Expected no error when parsing config")
 
-	err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvEmpty)
+	results, err := applyContainerSchemaChecks(&parsedConf, controller, emptyContainer, false)
 	if err != nil {
 		panic(err)
 	}
-	assert.Len(t, cvEmpty.Errors, 1)
-	assert.Len(t, cvEmpty.Warnings, 1)
+	assert.Equal(t, uint(1), results.GetSummary().Errors)
+	assert.Equal(t, uint(1), results.GetSummary().Warnings)
 
-	err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvInit)
+	results, err = applyContainerSchemaChecks(&parsedConf, controller, emptyContainer, true)
 	if err != nil {
 		panic(err)
 	}
-	assert.Len(t, cvInit.Errors, 0)
+	assert.Equal(t, uint(0), results.GetSummary().Errors)
+	assert.Equal(t, uint(0), results.GetSummary().Warnings)
 }
 
 func TestValidateResourcesFullyValid(t *testing.T) {
@@ -188,51 +185,57 @@ func TestValidateResourcesFullyValid(t *testing.T) {
 		},
 	}
 
-	expectedSuccesses := []*ResultMessage{
+	expectedSuccesses := []ResultMessage{
 		{
 			ID:       "memoryRequestsRange",
-			Type:     "success",
+			Success:  true,
+			Severity: "error",
 			Message:  "Memory requests are within the required range",
 			Category: "Resources",
 		},
 		{
 			ID:       "memoryLimitsRange",
-			Type:     "success",
+			Success:  true,
+			Severity: "warning",
 			Message:  "Memory limits are within the required range",
 			Category: "Resources",
 		},
 	}
 
-	testValidate(t, &container, &resourceConfRanges, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses)
+	testValidate(t, &container, &resourceConfRanges, "foo", []ResultMessage{}, []ResultMessage{}, expectedSuccesses)
 
-	expectedSuccesses = []*ResultMessage{
+	expectedSuccesses = []ResultMessage{
 		{
 			ID:       "cpuRequestsMissing",
-			Type:     "success",
+			Success:  true,
+			Severity: "warning",
 			Message:  "CPU requests are set",
 			Category: "Resources",
 		},
 		{
 			ID:       "memoryRequestsMissing",
-			Type:     "success",
+			Success:  true,
+			Severity: "warning",
 			Message:  "Memory requests are set",
 			Category: "Resources",
 		},
 		{
 			ID:       "cpuLimitsMissing",
-			Type:     "success",
+			Success:  true,
+			Severity: "error",
 			Message:  "CPU limits are set",
 			Category: "Resources",
 		},
 		{
 			ID:       "memoryLimitsMissing",
-			Type:     "success",
+			Success:  true,
+			Severity: "error",
 			Message:  "Memory limits are set",
 			Category: "Resources",
 		},
 	}
 
-	testValidate(t, &container, &resourceConfMinimal, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses)
+	testValidate(t, &container, &resourceConfMinimal, "foo", []ResultMessage{}, []ResultMessage{}, expectedSuccesses)
 }
 
 func TestValidateCustomCheckExemptions(t *testing.T) {
@@ -241,15 +244,16 @@ func TestValidateCustomCheckExemptions(t *testing.T) {
 		Image: "hub.docker.com/foo",
 	}
 
-	expectedWarnings := []*ResultMessage{}
-	expectedErrors := []*ResultMessage{}
-	expectedSuccesses := []*ResultMessage{}
+	expectedWarnings := []ResultMessage{}
+	expectedErrors := []ResultMessage{}
+	expectedSuccesses := []ResultMessage{}
 	testValidate(t, &container, &customCheckExemptions, "exempt", expectedErrors, expectedWarnings, expectedSuccesses)
 
-	expectedErrors = []*ResultMessage{
+	expectedErrors = []ResultMessage{
 		{
 			ID:       "foo",
-			Type:     "error",
+			Success:  false,
+			Severity: "error",
 			Message:  "fail!",
 			Category: "Security",
 		},