Skip to content

Commit

Permalink
Add version, cluster stats to output and UI (#61)
Browse files Browse the repository at this point in the history
* add version, cluster stats to output

* add comment

* fix tests

* add categories to messages

* fix tests

* update UI

* remove empty category totals field

* k8smeta -> metav1
  • Loading branch information
rbren authored Apr 22, 2019
1 parent c3de019 commit 3ce7e12
Show file tree
Hide file tree
Showing 14 changed files with 331 additions and 182 deletions.
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func runAudit(c conf.Configuration, outputFile string, outputURL string) {
}
os.Stdout.Write(yamlBytes)
} else {
jsonData, err := json.Marshal(auditData)
jsonData, err := json.MarshalIndent(auditData, "", " ")
if err != nil {
panic(err)
}
Expand Down
56 changes: 41 additions & 15 deletions pkg/dashboard/templates/dashboard.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
window.fairwindsAuditData = {{ .JSON }};
</script>
<script type="text/javascript" src="/static/js/main.js"></script>

</head>

<body>
Expand All @@ -38,47 +37,74 @@
</div>

<div class="dashboard-content">
<div class="namespace">
<div class="card cluster">
<h3>Cluster Overview</h3>
<div class="cluster-overview">
<div class="cluster-score">
<div class="weather"><i class="fas {{ getWeatherIcon .AuditData.ClusterSummary }}"></i></div>
<div class="sailing">{{ getWeatherText .AuditData.ClusterSummary }}</div>
<div class="scores">Grade: <strong>{{ getGrade .AuditData.ClusterSummary }}</strong> | Score: <strong>{{ getScore .AuditData.ClusterSummary }}%</strong></div>
<div class="weather"><i class="fas {{ getWeatherIcon .AuditData.ClusterSummary.Results }}"></i></div>
<div class="sailing">{{ getWeatherText .AuditData.ClusterSummary.Results }}</div>
<div class="scores">Grade: <strong>{{ getGrade .AuditData.ClusterSummary.Results }}</strong> | Score: <strong>{{ getScore .AuditData.ClusterSummary.Results }}%</strong></div>
</div>
<div class="result-messages">
<ul>
<li class="success"><i class="fas fa-check"></i> {{ .AuditData.ClusterSummary.Successes }} checks passed</li>
<li class="warning"><i class="fas fa-exclamation"></i> {{ .AuditData.ClusterSummary.Warnings }} checks had warnings</li>
<li class="error"><i class="fas fa-times"></i> {{ .AuditData.ClusterSummary.Errors }} checks had errors</li>
<ul class="message-list">
<li class="success"><i class="fas fa-check"></i> {{ .AuditData.ClusterSummary.Results.Successes }} checks passed</li>
<li class="warning"><i class="fas fa-exclamation"></i> {{ .AuditData.ClusterSummary.Results.Warnings }} checks had warnings</li>
<li class="error"><i class="fas fa-times"></i> {{ .AuditData.ClusterSummary.Results.Errors }} checks had errors</li>
</ul>
</div>
<canvas id="clusterScoreChart"></canvas>
</div>
<table class="expandable-table" cellspacing="0">
<tr>
<td class="resource-info">
<div class="name"><span class="caret-expander"></span>Cluster details</div>
<div class="expandable-content">
<ul class="message-list">
<li>
<span class="detail-label">Kubernetes Version:</span>
<span class="detail-value">{{ .AuditData.ClusterSummary.Version }}</span>
</li>
<li>
<span class="detail-label">Nodes:</span>
<span class="detail-value">{{ .AuditData.ClusterSummary.Nodes }}</span>
</li>
<li>
<span class="detail-label">Pods:</span>
<span class="detail-value">{{ .AuditData.ClusterSummary.Pods }}</span>
</li>
<li>
<span class="detail-label">Namespaces:</span>
<span class="detail-value">{{ .AuditData.ClusterSummary.Namespaces }}</span>
</li>
</ul>
</div>
</td>
</tr>
</table>
</div>

{{ range $namespace, $results := .AuditData.NamespacedResults }}
<div class="namespace">
<div class="card namespace">
<h3>Namespace: <strong>{{ $namespace }}</strong></h3>
<table class="namespace-content" cellspacing="0">
<table class="expandable-table" cellspacing="0">
{{ range $results.Results }}
<tr>
<td class="resource-info">
<div class="name"><span class="caret-expander"></span>{{ .Type }}: <strong>{{ .Name }}</strong></div>

{{ range .PodResults}}
<div class="result-messages">
<div class="result-messages expandable-content">
<h4>Pod Spec:</h4>
<ul>
<ul class="message-list">
{{ range $message := .Messages}}
<li class="{{ .Type }}"><i class="{{ getIcon $message }}"></i> {{ .Message }}</li>
{{ end }}
</ul>
</div>
{{ range .ContainerResults}}
<div class="result-messages">
<div class="result-messages expandable-content">
<h4>Container: {{ .Name }}</h4>
<ul>
<ul class="message-list">
{{ range $message := .Messages}}
<li class="{{ .Type }}"><i class="{{ getIcon $message }}"></i> {{ .Message }}</li>
{{ end }}
Expand Down
69 changes: 38 additions & 31 deletions pkg/validator/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,28 +61,29 @@ func ValidateContainer(cnConf *conf.Configuration, container *corev1.Container)
}

