1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00
kyverno/pkg/pss/evaluate.go
Alex Hamlin 218877dc03
Evaluate one version of each pod security standard (#10924)
The original logic for evaluating pod security standards took two steps
for each defined check:

1. If the policy author requested the latest version of the standard,
   find the newest version of the check and evaluate the pod against it,
   adding any failure to the final results.

2. Otherwise, evaluate the pod against *each version of the check* whose
   minimum version is below the requested version, adding any failures
   to the final results.

This second step can be problematic, as new PSS versions may permit a
broader range of values for a restricted field compared to old versions.
As a concrete example, versioned podSecurity rules don't permit some of
the newer sysctls allowed by Kubernetes v1.27 and v1.29, since Kyverno
still evaluates v1.0 of the check.

With this change, Kyverno identifies the highest version of the check
that the podSecurity rule allows, and only executes that version of the
check against the pod. Since the "latest" version is special-cased to
compare newer than all non-latest versions, no special logic is required
in that case.

I've added unit tests for several combinations of sysctl and policy
version, especially to check that policy v1.27 permits the new sysctl
allowed in v1.27 but not the sysctls allowed in v1.29. I've also taken
the liberty of changing `assert.Assert` to `assert.Check`, to collect
multiple failures from a single unit test run.

Signed-off-by: Alex Hamlin <alexanderh@qualtrics.com>
2024-09-03 18:58:40 +00:00

338 lines
11 KiB
Go

package pss
import (
"fmt"
"regexp"
"strconv"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/ext/wildcard"
pssutils "github.com/kyverno/kyverno/pkg/pss/utils"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/pod-security-admission/api"
"k8s.io/pod-security-admission/policy"
)
var (
regexIndex = regexp.MustCompile(`\d+`)
regexStr = regexp.MustCompile(`[a-zA-Z]+`)
)
// Evaluate Pod's specified containers only and get PSSCheckResults
func evaluatePSS(level *api.LevelVersion, pod corev1.Pod) (results []pssutils.PSSCheckResult) {
checks := policy.DefaultChecks()
for _, check := range checks {
if level.Level == api.LevelBaseline && check.Level != level.Level {
continue
}
selectedCheck := check.Versions[0]
for i := 1; i < len(check.Versions); i++ {
nextCheck := check.Versions[i]
if !level.Version.Older(nextCheck.MinimumVersion) && selectedCheck.MinimumVersion.Older(nextCheck.MinimumVersion) {
selectedCheck = nextCheck
}
}
checkResult := selectedCheck.CheckPod(&pod.ObjectMeta, &pod.Spec, policy.WithFieldErrors())
if !checkResult.Allowed {
results = append(results, pssutils.PSSCheckResult{
ID: string(check.ID),
CheckResult: checkResult,
RestrictedFields: GetRestrictedFields(check),
})
}
}
return results
}
func exemptExclusions(defaultCheckResults, excludeCheckResults []pssutils.PSSCheckResult, exclude kyvernov1.PodSecurityStandard, pod *corev1.Pod, matching *corev1.Pod, isContainerLevelExclusion bool) ([]pssutils.PSSCheckResult, error) {
defaultCheckResultsMap := make(map[string]pssutils.PSSCheckResult, len(defaultCheckResults))
for _, result := range defaultCheckResults {
defaultCheckResultsMap[result.ID] = result
}
for _, excludeResult := range excludeCheckResults {
for _, checkID := range pssutils.PSS_control_name_to_ids[exclude.ControlName] {
if excludeResult.ID == checkID {
if excludeResult.CheckResult.ErrList != nil {
for _, excludeFieldErr := range *excludeResult.CheckResult.ErrList {
var excludeField, excludeContainerType string
var excludeIndexes []int
var isContainerLevelField bool = false
var excludeContainer corev1.Container
if isContainerLevelExclusion {
excludeField, excludeIndexes, excludeContainerType, isContainerLevelField = parseField(excludeFieldErr.Field)
} else {
excludeField = regexIndex.ReplaceAllString(excludeFieldErr.Field, "*")
}
if isContainerLevelField {
excludeContainer = getContainerInfo(matching, excludeIndexes[0], excludeContainerType)
}
excludeBadValues := extractBadValues(excludeFieldErr)
if excludeField == exclude.RestrictedField || len(exclude.RestrictedField) == 0 {
flag := true
if len(exclude.Values) != 0 {
for _, badValue := range excludeBadValues {
if !wildcard.CheckPatterns(exclude.Values, badValue) {
flag = false
break
}
}
}
if flag {
defaultCheckResult := defaultCheckResultsMap[checkID]
if defaultCheckResult.CheckResult.ErrList != nil {
for idx, defaultFieldErr := range *defaultCheckResult.CheckResult.ErrList {
var defaultField, defaultContainerType string
var defaultIndexes []int
var isContainerLevelField bool = false
var defaultContainer corev1.Container
if isContainerLevelExclusion {
defaultField, defaultIndexes, defaultContainerType, isContainerLevelField = parseField(defaultFieldErr.Field)
} else {
defaultField = regexIndex.ReplaceAllString(defaultFieldErr.Field, "*")
}
if isContainerLevelField {
defaultContainer = getContainerInfo(pod, defaultIndexes[0], defaultContainerType)
if excludeField == defaultField && excludeContainer.Name == defaultContainer.Name {
remove(defaultCheckResult.CheckResult.ErrList, idx)
break
}
} else {
if excludeField == defaultField {
remove(defaultCheckResult.CheckResult.ErrList, idx)
break
}
}
}
if len(*defaultCheckResult.CheckResult.ErrList) == 0 {
delete(defaultCheckResultsMap, checkID)
} else {
defaultCheckResultsMap[checkID] = defaultCheckResult
}
}
}
}
}
}
}
}
}
newDefaultCheckResults := make([]pssutils.PSSCheckResult, 0, len(defaultCheckResultsMap))
for _, result := range defaultCheckResultsMap {
newDefaultCheckResults = append(newDefaultCheckResults, result)
}
return newDefaultCheckResults, nil
}
func extractBadValues(excludeFieldErr *field.Error) []string {
var excludeBadValues []string
switch excludeFieldErr.BadValue.(type) {
case string:
badValue := excludeFieldErr.BadValue.(string)
if badValue == "" {
break
}
excludeBadValues = append(excludeBadValues, badValue)
case bool:
excludeBadValues = append(excludeBadValues, strconv.FormatBool(excludeFieldErr.BadValue.(bool)))
case int:
excludeBadValues = append(excludeBadValues, strconv.Itoa(excludeFieldErr.BadValue.(int)))
case []string:
excludeBadValues = append(excludeBadValues, excludeFieldErr.BadValue.([]string)...)
}
return excludeBadValues
}
func remove(s *field.ErrorList, i int) {
(*s)[i] = (*s)[len(*s)-1]
*s = (*s)[:len(*s)-1]
}
func isContainerType(str string) bool {
return str == "containers" || str == "initContainers" || str == "ephemeralContainers"
}
func parseField(field string) (string, []int, string, bool) {
matchesIdx := regexIndex.FindAllStringSubmatch(field, -1)
matchesStr := regexStr.FindAllString(field, -1)
field = regexIndex.ReplaceAllString(field, "*")
indexes := make([]int, 0, len(matchesIdx))
for _, match := range matchesIdx {
index, _ := strconv.Atoi(match[0])
indexes = append(indexes, index)
}
return field, indexes, matchesStr[1], isContainerType(matchesStr[1])
}
func getContainerInfo(pod *corev1.Pod, index int, containerType string) corev1.Container {
var container corev1.Container
switch {
case containerType == "containers":
container = pod.Spec.Containers[index]
case containerType == "initContainers":
container = pod.Spec.InitContainers[index]
case containerType == "ephemeralContainers":
container = (corev1.Container)(pod.Spec.EphemeralContainers[index].EphemeralContainerCommon)
default:
}
return container
}
func ParseVersion(level api.Level, version string) (*api.LevelVersion, error) {
// Get pod security admission version
var apiVersion api.Version
// Version set to "latest" by default
if version == "" || version == "latest" {
apiVersion = api.LatestVersion()
} else {
parsedApiVersion, err := api.ParseVersion(version)
if err != nil {
return nil, err
}
apiVersion = api.MajorMinorVersion(parsedApiVersion.Major(), parsedApiVersion.Minor())
}
return &api.LevelVersion{
Level: level,
Version: apiVersion,
}, nil
}
// EvaluatePod applies PSS checks to the pod and exempts controls specified in the rule
func EvaluatePod(levelVersion *api.LevelVersion, excludes []kyvernov1.PodSecurityStandard, pod *corev1.Pod) (bool, []pssutils.PSSCheckResult) {
var err error
// apply the pod security checks on pods
defaultCheckResults := evaluatePSS(levelVersion, *pod)
// exclude pod security controls if specified
if len(excludes) > 0 {
defaultCheckResults, err = ApplyPodSecurityExclusion(levelVersion, excludes, defaultCheckResults, pod)
}
return (len(defaultCheckResults) == 0 && err == nil), defaultCheckResults
}
// ApplyPodSecurityExclusion excludes pod security controls
func ApplyPodSecurityExclusion(
levelVersion *api.LevelVersion,
excludes []kyvernov1.PodSecurityStandard,
defaultCheckResults []pssutils.PSSCheckResult,
pod *corev1.Pod,
) ([]pssutils.PSSCheckResult, error) {
var err error
for _, exclude := range excludes {
spec, matching := GetPodWithMatchingContainers(exclude, pod)
switch {
// exclude pod level checks
case spec != nil:
excludeCheckResults := evaluatePSS(levelVersion, *spec)
defaultCheckResults, err = exemptExclusions(defaultCheckResults, excludeCheckResults, exclude, pod, matching, false)
// exclude container level checks
default:
excludeCheckResults := evaluatePSS(levelVersion, *matching)
defaultCheckResults, err = exemptExclusions(defaultCheckResults, excludeCheckResults, exclude, pod, matching, true)
}
}
return defaultCheckResults, err
}
// GetPodWithMatchingContainers extracts matching container/pod info by the given exclude rule
// and returns pod manifests containing spec and container info respectively
func GetPodWithMatchingContainers(exclude kyvernov1.PodSecurityStandard, pod *corev1.Pod) (podSpec, matching *corev1.Pod) {
if len(exclude.Images) == 0 {
podSpec = pod.DeepCopy()
podSpec.Spec.Containers = []corev1.Container{{Name: "fake"}}
podSpec.Spec.InitContainers = nil
podSpec.Spec.EphemeralContainers = nil
return podSpec, nil
}
matchingImages := exclude.Images
matching = &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: pod.GetName(),
Namespace: pod.GetNamespace(),
},
}
for _, container := range pod.Spec.Containers {
if wildcard.CheckPatterns(matchingImages, container.Image) {
matching.Spec.Containers = append(matching.Spec.Containers, container)
}
}
for _, container := range pod.Spec.InitContainers {
if wildcard.CheckPatterns(matchingImages, container.Image) {
matching.Spec.InitContainers = append(matching.Spec.InitContainers, container)
}
}
for _, container := range pod.Spec.EphemeralContainers {
if wildcard.CheckPatterns(matchingImages, container.Image) {
matching.Spec.EphemeralContainers = append(matching.Spec.EphemeralContainers, container)
}
}
return nil, matching
}
// Get restrictedFields from Check.ID
func GetRestrictedFields(check policy.Check) []pssutils.RestrictedField {
for _, control := range pssutils.PSS_control_name_to_ids {
for _, checkID := range control {
if string(check.ID) == checkID {
return pssutils.PSS_controls[checkID]
}
}
}
return nil
}
func FormatChecksPrint(checks []pssutils.PSSCheckResult) string {
var str string
for _, check := range checks {
str += fmt.Sprintf("(Forbidden reason: %s, field error list: [", check.CheckResult.ForbiddenReason)
for idx, err := range *check.CheckResult.ErrList {
badValueExist := true
switch err.BadValue.(type) {
case string:
badValue := err.BadValue.(string)
if badValue == "" {
badValueExist = false
}
default:
}
switch err.Type {
case field.ErrorTypeForbidden:
if badValueExist {
str += fmt.Sprintf("%s is forbidden, forbidden values found: %+v", err.Field, err.BadValue)
} else {
str += err.Error()
}
default:
str += err.Error()
}
if idx != len(*check.CheckResult.ErrList)-1 {
str += ", "
}
}
str += "])"
}
return str
}