mirror of
https://github.com/FairwindsOps/polaris.git
synced 2026-02-14 09:59:53 +00:00
944 lines
26 KiB
Go
944 lines
26 KiB
Go
// Copyright 2019 FairwindsOps Inc
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package validator
|
|
|
|
import (
|
|
"testing"
|
|
|
|
conf "github.com/fairwindsops/polaris/pkg/config"
|
|
"github.com/stretchr/testify/assert"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"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 = `---
|
|
resources:
|
|
cpuRequestsMissing: warning
|
|
memoryRequestsMissing: warning
|
|
cpuLimitsMissing: error
|
|
memoryLimitsMissing: error
|
|
`
|
|
|
|
func TestValidateResourcesEmptyConfig(t *testing.T) {
|
|
container := corev1.Container{
|
|
Name: "Empty",
|
|
}
|
|
|
|
cv := ContainerValidation{
|
|
Container: &container,
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
expected := conf.Resources{}
|
|
|
|
cv.validateResources(&expected)
|
|
assert.Len(t, cv.Errors, 0)
|
|
}
|
|
|
|
func TestValidateResourcesEmptyContainer(t *testing.T) {
|
|
container := corev1.Container{
|
|
Name: "Empty",
|
|
}
|
|
|
|
expectedWarnings := []*ResultMessage{
|
|
{
|
|
Type: "warning",
|
|
Message: "CPU requests should be set",
|
|
Category: "Resources",
|
|
},
|
|
{
|
|
Type: "warning",
|
|
Message: "Memory requests should be set",
|
|
Category: "Resources",
|
|
},
|
|
}
|
|
|
|
expectedErrors := []*ResultMessage{
|
|
{
|
|
Type: "error",
|
|
Message: "CPU limits should be set",
|
|
Category: "Resources",
|
|
},
|
|
{
|
|
Type: "error",
|
|
Message: "Memory limits should be set",
|
|
Category: "Resources",
|
|
},
|
|
}
|
|
|
|
testValidateResources(t, &container, &resourceConf2, &expectedErrors, &expectedWarnings)
|
|
}
|
|
|
|
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{
|
|
{
|
|
Type: "warning",
|
|
Message: "CPU requests should be higher than 200m",
|
|
Category: "Resources",
|
|
},
|
|
{
|
|
Type: "warning",
|
|
Message: "CPU limits should be higher than 300m",
|
|
Category: "Resources",
|
|
},
|
|
}
|
|
|
|
expectedErrors := []*ResultMessage{
|
|
{
|
|
Type: "error",
|
|
Message: "Memory requests should be higher than 100M",
|
|
Category: "Resources",
|
|
},
|
|
{
|
|
Type: "error",
|
|
Message: "Memory limits should be higher than 200M",
|
|
Category: "Resources",
|
|
},
|
|
}
|
|
|
|
testValidateResources(t, &container, &resourceConf1, &expectedErrors, &expectedWarnings)
|
|
}
|
|
|
|
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(resourceConf1))
|
|
assert.NoError(t, err, "Expected no error when parsing config")
|
|
|
|
cvEmpty.validateResources(&parsedConf.Resources)
|
|
assert.Len(t, cvEmpty.Errors, 4)
|
|
|
|
cvInit.validateResources(&parsedConf.Resources)
|
|
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,
|
|
},
|
|
},
|
|
}
|
|
|
|
testValidateResources(t, &container, &resourceConf1, &[]*ResultMessage{}, &[]*ResultMessage{})
|
|
}
|
|
|
|
func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, expectedErrors *[]*ResultMessage, expectedWarnings *[]*ResultMessage) {
|
|
cv := ContainerValidation{
|
|
Container: container,
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
parsedConf, err := conf.Parse([]byte(*resourceConf))
|
|
assert.NoError(t, err, "Expected no error when parsing config")
|
|
|
|
cv.validateResources(&parsedConf.Resources)
|
|
assert.Len(t, cv.Warnings, len(*expectedWarnings))
|
|
assert.ElementsMatch(t, cv.Warnings, *expectedWarnings)
|
|
|
|
assert.Len(t, cv.Errors, len(*expectedErrors))
|
|
assert.ElementsMatch(t, cv.Errors, *expectedErrors)
|
|
}
|
|
|
|
func TestValidateHealthChecks(t *testing.T) {
|
|
|
|
// Test setup.
|
|
p1 := conf.HealthChecks{}
|
|
p2 := conf.HealthChecks{
|
|
ReadinessProbeMissing: conf.SeverityIgnore,
|
|
LivenessProbeMissing: conf.SeverityIgnore,
|
|
}
|
|
p3 := conf.HealthChecks{
|
|
ReadinessProbeMissing: conf.SeverityError,
|
|
LivenessProbeMissing: conf.SeverityWarning,
|
|
}
|
|
|
|
probe := corev1.Probe{}
|
|
emptyCV := ContainerValidation{
|
|
Container: &corev1.Container{Name: ""},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
emptyCVInit := ContainerValidation{
|
|
Container: &corev1.Container{Name: ""},
|
|
ResourceValidation: &ResourceValidation{},
|
|
IsInitContainer: true,
|
|
}
|
|
goodCV := ContainerValidation{
|
|
Container: &corev1.Container{
|
|
Name: "",
|
|
LivenessProbe: &probe,
|
|
ReadinessProbe: &probe,
|
|
},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
l := &ResultMessage{Type: "warning", Message: "Liveness probe should be configured", Category: "Health Checks"}
|
|
r := &ResultMessage{Type: "error", Message: "Readiness probe should be configured", Category: "Health Checks"}
|
|
f1 := []*ResultMessage{}
|
|
f2 := []*ResultMessage{r}
|
|
w1 := []*ResultMessage{l}
|
|
|
|
var testCases = []struct {
|
|
name string
|
|
probes conf.HealthChecks
|
|
cv ContainerValidation
|
|
errors *[]*ResultMessage
|
|
warnings *[]*ResultMessage
|
|
}{
|
|
{name: "probes not configured", probes: p1, cv: emptyCV, errors: &f1},
|
|
{name: "probes not required", probes: p2, cv: emptyCV, errors: &f1},
|
|
{name: "probes required & configured", probes: p3, cv: goodCV, errors: &f1},
|
|
{name: "probes required, not configured, but init", probes: p3, cv: emptyCVInit, errors: &f1},
|
|
{name: "probes required & not configured", probes: p3, cv: emptyCV, errors: &f2, warnings: &w1},
|
|
{name: "probes configured, but not required", probes: p2, cv: goodCV, errors: &f1},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.cv.validateHealthChecks(&tt.probes)
|
|
|
|
if tt.warnings != nil {
|
|
assert.Len(t, tt.cv.Warnings, len(*tt.warnings))
|
|
assert.ElementsMatch(t, tt.cv.Warnings, *tt.warnings)
|
|
}
|
|
|
|
assert.Len(t, tt.cv.Errors, len(*tt.errors))
|
|
assert.ElementsMatch(t, tt.cv.Errors, *tt.errors)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateImage(t *testing.T) {
|
|
emptyConf := conf.Images{}
|
|
standardConf := conf.Images{
|
|
TagNotSpecified: conf.SeverityError,
|
|
PullPolicyNotAlways: conf.SeverityIgnore,
|
|
}
|
|
strongConf := conf.Images{
|
|
TagNotSpecified: conf.SeverityError,
|
|
PullPolicyNotAlways: conf.SeverityError,
|
|
}
|
|
|
|
emptyCV := ContainerValidation{
|
|
Container: &corev1.Container{},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
badCV := ContainerValidation{
|
|
Container: &corev1.Container{Image: "test"},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
lessBadCV := ContainerValidation{
|
|
Container: &corev1.Container{Image: "test:latest", ImagePullPolicy: ""},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
goodCV := ContainerValidation{
|
|
Container: &corev1.Container{Image: "test:0.1.0", ImagePullPolicy: "Always"},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
var testCases = []struct {
|
|
name string
|
|
image conf.Images
|
|
cv ContainerValidation
|
|
expected []*ResultMessage
|
|
}{
|
|
{
|
|
name: "emptyConf + emptyCV",
|
|
image: emptyConf,
|
|
cv: emptyCV,
|
|
expected: []*ResultMessage{},
|
|
},
|
|
{
|
|
name: "standardConf + emptyCV",
|
|
image: standardConf,
|
|
cv: emptyCV,
|
|
expected: []*ResultMessage{{
|
|
Message: "Image tag should be specified",
|
|
Type: "error",
|
|
Category: "Images",
|
|
}},
|
|
},
|
|
{
|
|
name: "standardConf + badCV",
|
|
image: standardConf,
|
|
cv: badCV,
|
|
expected: []*ResultMessage{{
|
|
Message: "Image tag should be specified",
|
|
Type: "error",
|
|
Category: "Images",
|
|
}},
|
|
},
|
|
{
|
|
name: "standardConf + lessBadCV",
|
|
image: standardConf,
|
|
cv: lessBadCV,
|
|
expected: []*ResultMessage{{
|
|
Message: "Image tag should be specified",
|
|
Type: "error",
|
|
Category: "Images",
|
|
}},
|
|
},
|
|
{
|
|
name: "strongConf + badCV",
|
|
image: strongConf,
|
|
cv: badCV,
|
|
expected: []*ResultMessage{{
|
|
Message: "Image pull policy should be \"Always\"",
|
|
Type: "error",
|
|
Category: "Images",
|
|
}, {
|
|
Message: "Image tag should be specified",
|
|
Type: "error",
|
|
Category: "Images",
|
|
}},
|
|
},
|
|
{
|
|
name: "strongConf + goodCV",
|
|
image: strongConf,
|
|
cv: goodCV,
|
|
expected: []*ResultMessage{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.cv = resetCV(tt.cv)
|
|
tt.cv.validateImage(&tt.image)
|
|
assert.Len(t, tt.cv.Errors, len(tt.expected))
|
|
assert.ElementsMatch(t, tt.cv.Errors, tt.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateNetworking(t *testing.T) {
|
|
// Test setup.
|
|
emptyConf := conf.Networking{}
|
|
standardConf := conf.Networking{
|
|
HostPortSet: conf.SeverityWarning,
|
|
}
|
|
strongConf := conf.Networking{
|
|
HostPortSet: conf.SeverityError,
|
|
}
|
|
|
|
emptyCV := ContainerValidation{
|
|
Container: &corev1.Container{Name: ""},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
badCV := ContainerValidation{
|
|
Container: &corev1.Container{
|
|
Ports: []corev1.ContainerPort{{
|
|
ContainerPort: 3000,
|
|
HostPort: 443,
|
|
}},
|
|
},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
goodCV := ContainerValidation{
|
|
Container: &corev1.Container{
|
|
Ports: []corev1.ContainerPort{{
|
|
ContainerPort: 3000,
|
|
}},
|
|
},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
var testCases = []struct {
|
|
name string
|
|
networkConf conf.Networking
|
|
cv ContainerValidation
|
|
expectedMessages []*ResultMessage
|
|
}{
|
|
{
|
|
name: "empty ports + empty validation config",
|
|
networkConf: emptyConf,
|
|
cv: emptyCV,
|
|
expectedMessages: []*ResultMessage{},
|
|
},
|
|
{
|
|
name: "empty ports + standard validation config",
|
|
networkConf: standardConf,
|
|
cv: emptyCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Host port is not configured",
|
|
Type: "success",
|
|
Category: "Networking",
|
|
}},
|
|
},
|
|
{
|
|
name: "empty ports + strong validation config",
|
|
networkConf: standardConf,
|
|
cv: emptyCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Host port is not configured",
|
|
Type: "success",
|
|
Category: "Networking",
|
|
}},
|
|
},
|
|
{
|
|
name: "host ports + empty validation config",
|
|
networkConf: emptyConf,
|
|
cv: badCV,
|
|
expectedMessages: []*ResultMessage{},
|
|
},
|
|
{
|
|
name: "host ports + standard validation config",
|
|
networkConf: standardConf,
|
|
cv: badCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Host port should not be configured",
|
|
Type: "warning",
|
|
Category: "Networking",
|
|
}},
|
|
},
|
|
{
|
|
name: "no host ports + standard validation config",
|
|
networkConf: standardConf,
|
|
cv: goodCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Host port is not configured",
|
|
Type: "success",
|
|
Category: "Networking",
|
|
}},
|
|
},
|
|
{
|
|
name: "host ports + strong validation config",
|
|
networkConf: strongConf,
|
|
cv: badCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Host port should not be configured",
|
|
Type: "error",
|
|
Category: "Networking",
|
|
}},
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.cv = resetCV(tt.cv)
|
|
tt.cv.validateNetworking(&tt.networkConf)
|
|
assert.Len(t, tt.cv.messages(), len(tt.expectedMessages))
|
|
assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateSecurity(t *testing.T) {
|
|
trueVar := true
|
|
falseVar := false
|
|
|
|
// Test setup.
|
|
emptyConf := conf.Security{}
|
|
standardConf := conf.Security{
|
|
RunAsRootAllowed: conf.SeverityWarning,
|
|
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"},
|
|
},
|
|
},
|
|
}
|
|
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"},
|
|
},
|
|
},
|
|
}
|
|
|
|
emptyCV := ContainerValidation{
|
|
Container: &corev1.Container{Name: ""},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
badCV := ContainerValidation{
|
|
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
|
RunAsNonRoot: &falseVar,
|
|
ReadOnlyRootFilesystem: &falseVar,
|
|
Privileged: &trueVar,
|
|
AllowPrivilegeEscalation: &trueVar,
|
|
Capabilities: &corev1.Capabilities{
|
|
Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"},
|
|
},
|
|
}},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
badCVWithGoodPodSpec := ContainerValidation{
|
|
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
|
RunAsNonRoot: &falseVar,
|
|
ReadOnlyRootFilesystem: &falseVar,
|
|
Privileged: &trueVar,
|
|
AllowPrivilegeEscalation: &trueVar,
|
|
Capabilities: &corev1.Capabilities{
|
|
Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"},
|
|
},
|
|
}},
|
|
ResourceValidation: &ResourceValidation{},
|
|
parentPodSpec: corev1.PodSpec{
|
|
SecurityContext: &corev1.PodSecurityContext{
|
|
RunAsNonRoot: &trueVar,
|
|
},
|
|
},
|
|
}
|
|
|
|
badCVWithBadPodSpec := ContainerValidation{
|
|
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
|
RunAsNonRoot: nil, // this will use the default from the podspec
|
|
ReadOnlyRootFilesystem: &falseVar,
|
|
Privileged: &trueVar,
|
|
AllowPrivilegeEscalation: &trueVar,
|
|
Capabilities: &corev1.Capabilities{
|
|
Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"},
|
|
},
|
|
}},
|
|
ResourceValidation: &ResourceValidation{},
|
|
parentPodSpec: corev1.PodSpec{
|
|
SecurityContext: &corev1.PodSecurityContext{
|
|
RunAsNonRoot: &falseVar,
|
|
},
|
|
},
|
|
}
|
|
|
|
goodCV := ContainerValidation{
|
|
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
|
RunAsNonRoot: &trueVar,
|
|
ReadOnlyRootFilesystem: &trueVar,
|
|
Privileged: &falseVar,
|
|
AllowPrivilegeEscalation: &falseVar,
|
|
Capabilities: &corev1.Capabilities{
|
|
Drop: []corev1.Capability{"NET_BIND_SERVICE", "FOWNER"},
|
|
},
|
|
}},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
strongCV := ContainerValidation{
|
|
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
|
RunAsNonRoot: &trueVar,
|
|
ReadOnlyRootFilesystem: &trueVar,
|
|
Privileged: &falseVar,
|
|
AllowPrivilegeEscalation: &falseVar,
|
|
Capabilities: &corev1.Capabilities{
|
|
Drop: []corev1.Capability{"ALL"},
|
|
},
|
|
}},
|
|
ResourceValidation: &ResourceValidation{},
|
|
}
|
|
|
|
strongCVWithPodSpecSecurityContext := ContainerValidation{
|
|
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
|
RunAsNonRoot: nil, // not set but overridden via podSpec
|
|
ReadOnlyRootFilesystem: &trueVar,
|
|
Privileged: &falseVar,
|
|
AllowPrivilegeEscalation: &falseVar,
|
|
Capabilities: &corev1.Capabilities{
|
|
Drop: []corev1.Capability{"ALL"},
|
|
},
|
|
}},
|
|
ResourceValidation: &ResourceValidation{},
|
|
parentPodSpec: corev1.PodSpec{
|
|
SecurityContext: &corev1.PodSecurityContext{
|
|
RunAsNonRoot: &trueVar,
|
|
},
|
|
},
|
|
}
|
|
|
|
strongCVWithBadPodSpecSecurityContext := ContainerValidation{
|
|
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
|
RunAsNonRoot: &trueVar, // will override the bad setting in PodSpec
|
|
ReadOnlyRootFilesystem: &trueVar,
|
|
Privileged: &falseVar,
|
|
AllowPrivilegeEscalation: &falseVar,
|
|
Capabilities: &corev1.Capabilities{
|
|
Drop: []corev1.Capability{"ALL"},
|
|
},
|
|
}},
|
|
ResourceValidation: &ResourceValidation{},
|
|
parentPodSpec: corev1.PodSpec{
|
|
SecurityContext: &corev1.PodSecurityContext{
|
|
RunAsNonRoot: &falseVar, // is overridden at container level with RunAsNonRoot:true
|
|
},
|
|
},
|
|
}
|
|
|
|
var testCases = []struct {
|
|
name string
|
|
securityConf conf.Security
|
|
cv ContainerValidation
|
|
expectedMessages []*ResultMessage
|
|
}{
|
|
{
|
|
name: "empty security context + empty validation config",
|
|
securityConf: emptyConf,
|
|
cv: emptyCV,
|
|
expectedMessages: []*ResultMessage{},
|
|
},
|
|
{
|
|
name: "empty security context + standard validation config",
|
|
securityConf: standardConf,
|
|
cv: emptyCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Should not be allowed to run as root",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem should be read only",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Not running as privileged",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation not allowed",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Security capabilities are within the configured limits",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "bad security context + standard validation config",
|
|
securityConf: standardConf,
|
|
cv: badCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation should not be allowed",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Should not be running as privileged",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Should not be allowed to run as root",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem should be read only",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "bad security context + standard validation config with good settings in podspec",
|
|
securityConf: standardConf,
|
|
cv: badCVWithGoodPodSpec,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation should not be allowed",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Should not be running as privileged",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Should not be allowed to run as root",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem should be read only",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "bad security context + standard validation config from default set in podspec",
|
|
securityConf: standardConf,
|
|
cv: badCVWithBadPodSpec,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation should not be allowed",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Should not be running as privileged",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Should not be allowed to run as root",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem should be read only",
|
|
Type: "warning",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "good security context + standard validation config",
|
|
securityConf: standardConf,
|
|
cv: goodCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Is not allowed to run as root",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem is read only",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Not running as privileged",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation not allowed",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Security capabilities are within the configured limits",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "good security context + strong validation config",
|
|
securityConf: strongConf,
|
|
cv: goodCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "The following security capabilities should be dropped: DAC_OVERRIDE, SYS_CHROOT",
|
|
Type: "error",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Is not allowed to run as root",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem is read only",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Not running as privileged",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation not allowed",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "strong security context + strong validation config",
|
|
securityConf: strongConf,
|
|
cv: strongCV,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Is not allowed to run as root",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem is read only",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Not running as privileged",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation not allowed",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Security capabilities are within the configured limits",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "strong security context + strong validation config via podspec default",
|
|
securityConf: strongConf,
|
|
cv: strongCVWithPodSpecSecurityContext,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Is not allowed to run as root",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem is read only",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Not running as privileged",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation not allowed",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Security capabilities are within the configured limits",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
{
|
|
name: "strong security context + strong validation config with bad setting in podspec default",
|
|
securityConf: strongConf,
|
|
cv: strongCVWithBadPodSpecSecurityContext,
|
|
expectedMessages: []*ResultMessage{{
|
|
Message: "Is not allowed to run as root",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Filesystem is read only",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Not running as privileged",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Privilege escalation not allowed",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}, {
|
|
Message: "Security capabilities are within the configured limits",
|
|
Type: "success",
|
|
Category: "Security",
|
|
}},
|
|
},
|
|
}
|
|
|
|
for _, tt := range testCases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.cv = resetCV(tt.cv)
|
|
tt.cv.validateSecurity(&tt.securityConf)
|
|
assert.Len(t, tt.cv.messages(), len(tt.expectedMessages))
|
|
assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages)
|
|
})
|
|
}
|
|
}
|
|
|
|
func resetCV(cv ContainerValidation) ContainerValidation {
|
|
cv.Errors = []*ResultMessage{}
|
|
cv.Successes = []*ResultMessage{}
|
|
cv.Warnings = []*ResultMessage{}
|
|
return cv
|
|
}
|