From 7b0fe81d016d41b26975d304481834aabcfb7643 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Thu, 26 Dec 2019 20:44:04 +0000 Subject: [PATCH 01/14] implement capabilities checks in JSON schema --- checks/dangerousCapabilities.yaml | 27 ++++++ checks/insecureCapabilities.yaml | 33 +++++++ examples/config.yaml | 24 +---- pkg/config/config.go | 29 ++---- pkg/validator/container.go | 146 ------------------------------ pkg/validator/container_test.go | 112 +++++++++++------------ pkg/validator/schema.go | 2 + 7 files changed, 126 insertions(+), 247 deletions(-) create mode 100644 checks/dangerousCapabilities.yaml create mode 100644 checks/insecureCapabilities.yaml diff --git a/checks/dangerousCapabilities.yaml b/checks/dangerousCapabilities.yaml new file mode 100644 index 00000000..6c88bd2c --- /dev/null +++ b/checks/dangerousCapabilities.yaml @@ -0,0 +1,27 @@ +name: DangerousCapabilities +id: dangerousCapabilities +successMessage: Container does not have any dangerous capabilities +failureMessage: Container should not have dangerous capabilities +category: Security +target: Container +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + properties: + securityContext: + type: object + properties: + capabilities: + type: object + properties: + add: + type: array + not: + contains: + const: ALL + not: + contains: + const: SYS_ADMIN + not: + contains: + const: NET_ADMIN diff --git a/checks/insecureCapabilities.yaml b/checks/insecureCapabilities.yaml new file mode 100644 index 00000000..fd987acf --- /dev/null +++ b/checks/insecureCapabilities.yaml @@ -0,0 +1,33 @@ +name: InsecureCapabilities +id: insecureCapabilities +successMessage: Container does not have any insecure capabilities +failureMessage: Container should not have insecure capabilities +category: Security +target: Container +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + properties: + securityContext: + type: object + properties: + capabilities: + type: object + properties: + add: + enum: + - CHOWN + - DAC_OVERRIDE + - FSETID + - FOWNER + - MKNOD + - NET_RAW + - SETGID + - SETUID + - SETFCAP + - SETPCAP + - NET_BIND_SERVICE + - SYS_CHROOT + - KILL + - AUDIT_WRITE + diff --git a/examples/config.yaml b/examples/config.yaml index 1e8ac06e..faf1c1ef 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -19,28 +19,8 @@ security: privilegeEscalationAllowed: error runAsRootAllowed: warning runAsPrivileged: error - capabilities: - error: - ifAnyAdded: - - SYS_ADMIN - - NET_ADMIN - - ALL - warning: - ifAnyAddedBeyond: - - CHOWN - - DAC_OVERRIDE - - FSETID - - FOWNER - - MKNOD - - NET_RAW - - SETGID - - SETUID - - SETFCAP - - SETPCAP - - NET_BIND_SERVICE - - SYS_CHROOT - - KILL - - AUDIT_WRITE + dangerousCapabilities: error + insecureCapabilities: warning controllers_to_scan: - Deployments - StatefulSets diff --git a/pkg/config/config.go b/pkg/config/config.go index 841d8ada..351f1277 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -23,7 +23,6 @@ import ( "strings" packr "github.com/gobuffalo/packr/v2" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/yaml" ) @@ -99,26 +98,14 @@ type Networking struct { // Security contains the config for security validations. type Security struct { - HostIPCSet Severity `json:"hostIPCSet"` - HostPIDSet Severity `json:"hostPIDSet"` - RunAsRootAllowed Severity `json:"runAsRootAllowed"` - RunAsPrivileged Severity `json:"runAsPrivileged"` - NotReadOnlyRootFileSystem Severity `json:"notReadOnlyRootFileSystem"` - PrivilegeEscalationAllowed Severity `json:"privilegeEscalationAllowed"` - Capabilities SecurityCapabilities `json:"capabilities"` -} - -// SecurityCapabilities contains the config for security capabilities validations. -type SecurityCapabilities struct { - Error SecurityCapabilityLists `json:"error"` - Warning SecurityCapabilityLists `json:"warning"` -} - -// SecurityCapabilityLists contains the config for security capabilitie list validations. -type SecurityCapabilityLists struct { - IfAnyAdded []corev1.Capability `json:"ifAnyAdded"` - IfAnyAddedBeyond []corev1.Capability `json:"ifAnyAddedBeyond"` - IfAnyNotDropped []corev1.Capability `json:"ifAnyNotDropped"` + HostIPCSet Severity `json:"hostIPCSet"` + HostPIDSet Severity `json:"hostPIDSet"` + RunAsRootAllowed Severity `json:"runAsRootAllowed"` + RunAsPrivileged Severity `json:"runAsPrivileged"` + NotReadOnlyRootFileSystem Severity `json:"notReadOnlyRootFileSystem"` + PrivilegeEscalationAllowed Severity `json:"privilegeEscalationAllowed"` + DangerousCapabilities Severity `json:"dangerousCapabilities"` + InsecureCapabilities Severity `json:"insecureCapabilities"` } // ParseFile parses config from a file. diff --git a/pkg/validator/container.go b/pkg/validator/container.go index 85beade5..81352684 100644 --- a/pkg/validator/container.go +++ b/pkg/validator/container.go @@ -64,8 +64,6 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, panic(err) } - cv.validateSecurity(conf, controllerName) - cRes := ContainerResult{ Name: container.Name, Messages: cv.messages(), @@ -153,147 +151,3 @@ func (cv *ContainerValidation) validateResourceRange(id, resourceName string, ra cv.addSuccess(fmt.Sprintf(messages.ResourceAmountSuccess, resourceName), category, id) } } - -func (cv *ContainerValidation) validateSecurity(conf *config.Configuration, controllerName string) { - securityContext := cv.Container.SecurityContext - podSecurityContext := cv.parentPodSpec.SecurityContext - - // Support an empty container security context - if securityContext == nil { - securityContext = &corev1.SecurityContext{} - } - - // Support an empty pod security context - if podSecurityContext == nil { - podSecurityContext = &corev1.PodSecurityContext{} - } - - name := "Capabilities" - if conf.IsActionable(conf.Security, name, controllerName) { - cv.validateCapabilities(&conf.Security.Capabilities.Warning, &conf.Security.Capabilities.Error) - } -} - -func (cv *ContainerValidation) validateCapabilities(warningLists *config.SecurityCapabilityLists, errorLists *config.SecurityCapabilityLists) { - category := messages.CategorySecurity - capabilities := &corev1.Capabilities{} - if cv.Container.SecurityContext != nil && cv.Container.SecurityContext.Capabilities != nil { - capabilities = cv.Container.SecurityContext.Capabilities - } - allLists := []*config.SecurityCapabilityLists{warningLists, errorLists} - - addID := "capabilitiesAdded" - hasAddFailure := false - hasAddCheck := false - for _, confLists := range allLists { - if len(confLists.IfAnyAdded) == 0 && len(confLists.IfAnyAddedBeyond) == 0 { - continue - } - hasAddCheck = true - var severity config.Severity - if confLists == warningLists { - severity = config.SeverityWarning - } else { - severity = config.SeverityError - } - badAdds := make([]corev1.Capability, 0) - if len(confLists.IfAnyAdded) > 0 { - intersectAdds := capIntersection(capabilities.Add, confLists.IfAnyAdded) - badAdds = append(badAdds, intersectAdds...) - } - if len(confLists.IfAnyAddedBeyond) > 0 { - differentAdds := capDifference(capabilities.Add, confLists.IfAnyAddedBeyond) - differentAdds = capDifference(differentAdds, badAdds) - badAdds = append(badAdds, differentAdds...) - } - if capContains(capabilities.Add, "ALL") && !capContains(badAdds, "ALL") { - badAdds = append(badAdds, "ALL") - } - if len(badAdds) > 0 { - hasAddFailure = true - capsString := commaSeparatedCapabilities(badAdds) - cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity, category, addID) - } - } - if hasAddCheck && !hasAddFailure { - cv.addSuccess(messages.SecurityCapabilitiesAddedSuccess, category, addID) - } - - dropID := "capabilitiesDropped" - hasDropCheck := false - hasDropFailure := false - for _, confLists := range allLists { - if len(confLists.IfAnyNotDropped) == 0 { - continue - } - hasDropCheck = true - var severity config.Severity - if confLists == warningLists { - severity = config.SeverityWarning - } else { - severity = config.SeverityError - } - missingDrops := capDifference(confLists.IfAnyNotDropped, capabilities.Drop) - id := "capabilitiesNotDropped" - if len(missingDrops) > 0 && !capContains(capabilities.Drop, "ALL") { - hasDropFailure = true - capsString := commaSeparatedCapabilities(missingDrops) - cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesNotDroppedFailure, capsString), severity, category, id) - } - } - if hasDropCheck && !hasDropFailure { - cv.addSuccess(messages.SecurityCapabilitiesNotDroppedSuccess, category, dropID) - } -} - -func commaSeparatedCapabilities(caps []corev1.Capability) string { - capsString := "" - for _, cap := range caps { - capsString = fmt.Sprintf("%s, %s", capsString, cap) - } - return capsString[2:] -} - -func capIntersection(a, b []corev1.Capability) []corev1.Capability { - result := []corev1.Capability{} - hash := map[corev1.Capability]bool{} - - for _, s := range a { - hash[s] = true - } - - for _, s := range b { - if hash[s] { - result = append(result, s) - } - } - - return result -} - -func capDifference(b, a []corev1.Capability) []corev1.Capability { - result := []corev1.Capability{} - hash := map[corev1.Capability]bool{} - - for _, s := range a { - hash[s] = true - } - - for _, s := range b { - if !hash[s] { - result = append(result, s) - } - } - - return result -} - -func capContains(list []corev1.Capability, val corev1.Capability) bool { - for _, s := range list { - if s == val { - return true - } - } - - return false -} diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index 20ff2c17..927aa5f5 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -671,29 +671,16 @@ func TestValidateSecurity(t *testing.T) { RunAsPrivileged: conf.SeverityError, NotReadOnlyRootFileSystem: conf.SeverityWarning, PrivilegeEscalationAllowed: conf.SeverityError, - Capabilities: conf.SecurityCapabilities{ - Error: conf.SecurityCapabilityLists{ - IfAnyAdded: []corev1.Capability{"ALL", "SYS_ADMIN", "NET_ADMIN"}, - }, - Warning: conf.SecurityCapabilityLists{ - IfAnyAddedBeyond: []corev1.Capability{"NONE"}, - }, - }, + DangerousCapabilities: conf.SeverityError, + InsecureCapabilities: conf.SeverityWarning, } strongConf := conf.Security{ RunAsRootAllowed: conf.SeverityError, RunAsPrivileged: conf.SeverityError, NotReadOnlyRootFileSystem: conf.SeverityError, PrivilegeEscalationAllowed: conf.SeverityError, - Capabilities: conf.SecurityCapabilities{ - Error: conf.SecurityCapabilityLists{ - IfAnyAdded: []corev1.Capability{"ALL", "SYS_ADMIN", "NET_ADMIN"}, - IfAnyNotDropped: []corev1.Capability{"NET_BIND_SERVICE", "DAC_OVERRIDE", "SYS_CHROOT"}, - }, - Warning: conf.SecurityCapabilityLists{ - IfAnyAddedBeyond: []corev1.Capability{"NONE"}, - }, - }, + DangerousCapabilities: conf.SeverityError, + InsecureCapabilities: conf.SeverityError, } emptyCV := ContainerValidation{ @@ -708,7 +695,7 @@ func TestValidateSecurity(t *testing.T) { Privileged: &trueVar, AllowPrivilegeEscalation: &trueVar, Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"}, + Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"}, }, }}, ResourceValidation: &ResourceValidation{}, @@ -721,7 +708,7 @@ func TestValidateSecurity(t *testing.T) { Privileged: &trueVar, AllowPrivilegeEscalation: &trueVar, Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"}, + Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"}, }, }}, ResourceValidation: &ResourceValidation{}, @@ -739,7 +726,7 @@ func TestValidateSecurity(t *testing.T) { Privileged: &trueVar, AllowPrivilegeEscalation: &trueVar, Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"}, + Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"}, }, }}, ResourceValidation: &ResourceValidation{}, @@ -849,8 +836,13 @@ func TestValidateSecurity(t *testing.T) { Type: "success", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "Disallowed security capabilities have not been added", + ID: "insecureCapabilities", + Message: "Container does not have any insecure capabilities", + Type: "success", + Category: "Security", + }, { + ID: "dangerousCapabilities", + Message: "Container does not have any dangerous capabilities", Type: "success", Category: "Security", }}, @@ -860,8 +852,8 @@ func TestValidateSecurity(t *testing.T) { securityConf: standardConf, cv: badCV, expectedMessages: []*ResultMessage{{ - ID: "capabilitiesAdded", - Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN", + ID: "dangerousCapabilities", + Message: "Container should not have dangerous capabilities", Type: "error", Category: "Security", }, { @@ -875,8 +867,8 @@ func TestValidateSecurity(t *testing.T) { Type: "error", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN", + ID: "insecureCapabilities", + Message: "Container should not have insecure capabilities", Type: "warning", Category: "Security", }, { @@ -896,8 +888,8 @@ func TestValidateSecurity(t *testing.T) { securityConf: standardConf, cv: badCVWithGoodPodSpec, expectedMessages: []*ResultMessage{{ - ID: "capabilitiesAdded", - Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN", + ID: "dangerousCapabilities", + Message: "Container should not have dangerous capabilities", Type: "error", Category: "Security", }, { @@ -911,8 +903,8 @@ func TestValidateSecurity(t *testing.T) { Type: "error", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN", + ID: "insecureCapabilities", + Message: "Container should not have insecure capabilities", Type: "warning", Category: "Security", }, { @@ -932,10 +924,15 @@ func TestValidateSecurity(t *testing.T) { securityConf: standardConf, cv: badCVWithBadPodSpec, expectedMessages: []*ResultMessage{{ - ID: "capabilitiesAdded", - Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN", + ID: "dangerousCapabilities", + Message: "Container should not have dangerous capabilities", Type: "error", Category: "Security", + }, { + ID: "insecureCapabilities", + Message: "Container should not have insecure capabilities", + Type: "warning", + Category: "Security", }, { ID: "privilegeEscalationAllowed", Message: "Privilege escalation should not be allowed", @@ -946,11 +943,6 @@ func TestValidateSecurity(t *testing.T) { Message: "Should not be running as privileged", Type: "error", Category: "Security", - }, { - ID: "capabilitiesAdded", - Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN", - Type: "warning", - Category: "Security", }, { ID: "runAsRootAllowed", Message: "Should not be allowed to run as root", @@ -988,8 +980,13 @@ func TestValidateSecurity(t *testing.T) { Type: "success", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "Disallowed security capabilities have not been added", + ID: "dangerousCapabilities", + Message: "Container does not have any dangerous capabilities", + Type: "success", + Category: "Security", + }, { + ID: "insecureCapabilities", + Message: "Container does not have any insecure capabilities", Type: "success", Category: "Security", }}, @@ -999,13 +996,13 @@ func TestValidateSecurity(t *testing.T) { securityConf: strongConf, cv: goodCV, expectedMessages: []*ResultMessage{{ - ID: "capabilitiesNotDropped", - Message: "The following security capabilities should be dropped: DAC_OVERRIDE, SYS_CHROOT", - Type: "error", + ID: "dangerousCapabilities", + Message: "Container does not have any dangerous capabilities", + Type: "success", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "Disallowed security capabilities have not been added", + ID: "insecureCapabilities", + Message: "Container does not have any insecure capabilities", Type: "success", Category: "Security", }, { @@ -1055,13 +1052,13 @@ func TestValidateSecurity(t *testing.T) { Type: "success", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "Disallowed security capabilities have not been added", + ID: "dangerousCapabilities", + Message: "Container does not have any dangerous capabilities", Type: "success", Category: "Security", }, { - ID: "capabilitiesDropped", - Message: "All disallowed security capabilities have been dropped", + ID: "insecureCapabilities", + Message: "Container does not have any insecure capabilities", Type: "success", Category: "Security", }}, @@ -1091,13 +1088,13 @@ func TestValidateSecurity(t *testing.T) { Type: "success", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "Disallowed security capabilities have not been added", + ID: "dangerousCapabilities", + Message: "Container does not have any dangerous capabilities", Type: "success", Category: "Security", }, { - ID: "capabilitiesDropped", - Message: "All disallowed security capabilities have been dropped", + ID: "insecureCapabilities", + Message: "Container does not have any insecure capabilities", Type: "success", Category: "Security", }}, @@ -1127,13 +1124,13 @@ func TestValidateSecurity(t *testing.T) { Type: "success", Category: "Security", }, { - ID: "capabilitiesAdded", - Message: "Disallowed security capabilities have not been added", + ID: "dangerousCapabilities", + Message: "Container does not have any dangerous capabilities", Type: "success", Category: "Security", }, { - ID: "capabilitiesDropped", - Message: "All disallowed security capabilities have been dropped", + ID: "insecureCapabilities", + Message: "Container does not have any insecure capabilities", Type: "success", Category: "Security", }}, @@ -1147,9 +1144,8 @@ func TestValidateSecurity(t *testing.T) { if err != nil { panic(err) } - tt.cv.validateSecurity(&conf.Configuration{Security: tt.securityConf}, "") assert.Len(t, tt.cv.messages(), len(tt.expectedMessages)) - assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages) + assert.ElementsMatch(t, tt.expectedMessages, tt.cv.messages()) }) } } diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go index f6762381..8b9a045b 100644 --- a/pkg/validator/schema.go +++ b/pkg/validator/schema.go @@ -63,6 +63,8 @@ var ( "runAsPrivileged", "notReadOnlyRootFileSystem", "privilegeEscalationAllowed", + "dangerousCapabilities", + "insecureCapabilities", } ) From 5efa416ea9d5fe7a39ad6dd487709e54890a330a Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Fri, 27 Dec 2019 19:42:49 +0000 Subject: [PATCH 02/14] implement custom checks, implement resource ranges as custom check --- checks/cpuLimitsMissing.yaml | 29 +++++ checks/cpuRequestsMissing.yaml | 29 +++++ checks/memoryLimitsMissing.yaml | 29 +++++ checks/memoryRequestsMissing.yaml | 29 +++++ examples/config-full.yaml | 46 +------ pkg/config/config.go | 52 +++----- pkg/config/config_test.go | 129 +++---------------- pkg/config/exemptions.go | 6 - pkg/config/schema.go | 164 +++++++++++++++++++++++ pkg/validator/container.go | 85 ------------ pkg/validator/container_test.go | 207 ++++++++++++++---------------- pkg/validator/schema.go | 146 +++++++-------------- 12 files changed, 464 insertions(+), 487 deletions(-) create mode 100644 checks/cpuLimitsMissing.yaml create mode 100644 checks/cpuRequestsMissing.yaml create mode 100644 checks/memoryLimitsMissing.yaml create mode 100644 checks/memoryRequestsMissing.yaml create mode 100644 pkg/config/schema.go diff --git a/checks/cpuLimitsMissing.yaml b/checks/cpuLimitsMissing.yaml new file mode 100644 index 00000000..478c80a0 --- /dev/null +++ b/checks/cpuLimitsMissing.yaml @@ -0,0 +1,29 @@ +name: CPULimitsMissing +id: cpuLimitsMissing +successMessage: CPU limits are set +failureMessage: CPU limits should be set +category: Resources +target: Container +containers: + exclude: + - initContainer +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - limits + properties: + limits: + type: object + required: + - cpu + properties: + cpu: + type: string + not: + const: '' diff --git a/checks/cpuRequestsMissing.yaml b/checks/cpuRequestsMissing.yaml new file mode 100644 index 00000000..1c3a445c --- /dev/null +++ b/checks/cpuRequestsMissing.yaml @@ -0,0 +1,29 @@ +name: CPURequestsMissing +id: cpuRequestsMissing +successMessage: CPU requests are set +failureMessage: CPU requests should be set +category: Resources +target: Container +containers: + exclude: + - initContainer +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - requests + properties: + requests: + type: object + required: + - cpu + properties: + cpu: + type: string + not: + const: '' diff --git a/checks/memoryLimitsMissing.yaml b/checks/memoryLimitsMissing.yaml new file mode 100644 index 00000000..e61926bf --- /dev/null +++ b/checks/memoryLimitsMissing.yaml @@ -0,0 +1,29 @@ +name: MemoryLimitsMissing +id: memoryLimitsMissing +successMessage: Memory limits are set +failureMessage: Memory limits should be set +category: Resources +target: Container +containers: + exclude: + - initContainer +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - limits + properties: + limits: + type: object + required: + - memory + properties: + memory: + type: string + not: + const: '' diff --git a/checks/memoryRequestsMissing.yaml b/checks/memoryRequestsMissing.yaml new file mode 100644 index 00000000..cfd339a9 --- /dev/null +++ b/checks/memoryRequestsMissing.yaml @@ -0,0 +1,29 @@ +name: MemoryRequestsMissing +id: memoryRequestsMissing +successMessage: Memory requests are set +failureMessage: Memory requests should be set +category: Resources +target: Container +containers: + exclude: + - initContainer +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - requests + properties: + requests: + type: object + required: + - memory + properties: + memory: + type: string + not: + const: '' diff --git a/examples/config-full.yaml b/examples/config-full.yaml index 011fe15b..eb370b85 100644 --- a/examples/config-full.yaml +++ b/examples/config-full.yaml @@ -1,45 +1,11 @@ resources: cpuRequestsMissing: warning - cpuRequestRanges: - warning: - below: 50m - above: 1000m - error: - below: 500m - above: 2000m cpuLimitsMissing: warning - cpuLimitRanges: - warning: - below: 50m - above: 1000m - error: - below: 500m - above: 2000m memoryRequestsMissing: warning - memoryRequestRanges: - warning: - below: 50M - above: 2G - error: - below: 100M - above: 4G memoryLimitsMissing: warning - memoryLimitRanges: - warning: - below: 50M - above: 2G - error: - below: 100M - above: 4G images: tagNotSpecified: error pullPolicyNotAlways: warning - whitelist: - error: - - gcr.io/* - blacklist: - warning: - - docker.io/* healthChecks: readinessProbeMissing: warning livenessProbeMissing: warning @@ -53,16 +19,8 @@ security: runAsPrivileged: error notReadOnlyRootFileSystem: warning privilegeEscalationAllowed: error - capabilities: - error: - ifAnyAdded: - - SYS_ADMIN - - ALL - ifAnyNotDropped: - - ALL - warning: - ifAnyAddedBeyond: - - NONE + dangerousCapabilities: error + insecureCapabilities: warning controllers_to_scan: - Deployments - StatefulSets diff --git a/pkg/config/config.go b/pkg/config/config.go index 351f1277..5aa75381 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -23,21 +23,22 @@ import ( "strings" packr "github.com/gobuffalo/packr/v2" - "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/yaml" ) // Configuration contains all of the config for the validation checks. type Configuration struct { - DisplayName string `json:"displayName"` - Resources Resources `json:"resources"` - HealthChecks HealthChecks `json:"healthChecks"` - Images Images `json:"images"` - Networking Networking `json:"networking"` - Security Security `json:"security"` - ControllersToScan []SupportedController `json:"controllers_to_scan"` - Exemptions []Exemption `json:"exemptions"` - DisallowExemptions bool `json:"disallowExemptions"` + DisplayName string `json:"displayName"` + Resources Resources `json:"resources"` + HealthChecks HealthChecks `json:"healthChecks"` + Images Images `json:"images"` + Networking Networking `json:"networking"` + Security Security `json:"security"` + Checks map[string]Severity `json:"checks"` + ControllersToScan []SupportedController `json:"controllers_to_scan"` + CustomChecks map[string]SchemaCheck `json:"customChecks"` + Exemptions []Exemption `json:"exemptions"` + DisallowExemptions bool `json:"disallowExemptions"` } // Exemption represents an exemption to normal rules @@ -48,26 +49,10 @@ type Exemption struct { // Resources contains config for resource requests and limits. type Resources struct { - CPURequestsMissing Severity `json:"cpuRequestsMissing"` - CPURequestRanges ResourceRanges `json:"cpuRequestRanges"` - CPULimitsMissing Severity `json:"cpuLimitsMissing"` - CPULimitRanges ResourceRanges `json:"cpuLimitRanges"` - MemoryRequestsMissing Severity `json:"memoryRequestsMissing"` - MemoryRequestRanges ResourceRanges `json:"memoryRequestRanges"` - MemoryLimitsMissing Severity `json:"memoryLimitsMissing"` - MemoryLimitRanges ResourceRanges `json:"memoryLimitRanges"` -} - -// ResourceRanges contains config for requests or limits for a specific resource. -type ResourceRanges struct { - Warning ResourceRange `json:"warning"` - Error ResourceRange `json:"error"` -} - -// ResourceRange can contain below and above conditions for validation. -type ResourceRange struct { - Below *resource.Quantity `json:"below"` - Above *resource.Quantity `json:"above"` + CPURequestsMissing Severity `json:"cpuRequestsMissing"` + CPULimitsMissing Severity `json:"cpuLimitsMissing"` + MemoryRequestsMissing Severity `json:"memoryRequestsMissing"` + MemoryLimitsMissing Severity `json:"memoryLimitsMissing"` } // HealthChecks contains config for readiness and liveness probes. @@ -140,9 +125,14 @@ func Parse(rawBytes []byte) (Configuration, error) { for { if err := d.Decode(&conf); err != nil { if err == io.EOF { - return conf, nil + break } return conf, fmt.Errorf("Decoding config failed: %v", err) } } + for key, check := range conf.CustomChecks { + check.ID = key + conf.CustomChecks[key] = check + } + return conf, nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index ba15b2f1..1677fed7 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -24,111 +24,41 @@ import ( "time" "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/api/resource" ) -var resourceConfInvalid1 = `test` +var confInvalid = `test` -var resourceConfYAML1 = `--- +var confValidYAML = ` resources: - cpuRequestRanges: - error: - below: 100m - above: 1 - warning: - below: 200m - above: 800m - memoryRequestRanges: - error: - below: 100M - above: 3G - warning: - below: 200M - above: 2G - cpuLimitRanges: - error: - below: 100m - above: 2 - warning: - below: 300m - above: 1800m - memoryLimitRanges: - error: - below: 200M - above: 6G - warning: - below: 300M - above: 4G + cpuRequestsMissing: warning controllers_to_scan: - Deployments - - StatefulSets - - Jobs - - CronJobs - - DaemonSets - - ReplicationControllers ` -var resourceConfJSON1 = `{ - "resources": { - "cpuRequestRanges": { - "error": { - "below": "100m", - "above": 1 - }, - "warning": { - "below": "200m", - "above": "800m" - } - }, - "memoryRequestRanges": { - "error": { - "below": "100M", - "above": "3G" - }, - "warning": { - "below": "200M", - "above": "2G" - } - }, - "cpuLimitRanges": { - "error": { - "below": "100m", - "above": 2 - }, - "warning": { - "below": "300m", - "above": "1800m" - } - }, - "memoryLimitRanges": { - "error": { - "below": "200M", - "above": "6G" - }, - "warning": { - "below": "300M", - "above": "4G" - } - } - }, - "controllers_to_scan": ["Deployments", "StatefulSets", "Jobs", "CronJobs", "DaemonSets", "ReplicationControllers"] -}` +var confValidJSON = ` +{ + "resources": { + "cpuRequestsMissing": "warning" + }, + "controllers_to_scan": ["Deployments"] +} +` func TestParseError(t *testing.T) { - _, err := Parse([]byte(resourceConfInvalid1)) + _, err := Parse([]byte(confInvalid)) expectedErr := "Decoding config failed: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type config.Configuration" assert.EqualError(t, err, expectedErr) } func TestParseYaml(t *testing.T) { - parsedConf, err := Parse([]byte(resourceConfYAML1)) + parsedConf, err := Parse([]byte(confValidYAML)) assert.NoError(t, err, "Expected no error when parsing YAML config") testParsedConfig(t, &parsedConf) } func TestParseJson(t *testing.T) { - parsedConf, err := Parse([]byte(resourceConfJSON1)) + parsedConf, err := Parse([]byte(confValidJSON)) assert.NoError(t, err, "Expected no error when parsing JSON config") testParsedConfig(t, &parsedConf) @@ -139,7 +69,7 @@ func TestConfigFromURL(t *testing.T) { var parsedConf Configuration srv := &http.Server{Addr: ":8081"} http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - io.WriteString(w, resourceConfYAML1) + io.WriteString(w, confValidYAML) }) go func() { @@ -166,30 +96,7 @@ func TestConfigNoServerError(t *testing.T) { } func testParsedConfig(t *testing.T, config *Configuration) { - cpuRequests := config.Resources.CPURequestRanges - assert.Equal(t, int64(100), cpuRequests.Error.Below.ScaledValue(resource.Milli)) - assert.Equal(t, int64(1000), cpuRequests.Error.Above.ScaledValue(resource.Milli)) - assert.Equal(t, int64(200), cpuRequests.Warning.Below.ScaledValue(resource.Milli)) - assert.Equal(t, int64(800), cpuRequests.Warning.Above.ScaledValue(resource.Milli)) - - memRequests := config.Resources.MemoryRequestRanges - assert.Equal(t, int64(100), memRequests.Error.Below.ScaledValue(resource.Mega)) - assert.Equal(t, int64(3000), memRequests.Error.Above.ScaledValue(resource.Mega)) - assert.Equal(t, int64(200), memRequests.Warning.Below.ScaledValue(resource.Mega)) - assert.Equal(t, int64(2000), memRequests.Warning.Above.ScaledValue(resource.Mega)) - - cpuLimits := config.Resources.CPULimitRanges - assert.Equal(t, int64(100), cpuLimits.Error.Below.ScaledValue(resource.Milli)) - assert.Equal(t, int64(2000), cpuLimits.Error.Above.ScaledValue(resource.Milli)) - assert.Equal(t, int64(300), cpuLimits.Warning.Below.ScaledValue(resource.Milli)) - assert.Equal(t, int64(1800), cpuLimits.Warning.Above.ScaledValue(resource.Milli)) - - memLimits := config.Resources.MemoryLimitRanges - assert.Equal(t, int64(200), memLimits.Error.Below.ScaledValue(resource.Mega)) - assert.Equal(t, int64(6000), memLimits.Error.Above.ScaledValue(resource.Mega)) - assert.Equal(t, int64(300), memLimits.Warning.Below.ScaledValue(resource.Mega)) - assert.Equal(t, int64(4000), memLimits.Warning.Above.ScaledValue(resource.Mega)) - - controllersToScan := config.ControllersToScan - assert.ElementsMatch(t, []SupportedController{Deployments, StatefulSets, Jobs, CronJobs, DaemonSets, ReplicationControllers}, controllersToScan) + assert.Equal(t, SeverityWarning, config.Resources.CPURequestsMissing) + assert.Equal(t, Severity(""), config.Resources.CPULimitsMissing) + assert.ElementsMatch(t, []SupportedController{Deployments}, config.ControllersToScan) } diff --git a/pkg/config/exemptions.go b/pkg/config/exemptions.go index 03f518ca..2da8778d 100644 --- a/pkg/config/exemptions.go +++ b/pkg/config/exemptions.go @@ -16,12 +16,6 @@ func (conf Configuration) IsActionable(subConf interface{}, ruleName, controller 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 } diff --git a/pkg/config/schema.go b/pkg/config/schema.go new file mode 100644 index 00000000..f072d88b --- /dev/null +++ b/pkg/config/schema.go @@ -0,0 +1,164 @@ +package config + +import ( + "encoding/json" + "fmt" + + "github.com/qri-io/jsonschema" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +type resourceMinimum string +type resourceMaximum string + +func newResourceMinimum() jsonschema.Validator { + return new(resourceMinimum) +} + +func newResourceMaximum() jsonschema.Validator { + return new(resourceMaximum) +} + +func (min resourceMinimum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) { + err := validateRange(path, string(min), data, true) + if err != nil { + *errs = append(*errs, *err...) + } +} + +func (max resourceMaximum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) { + err := validateRange(path, string(max), data, false) + if err != nil { + *errs = append(*errs, *err...) + } +} + +func parseQuantity(i interface{}) (resource.Quantity, *[]jsonschema.ValError) { + resStr, ok := i.(string) + if !ok { + return resource.Quantity{}, &[]jsonschema.ValError{ + {Message: fmt.Sprintf("Resource quantity %v is not a string", i)}, + } + } + q, err := resource.ParseQuantity(resStr) + if err != nil { + return resource.Quantity{}, &[]jsonschema.ValError{ + {Message: fmt.Sprintf("Could not parse resource quantity: %s", resStr)}, + } + } + return q, nil +} + +func validateRange(path string, limit interface{}, data interface{}, isMinimum bool) *[]jsonschema.ValError { + limitQuantity, err := parseQuantity(limit) + if err != nil { + return err + } + actualQuantity, err := parseQuantity(data) + if err != nil { + return err + } + cmp := limitQuantity.Cmp(actualQuantity) + if isMinimum { + if cmp == 1 { + return &[]jsonschema.ValError{ + {Message: fmt.Sprintf("%s quantity %v is > %v", path, actualQuantity, limitQuantity)}, + } + } + } else { + if cmp == -1 { + return &[]jsonschema.ValError{ + {Message: fmt.Sprintf("%s quantity %v is < %v", path, actualQuantity, limitQuantity)}, + } + } + } + return nil +} + +func init() { + jsonschema.RegisterValidator("resourceMinimum", newResourceMinimum) + jsonschema.RegisterValidator("resourceMaximum", newResourceMaximum) +} + +type includeExcludeList struct { + Include []string `yaml:"include"` + Exclude []string `yaml:"exclude"` +} + +type TargetKind string + +const ( + TargetContainer TargetKind = "Container" + TargetPod TargetKind = "Pod" +) + +// SchemaCheck is a Polaris check that runs using JSON Schema +type SchemaCheck struct { + Name string `yaml:"name"` + ID string `yaml:"id"` + Category string `yaml:"category"` + SuccessMessage string `yaml:"successMessage"` + FailureMessage string `yaml:"failureMessage"` + Controllers includeExcludeList `yaml:"controllers"` + Containers includeExcludeList `yaml:"containers"` + Target TargetKind `yaml:"target"` + SchemaTarget TargetKind `yaml:"schemaTarget"` + Schema jsonschema.RootSchema `yaml:"schema"` +} + +func (check SchemaCheck) CheckPod(pod *corev1.PodSpec) (bool, error) { + return check.CheckObject(pod) +} + +func (check SchemaCheck) CheckContainer(container *corev1.Container) (bool, error) { + return check.CheckObject(container) +} + +func (check SchemaCheck) CheckObject(obj interface{}) (bool, error) { + bytes, err := json.Marshal(obj) + if err != nil { + return false, err + } + errs, err := check.Schema.ValidateBytes(bytes) + return len(errs) == 0, err +} + +func (check SchemaCheck) IsActionable(target TargetKind, controllerType SupportedController, isInit bool) bool { + if check.Target != target { + return false + } + isIncluded := len(check.Controllers.Include) == 0 + for _, inclusion := range check.Controllers.Include { + if GetSupportedControllerFromString(inclusion) == controllerType { + isIncluded = true + break + } + } + if !isIncluded { + return false + } + for _, exclusion := range check.Controllers.Exclude { + if GetSupportedControllerFromString(exclusion) == controllerType { + return false + } + } + if check.Target == TargetContainer { + isIncluded := len(check.Containers.Include) == 0 + for _, inclusion := range check.Containers.Include { + if (inclusion == "initContainer" && isInit) || (inclusion == "container" && !isInit) { + isIncluded = true + break + } + } + if !isIncluded { + return false + } + for _, exclusion := range check.Containers.Exclude { + if (exclusion == "initContainer" && isInit) || (exclusion == "container" && !isInit) { + return false + } + } + } + return true +} diff --git a/pkg/validator/container.go b/pkg/validator/container.go index 81352684..0926cf71 100644 --- a/pkg/validator/container.go +++ b/pkg/validator/container.go @@ -15,12 +15,8 @@ package validator import ( - "fmt" - "github.com/fairwindsops/polaris/pkg/config" - "github.com/fairwindsops/polaris/pkg/validator/messages" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" ) // ContainerValidation tracks validation failures associated with a Container. @@ -56,8 +52,6 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, cv.parentPodSpec = parentPodResult.podSpec } - cv.validateResources(conf, controllerName) - err := applyContainerSchemaChecks(conf, controllerName, controllerType, &cv) // FIXME: don't panic if err != nil { @@ -72,82 +66,3 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, return cRes } - -func (cv *ContainerValidation) validateResources(conf *config.Configuration, controllerName string) { - // Only validate resources for primary containers. Although it can - // be helpful to set these in certain cases, it usually isn't - if cv.IsInitContainer { - return - } - - category := messages.CategoryResources - res := cv.Container.Resources - - missingName := "CPURequestsMissing" - rangeName := "CPURequestRanges" - id := config.GetIDFromField(conf.Resources, missingName) - if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Requests.Cpu().MilliValue() == 0 { - 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 { - 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 { - 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 { - 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) - } -} - -func (cv *ContainerValidation) validateResourceRange(id, resourceName string, rangeConf *config.ResourceRanges, res *resource.Quantity) { - warnAbove := rangeConf.Warning.Above - 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()), category, id) - } else if warnAbove != nil && warnAbove.MilliValue() < res.MilliValue() { - cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, warnAbove.String()), category, id) - } else if errorBelow != nil && errorBelow.MilliValue() > res.MilliValue() { - cv.addError(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, errorBelow.String()), category, id) - } else if warnBelow != nil && warnBelow.MilliValue() > res.MilliValue() { - cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, warnBelow.String()), category, id) - } else if errorAbove != nil && warnAbove != nil && errorBelow != nil && warnBelow != nil { - cv.addSuccess(fmt.Sprintf(messages.ResourceAmountSuccess, resourceName), category, id) - } -} diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index 927aa5f5..4c958754 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -24,39 +24,7 @@ import ( "k8s.io/apimachinery/pkg/api/resource" ) -var resourceConf1 = `--- -resources: - cpuRequestRanges: - error: - below: 100m - above: 1 - warning: - below: 200m - above: 800m - memoryRequestRanges: - error: - below: 100M - above: 3G - warning: - below: 200M - above: 2G - cpuLimitRanges: - error: - below: 100m - above: 2 - warning: - below: 300m - above: 1800m - memoryLimitRanges: - error: - below: 200M - above: 6G - warning: - below: 300M - above: 4G -` - -var resourceConf2 = `--- +var resourceConfMinimal = `--- resources: cpuRequestsMissing: warning memoryRequestsMissing: warning @@ -80,44 +48,66 @@ exemptions: - foo ` -var resourceConfRangeExemptions = `--- -resources: - cpuRequestRanges: - error: - below: 100m - above: 1 - warning: - below: 200m - above: 800m - memoryRequestRanges: - error: - below: 100M - above: 3G - warning: - below: 200M - above: 2G - cpuLimitRanges: - error: - below: 100m - above: 2 - warning: - below: 300m - above: 1800m - memoryLimitRanges: - error: - below: 200M - above: 6G - warning: - below: 300M - above: 4G -exemptions: - - rules: - - cpuRequestRanges - - memoryRequestRanges - - cpuLimitRanges - - memoryLimitRanges - controllerNames: - - foo +var resourceConfRanges = ` +checks: + memoryRequestsRange: error + memoryLimitsRange: warning +customChecks: + memoryLimitsRange: + containers: + exclude: + - initContainer + successMessage: Memory limits are within the required range + failureMessage: Memory limits should be within the required range + category: Resources + target: Container + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - limits + properties: + limits: + type: object + required: + - memory + properties: + memory: + type: string + resourceMinimum: 200M + resourceMaximum: 6G + memoryRequestsRange: + successMessage: Memory requests are within the required range + failureMessage: Memory requests should be within the required range + category: Resources + target: Container + containers: + exclude: + - initContainer + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - requests + properties: + requests: + required: + - memory + properties: + memory: + type: string + resourceMinimum: 200M + resourceMaximum: 3G ` func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []*ResultMessage, expectedWarnings []*ResultMessage, expectedSuccesses []*ResultMessage) { @@ -129,7 +119,10 @@ func testValidateResources(t *testing.T, container *corev1.Container, resourceCo parsedConf, err := conf.Parse([]byte(*resourceConf)) assert.NoError(t, err, "Expected no error when parsing config") - cv.validateResources(&parsedConf, controllerName) + err = applyContainerSchemaChecks(&parsedConf, controllerName, conf.Deployments, &cv) + if err != nil { + panic(err) + } assert.Len(t, cv.Warnings, len(expectedWarnings)) assert.ElementsMatch(t, expectedWarnings, cv.Warnings) @@ -151,7 +144,10 @@ func TestValidateResourcesEmptyConfig(t *testing.T) { ResourceValidation: &ResourceValidation{}, } - cv.validateResources(&conf.Configuration{}, "") + err := applyContainerSchemaChecks(&conf.Configuration{}, "", conf.Deployments, &cv) + if err != nil { + panic(err) + } assert.Len(t, cv.Errors, 0) } @@ -192,7 +188,7 @@ func TestValidateResourcesEmptyContainer(t *testing.T) { expectedSuccesses := []*ResultMessage{} - testValidateResources(t, &container, &resourceConf2, "foo", expectedErrors, expectedWarnings, expectedSuccesses) + testValidateResources(t, &container, &resourceConfMinimal, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } func TestValidateResourcesPartiallyValid(t *testing.T) { @@ -216,37 +212,25 @@ func TestValidateResourcesPartiallyValid(t *testing.T) { expectedWarnings := []*ResultMessage{ { - ID: "cpuRequestRanges", + ID: "memoryLimitsRange", Type: "warning", - Message: "CPU requests should be higher than 200m", - Category: "Resources", - }, - { - ID: "cpuLimitRanges", - Type: "warning", - Message: "CPU limits should be higher than 300m", + Message: "Memory limits should be within the required range", Category: "Resources", }, } expectedErrors := []*ResultMessage{ { - ID: "memoryRequestRanges", + ID: "memoryRequestsRange", Type: "error", - Message: "Memory requests should be higher than 100M", - Category: "Resources", - }, - { - ID: "memoryLimitRanges", - Type: "error", - Message: "Memory limits should be higher than 200M", + Message: "Memory requests should be within the required range", Category: "Resources", }, } expectedSuccesses := []*ResultMessage{} - testValidateResources(t, &container, &resourceConf1, "foo", expectedErrors, expectedWarnings, expectedSuccesses) + testValidateResources(t, &container, &resourceConfRanges, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } func TestValidateResourcesInit(t *testing.T) { @@ -260,13 +244,20 @@ func TestValidateResourcesInit(t *testing.T) { IsInitContainer: true, } - parsedConf, err := conf.Parse([]byte(resourceConf1)) + parsedConf, err := conf.Parse([]byte(resourceConfRanges)) assert.NoError(t, err, "Expected no error when parsing config") - cvEmpty.validateResources(&parsedConf, "") - assert.Len(t, cvEmpty.Errors, 4) + err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvEmpty) + if err != nil { + panic(err) + } + assert.Len(t, cvEmpty.Errors, 1) + assert.Len(t, cvEmpty.Warnings, 1) - cvInit.validateResources(&parsedConf, "") + err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvInit) + if err != nil { + panic(err) + } assert.Len(t, cvInit.Errors, 0) } @@ -299,32 +290,20 @@ func TestValidateResourcesFullyValid(t *testing.T) { expectedSuccesses := []*ResultMessage{ { - ID: "cpuRequestRanges", + ID: "memoryRequestsRange", Type: "success", - Message: "CPU requests are within the expected range", + Message: "Memory requests are within the required range", Category: "Resources", }, { - ID: "memoryRequestRanges", + ID: "memoryLimitsRange", 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", + Message: "Memory limits are within the required range", Category: "Resources", }, } - testValidateResources(t, &container, &resourceConf1, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) + testValidateResources(t, &container, &resourceConfRanges, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) expectedSuccesses = []*ResultMessage{ { @@ -353,7 +332,7 @@ func TestValidateResourcesFullyValid(t *testing.T) { }, } - testValidateResources(t, &container, &resourceConf2, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) + testValidateResources(t, &container, &resourceConfMinimal, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) } func TestValidateHealthChecks(t *testing.T) { @@ -1339,6 +1318,7 @@ func TestValidateResourcesExemption(t *testing.T) { testValidateResources(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } +/* func TestValidateResourceRangeExemption(t *testing.T) { container := corev1.Container{ Name: "Empty", @@ -1350,6 +1330,7 @@ func TestValidateResourceRangeExemption(t *testing.T) { testValidateResources(t, &container, &resourceConfRangeExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } +*/ func resetCV(cv ContainerValidation) ContainerValidation { cv.Errors = []*ResultMessage{} diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go index 8b9a045b..e02341ea 100644 --- a/pkg/validator/schema.go +++ b/pkg/validator/schema.go @@ -2,49 +2,21 @@ package validator import ( "bytes" - "encoding/json" "fmt" "io" packr "github.com/gobuffalo/packr/v2" - "github.com/qri-io/jsonschema" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/yaml" "github.com/fairwindsops/polaris/pkg/config" ) -type includeExcludeList struct { - Include []string `yaml:"include"` - Exclude []string `yaml:"exclude"` -} - -type target string - -const ( - targetContainer target = "Container" - targetPod target = "Pod" -) - -// SchemaCheck is a Polaris check that runs using JSON Schema -type SchemaCheck struct { - Name string `yaml:"name"` - ID string `yaml:"id"` - Category string `yaml:"category"` - SuccessMessage string `yaml:"successMessage"` - FailureMessage string `yaml:"failureMessage"` - Controllers includeExcludeList `yaml:"controllers"` - Containers includeExcludeList `yaml:"containers"` - Target target `yaml:"target"` - SchemaTarget target `yaml:"schemaTarget"` - Schema jsonschema.RootSchema `yaml:"schema"` -} - var ( schemaBox = (*packr.Box)(nil) - checks = map[target][]SchemaCheck{ - targetContainer: []SchemaCheck{}, - targetPod: []SchemaCheck{}, + checks = map[config.TargetKind][]config.SchemaCheck{ + config.TargetContainer: []config.SchemaCheck{}, + config.TargetPod: []config.SchemaCheck{}, } // We explicitly set the order to avoid thrash in the // tests as we migrate toward JSON schema @@ -54,6 +26,10 @@ var ( "hostPID", "hostNetwork", // Container checks + "memoryLimitsMissing", + "memoryRequestsMissing", + "cpuLimitsMissing", + "cpuRequestsMissing", "readinessProbe", "livenessProbe", "pullPolicyNotAlways", @@ -83,9 +59,9 @@ func init() { } } -func parseCheck(rawBytes []byte) (SchemaCheck, error) { +func parseCheck(rawBytes []byte) (config.SchemaCheck, error) { reader := bytes.NewReader(rawBytes) - check := SchemaCheck{} + check := config.SchemaCheck{} d := yaml.NewYAMLOrJSONDecoder(reader, 4096) for { if err := d.Decode(&check); err != nil { @@ -97,72 +73,16 @@ func parseCheck(rawBytes []byte) (SchemaCheck, error) { } } -func (check SchemaCheck) checkPod(pod *corev1.PodSpec) (bool, error) { - return check.checkObject(pod) -} - -func (check SchemaCheck) checkContainer(container *corev1.Container) (bool, error) { - return check.checkObject(container) -} - -func (check SchemaCheck) checkObject(obj interface{}) (bool, error) { - bytes, err := json.Marshal(obj) - if err != nil { - return false, err - } - errors, err := check.Schema.ValidateBytes(bytes) - return len(errors) == 0, err -} - -func (check SchemaCheck) isActionable(target target, controllerType config.SupportedController, isInit bool) bool { - if check.Target != target { - return false - } - isIncluded := len(check.Controllers.Include) == 0 - for _, inclusion := range check.Controllers.Include { - if config.GetSupportedControllerFromString(inclusion) == controllerType { - isIncluded = true - break - } - } - if !isIncluded { - return false - } - for _, exclusion := range check.Controllers.Exclude { - if config.GetSupportedControllerFromString(exclusion) == controllerType { - return false - } - } - if check.Target == targetContainer { - isIncluded := len(check.Containers.Include) == 0 - for _, inclusion := range check.Containers.Include { - if (inclusion == "initContainer" && isInit) || (inclusion == "container" && !isInit) { - isIncluded = true - break - } - } - if !isIncluded { - return false - } - for _, exclusion := range check.Containers.Exclude { - if (exclusion == "initContainer" && isInit) || (exclusion == "container" && !isInit) { - return false - } - } - } - return true -} - func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController, pv *PodValidation) error { - for _, check := range checks[targetPod] { + for _, check := range checks[config.TargetPod] { if !conf.IsActionable(check.Category, check.Name, controllerName) { continue } - if !check.isActionable(targetPod, controllerType, false) { + if !check.IsActionable(config.TargetPod, controllerType, false) { continue } severity := conf.GetSeverity(check.Category, check.Name) - passes, err := check.checkPod(pod) + passes, err := check.CheckPod(pod) if err != nil { return err } @@ -176,22 +96,54 @@ func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, contr } func applyContainerSchemaChecks(conf *config.Configuration, controllerName string, controllerType config.SupportedController, cv *ContainerValidation) error { - for _, check := range checks[targetContainer] { + for _, check := range checks[config.TargetContainer] { if !conf.IsActionable(check.Category, check.Name, controllerName) { continue } - if !check.isActionable(targetContainer, controllerType, cv.IsInitContainer) { + if !check.IsActionable(config.TargetContainer, controllerType, cv.IsInitContainer) { continue } severity := conf.GetSeverity(check.Category, check.Name) var passes bool var err error - if check.SchemaTarget == targetPod { + if check.SchemaTarget == config.TargetPod { cv.parentPodSpec.Containers = []corev1.Container{*cv.Container} - passes, err = check.checkPod(&cv.parentPodSpec) + passes, err = check.CheckPod(&cv.parentPodSpec) cv.parentPodSpec.Containers = []corev1.Container{} } else { - passes, err = check.checkContainer(cv.Container) + passes, err = check.CheckContainer(cv.Container) + } + if err != nil { + return err + } + if passes { + cv.addSuccess(check.SuccessMessage, check.Category, check.ID) + } else { + cv.addFailure(check.FailureMessage, severity, check.Category, check.ID) + } + } + for checkName, severity := range conf.Checks { + check, ok := conf.CustomChecks[checkName] + if !ok { + return fmt.Errorf("Custom check %s not found", checkName) + } + // FIXME: check actionability here + /* + if !conf.IsActionable(check.Category, check.Name, controllerName) { + continue + } + */ + if !check.IsActionable(config.TargetContainer, controllerType, cv.IsInitContainer) { + 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{} + } else { + passes, err = check.CheckContainer(cv.Container) } if err != nil { return err From 04da47d83ec6c509d903a216792b33e224bb5ef5 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Fri, 27 Dec 2019 21:06:06 +0000 Subject: [PATCH 03/14] change input config to simplify things --- checks/cpuLimitsMissing.yaml | 2 - checks/cpuRequestsMissing.yaml | 2 - checks/dangerousCapabilities.yaml | 2 - checks/{hostIPC.yaml => hostIPCSet.yaml} | 2 - .../{hostNetwork.yaml => hostNetworkSet.yaml} | 2 - checks/{hostPID.yaml => hostPIDSet.yaml} | 2 - checks/hostPortSet.yaml | 2 - checks/insecureCapabilities.yaml | 2 - ...ssProbe.yaml => livenessProbeMissing.yaml} | 2 - checks/memoryLimitsMissing.yaml | 2 - checks/memoryRequestsMissing.yaml | 2 - checks/notReadOnlyRootFileSystem.yaml | 2 - checks/privilegeEscalationAllowed.yaml | 2 - checks/pullPolicyNotAlways.yaml | 2 - ...sProbe.yaml => readinessProbeMissing.yaml} | 2 - checks/runAsPrivileged.yaml | 2 - checks/runAsRootAllowed.yaml | 2 - checks/tagNotSpecified.yaml | 2 - deploy/dashboard.yaml | 1 + examples/config.yaml | 13 +-- pkg/config/config.go | 7 +- pkg/config/config_test.go | 12 +-- pkg/config/exemptions.go | 39 +------- pkg/config/schema.go | 1 - pkg/validator/container_test.go | 92 +++++++++--------- pkg/validator/controller_test.go | 18 ++-- pkg/validator/fullaudit_test.go | 6 +- pkg/validator/pod_test.go | 66 ++++++------- pkg/validator/schema.go | 94 +++++++++---------- 29 files changed, 146 insertions(+), 239 deletions(-) rename checks/{hostIPC.yaml => hostIPCSet.yaml} (88%) rename checks/{hostNetwork.yaml => hostNetworkSet.yaml} (87%) rename checks/{hostPID.yaml => hostPIDSet.yaml} (88%) rename checks/{livenessProbe.yaml => livenessProbeMissing.yaml} (88%) rename checks/{readinessProbe.yaml => readinessProbeMissing.yaml} (88%) diff --git a/checks/cpuLimitsMissing.yaml b/checks/cpuLimitsMissing.yaml index 478c80a0..ccf1ddfe 100644 --- a/checks/cpuLimitsMissing.yaml +++ b/checks/cpuLimitsMissing.yaml @@ -1,5 +1,3 @@ -name: CPULimitsMissing -id: cpuLimitsMissing successMessage: CPU limits are set failureMessage: CPU limits should be set category: Resources diff --git a/checks/cpuRequestsMissing.yaml b/checks/cpuRequestsMissing.yaml index 1c3a445c..77290d8b 100644 --- a/checks/cpuRequestsMissing.yaml +++ b/checks/cpuRequestsMissing.yaml @@ -1,5 +1,3 @@ -name: CPURequestsMissing -id: cpuRequestsMissing successMessage: CPU requests are set failureMessage: CPU requests should be set category: Resources diff --git a/checks/dangerousCapabilities.yaml b/checks/dangerousCapabilities.yaml index 6c88bd2c..d1daa414 100644 --- a/checks/dangerousCapabilities.yaml +++ b/checks/dangerousCapabilities.yaml @@ -1,5 +1,3 @@ -name: DangerousCapabilities -id: dangerousCapabilities successMessage: Container does not have any dangerous capabilities failureMessage: Container should not have dangerous capabilities category: Security diff --git a/checks/hostIPC.yaml b/checks/hostIPCSet.yaml similarity index 88% rename from checks/hostIPC.yaml rename to checks/hostIPCSet.yaml index 158931c0..db62a24f 100644 --- a/checks/hostIPC.yaml +++ b/checks/hostIPCSet.yaml @@ -1,5 +1,3 @@ -name: HostIPCSet -id: hostIPCSet successMessage: Host IPC is not configured failureMessage: Host IPC should not be configured category: Security diff --git a/checks/hostNetwork.yaml b/checks/hostNetworkSet.yaml similarity index 87% rename from checks/hostNetwork.yaml rename to checks/hostNetworkSet.yaml index 191c174b..d54419bf 100644 --- a/checks/hostNetwork.yaml +++ b/checks/hostNetworkSet.yaml @@ -1,5 +1,3 @@ -name: HostNetworkSet -id: hostNetworkSet successMessage: Host network is not configured failureMessage: Host network should not be configured category: Networking diff --git a/checks/hostPID.yaml b/checks/hostPIDSet.yaml similarity index 88% rename from checks/hostPID.yaml rename to checks/hostPIDSet.yaml index 6f3ed894..d2b0a65c 100644 --- a/checks/hostPID.yaml +++ b/checks/hostPIDSet.yaml @@ -1,5 +1,3 @@ -name: HostPIDSet -id: hostPIDSet successMessage: Host PID is not configured failureMessage: Host PID should not be configured category: Security diff --git a/checks/hostPortSet.yaml b/checks/hostPortSet.yaml index 2c26cc2a..cd193ea6 100644 --- a/checks/hostPortSet.yaml +++ b/checks/hostPortSet.yaml @@ -1,5 +1,3 @@ -name: HostPortSet -id: hostPortSet successMessage: Host port is not configured failureMessage: Host port should not be configured category: Networking diff --git a/checks/insecureCapabilities.yaml b/checks/insecureCapabilities.yaml index fd987acf..e5afcfd0 100644 --- a/checks/insecureCapabilities.yaml +++ b/checks/insecureCapabilities.yaml @@ -1,5 +1,3 @@ -name: InsecureCapabilities -id: insecureCapabilities successMessage: Container does not have any insecure capabilities failureMessage: Container should not have insecure capabilities category: Security diff --git a/checks/livenessProbe.yaml b/checks/livenessProbeMissing.yaml similarity index 88% rename from checks/livenessProbe.yaml rename to checks/livenessProbeMissing.yaml index a60d719f..a7827304 100644 --- a/checks/livenessProbe.yaml +++ b/checks/livenessProbeMissing.yaml @@ -1,5 +1,3 @@ -name: LivenessProbeMissing -id: livenessProbeMissing successMessage: Liveness probe is configured failureMessage: Liveness probe should be configured category: Health Checks diff --git a/checks/memoryLimitsMissing.yaml b/checks/memoryLimitsMissing.yaml index e61926bf..f3c14524 100644 --- a/checks/memoryLimitsMissing.yaml +++ b/checks/memoryLimitsMissing.yaml @@ -1,5 +1,3 @@ -name: MemoryLimitsMissing -id: memoryLimitsMissing successMessage: Memory limits are set failureMessage: Memory limits should be set category: Resources diff --git a/checks/memoryRequestsMissing.yaml b/checks/memoryRequestsMissing.yaml index cfd339a9..a3f91781 100644 --- a/checks/memoryRequestsMissing.yaml +++ b/checks/memoryRequestsMissing.yaml @@ -1,5 +1,3 @@ -name: MemoryRequestsMissing -id: memoryRequestsMissing successMessage: Memory requests are set failureMessage: Memory requests should be set category: Resources diff --git a/checks/notReadOnlyRootFileSystem.yaml b/checks/notReadOnlyRootFileSystem.yaml index 509bf2e8..d4e203fe 100644 --- a/checks/notReadOnlyRootFileSystem.yaml +++ b/checks/notReadOnlyRootFileSystem.yaml @@ -1,5 +1,3 @@ -name: NotReadOnlyRootFileSystem -id: notReadOnlyRootFileSystem successMessage: Filesystem is read only failureMessage: Filesystem should be read only category: Security diff --git a/checks/privilegeEscalationAllowed.yaml b/checks/privilegeEscalationAllowed.yaml index 912db92d..6db1f836 100644 --- a/checks/privilegeEscalationAllowed.yaml +++ b/checks/privilegeEscalationAllowed.yaml @@ -1,5 +1,3 @@ -name: PrivilegeEscalationAllowed -id: privilegeEscalationAllowed successMessage: Privilege escalation not allowed failureMessage: Privilege escalation should not be allowed category: Security diff --git a/checks/pullPolicyNotAlways.yaml b/checks/pullPolicyNotAlways.yaml index 5aeac39a..89e591f5 100644 --- a/checks/pullPolicyNotAlways.yaml +++ b/checks/pullPolicyNotAlways.yaml @@ -1,5 +1,3 @@ -name: PullPolicyNotAlways -id: pullPolicyNotAlways successMessage: Image pull policy is "Always" failureMessage: Image pull policy should be "Always" category: Images diff --git a/checks/readinessProbe.yaml b/checks/readinessProbeMissing.yaml similarity index 88% rename from checks/readinessProbe.yaml rename to checks/readinessProbeMissing.yaml index c9d0dba8..1309bab8 100644 --- a/checks/readinessProbe.yaml +++ b/checks/readinessProbeMissing.yaml @@ -1,5 +1,3 @@ -name: ReadinessProbeMissing -id: readinessProbeMissing successMessage: Readiness probe is configured failureMessage: Readiness probe should be configured category: Health Checks diff --git a/checks/runAsPrivileged.yaml b/checks/runAsPrivileged.yaml index 8c9b6ff1..47be7cb7 100644 --- a/checks/runAsPrivileged.yaml +++ b/checks/runAsPrivileged.yaml @@ -1,5 +1,3 @@ -name: RunAsPrivileged -id: runAsPrivileged successMessage: Not running as privileged failureMessage: Should not be running as privileged category: Security diff --git a/checks/runAsRootAllowed.yaml b/checks/runAsRootAllowed.yaml index 468f51e6..3ae2fe49 100644 --- a/checks/runAsRootAllowed.yaml +++ b/checks/runAsRootAllowed.yaml @@ -1,5 +1,3 @@ -name: RunAsRootAllowed -id: runAsRootAllowed successMessage: Is not allowed to run as root failureMessage: Should not be allowed to run as root category: Security diff --git a/checks/tagNotSpecified.yaml b/checks/tagNotSpecified.yaml index 322d806d..102d34c7 100644 --- a/checks/tagNotSpecified.yaml +++ b/checks/tagNotSpecified.yaml @@ -1,5 +1,3 @@ -name: TagNotSpecified -id: tagNotSpecified successMessage: Image tag is specified failureMessage: Image tag should be specified category: Images diff --git a/deploy/dashboard.yaml b/deploy/dashboard.yaml index 2af9813c..ad4d0cda 100644 --- a/deploy/dashboard.yaml +++ b/deploy/dashboard.yaml @@ -154,6 +154,7 @@ metadata: namespace: polaris labels: app: polaris + foo: bar component: dashboard spec: replicas: 1 diff --git a/examples/config.yaml b/examples/config.yaml index faf1c1ef..e468175f 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -1,18 +1,19 @@ -resources: +checks: + # resources cpuRequestsMissing: warning cpuLimitsMissing: warning memoryRequestsMissing: warning memoryLimitsMissing: warning -images: + # images tagNotSpecified: error pullPolicyNotAlways: ignore -healthChecks: + # healthChecks readinessProbeMissing: warning livenessProbeMissing: warning -networking: + # networking hostNetworkSet: warning hostPortSet: warning -security: + # security hostIPCSet: error hostPIDSet: error notReadOnlyRootFileSystem: warning @@ -21,7 +22,7 @@ security: runAsPrivileged: error dangerousCapabilities: error insecureCapabilities: warning -controllers_to_scan: +controllersToScan: - Deployments - StatefulSets - DaemonSets diff --git a/pkg/config/config.go b/pkg/config/config.go index 5aa75381..584539ee 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -29,13 +29,8 @@ import ( // Configuration contains all of the config for the validation checks. type Configuration struct { DisplayName string `json:"displayName"` - Resources Resources `json:"resources"` - HealthChecks HealthChecks `json:"healthChecks"` - Images Images `json:"images"` - Networking Networking `json:"networking"` - Security Security `json:"security"` Checks map[string]Severity `json:"checks"` - ControllersToScan []SupportedController `json:"controllers_to_scan"` + ControllersToScan []SupportedController `json:"controllersToScan"` CustomChecks map[string]SchemaCheck `json:"customChecks"` Exemptions []Exemption `json:"exemptions"` DisallowExemptions bool `json:"disallowExemptions"` diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 1677fed7..121e40b2 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -29,18 +29,18 @@ import ( var confInvalid = `test` var confValidYAML = ` -resources: +checks: cpuRequestsMissing: warning -controllers_to_scan: +controllersToScan: - Deployments ` var confValidJSON = ` { - "resources": { + "checks": { "cpuRequestsMissing": "warning" }, - "controllers_to_scan": ["Deployments"] + "controllersToScan": ["Deployments"] } ` @@ -96,7 +96,7 @@ func TestConfigNoServerError(t *testing.T) { } func testParsedConfig(t *testing.T, config *Configuration) { - assert.Equal(t, SeverityWarning, config.Resources.CPURequestsMissing) - assert.Equal(t, Severity(""), config.Resources.CPULimitsMissing) + assert.Equal(t, SeverityWarning, config.Checks["cpuRequestsMissing"]) + assert.Equal(t, Severity(""), config.Checks["cpuLimitsMissing"]) assert.ElementsMatch(t, []SupportedController{Deployments}, config.ControllersToScan) } diff --git a/pkg/config/exemptions.go b/pkg/config/exemptions.go index 2da8778d..7b5f55f9 100644 --- a/pkg/config/exemptions.go +++ b/pkg/config/exemptions.go @@ -1,19 +1,12 @@ 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 { - if subConfStr, ok := subConf.(string); ok { - subConf = conf.GetCategoryConfig(subConfStr) - } - ruleID := GetIDFromField(subConf, ruleName) - subConfRef := reflect.ValueOf(subConf) - fieldVal := reflect.Indirect(subConfRef).FieldByName(ruleName).Interface() - if severity, ok := fieldVal.(Severity); ok && !severity.IsActionable() { +func (conf Configuration) IsActionable(ruleID, controllerName string) bool { + if severity, ok := conf.Checks[ruleID]; !ok || !severity.IsActionable() { return false } if conf.DisallowExemptions { @@ -33,31 +26,3 @@ func (conf Configuration) IsActionable(subConf interface{}, ruleName, controller } return true } - -// GetCategoryConfig returns the configuration for a particular category name -func (conf Configuration) GetCategoryConfig(category string) interface{} { - if category == "Networking" { - return conf.Networking - } else if category == "Security" { - return conf.Security - } else if category == "Health Checks" { - return conf.HealthChecks - } else if category == "Resources" { - return conf.Resources - } else if category == "Images" { - return conf.Images - } - return nil -} - -// GetSeverity returns the severity configured for a particular check -func (conf Configuration) GetSeverity(category string, name string) Severity { - subConf := conf.GetCategoryConfig(category) - subConfRef := reflect.ValueOf(subConf) - fieldVal := reflect.Indirect(subConfRef).FieldByName(name).Interface() - if severity, ok := fieldVal.(Severity); ok { - return severity - } - // TODO: don't panic - panic("Unknown severity: " + category + "/" + name) -} diff --git a/pkg/config/schema.go b/pkg/config/schema.go index f072d88b..073a0a9d 100644 --- a/pkg/config/schema.go +++ b/pkg/config/schema.go @@ -95,7 +95,6 @@ const ( // SchemaCheck is a Polaris check that runs using JSON Schema type SchemaCheck struct { - Name string `yaml:"name"` ID string `yaml:"id"` Category string `yaml:"category"` SuccessMessage string `yaml:"successMessage"` diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index 4c958754..1fdd02d0 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -25,7 +25,7 @@ import ( ) var resourceConfMinimal = `--- -resources: +checks: cpuRequestsMissing: warning memoryRequestsMissing: warning cpuLimitsMissing: error @@ -33,7 +33,7 @@ resources: ` var resourceConfExemptions = `--- -resources: +checks: cpuRequestsMissing: warning memoryRequestsMissing: warning cpuLimitsMissing: error @@ -338,14 +338,14 @@ func TestValidateResourcesFullyValid(t *testing.T) { func TestValidateHealthChecks(t *testing.T) { // Test setup. - p1 := conf.HealthChecks{} - p2 := conf.HealthChecks{ - ReadinessProbeMissing: conf.SeverityIgnore, - LivenessProbeMissing: conf.SeverityIgnore, + p1 := make(map[string]conf.Severity) + p2 := map[string]conf.Severity{ + "readinessProbeMissing": conf.SeverityIgnore, + "livenessProbeMissing": conf.SeverityIgnore, } - p3 := conf.HealthChecks{ - ReadinessProbeMissing: conf.SeverityError, - LivenessProbeMissing: conf.SeverityWarning, + p3 := map[string]conf.Severity{ + "readinessProbeMissing": conf.SeverityError, + "livenessProbeMissing": conf.SeverityWarning, } probe := corev1.Probe{} @@ -375,7 +375,7 @@ func TestValidateHealthChecks(t *testing.T) { var testCases = []struct { name string - probes conf.HealthChecks + probes map[string]conf.Severity cv ContainerValidation errors *[]*ResultMessage warnings *[]*ResultMessage @@ -390,7 +390,7 @@ func TestValidateHealthChecks(t *testing.T) { for idx, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - err := applyContainerSchemaChecks(&conf.Configuration{HealthChecks: tt.probes}, "", conf.Deployments, &tt.cv) + err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.probes}, "", conf.Deployments, &tt.cv) if err != nil { panic(err) } @@ -408,14 +408,14 @@ func TestValidateHealthChecks(t *testing.T) { } func TestValidateImage(t *testing.T) { - emptyConf := conf.Images{} - standardConf := conf.Images{ - TagNotSpecified: conf.SeverityError, - PullPolicyNotAlways: conf.SeverityIgnore, + emptyConf := make(map[string]conf.Severity) + standardConf := map[string]conf.Severity{ + "tagNotSpecified": conf.SeverityError, + "pullPolicyNotAlways": conf.SeverityIgnore, } - strongConf := conf.Images{ - TagNotSpecified: conf.SeverityError, - PullPolicyNotAlways: conf.SeverityError, + strongConf := map[string]conf.Severity{ + "tagNotSpecified": conf.SeverityError, + "pullPolicyNotAlways": conf.SeverityError, } emptyCV := ContainerValidation{ @@ -437,7 +437,7 @@ func TestValidateImage(t *testing.T) { var testCases = []struct { name string - image conf.Images + image map[string]conf.Severity cv ContainerValidation expected []*ResultMessage }{ @@ -507,7 +507,7 @@ func TestValidateImage(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { tt.cv = resetCV(tt.cv) - err := applyContainerSchemaChecks(&conf.Configuration{Images: tt.image}, "", conf.Deployments, &tt.cv) + err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.image}, "", conf.Deployments, &tt.cv) if err != nil { panic(err) } @@ -519,12 +519,12 @@ func TestValidateImage(t *testing.T) { func TestValidateNetworking(t *testing.T) { // Test setup. - emptyConf := conf.Networking{} - standardConf := conf.Networking{ - HostPortSet: conf.SeverityWarning, + emptyConf := make(map[string]conf.Severity) + standardConf := map[string]conf.Severity{ + "hostPortSet": conf.SeverityWarning, } - strongConf := conf.Networking{ - HostPortSet: conf.SeverityError, + strongConf := map[string]conf.Severity{ + "hostPortSet": conf.SeverityError, } emptyCV := ContainerValidation{ @@ -553,7 +553,7 @@ func TestValidateNetworking(t *testing.T) { var testCases = []struct { name string - networkConf conf.Networking + networkConf map[string]conf.Severity cv ContainerValidation expectedMessages []*ResultMessage }{ @@ -629,7 +629,7 @@ 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{Networking: tt.networkConf}, "", conf.Deployments, &tt.cv) + err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.networkConf}, "", conf.Deployments, &tt.cv) if err != nil { panic(err) } @@ -644,22 +644,22 @@ func TestValidateSecurity(t *testing.T) { falseVar := false // Test setup. - emptyConf := conf.Security{} - standardConf := conf.Security{ - RunAsRootAllowed: conf.SeverityWarning, - RunAsPrivileged: conf.SeverityError, - NotReadOnlyRootFileSystem: conf.SeverityWarning, - PrivilegeEscalationAllowed: conf.SeverityError, - DangerousCapabilities: conf.SeverityError, - InsecureCapabilities: conf.SeverityWarning, + emptyConf := map[string]conf.Severity{} + standardConf := map[string]conf.Severity{ + "runAsRootAllowed": conf.SeverityWarning, + "runAsPrivileged": conf.SeverityError, + "notReadOnlyRootFileSystem": conf.SeverityWarning, + "privilegeEscalationAllowed": conf.SeverityError, + "dangerousCapabilities": conf.SeverityError, + "insecureCapabilities": conf.SeverityWarning, } - strongConf := conf.Security{ - RunAsRootAllowed: conf.SeverityError, - RunAsPrivileged: conf.SeverityError, - NotReadOnlyRootFileSystem: conf.SeverityError, - PrivilegeEscalationAllowed: conf.SeverityError, - DangerousCapabilities: conf.SeverityError, - InsecureCapabilities: conf.SeverityError, + strongConf := map[string]conf.Severity{ + "runAsRootAllowed": conf.SeverityError, + "runAsPrivileged": conf.SeverityError, + "notReadOnlyRootFileSystem": conf.SeverityError, + "privilegeEscalationAllowed": conf.SeverityError, + "dangerousCapabilities": conf.SeverityError, + "insecureCapabilities": conf.SeverityError, } emptyCV := ContainerValidation{ @@ -780,7 +780,7 @@ func TestValidateSecurity(t *testing.T) { var testCases = []struct { name string - securityConf conf.Security + securityConf map[string]conf.Severity cv ContainerValidation expectedMessages []*ResultMessage }{ @@ -1119,7 +1119,7 @@ func TestValidateSecurity(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { tt.cv = resetCV(tt.cv) - err := applyContainerSchemaChecks(&conf.Configuration{Security: tt.securityConf}, "", conf.Deployments, &tt.cv) + err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.securityConf}, "", conf.Deployments, &tt.cv) if err != nil { panic(err) } @@ -1135,8 +1135,8 @@ func TestValidateRunAsRoot(t *testing.T) { nonRootUser := int64(1000) rootUser := int64(0) config := conf.Configuration{ - Security: conf.Security{ - RunAsRootAllowed: conf.SeverityWarning, + Checks: map[string]conf.Severity{ + "runAsRootAllowed": conf.SeverityWarning, }, } testCases := []struct { diff --git a/pkg/validator/controller_test.go b/pkg/validator/controller_test.go index 860984f1..90a67486 100644 --- a/pkg/validator/controller_test.go +++ b/pkg/validator/controller_test.go @@ -29,9 +29,9 @@ import ( func TestValidateController(t *testing.T) { c := conf.Configuration{ - Security: conf.Security{ - HostIPCSet: conf.SeverityError, - HostPIDSet: conf.SeverityError, + Checks: map[string]conf.Severity{ + "hostIPCSet": conf.SeverityError, + "hostPIDSet": conf.SeverityError, }, } deployment := controller.NewDeploymentController(test.MockDeploy()) @@ -64,9 +64,9 @@ func TestValidateController(t *testing.T) { func TestSkipHealthChecks(t *testing.T) { c := conf.Configuration{ - HealthChecks: conf.HealthChecks{ - ReadinessProbeMissing: conf.SeverityError, - LivenessProbeMissing: conf.SeverityWarning, + Checks: map[string]conf.Severity{ + "readinessProbeMissing": conf.SeverityError, + "livenessProbeMissing": conf.SeverityWarning, }, ControllersToScan: []conf.SupportedController{ conf.Deployments, @@ -139,9 +139,9 @@ func TestSkipHealthChecks(t *testing.T) { func TestControllerExemptions(t *testing.T) { c := conf.Configuration{ - HealthChecks: conf.HealthChecks{ - ReadinessProbeMissing: conf.SeverityError, - LivenessProbeMissing: conf.SeverityWarning, + Checks: map[string]conf.Severity{ + "readinessProbeMissing": conf.SeverityError, + "livenessProbeMissing": conf.SeverityWarning, }, ControllersToScan: []conf.SupportedController{ conf.Deployments, diff --git a/pkg/validator/fullaudit_test.go b/pkg/validator/fullaudit_test.go index 863e35e4..af9411c0 100644 --- a/pkg/validator/fullaudit_test.go +++ b/pkg/validator/fullaudit_test.go @@ -16,9 +16,9 @@ func TestGetTemplateData(t *testing.T) { assert.Equal(t, err, nil, "error should be nil") c := conf.Configuration{ - HealthChecks: conf.HealthChecks{ - ReadinessProbeMissing: conf.SeverityError, - LivenessProbeMissing: conf.SeverityWarning, + Checks: map[string]conf.Severity{ + "readinessProbeMissing": conf.SeverityError, + "livenessProbeMissing": conf.SeverityWarning, }, ControllersToScan: []conf.SupportedController{ conf.Deployments, diff --git a/pkg/validator/pod_test.go b/pkg/validator/pod_test.go index d0552a70..fd9df744 100644 --- a/pkg/validator/pod_test.go +++ b/pkg/validator/pod_test.go @@ -24,13 +24,11 @@ import ( func TestValidatePod(t *testing.T) { c := conf.Configuration{ - Security: conf.Security{ - HostIPCSet: conf.SeverityError, - HostPIDSet: conf.SeverityError, - }, - Networking: conf.Networking{ - HostNetworkSet: conf.SeverityWarning, - HostPortSet: conf.SeverityError, + Checks: map[string]conf.Severity{ + "hostIPCSet": conf.SeverityError, + "hostPIDSet": conf.SeverityError, + "hostNetworkSet": conf.SeverityWarning, + "hostPortSet": conf.SeverityError, }, } @@ -59,8 +57,8 @@ func TestValidatePod(t *testing.T) { 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"}, {ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"}, + {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"}, } actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments) @@ -72,13 +70,11 @@ func TestValidatePod(t *testing.T) { func TestInvalidIPCPod(t *testing.T) { c := conf.Configuration{ - Security: conf.Security{ - HostIPCSet: conf.SeverityError, - HostPIDSet: conf.SeverityError, - }, - Networking: conf.Networking{ - HostNetworkSet: conf.SeverityWarning, - HostPortSet: conf.SeverityError, + Checks: map[string]conf.Severity{ + "hostIPCSet": conf.SeverityError, + "hostPIDSet": conf.SeverityError, + "hostNetworkSet": conf.SeverityWarning, + "hostPortSet": conf.SeverityError, }, } @@ -107,8 +103,8 @@ func TestInvalidIPCPod(t *testing.T) { } expectedMessages := []*ResultMessage{ {ID: "hostIPCSet", Message: "Host IPC should not be configured", Type: "error", Category: "Security"}, - {ID: "hostPIDSet", Message: "Host PID 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"}, } actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments) @@ -120,13 +116,11 @@ func TestInvalidIPCPod(t *testing.T) { func TestInvalidNeworkPod(t *testing.T) { c := conf.Configuration{ - Networking: conf.Networking{ - HostNetworkSet: conf.SeverityWarning, - HostPortSet: conf.SeverityError, - }, - Security: conf.Security{ - HostIPCSet: conf.SeverityError, - HostPIDSet: conf.SeverityError, + Checks: map[string]conf.Severity{ + "hostNetworkSet": conf.SeverityWarning, + "hostPortSet": conf.SeverityError, + "hostIPCSet": conf.SeverityError, + "hostPIDSet": conf.SeverityError, }, } @@ -170,13 +164,11 @@ func TestInvalidNeworkPod(t *testing.T) { func TestInvalidPIDPod(t *testing.T) { c := conf.Configuration{ - Security: conf.Security{ - HostIPCSet: conf.SeverityError, - HostPIDSet: conf.SeverityError, - }, - Networking: conf.Networking{ - HostNetworkSet: conf.SeverityWarning, - HostPortSet: conf.SeverityError, + Checks: map[string]conf.Severity{ + "hostIPCSet": conf.SeverityError, + "hostPIDSet": conf.SeverityError, + "hostNetworkSet": conf.SeverityWarning, + "hostPortSet": conf.SeverityError, }, } @@ -219,13 +211,11 @@ func TestInvalidPIDPod(t *testing.T) { func TestExemption(t *testing.T) { c := conf.Configuration{ - Security: conf.Security{ - HostIPCSet: conf.SeverityError, - HostPIDSet: conf.SeverityError, - }, - Networking: conf.Networking{ - HostNetworkSet: conf.SeverityWarning, - HostPortSet: conf.SeverityError, + Checks: map[string]conf.Severity{ + "hostIPCSet": conf.SeverityError, + "hostNetworkSet": conf.SeverityWarning, + "hostPIDSet": conf.SeverityError, + "hostPortSet": conf.SeverityError, }, Exemptions: []conf.Exemption{ conf.Exemption{ @@ -259,8 +249,8 @@ func TestExemption(t *testing.T) { Errors: uint(0), } expectedMessages := []*ResultMessage{ - {ID: "hostPIDSet", Message: "Host PID 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"}, } actualPodResult := ValidatePod(c, &pod.Spec, "foo", conf.Deployments) diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go index e02341ea..2060991d 100644 --- a/pkg/validator/schema.go +++ b/pkg/validator/schema.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "sort" packr "github.com/gobuffalo/packr/v2" corev1 "k8s.io/api/core/v1" @@ -13,25 +14,22 @@ import ( ) var ( - schemaBox = (*packr.Box)(nil) - checks = map[config.TargetKind][]config.SchemaCheck{ - config.TargetContainer: []config.SchemaCheck{}, - config.TargetPod: []config.SchemaCheck{}, - } + schemaBox = (*packr.Box)(nil) + builtInChecks = map[string]config.SchemaCheck{} // We explicitly set the order to avoid thrash in the // tests as we migrate toward JSON schema checkOrder = []string{ // Pod checks - "hostIPC", - "hostPID", - "hostNetwork", + "hostIPCSet", + "hostPIDSet", + "hostNetworkSet", // Container checks "memoryLimitsMissing", "memoryRequestsMissing", "cpuLimitsMissing", "cpuRequestsMissing", - "readinessProbe", - "livenessProbe", + "readinessProbeMissing", + "livenessProbeMissing", "pullPolicyNotAlways", "tagNotSpecified", "hostPortSet", @@ -46,8 +44,8 @@ var ( func init() { schemaBox = packr.New("Schemas", "../../checks") - for _, file := range checkOrder { - contents, err := schemaBox.Find(file + ".yaml") + for _, checkID := range checkOrder { + contents, err := schemaBox.Find(checkID + ".yaml") if err != nil { panic(err) } @@ -55,7 +53,8 @@ func init() { if err != nil { panic(err) } - checks[check.Target] = append(checks[check.Target], check) + check.ID = checkID + builtInChecks[checkID] = check } } @@ -74,14 +73,21 @@ func parseCheck(rawBytes []byte) (config.SchemaCheck, error) { } func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController, pv *PodValidation) error { - for _, check := range checks[config.TargetPod] { - if !conf.IsActionable(check.Category, check.Name, controllerName) { + 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.TargetPod, controllerType, false) { continue } - severity := conf.GetSeverity(check.Category, check.Name) passes, err := check.CheckPod(pod) if err != nil { return err @@ -89,6 +95,7 @@ func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, contr if passes { pv.addSuccess(check.SuccessMessage, check.Category, check.ID) } else { + severity := conf.Checks[checkID] pv.addFailure(check.FailureMessage, severity, check.Category, check.ID) } } @@ -96,43 +103,18 @@ func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, contr } func applyContainerSchemaChecks(conf *config.Configuration, controllerName string, controllerType config.SupportedController, cv *ContainerValidation) error { - for _, check := range checks[config.TargetContainer] { - if !conf.IsActionable(check.Category, check.Name, controllerName) { - continue - } - if !check.IsActionable(config.TargetContainer, controllerType, cv.IsInitContainer) { - continue - } - severity := conf.GetSeverity(check.Category, check.Name) - 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{} - } else { - passes, err = check.CheckContainer(cv.Container) - } - if err != nil { - return err - } - if passes { - cv.addSuccess(check.SuccessMessage, check.Category, check.ID) - } else { - cv.addFailure(check.FailureMessage, severity, check.Category, check.ID) - } - } - for checkName, severity := range conf.Checks { - check, ok := conf.CustomChecks[checkName] + checkIDs := getSortedKeys(conf.Checks) + for _, checkID := range checkIDs { + check, ok := conf.CustomChecks[checkID] if !ok { - return fmt.Errorf("Custom check %s not found", checkName) + check, ok = builtInChecks[checkID] + } + if !ok { + return fmt.Errorf("Check %s not found", checkID) + } + if !conf.IsActionable(check.ID, controllerName) { + continue } - // FIXME: check actionability here - /* - if !conf.IsActionable(check.Category, check.Name, controllerName) { - continue - } - */ if !check.IsActionable(config.TargetContainer, controllerType, cv.IsInitContainer) { continue } @@ -151,8 +133,18 @@ func applyContainerSchemaChecks(conf *config.Configuration, controllerName strin 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 } + +func getSortedKeys(m map[string]config.Severity) []string { + keys := make([]string, 0, len(m)) + for key := range m { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} From 5f060801ef0324264303aaf5572fb221871abaa2 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Thu, 2 Jan 2020 17:58:22 +0000 Subject: [PATCH 04/14] remove unused config structs --- pkg/config/config.go | 46 -------------------------------------------- 1 file changed, 46 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 584539ee..48c8ea5f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -42,52 +42,6 @@ type Exemption struct { ControllerNames []string `json:"controllerNames"` } -// Resources contains config for resource requests and limits. -type Resources struct { - CPURequestsMissing Severity `json:"cpuRequestsMissing"` - CPULimitsMissing Severity `json:"cpuLimitsMissing"` - MemoryRequestsMissing Severity `json:"memoryRequestsMissing"` - MemoryLimitsMissing Severity `json:"memoryLimitsMissing"` -} - -// HealthChecks contains config for readiness and liveness probes. -type HealthChecks struct { - ReadinessProbeMissing Severity `json:"readinessProbeMissing"` - LivenessProbeMissing Severity `json:"livenessProbeMissing"` -} - -// Images contains the config for images. -type Images struct { - TagNotSpecified Severity `json:"tagNotSpecified"` - PullPolicyNotAlways Severity `json:"pullPolicyNotAlways"` - Whitelist ErrorWarningLists `json:"whitelist"` - Blacklist ErrorWarningLists `json:"blacklist"` -} - -// ErrorWarningLists provides lists of patterns to match or avoid in image tags. -type ErrorWarningLists struct { - Error []string `json:"error"` - Warning []string `json:"warning"` -} - -// Networking contains the config for networking validations. -type Networking struct { - HostNetworkSet Severity `json:"hostNetworkSet"` - HostPortSet Severity `json:"hostPortSet"` -} - -// Security contains the config for security validations. -type Security struct { - HostIPCSet Severity `json:"hostIPCSet"` - HostPIDSet Severity `json:"hostPIDSet"` - RunAsRootAllowed Severity `json:"runAsRootAllowed"` - RunAsPrivileged Severity `json:"runAsPrivileged"` - NotReadOnlyRootFileSystem Severity `json:"notReadOnlyRootFileSystem"` - PrivilegeEscalationAllowed Severity `json:"privilegeEscalationAllowed"` - DangerousCapabilities Severity `json:"dangerousCapabilities"` - InsecureCapabilities Severity `json:"insecureCapabilities"` -} - // ParseFile parses config from a file. func ParseFile(path string) (Configuration, error) { var rawBytes []byte From f8d5ce70cfee3a63978eed6e36f6e203b0fb14d4 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Thu, 2 Jan 2020 18:59:11 +0000 Subject: [PATCH 05/14] support schemas that use json strings --- pkg/config/config.go | 5 ++- pkg/config/config_test.go | 62 +++++++++++++++++++++++++++++++ pkg/config/schema.go | 77 +++++++++++++++++++++++++-------------- 3 files changed, 115 insertions(+), 29 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 48c8ea5f..11671700 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -80,7 +80,10 @@ func Parse(rawBytes []byte) (Configuration, error) { } } for key, check := range conf.CustomChecks { - check.ID = key + err := check.Initialize(key) + if err != nil { + return conf, err + } conf.CustomChecks[key] = check } return conf, nil diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 121e40b2..b13feb75 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -44,6 +44,39 @@ var confValidJSON = ` } ` +var confCustomChecks = ` +checks: + foo: warning +customChecks: + foo: + successMessage: Security context is set + failureMessage: Security context should be set + category: Security + target: Container + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - securityContext +` + +var confCustomChecksWithJSONSchema = ` +checks: + foo: warning +customChecks: + foo: + successMessage: Security context is set + failureMessage: Security context should be set + category: Security + target: Container + jsonSchema: > + { + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "required": ["securityContext"] + } +` + func TestParseError(t *testing.T) { _, err := Parse([]byte(confInvalid)) expectedErr := "Decoding config failed: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type config.Configuration" @@ -95,6 +128,35 @@ func TestConfigNoServerError(t *testing.T) { assert.Regexp(t, regexp.MustCompile("connection refused"), err.Error()) } +func TestConfigWithCustomChecks(t *testing.T) { + valid := map[string]interface{}{ + "securityContext": map[string]interface{}{}, + } + invalid := map[string]interface{}{ + "notSecurityContext": map[string]interface{}{}, + } + + parsedConf, err := Parse([]byte(confCustomChecks)) + assert.NoError(t, err, "Expected no error when parsing YAML config") + assert.Equal(t, 1, len(parsedConf.CustomChecks)) + isValid, err := parsedConf.CustomChecks["foo"].CheckObject(valid) + assert.NoError(t, err) + assert.Equal(t, true, isValid) + isValid, err = parsedConf.CustomChecks["foo"].CheckObject(invalid) + assert.NoError(t, err) + assert.Equal(t, false, isValid) + + parsedConf, err = Parse([]byte(confCustomChecksWithJSONSchema)) + assert.NoError(t, err, "Expected no error when parsing YAML config") + assert.Equal(t, 1, len(parsedConf.CustomChecks)) + isValid, err = parsedConf.CustomChecks["foo"].CheckObject(valid) + assert.NoError(t, err) + assert.Equal(t, true, isValid) + isValid, err = parsedConf.CustomChecks["foo"].CheckObject(invalid) + assert.NoError(t, err) + assert.Equal(t, false, isValid) +} + func testParsedConfig(t *testing.T, config *Configuration) { assert.Equal(t, SeverityWarning, config.Checks["cpuRequestsMissing"]) assert.Equal(t, Severity(""), config.Checks["cpuLimitsMissing"]) diff --git a/pkg/config/schema.go b/pkg/config/schema.go index 073a0a9d..721e727c 100644 --- a/pkg/config/schema.go +++ b/pkg/config/schema.go @@ -9,9 +9,42 @@ import ( "k8s.io/apimachinery/pkg/api/resource" ) +type TargetKind string + +const ( + // TargetContainer points to the container spec + TargetContainer TargetKind = "Container" + // TargetPod points to the pod spec + TargetPod TargetKind = "Pod" +) + +// SchemaCheck is a Polaris check that runs using JSON Schema +type SchemaCheck struct { + ID string `yaml:"id"` + Category string `yaml:"category"` + SuccessMessage string `yaml:"successMessage"` + FailureMessage string `yaml:"failureMessage"` + Controllers includeExcludeList `yaml:"controllers"` + Containers includeExcludeList `yaml:"containers"` + Target TargetKind `yaml:"target"` + SchemaTarget TargetKind `yaml:"schemaTarget"` + Schema jsonschema.RootSchema `yaml:"schema"` + JsonSchema string `yaml:"jsonSchema"` +} + type resourceMinimum string type resourceMaximum string +func init() { + jsonschema.RegisterValidator("resourceMinimum", newResourceMinimum) + jsonschema.RegisterValidator("resourceMaximum", newResourceMaximum) +} + +type includeExcludeList struct { + Include []string `yaml:"include"` + Exclude []string `yaml:"exclude"` +} + func newResourceMinimum() jsonschema.Validator { return new(resourceMinimum) } @@ -20,6 +53,7 @@ func newResourceMaximum() jsonschema.Validator { return new(resourceMaximum) } +// Validate checks that a specified quanitity is not less than the minimum func (min resourceMinimum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) { err := validateRange(path, string(min), data, true) if err != nil { @@ -27,6 +61,7 @@ func (min resourceMinimum) Validate(path string, data interface{}, errs *[]jsons } } +// Validate checks that a specified quanitity is not greater than the maximum func (max resourceMaximum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) { err := validateRange(path, string(max), data, false) if err != nil { @@ -76,44 +111,29 @@ func validateRange(path string, limit interface{}, data interface{}, isMinimum b return nil } -func init() { - jsonschema.RegisterValidator("resourceMinimum", newResourceMinimum) - jsonschema.RegisterValidator("resourceMaximum", newResourceMaximum) -} - -type includeExcludeList struct { - Include []string `yaml:"include"` - Exclude []string `yaml:"exclude"` -} - -type TargetKind string - -const ( - TargetContainer TargetKind = "Container" - TargetPod TargetKind = "Pod" -) - -// SchemaCheck is a Polaris check that runs using JSON Schema -type SchemaCheck struct { - ID string `yaml:"id"` - Category string `yaml:"category"` - SuccessMessage string `yaml:"successMessage"` - FailureMessage string `yaml:"failureMessage"` - Controllers includeExcludeList `yaml:"controllers"` - Containers includeExcludeList `yaml:"containers"` - Target TargetKind `yaml:"target"` - SchemaTarget TargetKind `yaml:"schemaTarget"` - Schema jsonschema.RootSchema `yaml:"schema"` +// Initialize sets up the schema +func (check *SchemaCheck) Initialize(id string) error { + check.ID = id + if check.JsonSchema != "" { + if err := json.Unmarshal([]byte(check.JsonSchema), &check.Schema); err != nil { + return err + } + } + fmt.Println(check.Schema) + return nil } +// CheckPod checks a pod spec against the schema func (check SchemaCheck) CheckPod(pod *corev1.PodSpec) (bool, error) { return check.CheckObject(pod) } +// CheckContainer checks a container spec against the schema func (check SchemaCheck) CheckContainer(container *corev1.Container) (bool, error) { return check.CheckObject(container) } +// CheckObject checks arbitrary data against the schema func (check SchemaCheck) CheckObject(obj interface{}) (bool, error) { bytes, err := json.Marshal(obj) if err != nil { @@ -123,6 +143,7 @@ func (check SchemaCheck) CheckObject(obj interface{}) (bool, error) { return len(errs) == 0, err } +// IsActionable decides if this check applies to a particular target func (check SchemaCheck) IsActionable(target TargetKind, controllerType SupportedController, isInit bool) bool { if check.Target != target { return false From 4d82220212e6c5a04dcfaabb33c58410e182f266 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Thu, 2 Jan 2020 19:26:03 +0000 Subject: [PATCH 06/14] fix up config-full --- examples/config-full.yaml | 73 +++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/examples/config-full.yaml b/examples/config-full.yaml index eb370b85..fcdd01aa 100644 --- a/examples/config-full.yaml +++ b/examples/config-full.yaml @@ -1,27 +1,78 @@ -resources: +checks: + # resources cpuRequestsMissing: warning cpuLimitsMissing: warning memoryRequestsMissing: warning memoryLimitsMissing: warning -images: + # images tagNotSpecified: error - pullPolicyNotAlways: warning -healthChecks: + pullPolicyNotAlways: ignore + # healthChecks readinessProbeMissing: warning livenessProbeMissing: warning -networking: - hostNetworkSet: error - hostPortSet: error -security: + # networking + hostNetworkSet: warning + hostPortSet: warning + # security hostIPCSet: error hostPIDSet: error - runAsRootAllowed: warning - runAsPrivileged: error notReadOnlyRootFileSystem: warning privilegeEscalationAllowed: error + runAsRootAllowed: warning + runAsPrivileged: error dangerousCapabilities: error insecureCapabilities: warning -controllers_to_scan: + +customChecks: + resourceLimits: + containers: + exclude: + - initContainer + successMessage: Resource limits are within the required range + failureMessage: Resource limits should be within the required range + category: Resources + target: Container + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - limits + properties: + limits: + type: object + required: + - memory + - cpu + properties: + memory: + type: string + resourceMinimum: 100M + resourceMaximum: 6G + cpu: + type: string + resourceMinimum: 100m + resourceMaximum: "2" + imageRegistry: + successMessage: Image comes from allowed registries + failureMessage: Image should not be from disallowed registry + category: Images + target: Container + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + properties: + image: + type: string + allOf: + not: + pattern: ^quay.io + +controllersToScan: - Deployments - StatefulSets - DaemonSets From f0834d30a21ec93209b0cd20dcd20394b782f873 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Thu, 2 Jan 2020 19:55:11 +0000 Subject: [PATCH 07/14] update docs --- docs/usage.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/usage.md b/docs/usage.md index 2109e01f..25390127 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -20,6 +20,34 @@ Polaris validation checks fall into several different categories: - [Resources](check-documentation/resources.md) - [Security](check-documentation/security.md) +#### Custom Checks +If you'd like to create your own checks, you can use [JSON Schema](https://json-schema.org/). For example, +to disallow images from quay.io: + +```yaml +checks: + imageRegistry: warning +customChecks: + imageRegistry: + successMessage: Image comes from allowed registries + failureMessage: Image should not be from disallowed registry + category: Images + target: Container # target can be "Container" or "Pod" + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + properties: + image: + type: string + not: + pattern: ^quay.io +``` + +We also extend JSON Schema with `resourceMinimum` and `resourceMaximum` fields to help compare memory and CPU resource +strings like `1000m` and `1G`. You can see an example in [the extended config](/examples/config-full.yaml) + +There are additional examples in the [checks folder](/checks). + ### Exemptions Exemptions can be added two ways: by annotating a controller, or editing the Polaris config. From 57ff1d5c069d8118596b9d8e27c265f4524c45c5 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Thu, 2 Jan 2020 20:07:55 +0000 Subject: [PATCH 08/14] remove messages --- pkg/validator/messages/messages.go | 101 ----------------------------- 1 file changed, 101 deletions(-) delete mode 100644 pkg/validator/messages/messages.go diff --git a/pkg/validator/messages/messages.go b/pkg/validator/messages/messages.go deleted file mode 100644 index 9c1193f8..00000000 --- a/pkg/validator/messages/messages.go +++ /dev/null @@ -1,101 +0,0 @@ -package messages - -const ( - // CategoryHealthChecks category - CategoryHealthChecks = "Health Checks" - // CategorySecurity category - CategorySecurity = "Security" - // CategoryNetworking category - CategoryNetworking = "Networking" - // CategoryResources category - CategoryResources = "Resources" - // CategoryImages category - CategoryImages = "Images" - - // CPURequestsLabel label - CPURequestsLabel = "CPU requests" - // CPULimitsLabel label - CPULimitsLabel = "CPU limits" - // MemoryRequestsLabel label - MemoryRequestsLabel = "Memory requests" - // MemoryLimitsLabel label - MemoryLimitsLabel = "Memory limits" - - // CPURequestsFailure message - CPURequestsFailure = "CPU requests should be set" - // CPULimitsFailure message - CPULimitsFailure = "CPU limits should be set" - // MemoryRequestsFailure message - MemoryRequestsFailure = "Memory requests should be set" - // MemoryLimitsFailure message - MemoryLimitsFailure = "Memory limits should be set" - // ResourceAmountTooHighFailure message - ResourceAmountTooHighFailure = "%s should be lower than %s" - // ResourceAmountTooLowFailure message - ResourceAmountTooLowFailure = "%s should be higher than %s" - // ResourceAmountSuccess message - ResourceAmountSuccess = "%s are within the expected range" - // ResourcePresentSuccess message - ResourcePresentSuccess = "%s are set" - // ReadinessProbeFailure message - ReadinessProbeFailure = "Readiness probe should be configured" - // ReadinessProbeSuccess message - ReadinessProbeSuccess = "Readiness probe configured" - // LivenessProbeFailure message - LivenessProbeFailure = "Liveness probe should be configured" - // LivenessProbeSuccess message - LivenessProbeSuccess = "Liveness probe is configured" - // ImageTagFailure message - ImageTagFailure = "Image tag should be specified" - // ImageTagSuccess message - ImageTagSuccess = "Image tag is specified" - // ImagePullPolicyFailure message - ImagePullPolicyFailure = "Image pull policy should be \"Always\"" - // ImagePullPolicySuccess message - ImagePullPolicySuccess = "Image pull policy is \"Always\"" - // HostPortFailure message - HostPortFailure = "Host port should not be configured" - // HostPortSuccess message - HostPortSuccess = "Host port is not configured" - // RunAsRootFailure message - RunAsRootFailure = "Should not be allowed to run as root" - // RunAsRootSuccess message - RunAsRootSuccess = "Is not allowed to run as root" - // RunAsPrivilegedFailure message - RunAsPrivilegedFailure = "Should not be running as privileged" - // RunAsPrivilegedSuccess message - RunAsPrivilegedSuccess = "Not running as privileged" - // ReadOnlyFilesystemSuccess message - ReadOnlyFilesystemSuccess = "Filesystem is read only" - // ReadOnlyFilesystemFailure message - ReadOnlyFilesystemFailure = "Filesystem should be read only" - // PrivilegeEscalationFailure message - PrivilegeEscalationFailure = "Privilege escalation should not be allowed" - // PrivilegeEscalationSuccess message - PrivilegeEscalationSuccess = "Privilege escalation not allowed" - // SecurityCapabilitiesAddedSuccess message - SecurityCapabilitiesAddedSuccess = "Disallowed security capabilities have not been added" - // SecurityCapabilitiesAddedFailure message - SecurityCapabilitiesAddedFailure = "The following security capabilities should not be added: %v" - // SecurityCapabilitiesNotDroppedSuccess message - SecurityCapabilitiesNotDroppedSuccess = "All disallowed security capabilities have been dropped" - // SecurityCapabilitiesNotDroppedFailure message - SecurityCapabilitiesNotDroppedFailure = "The following security capabilities should be dropped: %v" - - // HostAliasFailure message - HostAliasFailure = "Host alias should not be configured" - // HostAliasSuccess message - HostAliasSuccess = "Host alias is not configured" - // HostIPCFailure message - HostIPCFailure = "Host IPC should not be configured" - // HostIPCSuccess message - HostIPCSuccess = "Host IPC is not configured" - // HostPIDFailure message - HostPIDFailure = "Host PID should not be configured" - // HostPIDSuccess message - HostPIDSuccess = "Host PID is not configured" - // HostNetworkFailure message - HostNetworkFailure = "Host network should not be configured" - // HostNetworkSuccess message - HostNetworkSuccess = "Host network is not configured" -) From d0e0009efc2976cdb971631079b8e7ce5ab4fce7 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Thu, 2 Jan 2020 20:10:22 +0000 Subject: [PATCH 09/14] revert dashboard.yaml --- deploy/dashboard.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/deploy/dashboard.yaml b/deploy/dashboard.yaml index ad4d0cda..2af9813c 100644 --- a/deploy/dashboard.yaml +++ b/deploy/dashboard.yaml @@ -154,7 +154,6 @@ metadata: namespace: polaris labels: app: polaris - foo: bar component: dashboard spec: replicas: 1 From fee55ba7f8eb09f76ae8bf003873227db6326fed Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Thu, 2 Jan 2020 20:17:56 +0000 Subject: [PATCH 10/14] fix lint errors --- pkg/config/schema.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/config/schema.go b/pkg/config/schema.go index 721e727c..c728ef17 100644 --- a/pkg/config/schema.go +++ b/pkg/config/schema.go @@ -9,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/api/resource" ) +// TargetKind represents the part of the config to be validated type TargetKind string const ( @@ -29,7 +30,7 @@ type SchemaCheck struct { Target TargetKind `yaml:"target"` SchemaTarget TargetKind `yaml:"schemaTarget"` Schema jsonschema.RootSchema `yaml:"schema"` - JsonSchema string `yaml:"jsonSchema"` + JSONSchema string `yaml:"jsonSchema"` } type resourceMinimum string @@ -114,8 +115,8 @@ func validateRange(path string, limit interface{}, data interface{}, isMinimum b // Initialize sets up the schema func (check *SchemaCheck) Initialize(id string) error { check.ID = id - if check.JsonSchema != "" { - if err := json.Unmarshal([]byte(check.JsonSchema), &check.Schema); err != nil { + if check.JSONSchema != "" { + if err := json.Unmarshal([]byte(check.JSONSchema), &check.Schema); err != nil { return err } } From 68166559c5129faa43ef353c89d2aa18da055176 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Thu, 2 Jan 2020 20:21:46 +0000 Subject: [PATCH 11/14] expand docs --- docs/usage.md | 13 ++++++++++++- pkg/config/schema.go | 1 - 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 25390127..610f63e0 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -43,7 +43,18 @@ customChecks: pattern: ^quay.io ``` -We also extend JSON Schema with `resourceMinimum` and `resourceMaximum` fields to help compare memory and CPU resource +Schemas can also be specified as JSON strings instead of YAML, for easier copy/pasting: +```yaml +customChecks: + foo: + jsonSchema: | + { + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object" + } +``` + +We extend JSON Schema with `resourceMinimum` and `resourceMaximum` fields to help compare memory and CPU resource strings like `1000m` and `1G`. You can see an example in [the extended config](/examples/config-full.yaml) There are additional examples in the [checks folder](/checks). diff --git a/pkg/config/schema.go b/pkg/config/schema.go index c728ef17..320a3843 100644 --- a/pkg/config/schema.go +++ b/pkg/config/schema.go @@ -120,7 +120,6 @@ func (check *SchemaCheck) Initialize(id string) error { return err } } - fmt.Println(check.Schema) return nil } From 917e630697e2ce8fcfb62e37f96ad41115bce561 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Thu, 2 Jan 2020 20:49:51 +0000 Subject: [PATCH 12/14] move some tests --- pkg/validator/container_test.go | 229 +--------------------------- pkg/validator/schema_test.go | 258 ++++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+), 225 deletions(-) create mode 100644 pkg/validator/schema_test.go diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index 1fdd02d0..66d20865 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -21,7 +21,6 @@ import ( conf "github.com/fairwindsops/polaris/pkg/config" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" ) var resourceConfMinimal = `--- @@ -48,69 +47,7 @@ exemptions: - foo ` -var resourceConfRanges = ` -checks: - memoryRequestsRange: error - memoryLimitsRange: warning -customChecks: - memoryLimitsRange: - containers: - exclude: - - initContainer - successMessage: Memory limits are within the required range - failureMessage: Memory limits should be within the required range - category: Resources - target: Container - schema: - '$schema': http://json-schema.org/draft-07/schema - type: object - required: - - resources - properties: - resources: - type: object - required: - - limits - properties: - limits: - type: object - required: - - memory - properties: - memory: - type: string - resourceMinimum: 200M - resourceMaximum: 6G - memoryRequestsRange: - successMessage: Memory requests are within the required range - failureMessage: Memory requests should be within the required range - category: Resources - target: Container - containers: - exclude: - - initContainer - schema: - '$schema': http://json-schema.org/draft-07/schema - type: object - required: - - resources - properties: - resources: - type: object - required: - - requests - properties: - requests: - required: - - memory - properties: - memory: - type: string - resourceMinimum: 200M - resourceMaximum: 3G -` - -func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []*ResultMessage, expectedWarnings []*ResultMessage, expectedSuccesses []*ResultMessage) { +func testValidate(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []*ResultMessage, expectedWarnings []*ResultMessage, expectedSuccesses []*ResultMessage) { cv := ContainerValidation{ Container: container, ResourceValidation: &ResourceValidation{}, @@ -188,151 +125,7 @@ func TestValidateResourcesEmptyContainer(t *testing.T) { expectedSuccesses := []*ResultMessage{} - testValidateResources(t, &container, &resourceConfMinimal, "foo", expectedErrors, expectedWarnings, expectedSuccesses) -} - -func TestValidateResourcesPartiallyValid(t *testing.T) { - cpuRequest, err := resource.ParseQuantity("100m") - assert.NoError(t, err, "Error parsing quantity") - - cpuLimit, err := resource.ParseQuantity("200m") - assert.NoError(t, err, "Error parsing quantity") - - container := corev1.Container{ - Name: "Empty", - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - "cpu": cpuRequest, - }, - Limits: corev1.ResourceList{ - "cpu": cpuLimit, - }, - }, - } - - expectedWarnings := []*ResultMessage{ - { - ID: "memoryLimitsRange", - Type: "warning", - Message: "Memory limits should be within the required range", - Category: "Resources", - }, - } - - expectedErrors := []*ResultMessage{ - { - ID: "memoryRequestsRange", - Type: "error", - Message: "Memory requests should be within the required range", - Category: "Resources", - }, - } - - expectedSuccesses := []*ResultMessage{} - - testValidateResources(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, - } - - parsedConf, err := conf.Parse([]byte(resourceConfRanges)) - assert.NoError(t, err, "Expected no error when parsing config") - - err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvEmpty) - if err != nil { - panic(err) - } - assert.Len(t, cvEmpty.Errors, 1) - assert.Len(t, cvEmpty.Warnings, 1) - - err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvInit) - if err != nil { - panic(err) - } - assert.Len(t, cvInit.Errors, 0) -} - -func TestValidateResourcesFullyValid(t *testing.T) { - cpuRequest, err := resource.ParseQuantity("300m") - assert.NoError(t, err, "Error parsing quantity") - - cpuLimit, err := resource.ParseQuantity("400m") - assert.NoError(t, err, "Error parsing quantity") - - memoryRequest, err := resource.ParseQuantity("400Mi") - assert.NoError(t, err, "Error parsing quantity") - - memoryLimit, err := resource.ParseQuantity("500Mi") - assert.NoError(t, err, "Error parsing quantity") - - container := corev1.Container{ - Name: "Empty", - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - "cpu": cpuRequest, - "memory": memoryRequest, - }, - Limits: corev1.ResourceList{ - "cpu": cpuLimit, - "memory": memoryLimit, - }, - }, - } - - expectedSuccesses := []*ResultMessage{ - { - ID: "memoryRequestsRange", - Type: "success", - Message: "Memory requests are within the required range", - Category: "Resources", - }, - { - ID: "memoryLimitsRange", - Type: "success", - Message: "Memory limits are within the required range", - Category: "Resources", - }, - } - - testValidateResources(t, &container, &resourceConfRanges, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) - - 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", - }, - } - - testValidateResources(t, &container, &resourceConfMinimal, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) + testValidate(t, &container, &resourceConfMinimal, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } func TestValidateHealthChecks(t *testing.T) { @@ -1281,7 +1074,7 @@ func TestValidateResourcesExemption(t *testing.T) { expectedErrors := []*ResultMessage{} expectedSuccesses := []*ResultMessage{} - testValidateResources(t, &container, &resourceConfExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses) + testValidate(t, &container, &resourceConfExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses) expectedWarnings = []*ResultMessage{ { @@ -1315,23 +1108,9 @@ func TestValidateResourcesExemption(t *testing.T) { disallowExemptionsConf := resourceConfExemptions + "\ndisallowExemptions: true" - testValidateResources(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses) + testValidate(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } -/* -func TestValidateResourceRangeExemption(t *testing.T) { - container := corev1.Container{ - Name: "Empty", - } - - expectedWarnings := []*ResultMessage{} - expectedErrors := []*ResultMessage{} - expectedSuccesses := []*ResultMessage{} - - testValidateResources(t, &container, &resourceConfRangeExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses) -} -*/ - func resetCV(cv ContainerValidation) ContainerValidation { cv.Errors = []*ResultMessage{} cv.Successes = []*ResultMessage{} diff --git a/pkg/validator/schema_test.go b/pkg/validator/schema_test.go new file mode 100644 index 00000000..a6d8737b --- /dev/null +++ b/pkg/validator/schema_test.go @@ -0,0 +1,258 @@ +package validator + +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" +) + +var customCheckExemptions = ` +checks: + foo: error +customChecks: + foo: + successMessage: success! + failureMessage: fail! + target: Container + category: Security + schema: + properties: + image: + pattern: ^quay.io +exemptions: +- controllerNames: + - exempt + rules: + - foo +` + +var resourceConfRanges = ` +checks: + memoryRequestsRange: error + memoryLimitsRange: warning +customChecks: + memoryLimitsRange: + containers: + exclude: + - initContainer + successMessage: Memory limits are within the required range + failureMessage: Memory limits should be within the required range + category: Resources + target: Container + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - limits + properties: + limits: + type: object + required: + - memory + properties: + memory: + type: string + resourceMinimum: 200M + resourceMaximum: 6G + memoryRequestsRange: + successMessage: Memory requests are within the required range + failureMessage: Memory requests should be within the required range + category: Resources + target: Container + containers: + exclude: + - initContainer + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - requests + properties: + requests: + required: + - memory + properties: + memory: + type: string + resourceMinimum: 200M + resourceMaximum: 3G +` + +func TestValidateResourcesPartiallyValid(t *testing.T) { + cpuRequest, err := resource.ParseQuantity("100m") + assert.NoError(t, err, "Error parsing quantity") + + cpuLimit, err := resource.ParseQuantity("200m") + assert.NoError(t, err, "Error parsing quantity") + + container := corev1.Container{ + Name: "Empty", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + "cpu": cpuRequest, + }, + Limits: corev1.ResourceList{ + "cpu": cpuLimit, + }, + }, + } + + expectedWarnings := []*ResultMessage{ + { + ID: "memoryLimitsRange", + Type: "warning", + Message: "Memory limits should be within the required range", + Category: "Resources", + }, + } + + expectedErrors := []*ResultMessage{ + { + ID: "memoryRequestsRange", + Type: "error", + Message: "Memory requests should be within the required range", + Category: "Resources", + }, + } + + 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, + } + + parsedConf, err := conf.Parse([]byte(resourceConfRanges)) + assert.NoError(t, err, "Expected no error when parsing config") + + err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvEmpty) + if err != nil { + panic(err) + } + assert.Len(t, cvEmpty.Errors, 1) + assert.Len(t, cvEmpty.Warnings, 1) + + err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvInit) + if err != nil { + panic(err) + } + assert.Len(t, cvInit.Errors, 0) +} + +func TestValidateResourcesFullyValid(t *testing.T) { + cpuRequest, err := resource.ParseQuantity("300m") + assert.NoError(t, err, "Error parsing quantity") + + cpuLimit, err := resource.ParseQuantity("400m") + assert.NoError(t, err, "Error parsing quantity") + + memoryRequest, err := resource.ParseQuantity("400Mi") + assert.NoError(t, err, "Error parsing quantity") + + memoryLimit, err := resource.ParseQuantity("500Mi") + assert.NoError(t, err, "Error parsing quantity") + + container := corev1.Container{ + Name: "Empty", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + "cpu": cpuRequest, + "memory": memoryRequest, + }, + Limits: corev1.ResourceList{ + "cpu": cpuLimit, + "memory": memoryLimit, + }, + }, + } + + expectedSuccesses := []*ResultMessage{ + { + ID: "memoryRequestsRange", + Type: "success", + Message: "Memory requests are within the required range", + Category: "Resources", + }, + { + ID: "memoryLimitsRange", + Type: "success", + Message: "Memory limits are within the required range", + Category: "Resources", + }, + } + + testValidate(t, &container, &resourceConfRanges, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) + + 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", + }, + } + + testValidate(t, &container, &resourceConfMinimal, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) +} + +func TestValidateCustomCheckExemptions(t *testing.T) { + container := corev1.Container{ + Name: "example", + Image: "hub.docker.com/foo", + } + + expectedWarnings := []*ResultMessage{} + expectedErrors := []*ResultMessage{} + expectedSuccesses := []*ResultMessage{} + testValidate(t, &container, &customCheckExemptions, "exempt", expectedErrors, expectedWarnings, expectedSuccesses) + + expectedErrors = []*ResultMessage{ + { + ID: "foo", + Type: "error", + Message: "fail!", + Category: "Security", + }, + } + testValidate(t, &container, &customCheckExemptions, "notexempt", expectedErrors, expectedWarnings, expectedSuccesses) +} From 051319fab7cc00ef156ac92509b4d9516439c0b6 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Fri, 3 Jan 2020 17:56:18 +0000 Subject: [PATCH 13/14] add logs to webhook test --- test/webhook_test.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/webhook_test.sh b/test/webhook_test.sh index c5d299fe..9909ad32 100755 --- a/test/webhook_test.sh +++ b/test/webhook_test.sh @@ -70,6 +70,7 @@ for filename in test/webhook_cases/passing_test.*.yaml; do if ! kubectl apply -f $filename &> /dev/null; then ALL_TESTS_PASSED=0 echo "Test Failed: Polaris prevented a deployment with no configuration issues." + kubectl logs -n polaris $(kubectl get po -oname -n polaris | grep webhook) fi done @@ -79,6 +80,7 @@ for filename in test/webhook_cases/failing_test.*.yaml; do if kubectl apply -f $filename &> /dev/null; then ALL_TESTS_PASSED=0 echo "Test Failed: Polaris should have prevented this deployment due to configuration issues." + kubectl logs -n polaris $(kubectl get po -oname -n polaris | grep webhook) fi done From e7baeac2e5c6e55924bdd750778ebefc0ed57aa7 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Fri, 3 Jan 2020 18:32:29 +0000 Subject: [PATCH 14/14] add script for generating deploy files --- CONTRIBUTING.md | 5 +- deploy/dashboard.yaml | 137 +++++++++++++++++++++------ deploy/webhook.yaml | 137 +++++++++++++++++++++------ scripts/generate-deployment-files.sh | 17 ++++ 4 files changed, 231 insertions(+), 65 deletions(-) create mode 100755 scripts/generate-deployment-files.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c99d6e47..39ff4703 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,9 +94,8 @@ The steps are: 2. Bump the version number in: 1. main.go 2. README.md - 3. Regenerate the deployment files. Assuming you've cloned the charts repo to `./charts`: - 1. `helm template ./charts/stable/polaris/ --name polaris --namespace polaris --set templateOnly=true > deploy/dashboard.yaml` - 2. `helm template ./charts/stable/polaris/ --name polaris --namespace polaris --set templateOnly=true --set webhook.enable=true --set dashboard.enable=false > deploy/webhook.yaml` + 3. Regenerate the deployment files. Assuming you've cloned the charts repo to `~/git/charts`: + 1. `CHARTS_DIR=~/git/charts ./scripts/generate-deployment-files.sh` 4. Update CHANGELOG.md 5. Merge your PR 3. Tag the latest branch for this repo diff --git a/deploy/dashboard.yaml b/deploy/dashboard.yaml index 2af9813c..ee514972 100644 --- a/deploy/dashboard.yaml +++ b/deploy/dashboard.yaml @@ -14,56 +14,131 @@ metadata: labels: app: polaris data: - config.yaml: | - resources: + config.yaml: |- + checks: + # resources cpuRequestsMissing: warning cpuLimitsMissing: warning memoryRequestsMissing: warning memoryLimitsMissing: warning - images: + # images tagNotSpecified: error - healthChecks: + pullPolicyNotAlways: ignore + # healthChecks readinessProbeMissing: warning livenessProbeMissing: warning - networking: + # networking hostNetworkSet: warning hostPortSet: warning - security: + # security hostIPCSet: error hostPIDSet: error notReadOnlyRootFileSystem: warning privilegeEscalationAllowed: error runAsRootAllowed: warning runAsPrivileged: error - capabilities: - error: - ifAnyAdded: - - SYS_ADMIN - - NET_ADMIN - - ALL - warning: - ifAnyAddedBeyond: - - CHOWN - - DAC_OVERRIDE - - FSETID - - FOWNER - - MKNOD - - NET_RAW - - SETGID - - SETUID - - SETFCAP - - SETPCAP - - NET_BIND_SERVICE - - SYS_CHROOT - - KILL - - AUDIT_WRITE - controllers_to_scan: + dangerousCapabilities: error + insecureCapabilities: warning + controllersToScan: - Deployments - StatefulSets - DaemonSets - - Jobs - CronJobs + - Jobs - ReplicationControllers + exemptions: + - controllerNames: + - dns-controller + - datadog-datadog + - kube-flannel-ds + - kube2iam + - aws-iam-authenticator + - datadog + - kube2iam + rules: + - hostNetworkSet + - controllerNames: + - aws-iam-authenticator + - aws-cluster-autoscaler + - kube-state-metrics + - dns-controller + - external-dns + - dnsmasq + - autoscaler + - kubernetes-dashboard + - install-cni + - kube2iam + rules: + - 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 + rules: + - 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 + rules: + - notReadOnlyRootFileSystem + - controllerNames: + - cert-manager + - dns-controller + - kubedns + - dnsmasq + - autoscaler + - insights-agent-goldilocks-vpa-install + rules: + - cpuRequestsMissing + - cpuLimitsMissing + - memoryRequestsMissing + - memoryLimitsMissing + - controllerNames: + - kube2iam + - kube-flannel-ds + rules: + - runAsPrivileged + - controllerNames: + - kube-hunter + rules: + - hostPIDSet + - controllerNames: + - polaris + - kube-hunter + - goldilocks + - insights-agent-goldilocks-vpa-install + rules: + - notReadOnlyRootFileSystem + - controllerNames: + - insights-agent-goldilocks-controller + rules: + - livenessProbeMissing + - readinessProbeMissing + - controllerNames: + - insights-agent-goldilocks-vpa-install + - kube-hunter + rules: + - runAsRootAllowed --- # Source: polaris/templates/dashboard.rbac.yaml @@ -164,7 +239,7 @@ spec: template: metadata: annotations: - checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2' + checksum/config: 'eb6d6b194c6786d62400fc0578dd5ea5158212b5b29d93d3cde3fa14da8ac501' labels: app: polaris component: dashboard diff --git a/deploy/webhook.yaml b/deploy/webhook.yaml index 8bc52f2d..f288d958 100644 --- a/deploy/webhook.yaml +++ b/deploy/webhook.yaml @@ -27,56 +27,131 @@ metadata: labels: app: polaris data: - config.yaml: | - resources: + config.yaml: |- + checks: + # resources cpuRequestsMissing: warning cpuLimitsMissing: warning memoryRequestsMissing: warning memoryLimitsMissing: warning - images: + # images tagNotSpecified: error - healthChecks: + pullPolicyNotAlways: ignore + # healthChecks readinessProbeMissing: warning livenessProbeMissing: warning - networking: + # networking hostNetworkSet: warning hostPortSet: warning - security: + # security hostIPCSet: error hostPIDSet: error notReadOnlyRootFileSystem: warning privilegeEscalationAllowed: error runAsRootAllowed: warning runAsPrivileged: error - capabilities: - error: - ifAnyAdded: - - SYS_ADMIN - - NET_ADMIN - - ALL - warning: - ifAnyAddedBeyond: - - CHOWN - - DAC_OVERRIDE - - FSETID - - FOWNER - - MKNOD - - NET_RAW - - SETGID - - SETUID - - SETFCAP - - SETPCAP - - NET_BIND_SERVICE - - SYS_CHROOT - - KILL - - AUDIT_WRITE - controllers_to_scan: + dangerousCapabilities: error + insecureCapabilities: warning + controllersToScan: - Deployments - StatefulSets - DaemonSets - - Jobs - CronJobs + - Jobs - ReplicationControllers + exemptions: + - controllerNames: + - dns-controller + - datadog-datadog + - kube-flannel-ds + - kube2iam + - aws-iam-authenticator + - datadog + - kube2iam + rules: + - hostNetworkSet + - controllerNames: + - aws-iam-authenticator + - aws-cluster-autoscaler + - kube-state-metrics + - dns-controller + - external-dns + - dnsmasq + - autoscaler + - kubernetes-dashboard + - install-cni + - kube2iam + rules: + - 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 + rules: + - 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 + rules: + - notReadOnlyRootFileSystem + - controllerNames: + - cert-manager + - dns-controller + - kubedns + - dnsmasq + - autoscaler + - insights-agent-goldilocks-vpa-install + rules: + - cpuRequestsMissing + - cpuLimitsMissing + - memoryRequestsMissing + - memoryLimitsMissing + - controllerNames: + - kube2iam + - kube-flannel-ds + rules: + - runAsPrivileged + - controllerNames: + - kube-hunter + rules: + - hostPIDSet + - controllerNames: + - polaris + - kube-hunter + - goldilocks + - insights-agent-goldilocks-vpa-install + rules: + - notReadOnlyRootFileSystem + - controllerNames: + - insights-agent-goldilocks-controller + rules: + - livenessProbeMissing + - readinessProbeMissing + - controllerNames: + - insights-agent-goldilocks-vpa-install + - kube-hunter + rules: + - runAsRootAllowed --- # Source: polaris/templates/webhook.rbac.yaml @@ -227,7 +302,7 @@ spec: template: metadata: annotations: - checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2' + checksum/config: 'eb6d6b194c6786d62400fc0578dd5ea5158212b5b29d93d3cde3fa14da8ac501' labels: app: polaris component: webhook diff --git a/scripts/generate-deployment-files.sh b/scripts/generate-deployment-files.sh new file mode 100755 index 00000000..252f412d --- /dev/null +++ b/scripts/generate-deployment-files.sh @@ -0,0 +1,17 @@ +# /bin/bash + +set -eo pipefail + +helm template $CHARTS_DIR/stable/polaris/ \ + --name polaris --namespace polaris \ + --set templateOnly=true \ + --set config="$(cat ./examples/config.yaml)" \ + > deploy/dashboard.yaml + +helm template $CHARTS_DIR/stable/polaris/ \ + --name polaris --namespace polaris \ + --set templateOnly=true \ + --set webhook.enable=true \ + --set dashboard.enable=false \ + --set config="$(cat ./examples/config.yaml)" \ + > deploy/webhook.yaml