Compare commits

...
9 Commits
Author SHA1 Message Date
Robert Brennan 7ce71f1632 bump to version 0.5.2 (#224) 2019-11-13 15:27:24 -05:00
Robert Brennan 98b47e0aeb Fix resource success messages (#223)
* add success messages when resources are set

* add tests
2019-11-13 14:07:32 -05:00
Robert Brennan 159c165468 bump version (#221) 2019-11-12 11:40:54 -05:00
Robert Brennan 4eeabb2c7f pass RunAsNonRoot if RunAsUser > 0 (#219) 2019-11-11 13:21:32 -05:00
Barnabas Makonda 7e35b03452 Merge pull request #218 from FairwindsOps/kind-exceptions
Checks exemption for kubehunter, polaris and goldilocks
2019-11-08 23:35:42 +02:00
MAKOSCAFEE c8e4f766a2 revert version upgrade for dashboard and webhook 2019-11-07 23:03:29 +02:00
MAKOSCAFEE 0ee39a4470 revert version upgrade 2019-11-07 23:01:47 +02:00
MAKOSCAFEE fc7c913122 update exemption rules and check controller name prefix 2019-11-07 19:16:06 +02:00
MAKOSCAFEE 3e15586597 add kind exception configurations 2019-11-07 15:37:13 +02:00
8 changed files with 323 additions and 97 deletions
+8
View File
@@ -1,5 +1,13 @@
# x.x.x (next release)
# 0.5.2
* Fixed missing success messages for resource requests/limits
# 0.5.1
* Added a few more exemptions
* Started checking exemptions based on controller name prefix
* `runAsUser != 0` now passes the `runAsNonRoot` check
# 0.5.0
* Added `--load-audit-file` flag to run the dashboard from an existing audit
* Added an `ID` field to each check in the output
+1 -1
View File
@@ -5,7 +5,7 @@
[![Version][version-image]][version-link] [![CircleCI][circleci-image]][circleci-link] [![Go Report Card][goreport-image]][goreport-link]
</div>
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.5.0&color=239922
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.5.2&color=239922
[version-link]: https://github.com/FairwindsOps/polaris
[goreport-image]: https://goreportcard.com/badge/github.com/FairwindsOps/polaris
+67 -57
View File
@@ -50,71 +50,81 @@ controllers_to_scan:
- ReplicationControllers
exemptions:
- controllerNames:
- dns-controller
- datadog-datadog
- kube-flannel-ds
- kube2iam
- aws-iam-authenticator
- datadog
- kube2iam
- dns-controller
- datadog-datadog
- kube-flannel-ds
- kube2iam
- aws-iam-authenticator
- datadog
- kube2iam
rules:
- hostNetworkSet
- hostNetworkSet
- controllerNames:
- aws-iam-authenticator
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- dnsmasq
- autoscaler
- kubernetes-dashboard
- install-cni
- kube2iam
- aws-iam-authenticator
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- dnsmasq
- autoscaler
- kubernetes-dashboard
- install-cni
- kube2iam
rules:
- readinessProbeMissing
- livenessProbeMissing
- readinessProbeMissing
- livenessProbeMissing
- controllerNames:
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
rules:
- runAsRootAllowed
- runAsRootAllowed
- controllerNames:
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
rules:
- notReadOnlyRootFileSystem
- notReadOnlyRootFileSystem
- controllerNames:
- cert-manager
- dns-controller
- kubedns
- dnsmasq
- autoscaler
- cert-manager
- dns-controller
- kubedns
- dnsmasq
- autoscaler
rules:
- cpuRequestsMissing
- cpuLimitsMissing
- memoryRequestsMissing
- memoryLimitsMissing
- cpuRequestsMissing
- cpuLimitsMissing
- memoryRequestsMissing
- memoryLimitsMissing
- controllerNames:
- kube2iam
- kube2iam
rules:
- runAsPrivileged
- runAsPrivileged
- controllerNames:
- kube-hunter
rules:
- hostPIDSet
- controllerNames:
- polaris
- kube-hunter
- goldilocks
rules:
- readOnlyRootFilesystem
+1 -1
View File
@@ -41,7 +41,7 @@ import (
const (
// Version represents the current release version of Polaris
Version = "0.5.0"
Version = "0.5.2"
)
func main() {
+1 -1
View File
@@ -125,8 +125,8 @@ type SecurityCapabilityLists struct {
func ParseFile(path string) (Configuration, error) {
var rawBytes []byte
var err error
configBox := packr.New("Config", "../../examples")
if path == "" {
configBox := packr.New("Config", "../../examples")
rawBytes, err = configBox.Find("config.yaml")
} else if strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://") {
//path is a url
+10 -3
View File
@@ -2,16 +2,23 @@ package config
import (
"reflect"
"strings"
)
// IsActionable determines whether a check is actionable given the current configuration
func (conf *Configuration) IsActionable(subConf interface{}, ruleName, controllerName string) bool {
ruleID := GetIDFromField(subConf, ruleName)
subConfRef := reflect.ValueOf(subConf)
severity, ok := reflect.Indirect(subConfRef).FieldByName(ruleName).Interface().(Severity)
if ok && !severity.IsActionable() {
fieldVal := reflect.Indirect(subConfRef).FieldByName(ruleName).Interface()
if severity, ok := fieldVal.(Severity); ok && !severity.IsActionable() {
return false
}
if ranges, ok := fieldVal.(ResourceRanges); ok {
if ranges.Warning.Above == nil && ranges.Warning.Below == nil &&
ranges.Error.Above == nil && ranges.Error.Below == nil {
return false
}
}
if conf.DisallowExemptions {
return true
}
@@ -21,7 +28,7 @@ func (conf *Configuration) IsActionable(subConf interface{}, ruleName, controlle
continue
}
for _, controller := range example.ControllerNames {
if controller == controllerName {
if strings.HasPrefix(controllerName, controller) {
return false
}
}
+20 -15
View File
@@ -86,42 +86,50 @@ func (cv *ContainerValidation) validateResources(conf *config.Configuration, con
missingName := "CPURequestsMissing"
rangeName := "CPURequestRanges"
id := config.GetIDFromField(conf.Resources, missingName)
if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Requests.Cpu().MilliValue() == 0 {
id := config.GetIDFromField(conf.Resources, missingName)
cv.addFailure(messages.CPURequestsFailure, conf.Resources.CPURequestsMissing, category, id)
} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
id := config.GetIDFromField(conf.Resources, rangeName)
cv.validateResourceRange(id, messages.CPURequestsLabel, &conf.Resources.CPURequestRanges, res.Requests.Cpu())
} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.CPURequestsLabel), category, id)
}
missingName = "CPULimitsMissing"
rangeName = "CPULimitRanges"
id = config.GetIDFromField(conf.Resources, missingName)
if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Limits.Cpu().MilliValue() == 0 {
id := config.GetIDFromField(conf.Resources, missingName)
cv.addFailure(messages.CPULimitsFailure, conf.Resources.CPULimitsMissing, category, id)
} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
id := config.GetIDFromField(conf.Resources, rangeName)
cv.validateResourceRange(id, messages.CPULimitsLabel, &conf.Resources.CPULimitRanges, res.Requests.Cpu())
} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.CPULimitsLabel), category, id)
}
missingName = "MemoryRequestsMissing"
rangeName = "MemoryRequestRanges"
id = config.GetIDFromField(conf.Resources, missingName)
if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Requests.Memory().MilliValue() == 0 {
id := config.GetIDFromField(conf.Resources, missingName)
cv.addFailure(messages.MemoryRequestsFailure, conf.Resources.MemoryRequestsMissing, category, id)
} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
id := config.GetIDFromField(conf.Resources, rangeName)
cv.validateResourceRange(id, messages.MemoryRequestsLabel, &conf.Resources.MemoryRequestRanges, res.Requests.Memory())
} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.MemoryRequestsLabel), category, id)
}
missingName = "MemoryLimitsMissing"
rangeName = "MemoryLimitRanges"
id = config.GetIDFromField(conf.Resources, missingName)
if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Limits.Memory().MilliValue() == 0 {
id := config.GetIDFromField(conf.Resources, missingName)
cv.addFailure(messages.MemoryLimitsFailure, conf.Resources.MemoryLimitsMissing, category, id)
} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
id := config.GetIDFromField(conf.Resources, rangeName)
cv.validateResourceRange(id, messages.MemoryLimitsLabel, &conf.Resources.MemoryLimitRanges, res.Limits.Memory())
} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.MemoryLimitsLabel), category, id)
}
}
@@ -235,19 +243,16 @@ func (cv *ContainerValidation) validateSecurity(conf *config.Configuration, cont
name := "RunAsRootAllowed"
if conf.IsActionable(conf.Security, name, controllerName) {
id := config.GetIDFromField(conf.Security, name)
if getBoolValue(securityContext.RunAsNonRoot) {
runAsRootSuccess := false
if getBoolValue(securityContext.RunAsNonRoot) || (securityContext.RunAsUser != nil && *securityContext.RunAsUser > 0) {
// Check if the container is explicitly set to True (pass)
runAsRootSuccess = true
} else if securityContext.RunAsNonRoot == nil && securityContext.RunAsUser == nil {
// Or if the container values are unset, check the pod values
runAsRootSuccess = getBoolValue(podSecurityContext.RunAsNonRoot) || (podSecurityContext.RunAsUser != nil && *podSecurityContext.RunAsUser > 0)
}
if runAsRootSuccess {
cv.addSuccess(messages.RunAsRootSuccess, category, id)
} else if securityContext.RunAsNonRoot == nil {
// Check if the value in the container spec if nil (thus defaulting to the podspec)
// Check if the container value is not set
if getBoolValue(podSecurityContext.RunAsNonRoot) {
// if the pod spec default for containers is true, then pass
cv.addSuccess(messages.RunAsRootSuccess, category, id)
} else {
// else fail as RunAsNonRoot defaults to false
cv.addFailure(messages.RunAsRootFailure, conf.Security.RunAsRootAllowed, category, id)
}
} else {
cv.addFailure(messages.RunAsRootFailure, conf.Security.RunAsRootAllowed, category, id)
}
+215 -19
View File
@@ -15,6 +15,7 @@
package validator
import (
"fmt"
"testing"
conf "github.com/fairwindsops/polaris/pkg/config"
@@ -119,6 +120,27 @@ exemptions:
- foo
`
func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []*ResultMessage, expectedWarnings []*ResultMessage, expectedSuccesses []*ResultMessage) {
cv := ContainerValidation{
Container: container,
ResourceValidation: &ResourceValidation{},
}
parsedConf, err := conf.Parse([]byte(*resourceConf))
assert.NoError(t, err, "Expected no error when parsing config")
cv.validateResources(&parsedConf, controllerName)
assert.Len(t, cv.Warnings, len(expectedWarnings))
assert.ElementsMatch(t, expectedWarnings, cv.Warnings)
assert.Len(t, cv.Errors, len(expectedErrors))
assert.ElementsMatch(t, expectedErrors, cv.Errors)
assert.Len(t, cv.Successes, len(expectedSuccesses))
assert.ElementsMatch(t, expectedSuccesses, cv.Successes)
}
func TestValidateResourcesEmptyConfig(t *testing.T) {
container := corev1.Container{
Name: "Empty",
@@ -168,7 +190,9 @@ func TestValidateResourcesEmptyContainer(t *testing.T) {
},
}
testValidateResources(t, &container, &resourceConf2, "foo", &expectedErrors, &expectedWarnings)
expectedSuccesses := []*ResultMessage{}
testValidateResources(t, &container, &resourceConf2, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
}
func TestValidateResourcesPartiallyValid(t *testing.T) {
@@ -220,7 +244,9 @@ func TestValidateResourcesPartiallyValid(t *testing.T) {
},
}
testValidateResources(t, &container, &resourceConf1, "foo", &expectedErrors, &expectedWarnings)
expectedSuccesses := []*ResultMessage{}
testValidateResources(t, &container, &resourceConf1, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
}
func TestValidateResourcesInit(t *testing.T) {
@@ -271,24 +297,63 @@ func TestValidateResourcesFullyValid(t *testing.T) {
},
}
testValidateResources(t, &container, &resourceConf1, "foo", &[]*ResultMessage{}, &[]*ResultMessage{})
}
func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors *[]*ResultMessage, expectedWarnings *[]*ResultMessage) {
cv := ContainerValidation{
Container: container,
ResourceValidation: &ResourceValidation{},
expectedSuccesses := []*ResultMessage{
{
ID: "cpuRequestRanges",
Type: "success",
Message: "CPU requests are within the expected range",
Category: "Resources",
},
{
ID: "memoryRequestRanges",
Type: "success",
Message: "Memory requests are within the expected range",
Category: "Resources",
},
{
ID: "cpuLimitRanges",
Type: "success",
Message: "CPU limits are within the expected range",
Category: "Resources",
},
{
ID: "memoryLimitRanges",
Type: "success",
Message: "Memory limits are within the expected range",
Category: "Resources",
},
}
parsedConf, err := conf.Parse([]byte(*resourceConf))
assert.NoError(t, err, "Expected no error when parsing config")
testValidateResources(t, &container, &resourceConf1, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses)
cv.validateResources(&parsedConf, controllerName)
assert.Len(t, cv.Warnings, len(*expectedWarnings))
assert.ElementsMatch(t, *expectedWarnings, cv.Warnings)
expectedSuccesses = []*ResultMessage{
{
ID: "cpuRequestsMissing",
Type: "success",
Message: "CPU requests are set",
Category: "Resources",
},
{
ID: "memoryRequestsMissing",
Type: "success",
Message: "Memory requests are set",
Category: "Resources",
},
{
ID: "cpuLimitsMissing",
Type: "success",
Message: "CPU limits are set",
Category: "Resources",
},
{
ID: "memoryLimitsMissing",
Type: "success",
Message: "Memory limits are set",
Category: "Resources",
},
}
assert.Len(t, cv.Errors, len(*expectedErrors))
assert.ElementsMatch(t, *expectedErrors, cv.Errors)
testValidateResources(t, &container, &resourceConf2, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses)
}
func TestValidateHealthChecks(t *testing.T) {
@@ -1075,6 +1140,135 @@ func TestValidateSecurity(t *testing.T) {
}
}
func TestValidateRunAsRoot(t *testing.T) {
falseVar := false
trueVar := true
nonRootUser := int64(1000)
rootUser := int64(0)
config := conf.Configuration{
Security: conf.Security{
RunAsRootAllowed: conf.SeverityWarning,
},
}
testCases := []struct {
cv ContainerValidation
message ResultMessage
}{
{
cv: ContainerValidation{
ResourceValidation: &ResourceValidation{},
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: nil,
}},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: &falseVar,
},
},
},
message: ResultMessage{
ID: "runAsRootAllowed",
Message: "Should not be allowed to run as root",
Type: "warning",
Category: "Security",
},
},
{
cv: ContainerValidation{
ResourceValidation: &ResourceValidation{},
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: &trueVar,
}},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: &falseVar,
},
},
},
message: ResultMessage{
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Type: "success",
Category: "Security",
},
},
{
cv: ContainerValidation{
ResourceValidation: &ResourceValidation{},
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsUser: &nonRootUser,
}},
},
message: ResultMessage{
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Type: "success",
Category: "Security",
},
},
{
cv: ContainerValidation{
ResourceValidation: &ResourceValidation{},
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{}},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsUser: &nonRootUser,
},
},
},
message: ResultMessage{
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Type: "success",
Category: "Security",
},
},
{
cv: ContainerValidation{
ResourceValidation: &ResourceValidation{},
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsUser: &rootUser,
}},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsUser: &nonRootUser,
},
},
},
message: ResultMessage{
ID: "runAsRootAllowed",
Message: "Should not be allowed to run as root",
Type: "warning",
Category: "Security",
},
},
{
cv: ContainerValidation{
ResourceValidation: &ResourceValidation{},
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: &falseVar,
}},
parentPodSpec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsUser: &nonRootUser,
},
},
},
message: ResultMessage{
ID: "runAsRootAllowed",
Message: "Should not be allowed to run as root",
Type: "warning",
Category: "Security",
},
},
}
for idx, tt := range testCases {
tt.cv.validateSecurity(&config, "")
assert.Len(t, tt.cv.messages(), 1)
assert.Equal(t, &tt.message, tt.cv.messages()[0], fmt.Sprintf("Test case %d failed", idx))
}
}
func TestValidateResourcesExemption(t *testing.T) {
container := corev1.Container{
Name: "Empty",
@@ -1082,8 +1276,9 @@ func TestValidateResourcesExemption(t *testing.T) {
expectedWarnings := []*ResultMessage{}
expectedErrors := []*ResultMessage{}
expectedSuccesses := []*ResultMessage{}
testValidateResources(t, &container, &resourceConfExemptions, "foo", &expectedErrors, &expectedWarnings)
testValidateResources(t, &container, &resourceConfExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
expectedWarnings = []*ResultMessage{
{
@@ -1117,7 +1312,7 @@ func TestValidateResourcesExemption(t *testing.T) {
disallowExemptionsConf := resourceConfExemptions + "\ndisallowExemptions: true"
testValidateResources(t, &container, &disallowExemptionsConf, "foo", &expectedErrors, &expectedWarnings)
testValidateResources(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
}
func TestValidateResourceRangeExemption(t *testing.T) {
@@ -1127,8 +1322,9 @@ func TestValidateResourceRangeExemption(t *testing.T) {
expectedWarnings := []*ResultMessage{}
expectedErrors := []*ResultMessage{}
expectedSuccesses := []*ResultMessage{}
testValidateResources(t, &container, &resourceConfRangeExemptions, "foo", &expectedErrors, &expectedWarnings)
testValidateResources(t, &container, &resourceConfRangeExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
}
func resetCV(cv ContainerValidation) ContainerValidation {