diff --git a/pkg/detection/mocks/policy_set.go b/pkg/detection/mocks/policy_set.go index ea70076843123..25ff607e12b75 100644 --- a/pkg/detection/mocks/policy_set.go +++ b/pkg/detection/mocks/policy_set.go @@ -109,6 +109,18 @@ func (mr *MockPolicySetMockRecorder) RemovePolicy(policyID any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePolicy", reflect.TypeOf((*MockPolicySet)(nil).RemovePolicy), policyID) } +// UpsertCompiledPolicy mocks base method. +func (m *MockPolicySet) UpsertCompiledPolicy(compiled detection.CompiledPolicy) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpsertCompiledPolicy", compiled) +} + +// UpsertCompiledPolicy indicates an expected call of UpsertCompiledPolicy. +func (mr *MockPolicySetMockRecorder) UpsertCompiledPolicy(compiled any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertCompiledPolicy", reflect.TypeOf((*MockPolicySet)(nil).UpsertCompiledPolicy), compiled) +} + // UpsertPolicy mocks base method. func (m *MockPolicySet) UpsertPolicy(arg0 *storage.Policy) error { m.ctrl.T.Helper() diff --git a/pkg/detection/policy_set.go b/pkg/detection/policy_set.go index 5d059d22fb425..b808ca28cab14 100644 --- a/pkg/detection/policy_set.go +++ b/pkg/detection/policy_set.go @@ -21,6 +21,7 @@ type PolicySet interface { Exists(id string) bool UpsertPolicy(*storage.Policy) error + UpsertCompiledPolicy(compiled CompiledPolicy) RemovePolicy(policyID string) } diff --git a/pkg/detection/policy_set_impl.go b/pkg/detection/policy_set_impl.go index 29cbc09d8226f..6962f5df7c6a3 100644 --- a/pkg/detection/policy_set_impl.go +++ b/pkg/detection/policy_set_impl.go @@ -48,6 +48,12 @@ func (p *setImpl) UpsertPolicy(policy *storage.Policy) error { return nil } +// UpsertCompiledPolicy adds or updates an already-compiled policy in the set, +// avoiding the recompilation that UpsertPolicy would perform. +func (p *setImpl) UpsertCompiledPolicy(compiled CompiledPolicy) { + p.policyIDToCompiled.Set(compiled.Policy().GetId(), compiled) +} + // RemovePolicy removes a policy from the set. func (p *setImpl) RemovePolicy(policyID string) { p.policyIDToCompiled.Delete(policyID) diff --git a/sensor/admission-control/manager/evaluate_deploytime.go b/sensor/admission-control/manager/evaluate_deploytime.go index 6477d97dc838c..d359b7e3e7166 100644 --- a/sensor/admission-control/manager/evaluate_deploytime.go +++ b/sensor/admission-control/manager/evaluate_deploytime.go @@ -11,6 +11,7 @@ import ( "github.com/stackrox/rox/pkg/booleanpolicy/policyfields" "github.com/stackrox/rox/pkg/detection/deploytime" "github.com/stackrox/rox/pkg/enforcers" + "github.com/stackrox/rox/pkg/images/types" "github.com/stackrox/rox/pkg/kubernetes" "github.com/stackrox/rox/pkg/namespaces" "github.com/stackrox/rox/pkg/protoconv/resources" @@ -161,6 +162,34 @@ func (m *manager) evaluateAdmissionRequest(s *state, req *admission.AdmissionReq } } + // Fast path: evaluate deployment-spec-only policies (no image fetches required). + fastPathImages := toPlaceholderImages(deployment) + fastPathAlerts, err := s.fastPathDeployDetector.Detect(context.Background(), detectionCtx, booleanpolicy.EnhancedDeployment{ + Deployment: deployment, + Images: fastPathImages, + }) + if err != nil { + observeAdmissionReview(reviewResultError, time.Since(start)) + return nil, errors.Wrap(err, "running StackRox detection") + } + if len(fastPathAlerts) > 0 { + if !pointer.BoolDeref(req.DryRun, false) { + go m.filterAndPutAttemptedAlertsOnChan(req.Operation, fastPathAlerts...) + } + slowPathCount := len(s.slowPathDeployDetector.PolicySet().GetCompiledPolicies()) + log.Debugf("Violated policies (fast path): %d, rejecting %s request on %s/%s [%s]", len(fastPathAlerts), req.Operation, req.Namespace, req.Name, req.Kind) + observeAdmissionReview(reviewResultDenied, time.Since(start)) + return fail(req.UID, message(fastPathAlerts, !s.GetClusterConfig().GetAdmissionControllerConfig().GetDisableBypass(), slowPathCount)), nil + } + + // Slow path: skip entirely if there are no policies that require image enrichment data. + if len(s.slowPathDeployDetector.PolicySet().GetCompiledPolicies()) == 0 { + log.Debugf("No policies violated, allowing %s request on %s/%s [%s]", req.Operation, req.Namespace, req.Name, req.Kind) + observeAdmissionReview(reviewResultAllowed, time.Since(start)) + return pass(req.UID), nil + } + + // Fetch images and evaluate slow path policies. If there are no modified images, skip the scan. var fetchImgCtx context.Context if timeoutSecs := s.GetClusterConfig().GetAdmissionControllerConfig().GetTimeoutSeconds(); timeoutSecs > 1 && hasModifiedImages(s, deployment, req) { var cancel context.CancelFunc @@ -169,7 +198,7 @@ func (m *manager) evaluateAdmissionRequest(s *state, req *admission.AdmissionReq } getAlertsFunc := func(dep *storage.Deployment, imgs []*storage.Image) ([]*storage.Alert, error) { - return s.deploytimeDetector.Detect(context.Background(), detectionCtx, booleanpolicy.EnhancedDeployment{ + return s.slowPathDeployDetector.Detect(context.Background(), detectionCtx, booleanpolicy.EnhancedDeployment{ Deployment: dep, Images: imgs, }) @@ -193,5 +222,13 @@ func (m *manager) evaluateAdmissionRequest(s *state, req *admission.AdmissionReq log.Debugf("Violated policies: %d, rejecting %s request on %s/%s [%s]", len(alerts), req.Operation, req.Namespace, req.Name, req.Kind) observeAdmissionReview(reviewResultDenied, time.Since(start)) - return fail(req.UID, message(alerts, !s.GetClusterConfig().GetAdmissionControllerConfig().GetDisableBypass())), nil + return fail(req.UID, message(alerts, !s.GetClusterConfig().GetAdmissionControllerConfig().GetDisableBypass(), 0)), nil +} + +func toPlaceholderImages(deployment *storage.Deployment) []*storage.Image { + images := make([]*storage.Image, len(deployment.GetContainers())) + for i, c := range deployment.GetContainers() { + images[i] = types.ToImage(c.GetImage()) + } + return images } diff --git a/sensor/admission-control/manager/evaluate_runtime.go b/sensor/admission-control/manager/evaluate_runtime.go index 469d86ed4252e..5127068cd0515 100644 --- a/sensor/admission-control/manager/evaluate_runtime.go +++ b/sensor/admission-control/manager/evaluate_runtime.go @@ -88,7 +88,7 @@ func (m *manager) evaluateRuntimeAdmissionRequest(s *state, req *admission.Admis if sendAlerts { go m.filterAndPutAttemptedAlertsOnChan(req.Operation, alerts...) } - return fail(req.UID, message(alerts, false)), nil + return fail(req.UID, message(alerts, false, 0)), nil } if sendAlerts { @@ -107,8 +107,8 @@ func (m *manager) evaluatePodEvent(s *state, req *admission.AdmissionRequest, ev // Fast path: skip image fetches when no k8s event policies require deploy-time fields. // Note: This webhook handles user-initiated commands (exec, port-forward) and // lacks the requirement for burst resilience at scale, so this optimization is intentionally kept simple - if len(s.slowPathDetector.PolicySet().GetCompiledPolicies()) == 0 { - alerts, err := s.fastPathDetector.DetectForDeploymentAndKubeEvent(context.Background(), + if len(s.slowPathK8sEventDetector.PolicySet().GetCompiledPolicies()) == 0 { + alerts, err := s.fastPathK8sEventDetector.DetectForDeploymentAndKubeEvent(context.Background(), booleanpolicy.EnhancedDeployment{ Deployment: deployment, Images: make([]*storage.Image, len(deployment.GetContainers())), @@ -151,7 +151,7 @@ func (m *manager) evaluatePodEvent(s *state, req *admission.AdmissionRequest, ev go m.waitForDeploymentAndDetect(s, event) } - alerts, err := s.fastPathDetector.DetectForDeploymentAndKubeEvent(context.Background(), booleanpolicy.EnhancedDeployment{}, event) + alerts, err := s.fastPathK8sEventDetector.DetectForDeploymentAndKubeEvent(context.Background(), booleanpolicy.EnhancedDeployment{}, event) if err != nil { return nil, false, errors.Wrap(err, "runtime detection without deployment enrichment") } @@ -160,7 +160,7 @@ func (m *manager) evaluatePodEvent(s *state, req *admission.AdmissionRequest, ev func (m *manager) waitForDeploymentAndDetect(s *state, event *storage.KubernetesEvent) { // No policies containing deploy-time fields; skip background image fetches and detection. - if len(s.slowPathDetector.PolicySet().GetCompiledPolicies()) == 0 { + if len(s.slowPathK8sEventDetector.PolicySet().GetCompiledPolicies()) == 0 { return } @@ -199,7 +199,7 @@ func (m *manager) waitForDeploymentAndDetect(s *state, event *storage.Kubernetes Deployment: dep, Images: imgs, } - return s.slowPathDetector.DetectForDeploymentAndKubeEvent(context.Background(), enhancedDeployment, event) + return s.slowPathK8sEventDetector.DetectForDeploymentAndKubeEvent(context.Background(), enhancedDeployment, event) } alerts, err := m.kickOffImgScansAndDetect(fetchImgCtx, s, getAlertsFunc, deployment) diff --git a/sensor/admission-control/manager/manager_impl.go b/sensor/admission-control/manager/manager_impl.go index 3dd59e365a37d..6e48985bb730b 100644 --- a/sensor/admission-control/manager/manager_impl.go +++ b/sensor/admission-control/manager/manager_impl.go @@ -44,11 +44,19 @@ var ( type state struct { *sensor.AdmissionControlSettings - deploytimeDetector deploytime.Detector + + // fastPathDeployDetector evaluates deploy policies that only reference + // deployment spec fields (privileged, capabilities, labels, etc.) and + // can produce a review response without requiring image enrichment data. + fastPathDeployDetector deploytime.Detector + + // slowPathDeployDetector evaluates deploy policies that require image + // enrichment data (scan results, image metadata, signatures). + slowPathDeployDetector deploytime.Detector allK8sEventPoliciesDetector runtime.Detector - fastPathDetector runtime.Detector - slowPathDetector runtime.Detector + slowPathK8sEventDetector runtime.Detector + fastPathK8sEventDetector runtime.Detector bypassForUsers, bypassForGroups set.FrozenStringSet enforcedOps map[admission.Operation]struct{} @@ -269,8 +277,8 @@ func (m *manager) ProcessNewSettings(newSettings *sensor.AdmissionControlSetting enforceOnCreates := newSettings.GetClusterConfig().GetAdmissionControllerConfig().GetEnabled() enforceOnUpdates := newSettings.GetClusterConfig().GetAdmissionControllerConfig().GetEnforceOnUpdates() - // TODO(ROX-33188): Wire cluster and namespace label providers. - deployTimePolicySet := detection.NewPolicySet(nil, nil) + fastPathPolicies := detection.NewPolicySet(nil, nil) + slowPathPolicies := detection.NewPolicySet(nil, nil) if enforceOnCreates || enforceOnUpdates { for _, policy := range newSettings.GetEnforcedDeployTimePolicies().GetPolicies() { if policyfields.AlertsOnMissingEnrichment(policy) && @@ -278,8 +286,15 @@ func (m *manager) ProcessNewSettings(newSettings *sensor.AdmissionControlSetting log.Warn(errors.ImageScanUnavailableMsg(policy)) continue } - if err := deployTimePolicySet.UpsertPolicy(policy); err != nil { - log.Errorf("Unable to upsert policy %q (%s), will not be able to enforce", policy.GetName(), policy.GetId()) + compiled, err := detection.CompilePolicy(policy, nil, nil) + if err != nil { + log.Errorf("Unable to compile policy %q (%s): %v", policy.GetName(), policy.GetId(), err) + continue + } + if compiled.RequiresImageEnrichment() { + slowPathPolicies.UpsertCompiledPolicy(compiled) + } else { + fastPathPolicies.UpsertCompiledPolicy(compiled) } } } @@ -296,10 +311,11 @@ func (m *manager) ProcessNewSettings(newSettings *sensor.AdmissionControlSetting oldState := m.currentState() newState := &state{ AdmissionControlSettings: newSettings, - deploytimeDetector: deploytime.NewDetector(deployTimePolicySet), + fastPathDeployDetector: deploytime.NewDetector(fastPathPolicies), + slowPathDeployDetector: deploytime.NewDetector(slowPathPolicies), allK8sEventPoliciesDetector: runtime.NewDetector(allK8sEventPolicySet), - slowPathDetector: runtime.NewDetector(k8sEventPoliciesWithDeployFields), - fastPathDetector: runtime.NewDetector(k8sEventPoliciesWithoutDeployFields), + slowPathK8sEventDetector: runtime.NewDetector(k8sEventPoliciesWithDeployFields), + fastPathK8sEventDetector: runtime.NewDetector(k8sEventPoliciesWithoutDeployFields), bypassForUsers: allowAlwaysUsers, bypassForGroups: allowAlwaysGroups, enforcedOps: enforcedOperations, @@ -346,10 +362,12 @@ func (m *manager) ProcessNewSettings(newSettings *sensor.AdmissionControlSetting } } log.Infof("Applied new admission control settings "+ - "(enforcing on %d deploy-time policies; "+ + "(enforcing on %d deploy-time policies: %d deployment metadata only, %d image enrichment data required; "+ "detecting on %d run-time policies; "+ "enforcing on %d run-time policies).", - len(deployTimePolicySet.GetCompiledPolicies()), + len(fastPathPolicies.GetCompiledPolicies())+len(slowPathPolicies.GetCompiledPolicies()), + len(fastPathPolicies.GetCompiledPolicies()), + len(slowPathPolicies.GetCompiledPolicies()), len(allK8sEventPolicySet.GetCompiledPolicies()), enforceablePolicies) diff --git a/sensor/admission-control/manager/responses.go b/sensor/admission-control/manager/responses.go index 294742f9fa60a..fb9675ee3753c 100644 --- a/sensor/admission-control/manager/responses.go +++ b/sensor/admission-control/manager/responses.go @@ -29,6 +29,9 @@ Policy: {{.Policy.Name}} - {{.Message}} {{- end}} +{{ end -}} +{{- if gt .UnevaluatedPolicyCount 0}} +{{.UnevaluatedPolicyCount}} additional {{if eq .UnevaluatedPolicyCount 1}}policy depends{{else}}policies depend{{end}} on image enrichment results and will be evaluated only after the above violations are addressed. {{ end -}} {{- if .BypassAnnotationKey}} In case of emergency, add the annotation {"{{.BypassAnnotationKey}}": "ticket-1234"} to your deployment with an updated ticket number @@ -62,12 +65,12 @@ func fail(uid types.UID, message string) *admission.AdmissionResponse { } } -func message(alerts []*storage.Alert, addBypassMsg bool) string { - +func message(alerts []*storage.Alert, addBypassMsg bool, unevaluatedPolicyCount int) string { // We add a line break at the beginning to look nicer in kubectl msgHeader := "\nThe attempted operation violated one or more enforced policies, described below:\n\n" data := map[string]interface{}{ - "Alerts": alerts, + "Alerts": alerts, + "UnevaluatedPolicyCount": unevaluatedPolicyCount, } if addBypassMsg { diff --git a/sensor/common/admissioncontroller/settings_manager_impl.go b/sensor/common/admissioncontroller/settings_manager_impl.go index 95c68389a379c..f53fdea9d6ef5 100644 --- a/sensor/common/admissioncontroller/settings_manager_impl.go +++ b/sensor/common/admissioncontroller/settings_manager_impl.go @@ -69,7 +69,10 @@ func (p *settingsManager) UpdatePolicies(policies []*storage.Policy) { if isEnforcedDeployTimePolicy(policy) { deploytimePolicies = append(deploytimePolicies, policy.CloneVT()) } + // Audit log event policies share field types (KubeEvent) with K8s webhook + // policies, so ContainsOneOf alone is insufficient to distinguish them. if pkgPolicies.AppliesAtRunTime(policy) && + policy.GetEventSource() == storage.EventSource_DEPLOYMENT_EVENT && booleanpolicy.ContainsOneOf(policy, booleanpolicy.KubeEvent) { runtimePolicies = append(runtimePolicies, policy.CloneVT()) }