func (cv *ContainerValidation) validateResources(resConf *conf.Resources) {
category := messages.CategoryResources
res := cv.Container.Resources

if resConf.CPURequestsMissing.IsActionable() && res.Requests.Cpu().MilliValue() == 0 {
cv.addFailure(messages.CPURequestsFailure, resConf.CPURequestsMissing)
cv.addFailure(messages.CPURequestsFailure, resConf.CPURequestsMissing, category)
} else {
cv.validateResourceRange(messages.CPURequestsLabel, &resConf.CPURequestRanges, res.Requests.Cpu())
}

if resConf.CPULimitsMissing.IsActionable() && res.Limits.Cpu().MilliValue() == 0 {
cv.addFailure(messages.CPULimitsFailure, resConf.CPULimitsMissing)
cv.addFailure(messages.CPULimitsFailure, resConf.CPULimitsMissing, category)
} else {
cv.validateResourceRange(messages.CPULimitsLabel, &resConf.CPULimitRanges, res.Requests.Cpu())
}

if resConf.MemoryRequestsMissing.IsActionable() && res.Requests.Memory().MilliValue() == 0 {
cv.addFailure(messages.MemoryRequestsFailure, resConf.MemoryRequestsMissing)
cv.addFailure(messages.MemoryRequestsFailure, resConf.MemoryRequestsMissing, category)
} else {
cv.validateResourceRange(messages.MemoryRequestsLabel, &resConf.MemoryRequestRanges, res.Requests.Memory())
}

if resConf.MemoryLimitsMissing.IsActionable() && res.Limits.Memory().MilliValue() == 0 {
cv.addFailure(messages.MemoryLimitsFailure, resConf.MemoryLimitsMissing)
cv.addFailure(messages.MemoryLimitsFailure, resConf.MemoryLimitsMissing, category)
} else {
cv.validateResourceRange(messages.MemoryLimitsLabel, &resConf.MemoryLimitRanges, res.Limits.Memory())
}
Expand All @@ -93,50 +94,54 @@ func (cv *ContainerValidation) validateResourceRange(resourceName string, rangeC
warnBelow := rangeConf.Warning.Below
errorAbove := rangeConf.Error.Above
errorBelow := rangeConf.Error.Below
category := messages.CategoryResources

if errorAbove != nil && errorAbove.MilliValue() < res.MilliValue() {
cv.addError(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, errorAbove.String()))
cv.addError(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, errorAbove.String()), category)
} else if warnAbove != nil && warnAbove.MilliValue() < res.MilliValue() {
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, warnAbove.String()))
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, warnAbove.String()), category)
} else if errorBelow != nil && errorBelow.MilliValue() > res.MilliValue() {
cv.addError(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, errorBelow.String()))
cv.addError(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, errorBelow.String()), category)
} else if warnBelow != nil && warnBelow.MilliValue() > res.MilliValue() {
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, warnBelow.String()))
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, warnBelow.String()), category)
} else {
cv.addSuccess(fmt.Sprintf(messages.ResourceAmountSuccess, resourceName))
cv.addSuccess(fmt.Sprintf(messages.ResourceAmountSuccess, resourceName), category)
}
}

