mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-28 18:38:40 +00:00
add control names and images to PSS results (#9869)
* add control names and images to PSS results Signed-off-by: Jim Bugwadia <jim@nirmata.com> * remove init Signed-off-by: Jim Bugwadia <jim@nirmata.com> * fix tets Signed-off-by: Jim Bugwadia <jim@nirmata.com> * update chainsaw tests Signed-off-by: Jim Bugwadia <jim@nirmata.com> * add unit test Signed-off-by: Jim Bugwadia <jim@nirmata.com> --------- Signed-off-by: Jim Bugwadia <jim@nirmata.com> Co-authored-by: shuting <shuting@nirmata.com>
This commit is contained in:
parent
02838a3cf7
commit
befcd73ea1
10 changed files with 139 additions and 49 deletions
|
@ -89,7 +89,7 @@ func Command() *cobra.Command {
|
|||
cmd.SilenceErrors = true
|
||||
printSkippedAndInvalidPolicies(out, skipInvalidPolicies)
|
||||
if applyCommandConfig.PolicyReport {
|
||||
printReport(out, responses, applyCommandConfig.AuditWarn)
|
||||
printReports(out, responses, applyCommandConfig.AuditWarn)
|
||||
} else if table {
|
||||
printTable(out, detailedResults, applyCommandConfig.AuditWarn, responses...)
|
||||
} else {
|
||||
|
@ -167,7 +167,7 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul
|
|||
if err != nil {
|
||||
return rc, resources1, skipInvalidPolicies, responses1, fmt.Errorf("Error: failed to load exceptions (%s)", err)
|
||||
}
|
||||
if !c.Stdin {
|
||||
if !c.Stdin && !c.PolicyReport {
|
||||
var policyRulesCount int
|
||||
for _, policy := range policies {
|
||||
policyRulesCount += len(autogen.ComputeRules(policy))
|
||||
|
@ -446,18 +446,17 @@ func printSkippedAndInvalidPolicies(out io.Writer, skipInvalidPolicies SkippedIn
|
|||
}
|
||||
}
|
||||
|
||||
func printReport(out io.Writer, engineResponses []engineapi.EngineResponse, auditWarn bool) {
|
||||
func printReports(out io.Writer, engineResponses []engineapi.EngineResponse, auditWarn bool) {
|
||||
clustered, namespaced := report.ComputePolicyReports(auditWarn, engineResponses...)
|
||||
if len(clustered) > 0 || len(namespaced) > 0 {
|
||||
fmt.Fprintln(out, divider)
|
||||
fmt.Fprintln(out, "POLICY REPORT:")
|
||||
fmt.Fprintln(out, divider)
|
||||
if len(clustered) > 0 {
|
||||
report := report.MergeClusterReports(clustered)
|
||||
yamlReport, _ := yaml.Marshal(report)
|
||||
fmt.Fprintln(out, string(yamlReport))
|
||||
} else {
|
||||
fmt.Fprintln(out, divider)
|
||||
fmt.Fprintln(out, "POLICY REPORT: skip generating policy report (no validate policy found/resource skipped)")
|
||||
}
|
||||
for _, r := range namespaced {
|
||||
fmt.Fprintln(out, string("---"))
|
||||
yamlReport, _ := yaml.Marshal(r)
|
||||
fmt.Fprintln(out, string(yamlReport))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ func (h validateImageHandler) Process(
|
|||
for _, v := range rule.VerifyImages {
|
||||
imageVerify := v.Convert()
|
||||
for _, infoMap := range policyContext.JSONContext().ImageInfo() {
|
||||
for name, imageInfo := range infoMap {
|
||||
for _, imageInfo := range infoMap {
|
||||
image := imageInfo.String()
|
||||
|
||||
if !engineutils.ImageMatches(image, imageVerify.ImageReferences) {
|
||||
|
@ -76,7 +76,7 @@ func (h validateImageHandler) Process(
|
|||
}
|
||||
|
||||
logger.V(4).Info("validating image", "image", image)
|
||||
if v, err := validateImage(policyContext, imageVerify, name, imageInfo, logger); err != nil {
|
||||
if v, err := validateImage(policyContext, imageVerify, imageInfo, logger); err != nil {
|
||||
return resource, handlers.WithFail(rule, engineapi.ImageVerify, err.Error())
|
||||
} else if v == engineapi.ImageVerificationSkip {
|
||||
skippedImages = append(skippedImages, image)
|
||||
|
@ -98,7 +98,7 @@ func (h validateImageHandler) Process(
|
|||
}
|
||||
}
|
||||
|
||||
func validateImage(ctx engineapi.PolicyContext, imageVerify *kyvernov1.ImageVerification, name string, imageInfo apiutils.ImageInfo, log logr.Logger) (engineapi.ImageVerificationMetadataStatus, error) {
|
||||
func validateImage(ctx engineapi.PolicyContext, imageVerify *kyvernov1.ImageVerification, imageInfo apiutils.ImageInfo, log logr.Logger) (engineapi.ImageVerificationMetadataStatus, error) {
|
||||
var verified engineapi.ImageVerificationMetadataStatus
|
||||
var err error
|
||||
image := imageInfo.String()
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
|
@ -14,6 +15,7 @@ import (
|
|||
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
|
||||
"github.com/kyverno/kyverno/pkg/pss"
|
||||
pssutils "github.com/kyverno/kyverno/pkg/pss/utils"
|
||||
"github.com/kyverno/kyverno/pkg/utils/api"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
@ -76,6 +78,7 @@ func (h validatePssHandler) Process(
|
|||
}
|
||||
allowed, pssChecks := pss.EvaluatePod(levelVersion, podSecurity.Exclude, pod)
|
||||
pssChecks = convertChecks(pssChecks, resource.GetKind())
|
||||
pssChecks = addImages(pssChecks, policyContext.JSONContext().ImageInfo())
|
||||
podSecurityChecks := engineapi.PodSecurityChecks{
|
||||
Level: podSecurity.Level,
|
||||
Version: podSecurity.Version,
|
||||
|
@ -134,12 +137,65 @@ func convertChecks(checks []pssutils.PSSCheckResult, kind string) (newChecks []p
|
|||
return checks
|
||||
}
|
||||
|
||||
// Extract container names from PSS error details. Here are some example inputs:
|
||||
// - "containers \"nginx\", \"busybox\" must set securityContext.allowPrivilegeEscalation=false"
|
||||
// - "containers \"nginx\", \"busybox\" must set securityContext.capabilities.drop=[\"ALL\"]"
|
||||
// - "pod or containers \"nginx\", \"busybox\" must set securityContext.runAsNonRoot=true"
|
||||
// - "pod or containers \"nginx\", \"busybox\" must set securityContext.seccompProfile.type to \"RuntimeDefault\" or \"Localhost\""
|
||||
// - "pod or container \"nginx\" must set securityContext.seccompProfile.type to \"RuntimeDefault\" or \"Localhost\""
|
||||
// - "container \"nginx\" must set securityContext.allowPrivilegeEscalation=false"
|
||||
var regexContainerNames = regexp.MustCompile(`container(?:s)?\s*(.*?)\s*must`)
|
||||
|
||||
func addImages(checks []pssutils.PSSCheckResult, imageInfos map[string]map[string]api.ImageInfo) []pssutils.PSSCheckResult {
|
||||
for i, check := range checks {
|
||||
text := check.CheckResult.ForbiddenDetail
|
||||
matches := regexContainerNames.FindAllStringSubmatch(text, -1)
|
||||
if len(matches) > 0 {
|
||||
s := strings.ReplaceAll(matches[0][1], "\"", "")
|
||||
s = strings.ReplaceAll(s, " ", "")
|
||||
containerNames := strings.Split(s, ",")
|
||||
checks[i].Images = getImages(containerNames, imageInfos)
|
||||
}
|
||||
}
|
||||
return checks
|
||||
}
|
||||
|
||||
// return image references for containers
|
||||
func getImages(containerNames []string, imageInfos map[string]map[string]api.ImageInfo) []string {
|
||||
var images []string
|
||||
for _, cn := range containerNames {
|
||||
image := getImageReference(cn, imageInfos)
|
||||
images = append(images, image)
|
||||
}
|
||||
return images
|
||||
}
|
||||
|
||||
// return an image references for a container name
|
||||
// if the image is not found, the name is returned
|
||||
func getImageReference(name string, imageInfos map[string]map[string]api.ImageInfo) string {
|
||||
if containers, ok := imageInfos["containers"]; ok {
|
||||
if imageInfo, ok := containers[name]; ok {
|
||||
return imageInfo.String()
|
||||
}
|
||||
}
|
||||
if initContainers, ok := imageInfos["initContainers"]; ok {
|
||||
if imageInfo, ok := initContainers[name]; ok {
|
||||
return imageInfo.String()
|
||||
}
|
||||
}
|
||||
if ephemeralContainers, ok := imageInfos["ephemeralContainers"]; ok {
|
||||
if imageInfo, ok := ephemeralContainers[name]; ok {
|
||||
return imageInfo.String()
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func getSpec(resource unstructured.Unstructured) (podSpec *corev1.PodSpec, metadata *metav1.ObjectMeta, err error) {
|
||||
kind := resource.GetKind()
|
||||
|
||||
if kind == "DaemonSet" || kind == "Deployment" || kind == "Job" || kind == "StatefulSet" || kind == "ReplicaSet" || kind == "ReplicationController" {
|
||||
var deployment appsv1.Deployment
|
||||
|
||||
resourceBytes, err := resource.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -153,7 +209,6 @@ func getSpec(resource unstructured.Unstructured) (podSpec *corev1.PodSpec, metad
|
|||
return podSpec, metadata, nil
|
||||
} else if kind == "CronJob" {
|
||||
var cronJob batchv1.CronJob
|
||||
|
||||
resourceBytes, err := resource.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -164,9 +219,9 @@ func getSpec(resource unstructured.Unstructured) (podSpec *corev1.PodSpec, metad
|
|||
}
|
||||
podSpec = &cronJob.Spec.JobTemplate.Spec.Template.Spec
|
||||
metadata = &cronJob.Spec.JobTemplate.ObjectMeta
|
||||
return podSpec, metadata, nil
|
||||
} else if kind == "Pod" {
|
||||
var pod corev1.Pod
|
||||
|
||||
resourceBytes, err := resource.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -178,11 +233,7 @@ func getSpec(resource unstructured.Unstructured) (podSpec *corev1.PodSpec, metad
|
|||
podSpec = &pod.Spec
|
||||
metadata = &pod.ObjectMeta
|
||||
return podSpec, metadata, nil
|
||||
} else {
|
||||
return nil, nil, fmt.Errorf("could not find correct resource type")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return podSpec, metadata, err
|
||||
|
||||
return nil, nil, fmt.Errorf("could not find correct resource type")
|
||||
}
|
||||
|
|
|
@ -331,7 +331,7 @@ func (v *validator) validatePatterns(resource unstructured.Unstructured) *engine
|
|||
return engineapi.RuleFail(v.rule.Name, engineapi.Validation, v.buildErrorMessage(err, pe.Path))
|
||||
}
|
||||
|
||||
return engineapi.RuleError(v.rule.Name, engineapi.Validation, v.buildErrorMessage(err, pe.Path), nil)
|
||||
return engineapi.RuleError(v.rule.Name, engineapi.Validation, v.buildErrorMessage(err, ""), nil)
|
||||
}
|
||||
|
||||
v.log.V(4).Info("successfully processed rule")
|
||||
|
|
|
@ -77,7 +77,7 @@ func exemptExclusions(defaultCheckResults, excludeCheckResults []pssutils.PSSChe
|
|||
}
|
||||
|
||||
for _, excludeResult := range excludeCheckResults {
|
||||
for _, checkID := range pssutils.PSS_controls_to_check_id[exclude.ControlName] {
|
||||
for _, checkID := range pssutils.PSS_control_name_to_ids[exclude.ControlName] {
|
||||
if excludeResult.ID == checkID {
|
||||
for _, excludeFieldErr := range *excludeResult.CheckResult.ErrList {
|
||||
var excludeField, excludeContainerType string
|
||||
|
@ -313,7 +313,7 @@ func GetPodWithMatchingContainers(exclude kyvernov1.PodSecurityStandard, pod *co
|
|||
|
||||
// Get restrictedFields from Check.ID
|
||||
func GetRestrictedFields(check policy.Check) []pssutils.RestrictedField {
|
||||
for _, control := range pssutils.PSS_controls_to_check_id {
|
||||
for _, control := range pssutils.PSS_control_name_to_ids {
|
||||
for _, checkID := range control {
|
||||
if string(check.ID) == checkID {
|
||||
return pssutils.PSS_controls[checkID]
|
||||
|
|
|
@ -42,7 +42,7 @@ var PSS_container_level_control = []string{
|
|||
// Translate PSS control to CheckResult.ID so that we can use PSS control in Kyverno policy
|
||||
// For PSS controls see: https://kubernetes.io/docs/concepts/security/pod-security-standards/
|
||||
// For CheckResult.ID see: https://github.com/kubernetes/pod-security-admission/tree/master/policy
|
||||
var PSS_controls_to_check_id = map[string][]string{
|
||||
var PSS_control_name_to_ids = map[string][]string{
|
||||
// Controls with 2 different controls for each level
|
||||
// container-level control
|
||||
"Capabilities": {
|
||||
|
@ -110,6 +110,20 @@ var PSS_controls_to_check_id = map[string][]string{
|
|||
},
|
||||
}
|
||||
|
||||
// reverse mapping of PSS_control_name_to_ids
|
||||
var pss_control_id_to_name = map[string]string{}
|
||||
|
||||
func PSSControlIDToName(id string) string {
|
||||
if len(pss_control_id_to_name) == 0 {
|
||||
for name, ids := range PSS_control_name_to_ids {
|
||||
for _, id := range ids {
|
||||
pss_control_id_to_name[id] = name
|
||||
}
|
||||
}
|
||||
}
|
||||
return pss_control_id_to_name[id]
|
||||
}
|
||||
|
||||
var PSS_controls = map[string][]RestrictedField{
|
||||
// Control name as key, same as ID field in CheckResult
|
||||
|
||||
|
|
|
@ -13,4 +13,5 @@ type PSSCheckResult struct {
|
|||
ID string
|
||||
CheckResult policy.CheckResult
|
||||
RestrictedFields []RestrictedField
|
||||
Images []string
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1"
|
||||
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
|
||||
controllerutils "github.com/kyverno/kyverno/pkg/utils/controller"
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
@ -169,7 +168,7 @@ func SetPolicyExceptionLabel(report kyvernov1alpha2.ReportInterface, exception k
|
|||
controllerutils.SetLabel(report, PolicyExceptionLabel(exception), exception.GetResourceVersion())
|
||||
}
|
||||
|
||||
func SetValidatingAdmissionPolicyBindingLabel(report kyvernov1alpha2.ReportInterface, binding v1alpha1.ValidatingAdmissionPolicyBinding) {
|
||||
func SetValidatingAdmissionPolicyBindingLabel(report kyvernov1alpha2.ReportInterface, binding admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding) {
|
||||
controllerutils.SetLabel(report, ValidatingAdmissionPolicyBindingLabel(binding), binding.GetResourceVersion())
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@ package report
|
|||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -12,6 +12,7 @@ import (
|
|||
kyvernov1alpha2 "github.com/kyverno/kyverno/api/kyverno/v1alpha2"
|
||||
policyreportv1alpha2 "github.com/kyverno/kyverno/api/policyreport/v1alpha2"
|
||||
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
|
||||
"github.com/kyverno/kyverno/pkg/pss/utils"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
@ -87,14 +88,6 @@ func SeverityFromString(severity string) policyreportv1alpha2.PolicySeverity {
|
|||
return ""
|
||||
}
|
||||
|
||||
func addProperty(k, v string, result *policyreportv1alpha2.PolicyReportResult) {
|
||||
if result.Properties == nil {
|
||||
result.Properties = map[string]string{}
|
||||
}
|
||||
|
||||
result.Properties[k] = v
|
||||
}
|
||||
|
||||
func ToPolicyReportResult(policyType engineapi.PolicyType, policyName string, ruleResult engineapi.RuleResponse, annotations map[string]string, resource *corev1.ObjectReference) policyreportv1alpha2.PolicyReportResult {
|
||||
result := policyreportv1alpha2.PolicyReportResult{
|
||||
Source: kyverno.ValueKyvernoApp,
|
||||
|
@ -122,18 +115,7 @@ func ToPolicyReportResult(policyType engineapi.PolicyType, policyName string, ru
|
|||
}
|
||||
pss := ruleResult.PodSecurityChecks()
|
||||
if pss != nil && len(pss.Checks) > 0 {
|
||||
var controls []string
|
||||
for _, check := range pss.Checks {
|
||||
if !check.CheckResult.Allowed {
|
||||
controls = append(controls, check.ID)
|
||||
}
|
||||
}
|
||||
if len(controls) > 0 {
|
||||
sort.Strings(controls)
|
||||
addProperty("standard", string(pss.Level), &result)
|
||||
addProperty("version", pss.Version, &result)
|
||||
addProperty("controls", strings.Join(controls, ","), &result)
|
||||
}
|
||||
addPodSecurityProperties(pss, &result)
|
||||
}
|
||||
if policyType == engineapi.ValidatingAdmissionPolicyType {
|
||||
result.Source = "ValidatingAdmissionPolicy"
|
||||
|
@ -145,6 +127,49 @@ func ToPolicyReportResult(policyType engineapi.PolicyType, policyName string, ru
|
|||
return result
|
||||
}
|
||||
|
||||
func addProperty(k, v string, result *policyreportv1alpha2.PolicyReportResult) {
|
||||
if result.Properties == nil {
|
||||
result.Properties = map[string]string{}
|
||||
}
|
||||
|
||||
result.Properties[k] = v
|
||||
}
|
||||
|
||||
type Control struct {
|
||||
ID string
|
||||
Name string
|
||||
Images []string
|
||||
}
|
||||
|
||||
func addPodSecurityProperties(pss *engineapi.PodSecurityChecks, result *policyreportv1alpha2.PolicyReportResult) {
|
||||
if pss == nil {
|
||||
return
|
||||
}
|
||||
if result.Properties == nil {
|
||||
result.Properties = map[string]string{}
|
||||
}
|
||||
var controls []Control
|
||||
var controlIDs []string
|
||||
for _, check := range pss.Checks {
|
||||
if !check.CheckResult.Allowed {
|
||||
controlName := utils.PSSControlIDToName(check.ID)
|
||||
controlIDs = append(controlIDs, check.ID)
|
||||
controls = append(controls, Control{
|
||||
ID: check.ID,
|
||||
Name: controlName,
|
||||
Images: check.Images,
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(controls) > 0 {
|
||||
controlsJson, _ := json.Marshal(controls)
|
||||
result.Properties["standard"] = string(pss.Level)
|
||||
result.Properties["version"] = pss.Version
|
||||
result.Properties["controls"] = strings.Join(controlIDs, ",")
|
||||
result.Properties["controlsJSON"] = string(controlsJson)
|
||||
}
|
||||
}
|
||||
|
||||
func EngineResponseToReportResults(response engineapi.EngineResponse) []policyreportv1alpha2.PolicyReportResult {
|
||||
pol := response.Policy()
|
||||
policyName, _ := cache.MetaNamespaceKeyFunc(pol.AsKyvernoPolicy())
|
||||
|
|
|
@ -19,6 +19,7 @@ results:
|
|||
policy: podsecurity-subrule-restricted
|
||||
properties:
|
||||
controls: capabilities_restricted
|
||||
controlsJSON: '[{"ID":"capabilities_restricted","Name":"Capabilities","Images":["docker.io/dummyimagename:latest"]}]'
|
||||
standard: restricted
|
||||
version: latest
|
||||
result: fail
|
||||
|
|
Loading…
Add table
Reference in a new issue