From 0fa3b64c8f8a4f109c2adc8bf27e1177cf66a32f Mon Sep 17 00:00:00 2001 From: Khushboo Sancheti Date: Sat, 10 Oct 2026 22:08:11 -0700 Subject: [PATCH] ROX-37161: strip exclude-by-kind rules for sensors without capability Co-authored-by: Cursor --- .../service/connection/connection_impl.go | 34 +++++++ .../service/connection/connection_test.go | 96 +++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/central/sensor/service/connection/connection_impl.go b/central/sensor/service/connection/connection_impl.go index 5be9321a48f0f..b59fdec096b88 100644 --- a/central/sensor/service/connection/connection_impl.go +++ b/central/sensor/service/connection/connection_impl.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "slices" "github.com/pkg/errors" compScanSetting "github.com/stackrox/rox/central/complianceoperator/v2/scanconfigurations/datastore" @@ -544,6 +545,13 @@ func (c *sensorConnection) getPolicySyncMsgFromPolicies(policies []*storage.Poli // Otherwise, sensor supports the same version or newer as central } + // Remove exclude_by_kind exclusions before syncing to a Sensor that + // lacks WorkloadKindExclusionCap. Sensor forwards this same list to + // the admission controller, so AC evaluates the same copy. + if !c.HasCapability(centralsensor.WorkloadKindExclusionCap) { + policies = stripExcludeByKindExclusions(policies) + } + return ¢ral.MsgToSensor{ Msg: ¢ral.MsgToSensor_PolicySync{ PolicySync: ¢ral.PolicySync{ @@ -553,6 +561,32 @@ func (c *sensorConnection) getPolicySyncMsgFromPolicies(policies []*storage.Poli }, nil } +// stripExcludeByKindExclusions returns a copy of policies with every exclusion +// that sets exclude_by_kind removed. Stored policies are not mutated. An old +// Sensor compiles an exclude_by_kind item as an unconstrained exclusion (nil +// name and nil scope match every workload). Sensor does not evaluate image +// exclusions, so an image+kind item with only the kind matcher removed is that +// same unconstrained exclusion and is removed entirely. An exclusion with no +// kind matcher is kept, including an image combined with a deployment name or +// scope. +func stripExcludeByKindExclusions(policies []*storage.Policy) []*storage.Policy { + out := make([]*storage.Policy, 0, len(policies)) + for _, p := range policies { + if !slices.ContainsFunc(p.GetExclusions(), exclusionSetsExcludeByKind) { + out = append(out, p) + continue + } + cloned := p.CloneVT() + cloned.Exclusions = slices.DeleteFunc(cloned.GetExclusions(), exclusionSetsExcludeByKind) + out = append(out, cloned) + } + return out +} + +func exclusionSetsExcludeByKind(e *storage.Exclusion) bool { + return e.GetExcludeByKind() != nil +} + func (c *sensorConnection) getNetworkBaselineSyncMsg(ctx context.Context) (*central.MsgToSensor, error) { var networkBaselines []*storage.NetworkBaseline walkFn := func() error { diff --git a/central/sensor/service/connection/connection_test.go b/central/sensor/service/connection/connection_test.go index a8fd0d52fd2fc..eaf8523107feb 100644 --- a/central/sensor/service/connection/connection_test.go +++ b/central/sensor/service/connection/connection_test.go @@ -365,6 +365,102 @@ func (s *testSuite) TestGetPolicySyncMsgFromPoliciesDoesntDowngradeInvalidVersio s.Equal(policyversion.CurrentVersion().String(), policySync.GetPolicies()[0].GetPolicyVersion()) } +func (s *testSuite) TestGetPolicySyncMsgFromPoliciesStripsExcludeByKind() { + kindExclusion := &storage.Exclusion{ + Name: "skip-cronjobs", + Matcher: &storage.Exclusion_ExcludeByKind_{ + ExcludeByKind: &storage.Exclusion_ExcludeByKind{ + Kinds: []storage.Exclusion_WorkloadKind{storage.Exclusion_CRON_JOB}, + }, + }, + } + nameExclusion := &storage.Exclusion{ + Name: "skip-web", + Matcher: &storage.Exclusion_Deployment_{ + Deployment: &storage.Exclusion_Deployment{Name: "web"}, + }, + } + imageExclusion := &storage.Exclusion{ + Name: "skip-image", + Image: &storage.Exclusion_Image{Name: "stackrox.io"}, + } + imageAndKindExclusion := &storage.Exclusion{ + Name: "skip-image-and-jobs", + Image: &storage.Exclusion_Image{Name: "example.io"}, + Matcher: &storage.Exclusion_ExcludeByKind_{ + ExcludeByKind: &storage.Exclusion_ExcludeByKind{ + Kinds: []storage.Exclusion_WorkloadKind{storage.Exclusion_JOB}, + }, + }, + } + imageAndDeploymentExclusion := &storage.Exclusion{ + Name: "skip-image-and-web", + Image: &storage.Exclusion_Image{Name: "nginx"}, + Matcher: &storage.Exclusion_Deployment_{ + Deployment: &storage.Exclusion_Deployment{Name: "web-image"}, + }, + } + policy := &storage.Policy{ + Name: "test-policy", + Exclusions: []*storage.Exclusion{ + kindExclusion, + nameExclusion, + imageExclusion, + imageAndKindExclusion, + imageAndDeploymentExclusion, + }, + } + + s.Run("without capability strips kind items and keeps others", func() { + original := policy.CloneVT() + msg, err := (&sensorConnection{}).getPolicySyncMsgFromPolicies([]*storage.Policy{policy}) + s.Require().NoError(err) + + synced := msg.GetPolicySync().GetPolicies() + s.Require().Len(synced, 1) + got := synced[0].GetExclusions() + s.Require().Len(got, 3) + s.Equal("skip-web", got[0].GetName()) + s.Equal("web", got[0].GetDeployment().GetName()) + s.Equal("skip-image", got[1].GetName()) + s.Equal("stackrox.io", got[1].GetImage().GetName()) + s.Equal("skip-image-and-web", got[2].GetName()) + s.Equal("nginx", got[2].GetImage().GetName()) + s.Equal("web-image", got[2].GetDeployment().GetName()) + protoassert.Equal(s.T(), original, policy) + }) + + s.Run("with capability keeps kind items", func() { + conn := &sensorConnection{ + capabilities: set.NewSet(centralsensor.WorkloadKindExclusionCap), + } + msg, err := conn.getPolicySyncMsgFromPolicies([]*storage.Policy{policy}) + s.Require().NoError(err) + + got := msg.GetPolicySync().GetPolicies()[0].GetExclusions() + s.Require().Len(got, 5) + s.NotNil(got[0].GetExcludeByKind()) + s.NotNil(got[3].GetExcludeByKind()) + s.Equal(storage.Exclusion_JOB, got[3].GetExcludeByKind().GetKinds()[0]) + s.Equal("example.io", got[3].GetImage().GetName()) + s.Equal("web-image", got[4].GetDeployment().GetName()) + }) + + s.Run("later capable sensor still sees original kind items", func() { + // PreparePoliciesAndBroadcast reuses the same slice across connections. + input := []*storage.Policy{policy} + _, err := (&sensorConnection{}).getPolicySyncMsgFromPolicies(input) + s.Require().NoError(err) + + capable := &sensorConnection{ + capabilities: set.NewSet(centralsensor.WorkloadKindExclusionCap), + } + msg, err := capable.getPolicySyncMsgFromPolicies(input) + s.Require().NoError(err) + s.NotNil(msg.GetPolicySync().GetPolicies()[0].GetExclusions()[0].GetExcludeByKind()) + }) +} + func (s *testSuite) TestSendsAuditLogSyncMessageIfEnabledOnRun() { ctx := context.Background() clusterID := "this-cluster"