Repository: FairwindsOps/polaris
Pull request #475 (2021-01-13 18:56:45)

Add Swaroop as Codeowner


Patch:
diff --git a/CODEOWNERS b/CODEOWNERS
index f17e0bd7..7dbc8393 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,1 +1,1 @@
-* @rbren @makoscafee @jordandoig @baderbuddy
+* @rbren @makoscafee @jordandoig @baderbuddy @shaswa
diff --git a/checks/tlsSettingsMissing.yaml b/checks/tlsSettingsMissing.yaml
new file mode 100644
index 00000000..c886689a
--- /dev/null
+++ b/checks/tlsSettingsMissing.yaml
@@ -0,0 +1,19 @@
+successMessage: Ingress has TLS configured
+failureMessage: Ingress does not have TLS configured
+category: Security
+target: Ingress
+schema:
+  '$schema': http://json-schema.org/draft-07/schema
+  type: object
+  required:
+  - spec
+  properties:
+    spec:
+      type: object
+      required:
+      - tls
+      properties:
+        tls:
+          type: array
+          not:
+            const: null
diff --git a/examples/config.yaml b/examples/config.yaml
index 586c0fd8..8e0b0fa3 100644
--- a/examples/config.yaml
+++ b/examples/config.yaml
@@ -22,6 +22,7 @@ checks:
   insecureCapabilities: warning
   hostNetworkSet: warning
   hostPortSet: warning
+  tlsSettingsMissing: warning
 
 exemptions:
   - namespace: kube-system
diff --git a/pkg/config/schema.go b/pkg/config/schema.go
index 255741b1..0ddea221 100644
--- a/pkg/config/schema.go
+++ b/pkg/config/schema.go
@@ -19,6 +19,8 @@ const (
 	TargetPod TargetKind = "Pod"
 	// TargetController points to the controller's spec
 	TargetController TargetKind = "Controller"
+	// TargetIngress points to the ingress spec
+	TargetIngress TargetKind = "Ingress"
 )
 
 // SchemaCheck is a Polaris check that runs using JSON Schema
diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go
index ba59a0b5..3a980962 100644
--- a/pkg/dashboard/dashboard.go
+++ b/pkg/dashboard/dashboard.go
@@ -152,7 +152,7 @@ func getConfigForQuery(base config.Configuration, query url.Values) config.Confi
 }
 
 func stripUnselectedNamespaces(data *validator.AuditData, selectedNamespaces []string) {
-	newResults := []validator.ControllerResult{}
+	newResults := []validator.Result{}
 	for _, res := range data.Results {
 		if stringInSlice(res.Namespace, selectedNamespaces) {
 			newResults = append(newResults, res)
diff --git a/pkg/dashboard/templates/dashboard.gohtml b/pkg/dashboard/templates/dashboard.gohtml
index 83963d8e..55a1fff2 100644
--- a/pkg/dashboard/templates/dashboard.gohtml
+++ b/pkg/dashboard/templates/dashboard.gohtml
@@ -141,9 +141,11 @@
             <div class="status-bar">
               <div class="status">
                 <div class="failing">
-                  <div class="warning" style="width: {{ getWarningWidth .PodResult.GetSummary 200 }}px;">
-                    <div class="passing" style="width: {{ getSuccessWidth .PodResult.GetSummary 200 }}px;"></div>
-                  </div>
+                  {{ if .PodResult }}
+                    <div class="warning" style="width: {{ getWarningWidth .PodResult.GetSummary 200 }}px;">
+                      <div class="passing" style="width: {{ getSuccessWidth .PodResult.GetSummary 200 }}px;"></div>
+                    </div>
+                  {{ end }}
                 </div>
               </div>
             </div>
@@ -153,7 +155,7 @@
               <strong>{{ .Name }}</strong></div>
 
               <div class="result-messages expandable-content">
-                <h4>Controller Spec:
+                <h4>Spec:</h4>
                   {{ if eq 0 (len .Results.GetSortedResults) }}
                     <i>no checks applied</i>
                   {{ end }}
@@ -171,34 +173,15 @@
                 </ul>
               </div>
 
-              <div class="result-messages expandable-content">
-                <h4>Pod Spec:
-                  {{ if eq 0 (len .PodResult.Results.GetSortedResults) }}
-                    <i>no checks applied</i>
-                  {{ end }}
-                </h4>
-                <ul class="message-list">
-                  {{ 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="{{ getCategoryLink .Category }}" target="_blank">
-                        <i class="far fa-question-circle"></i>
-                      </a>
-                    </li>
-                  {{ end }}
-                </ul>
-              </div>
-
-              {{ range .PodResult.ContainerResults }}
+              {{ if .PodResult }}
                 <div class="result-messages expandable-content">
-                  <h4>Container {{ .Name }}:
-                    {{ if eq 0 (len .Results.GetSortedResults) }}
+                  <h4>Pod Spec:
+                    {{ if eq 0 (len .PodResult.Results.GetSortedResults) }}
                       <i>no checks applied</i>
                     {{ end }}
                   </h4>
                   <ul class="message-list">
-                    {{ range $message := .Results.GetSortedResults }}
+                    {{ range $message := .PodResult.Results.GetSortedResults }}
                       <li class="{{ getResultClass . }}">
                         <i class="message-icon {{ getIcon $message }}"></i>
                         <span class="message">{{ .Message }}</span>
@@ -209,7 +192,30 @@
                     {{ end }}
                   </ul>
                 </div>
-              {{ end }} {{/* end range .PodResult.ContainerResults */}}
+              {{ end }}
+
+              {{ if .PodResult }}
+                {{ range .PodResult.ContainerResults }}
+                  <div class="result-messages expandable-content">
+                    <h4>Container {{ .Name }}:
+                      {{ if eq 0 (len .Results.GetSortedResults) }}
+                        <i>no checks applied</i>
+                      {{ end }}
+                    </h4>
+                    <ul class="message-list">
+                      {{ range $message := .Results.GetSortedResults }}
+                        <li class="{{ getResultClass . }}">
+                          <i class="message-icon {{ getIcon $message }}"></i>
+                          <span class="message">{{ .Message }}</span>
+                          <a class="more-info" href="{{ getCategoryLink .Category }}" target="_blank">
+                            <i class="far fa-question-circle"></i>
+                          </a>
+                        </li>
+                      {{ end }}
+                    </ul>
+                  </div>
+                {{ end }} {{/* end range .PodResult.ContainerResults */}}
+              {{ end }} {{/* end if .PodResult */}}
             </div>
         {{ end }} {{/* end range .Results.GetSortedResults */}}
       </div>
diff --git a/pkg/kube/resources.go b/pkg/kube/resources.go
index 5677e74c..5c30777a 100644
--- a/pkg/kube/resources.go
+++ b/pkg/kube/resources.go
@@ -14,6 +14,7 @@ import (
 
 	"github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
+	v1beta1 "k8s.io/api/extensions/v1beta1"
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -34,6 +35,7 @@ type ResourceProvider struct {
 	Nodes         []corev1.Node
 	Namespaces    []corev1.Namespace
 	Controllers   []GenericWorkload
+	Ingresses     []v1beta1.Ingress
 }
 
 type k8sResource struct {
@@ -198,6 +200,11 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
 		logrus.Errorf("Error fetching Pods: %v", err)
 		return nil, err
 	}
+	ingressList, err := kube.ExtensionsV1beta1().Ingresses("").List(ctx, listOpts)
+	if err != nil {
+		logrus.Errorf("Error fetching Ingresses: %v", err)
+		return nil, err
+	}
 
 	resources, err := restmapper.GetAPIGroupResources(kube.Discovery())
 	if err != nil {
@@ -222,6 +229,7 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
 		Nodes:         nodes.Items,
 		Namespaces:    namespaces.Items,
 		Controllers:   controllers,
+		Ingresses:     ingressList.Items,
 	}
 	return &api, nil
 }
@@ -319,6 +327,10 @@ func addResourceFromString(contents string, resources *ResourceProvider) error {
 			return err
 		}
 		resources.Controllers = append(resources.Controllers, workload)
+	} else if resource.Kind == "Ingress" {
+		ingress := v1beta1.Ingress{}
+		err = decoder.Decode(&ingress)
+		resources.Ingresses = append(resources.Ingresses, ingress)
 	} else {
 		newController, err := GetWorkloadFromBytes(contentBytes)
 		if err != nil || newController == nil {
diff --git a/pkg/validator/controller.go b/pkg/validator/controller.go
index d6a49729..030b4c17 100644
--- a/pkg/validator/controller.go
+++ b/pkg/validator/controller.go
@@ -25,25 +25,25 @@ import (
 
 const exemptionAnnotationKey = "polaris.fairwinds.com/exempt"
 
-// ValidateController validates a single controller, returns a ControllerResult.
-func ValidateController(conf *conf.Configuration, controller kube.GenericWorkload) (ControllerResult, error) {
+// ValidateController validates a single controller, returns a Result.
+func ValidateController(conf *conf.Configuration, controller kube.GenericWorkload) (Result, error) {
 	podResult, err := ValidatePod(conf, controller)
 	if err != nil {
-		return ControllerResult{}, err
+		return Result{}, err
 	}
 
 	var controllerResult ResultSet
 	controllerResult, err = applyControllerSchemaChecks(conf, controller)
 	if err != nil {
-		return ControllerResult{}, err
+		return Result{}, err
 	}
 
-	result := ControllerResult{
+	result := Result{
 		Kind:      controller.Kind,
 		Name:      controller.ObjectMeta.GetName(),
 		Namespace: controller.ObjectMeta.GetNamespace(),
 		Results:   controllerResult,
-		PodResult: podResult,
+		PodResult: &podResult,
 	}
 
 	return result, nil
@@ -51,10 +51,10 @@ func ValidateController(conf *conf.Configuration, controller kube.GenericWorkloa
 
 // 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) ([]ControllerResult, error) {
+func ValidateControllers(config *conf.Configuration, kubeResources *kube.ResourceProvider) ([]Result, error) {
 	controllersToAudit := kubeResources.Controllers
 
-	results := []ControllerResult{}
+	results := []Result{}
 	for _, controller := range controllersToAudit {
 		if !config.DisallowExemptions && hasExemptionAnnotation(controller) {
 			continue
diff --git a/pkg/validator/fullaudit.go b/pkg/validator/fullaudit.go
index 1fe4b020..54d85ac4 100644
--- a/pkg/validator/fullaudit.go
+++ b/pkg/validator/fullaudit.go
@@ -26,6 +26,13 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
 	if err != nil {
 		return AuditData{}, err
 	}
+	controllerCount := len(results)
+
+	ingressResults, err := ValidateIngresses(&config, kubeResources)
+	if err != nil {
+		return AuditData{}, err
+	}
+	results = append(results, ingressResults...)
 
 	auditData := AuditData{
 		PolarisOutputVersion: PolarisOutputVersion,
@@ -38,7 +45,7 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
 			Nodes:       len(kubeResources.Nodes),
 			Pods:        len(kubeResources.Controllers), // TODO validate that this is still valuable
 			Namespaces:  len(kubeResources.Namespaces),
-			Controllers: len(results),
+			Controllers: controllerCount,
 		},
 		Results: results,
 	}
diff --git a/pkg/validator/ingress.go b/pkg/validator/ingress.go
new file mode 100644
index 00000000..849f6e4f
--- /dev/null
+++ b/pkg/validator/ingress.go
@@ -0,0 +1,51 @@
+// 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/fairwindsops/polaris/pkg/kube"
+	"k8s.io/api/extensions/v1beta1"
+)
+
+// ValidateIngresses validates all the ingresses in a ResourceProvider
+func ValidateIngresses(config *conf.Configuration, kubeResources *kube.ResourceProvider) ([]Result, error) {
+	var results []Result
+	for _, ingress := range kubeResources.Ingresses {
+		result, err := ValidateIngress(config, ingress)
+		if err != nil {
+			return []Result{}, err
+		}
+		results = append(results, result)
+	}
+	return results, nil
+}
+
+// ValidateIngress validates a single ingress
+func ValidateIngress(config *conf.Configuration, ingress v1beta1.Ingress) (Result, error) {
+	results, err := applyIngressSchemaChecks(config, ingress)
+	if err != nil {
+		return Result{}, err
+	}
+
+	result := Result{
+		Kind:      "Ingress",
+		Name:      ingress.ObjectMeta.GetName(),
+		Namespace: ingress.ObjectMeta.GetNamespace(),
+		Results:   results,
+	}
+
+	return result, nil
+}
diff --git a/pkg/validator/output.go b/pkg/validator/output.go
index 845daaeb..9d5763cd 100644
--- a/pkg/validator/output.go
+++ b/pkg/validator/output.go
@@ -33,7 +33,7 @@ type AuditData struct {
 	SourceName           string
 	DisplayName          string
 	ClusterInfo          ClusterInfo
-	Results              []ControllerResult
+	Results              []Result
 }
 
 // ClusterInfo contains Polaris results as well as some high-level stats
@@ -57,13 +57,13 @@ type ResultMessage struct {
 // ResultSet contiains the results for a set of checks
 type ResultSet map[string]ResultMessage
 
-// ControllerResult provides results for a controller
-type ControllerResult struct {
+// Result provides results for a Kubernetes object
+type Result struct {
 	Name        string
 	Namespace   string
 	Kind        string
 	Results     ResultSet
-	PodResult   PodResult
+	PodResult   *PodResult
 	CreatedTime time.Time
 }
 
diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go
index 9b1ab655..1911b184 100644
--- a/pkg/validator/schema.go
+++ b/pkg/validator/schema.go
@@ -9,6 +9,8 @@ import (
 
 	"github.com/gobuffalo/packr/v2"
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/api/extensions/v1beta1"
+	metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/yaml"
 
 	"github.com/fairwindsops/polaris/pkg/config"
@@ -44,6 +46,8 @@ var (
 		"dangerousCapabilities",
 		"insecureCapabilities",
 		"priorityClassNotSet",
+		// Ingress checks
+		"tlsSettingsMissing",
 	}
 )
 
@@ -77,7 +81,7 @@ func parseCheck(rawBytes []byte) (config.SchemaCheck, error) {
 	}
 }
 
-func resolveCheck(conf *config.Configuration, checkID string, controller kube.GenericWorkload, container *corev1.Container, target config.TargetKind, isInitContainer bool) (*config.SchemaCheck, error) {
+func resolveCheck(conf *config.Configuration, checkID, kind string, target config.TargetKind, meta metaV1.Object, containerName string, isInitContainer bool) (*config.SchemaCheck, error) {
 	check, ok := conf.CustomChecks[checkID]
 	if !ok {
 		check, ok = builtInChecks[checkID]
@@ -86,14 +90,12 @@ func resolveCheck(conf *config.Configuration, checkID string, controller kube.Ge
 		return nil, fmt.Errorf("Check %s not found", checkID)
 	}
 
-	containerName := ""
-	if container != nil {
-		containerName = container.Name
-	}
-	if !conf.IsActionable(check.ID, controller.ObjectMeta.GetNamespace(), controller.ObjectMeta.GetName(), containerName) {
+	namespace := meta.GetNamespace()
+	name := meta.GetName()
+	if !conf.IsActionable(check.ID, namespace, name, containerName) {
 		return nil, nil
 	}
-	if !check.IsActionable(target, controller.Kind, isInitContainer) {
+	if !check.IsActionable(target, kind, isInitContainer) {
 		return nil, nil
 	}
 	return &check, nil
@@ -127,7 +129,7 @@ func applyPodSchemaChecks(conf *config.Configuration, controller kube.GenericWor
 		if strings.ToLower(exemptValue) == "true" {
 			continue
 		}
-		check, err := resolveCheck(conf, checkID, controller, nil, config.TargetPod, false)
+		check, err := resolveCheck(conf, checkID, controller.Kind, config.TargetPod, controller.ObjectMeta, "", false)
 
 		if err != nil {
 			return nil, err
@@ -152,7 +154,7 @@ func applyControllerSchemaChecks(conf *config.Configuration, controller kube.Gen
 		if strings.ToLower(exemptValue) == "true" {
 			continue
 		}
-		check, err := resolveCheck(conf, checkID, controller, nil, config.TargetController, false)
+		check, err := resolveCheck(conf, checkID, controller.Kind, config.TargetController, controller.ObjectMeta, "", false)
 
 		if err != nil {
 			return nil, err
@@ -177,7 +179,7 @@ func applyContainerSchemaChecks(conf *config.Configuration, controller kube.Gene
 		if strings.ToLower(exemptValue) == "true" {
 			continue
 		}
-		check, err := resolveCheck(conf, checkID, controller, container, config.TargetContainer, isInit)
+		check, err := resolveCheck(conf, checkID, controller.Kind, config.TargetContainer, controller.ObjectMeta, container.Name, isInit)
 		if err != nil {
 			return nil, err
 		} else if check == nil {
@@ -200,6 +202,26 @@ func applyContainerSchemaChecks(conf *config.Configuration, controller kube.Gene
 	return results, nil
 }
 
+func applyIngressSchemaChecks(conf *config.Configuration, ingress v1beta1.Ingress) (ResultSet, error) {
+	results := ResultSet{}
+	checkIDs := getSortedKeys(conf.Checks)
+	for _, checkID := range checkIDs {
+		check, err := resolveCheck(conf, checkID, ingress.Kind, config.TargetIngress, ingress.ObjectMeta.GetObjectMeta(), "", false)
+
+		if err != nil {
+			return nil, err
+		} else if check == nil {
+			continue
+		}
+		passes, err := check.CheckObject(ingress)
+		if err != nil {
+			return nil, err
+		}
+		results[check.ID] = makeResult(conf, check, passes)
+	}
+	return results, nil
+}
+
 func getSortedKeys(m map[string]config.Severity) []string {
 	keys := make([]string, 0, len(m))
 	for key := range m {
diff --git a/pkg/validator/summary.go b/pkg/validator/summary.go
index b4e6b221..4be51d6c 100644
--- a/pkg/validator/summary.go
+++ b/pkg/validator/summary.go
@@ -103,15 +103,15 @@ func (p PodResult) GetSummaryByCategory() CountSummaryByCategory {
 	return summaries
 }
 
-// GetSummary summarizes a ControllerResult
-func (c ControllerResult) GetSummary() CountSummary {
+// GetSummary summarizes a Result
+func (c Result) GetSummary() CountSummary {
 	summary := c.Results.GetSummary()
 	summary.AddSummary(c.PodResult.GetSummary())
 	return summary
 }
 
-// GetSummaryByCategory summarizes a ControllerResult
-func (c ControllerResult) GetSummaryByCategory() CountSummaryByCategory {
+// GetSummaryByCategory summarizes a Result
+func (c Result) GetSummaryByCategory() CountSummaryByCategory {
 	summary := c.Results.GetSummaryByCategory()
 	summary.AddSummary(c.PodResult.GetSummaryByCategory())
 	return summary
@@ -121,7 +121,9 @@ func (c ControllerResult) GetSummaryByCategory() CountSummaryByCategory {
 func (a AuditData) GetSummary() CountSummary {
 	summary := CountSummary{}
 	for _, ctrlResult := range a.Results {
-		summary.AddSummary(ctrlResult.GetSummary())
+		if ctrlResult.PodResult != nil {
+			summary.AddSummary(ctrlResult.GetSummary())
+		}
 	}
 	return summary
 }
@@ -130,18 +132,20 @@ func (a AuditData) GetSummary() CountSummary {
 func (a AuditData) GetSummaryByCategory() CountSummaryByCategory {
 	summaries := CountSummaryByCategory{}
 	for _, ctrlResult := range a.Results {
-		summaries.AddSummary(ctrlResult.GetSummaryByCategory())
+		if ctrlResult.PodResult != nil {
+			summaries.AddSummary(ctrlResult.GetSummaryByCategory())
+		}
 	}
 	return summaries
 }
 
 // GetResultsByNamespace organizes results by namespace
-func (a AuditData) GetResultsByNamespace() map[string][]*ControllerResult {
-	allResults := map[string][]*ControllerResult{}
+func (a AuditData) GetResultsByNamespace() map[string][]*Result {
+	allResults := map[string][]*Result{}
 	for idx, ctrlResult := range a.Results {
 		nsResults, ok := allResults[ctrlResult.Namespace]
 		if !ok {
-			nsResults = []*ControllerResult{}
+			nsResults = []*Result{}
 		}
 		nsResults = append(nsResults, &a.Results[idx])
 		allResults[ctrlResult.Namespace] = nsResults
diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go
index c0afcd34..eb53befd 100644
--- a/pkg/webhook/webhook.go
+++ b/pkg/webhook/webhook.go
@@ -104,12 +104,12 @@ func (v *Validator) handleInternal(req admission.Request) (*validator.PodResult,
 		return nil, err
 	}
 	controller.Kind = req.AdmissionRequest.Kind.Kind
-	var controllerResult validator.ControllerResult
+	var controllerResult validator.Result
 	controllerResult, err = validator.ValidateController(&v.Config, controller)
 	if err != nil {
 		return nil, err
 	}
-	return &controllerResult.PodResult, nil
+	return controllerResult.PodResult, nil
 }
 
 // Handle for Validator to run validation checks.


Test patch:
diff --git a/pkg/kube/resources_test.go b/pkg/kube/resources_test.go
index dda9e7b6..0ade609c 100644
--- a/pkg/kube/resources_test.go
+++ b/pkg/kube/resources_test.go
@@ -96,6 +96,7 @@ func TestGetResourceFromAPI(t *testing.T) {
 	assert.IsType(t, time.Now(), resources.CreationTime, "Creation time should be set")
 
 	assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
+	assert.Equal(t, 0, len(resources.Ingresses), "Should not have any ingresses")
 	assert.Equal(t, 5, len(resources.Controllers), "Should have 5 controllers")
 
 	expectedNames := map[string]bool{
diff --git a/pkg/validator/controller_test.go b/pkg/validator/controller_test.go
index e75f0101..398504de 100644
--- a/pkg/validator/controller_test.go
+++ b/pkg/validator/controller_test.go
@@ -48,7 +48,7 @@ func TestValidateController(t *testing.T) {
 		"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
 	}
 
-	var actualResult ControllerResult
+	var actualResult Result
 	actualResult, err = ValidateController(&c, deployment)
 	if err != nil {
 		panic(err)
@@ -138,7 +138,7 @@ func TestSkipHealthChecks(t *testing.T) {
 		"readinessProbeMissing": {ID: "readinessProbeMissing", Message: "Readiness probe should be configured", Success: false, Severity: "danger", Category: "Reliability"},
 		"livenessProbeMissing":  {ID: "livenessProbeMissing", Message: "Liveness probe should be configured", Success: false, Severity: "warning", Category: "Reliability"},
 	}
-	var actualResult ControllerResult
+	var actualResult Result
 	actualResult, err = ValidateController(&c, deployment)
 	if err != nil {
 		panic(err)
@@ -206,7 +206,7 @@ func TestControllerExemptions(t *testing.T) {
 		Warnings:  uint(1),
 		Dangers:   uint(1),
 	}
-	var actualResults []ControllerResult
+	var actualResults []Result
 	actualResults, err = ValidateControllers(&c, resources)
 	if err != nil {
 		panic(err)
diff --git a/pkg/validator/ingress_test.go b/pkg/validator/ingress_test.go
new file mode 100644
index 00000000..da5a7c59
--- /dev/null
+++ b/pkg/validator/ingress_test.go
@@ -0,0 +1,64 @@
+// 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 (
+	"testing"
+
+	conf "github.com/fairwindsops/polaris/pkg/config"
+	"github.com/fairwindsops/polaris/test"
+	"github.com/stretchr/testify/assert"
+
+	extv1beta1 "k8s.io/api/extensions/v1beta1"
+)
+
+func TestValidateIngress(t *testing.T) {
+	c := conf.Configuration{
+		Checks: map[string]conf.Severity{
+			"tlsSettingsMissing": conf.SeverityWarning,
+		},
+	}
+	ingress := test.MockIngress()
+
+	var actualResult Result
+	actualResult, err := ValidateIngress(&c, ingress)
+	if err != nil {
+		panic(err)
+	}
+	results := actualResult.Results["tlsSettingsMissing"]
+
+	assert.False(t, results.Success)
+	assert.Equal(t, conf.Severity("warning"), results.Severity)
+	assert.Equal(t, "Security", results.Category)
+	assert.EqualValues(t, "Ingress does not have TLS configured", results.Message)
+
+	tls := extv1beta1.IngressTLS{
+		Hosts:      []string{"test"},
+		SecretName: "secret",
+	}
+
+	ingress.Spec.TLS = []extv1beta1.IngressTLS{tls}
+	actualResult, err = ValidateIngress(&c, ingress)
+	if err != nil {
+		panic(err)
+	}
+	results = actualResult.Results["tlsSettingsMissing"]
+
+	assert.True(t, results.Success)
+	assert.Equal(t, conf.Severity("warning"), results.Severity)
+	assert.Equal(t, "Security", results.Category)
+	assert.EqualValues(t, "Ingress has TLS configured", results.Message)
+
+}
diff --git a/test/checks_test.go b/test/checks_test.go
index 7e493b42..4b37de2c 100644
--- a/test/checks_test.go
+++ b/test/checks_test.go
@@ -56,7 +56,7 @@ func TestChecks(t *testing.T) {
 		assert.NoError(t, err)
 		c, err := config.Parse([]byte("checks:\n  " + tc.check + ": danger"))
 		assert.NoError(t, err)
-		var result validator.ControllerResult
+		var result validator.Result
 		result, err = validator.ValidateController(&c, *workload)
 		assert.NoError(t, err)
 		summary := result.GetSummary()
diff --git a/test/fixtures.go b/test/fixtures.go
index b82327e6..08b18439 100644
--- a/test/fixtures.go
+++ b/test/fixtures.go
@@ -9,6 +9,7 @@ import (
 	batchv1 "k8s.io/api/batch/v1"
 	batchv1beta1 "k8s.io/api/batch/v1beta1"
 	corev1 "k8s.io/api/core/v1"
+	extv1beta1 "k8s.io/api/extensions/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -60,6 +61,13 @@ func MockNakedPod() corev1.Pod {
 	}
 }
 
+// MockIngress creates an ingress object
+func MockIngress() extv1beta1.Ingress {
+	return extv1beta1.Ingress{
+		Spec: extv1beta1.IngressSpec{},
+	}
+}
+
 // MockController creates a mock controller and pod
 func MockController(apiVersion, kind, namespace, name string, spec interface{}, podSpec corev1.PodSpec, dest interface{}) corev1.Pod {
 	unst := newUnstructured(apiVersion, kind, namespace, name, spec)