func (cv *ContainerValidation) validateHealthChecks(conf *conf.HealthChecks) {
category := messages.CategoryHealthChecks
if conf.ReadinessProbeMissing.IsActionable() {
if cv.Container.ReadinessProbe == nil {
cv.addFailure(messages.ReadinessProbeFailure, conf.ReadinessProbeMissing)
cv.addFailure(messages.ReadinessProbeFailure, conf.ReadinessProbeMissing, category)
} else {
cv.addSuccess(messages.ReadinessProbeSuccess)
cv.addSuccess(messages.ReadinessProbeSuccess, category)
}
}

if conf.LivenessProbeMissing.IsActionable() {
if cv.Container.LivenessProbe == nil {
cv.addFailure(messages.LivenessProbeFailure, conf.LivenessProbeMissing)
cv.addFailure(messages.LivenessProbeFailure, conf.LivenessProbeMissing, category)
} else {
cv.addSuccess(messages.LivenessProbeSuccess)
cv.addSuccess(messages.LivenessProbeSuccess, category)
}
}
}

func (cv *ContainerValidation) validateImage(imageConf *conf.Images) {
category := messages.CategoryImages
if imageConf.TagNotSpecified.IsActionable() {
img := strings.Split(cv.Container.Image, ":")
if len(img) == 1 || img[1] == "latest" {
cv.addFailure(messages.ImageTagFailure, imageConf.TagNotSpecified)
cv.addFailure(messages.ImageTagFailure, imageConf.TagNotSpecified, category)
} else {
cv.addSuccess(messages.ImageTagSuccess)
cv.addSuccess(messages.ImageTagSuccess, category)
}
}
}

func (cv *ContainerValidation) validateNetworking(networkConf *conf.Networking) {
category := messages.CategoryNetworking
if networkConf.HostPortSet.IsActionable() {
hostPortSet := false
for _, port := range cv.Container.Ports {
Expand All @@ -147,48 +152,49 @@ func (cv *ContainerValidation) validateNetworking(networkConf *conf.Networking)
}

if hostPortSet {
cv.addFailure(messages.HostPortFailure, networkConf.HostAliasSet)
cv.addFailure(messages.HostPortFailure, networkConf.HostAliasSet, category)
} else {
cv.addSuccess(messages.HostPortSuccess)
cv.addSuccess(messages.HostPortSuccess, category)
}
}
}

func (cv *ContainerValidation) validateSecurity(securityConf *conf.Security) {
category := messages.CategorySecurity
securityContext := cv.Container.SecurityContext
if securityContext == nil {
securityContext = &corev1.SecurityContext{}
}

if securityConf.RunAsRootAllowed.IsActionable() {
if securityContext.RunAsNonRoot == (*bool)(nil) || !*securityContext.RunAsNonRoot {
cv.addFailure(messages.RunAsRootFailure, securityConf.RunAsRootAllowed)
cv.addFailure(messages.RunAsRootFailure, securityConf.RunAsRootAllowed, category)
} else {
cv.addSuccess(messages.RunAsRootSuccess)
cv.addSuccess(messages.RunAsRootSuccess, category)
}
}

if securityConf.RunAsPrivileged.IsActionable() {
if securityContext.Privileged == (*bool)(nil) || !*securityContext.Privileged {
cv.addSuccess(messages.RunAsPrivilegedSuccess)
cv.addSuccess(messages.RunAsPrivilegedSuccess, category)
} else {
cv.addFailure(messages.RunAsPrivilegedFailure, securityConf.RunAsPrivileged)
cv.addFailure(messages.RunAsPrivilegedFailure, securityConf.RunAsPrivileged, category)
}
}

if securityConf.NotReadOnlyRootFileSystem.IsActionable() {
if securityContext.ReadOnlyRootFilesystem == (*bool)(nil) || !*securityContext.ReadOnlyRootFilesystem {
cv.addFailure(messages.ReadOnlyFilesystemFailure, securityConf.NotReadOnlyRootFileSystem)
cv.addFailure(messages.ReadOnlyFilesystemFailure, securityConf.NotReadOnlyRootFileSystem, category)
} else {
cv.addSuccess(messages.ReadOnlyFilesystemSuccess)
cv.addSuccess(messages.ReadOnlyFilesystemSuccess, category)
}
}

if securityConf.PrivilegeEscalationAllowed.IsActionable() {
if securityContext.AllowPrivilegeEscalation == (*bool)(nil) || !*securityContext.AllowPrivilegeEscalation {
cv.addSuccess(messages.PrivilegeEscalationSuccess)
cv.addSuccess(messages.PrivilegeEscalationSuccess, category)
} else {
cv.addFailure(messages.PrivilegeEscalationFailure, securityConf.PrivilegeEscalationAllowed)
cv.addFailure(messages.PrivilegeEscalationFailure, securityConf.PrivilegeEscalationAllowed, category)
}
}

Expand All @@ -204,11 +210,12 @@ func (cv *ContainerValidation) validateSecurity(securityConf *conf.Security) {
if !hasSecurityError && !hasSecurityWarning &&
(hasSecurityCheck(securityConf.Capabilities.Error) ||
hasSecurityCheck(securityConf.Capabilities.Warning)) {
cv.addSuccess(messages.SecurityCapabilitiesSuccess)
cv.addSuccess(messages.SecurityCapabilitiesSuccess, category)
}
}

func (cv *ContainerValidation) validateCapabilities(confLists conf.SecurityCapabilityLists, severity conf.Severity) bool {
category := messages.CategorySecurity
capabilities := &corev1.Capabilities{}
if cv.Container.SecurityContext != nil && cv.Container.SecurityContext.Capabilities != nil {
capabilities = cv.Container.SecurityContext.Capabilities
Expand All @@ -219,10 +226,10 @@ func (cv *ContainerValidation) validateCapabilities(confLists conf.SecurityCapab
intersectAdds := capIntersection(capabilities.Add, confLists.IfAnyAdded)
if len(intersectAdds) > 0 {
capsString := commaSeparatedCapabilities(intersectAdds)
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity)
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity, category)
everythingOK = false
} else if capContains(capabilities.Add, "ALL") {
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, "ALL"), severity)
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, "ALL"), severity, category)
everythingOK = false
}
}
Expand All @@ -231,10 +238,10 @@ func (cv *ContainerValidation) validateCapabilities(confLists conf.SecurityCapab
differentAdds := capDifference(capabilities.Add, confLists.IfAnyAddedBeyond)
if len(differentAdds) > 0 {
capsString := commaSeparatedCapabilities(differentAdds)
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity)
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity, category)
everythingOK = false
} else if capContains(capabilities.Add, "ALL") {
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, "ALL"), severity)
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, "ALL"), severity, category)
everythingOK = false
}
}
Expand All @@ -243,7 +250,7 @@ func (cv *ContainerValidation) validateCapabilities(confLists conf.SecurityCapab
missingDrops := capDifference(confLists.IfAnyNotDropped, capabilities.Drop)
if len(missingDrops) > 0 && !capContains(capabilities.Drop, "ALL") {
capsString := commaSeparatedCapabilities(missingDrops)
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesNotDroppedFailure, capsString), severity)
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesNotDroppedFailure, capsString), severity, category)
everythingOK = false
}
}
Expand Down
Loading

0 comments on commit 3ce7e12

Please sign in to comment.