Skip to content

Commit 65f9e84

Browse files
clickboocursoragent
andcommitted
ROX-37161: strip exclude-by-kind rules for sensors without capability
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4e0382b commit 65f9e84

2 files changed

Lines changed: 130 additions & 0 deletions

File tree

‎central/sensor/service/connection/connection_impl.go‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"math"
7+
"slices"
78

89
"github.com/pkg/errors"
910
compScanSetting "github.com/stackrox/rox/central/complianceoperator/v2/scanconfigurations/datastore"
@@ -544,6 +545,13 @@ func (c *sensorConnection) getPolicySyncMsgFromPolicies(policies []*storage.Poli
544545
// Otherwise, sensor supports the same version or newer as central
545546
}
546547

548+
// Remove exclude_by_kind exclusions before syncing to a Sensor that
549+
// lacks WorkloadKindExclusionCap. Sensor forwards this same list to
550+
// the admission controller, so AC evaluates the same copy.
551+
if !c.HasCapability(centralsensor.WorkloadKindExclusionCap) {
552+
policies = stripExcludeByKindExclusions(policies)
553+
}
554+
547555
return &central.MsgToSensor{
548556
Msg: &central.MsgToSensor_PolicySync{
549557
PolicySync: &central.PolicySync{
@@ -553,6 +561,32 @@ func (c *sensorConnection) getPolicySyncMsgFromPolicies(policies []*storage.Poli
553561
}, nil
554562
}
555563

564+
// stripExcludeByKindExclusions returns a copy of policies with every exclusion
565+
// that sets exclude_by_kind removed. Stored policies are not mutated. An old
566+
// Sensor compiles an exclude_by_kind item as an unconstrained exclusion (nil
567+
// name and nil scope match every workload). Sensor does not evaluate image
568+
// exclusions, so an image+kind item with only the kind matcher removed is that
569+
// same unconstrained exclusion and is removed entirely. An exclusion with no
570+
// kind matcher is kept, including an image combined with a deployment name or
571+
// scope.
572+
func stripExcludeByKindExclusions(policies []*storage.Policy) []*storage.Policy {
573+
out := make([]*storage.Policy, 0, len(policies))
574+
for _, p := range policies {
575+
if !slices.ContainsFunc(p.GetExclusions(), exclusionSetsExcludeByKind) {
576+
out = append(out, p)
577+
continue
578+
}
579+
cloned := p.CloneVT()
580+
cloned.Exclusions = slices.DeleteFunc(cloned.GetExclusions(), exclusionSetsExcludeByKind)
581+
out = append(out, cloned)
582+
}
583+
return out
584+
}
585+
586+
func exclusionSetsExcludeByKind(e *storage.Exclusion) bool {
587+
return e.GetExcludeByKind() != nil
588+
}
589+
556590
func (c *sensorConnection) getNetworkBaselineSyncMsg(ctx context.Context) (*central.MsgToSensor, error) {
557591
var networkBaselines []*storage.NetworkBaseline
558592
walkFn := func() error {

‎central/sensor/service/connection/connection_test.go‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,102 @@ func (s *testSuite) TestGetPolicySyncMsgFromPoliciesDoesntDowngradeInvalidVersio
365365
s.Equal(policyversion.CurrentVersion().String(), policySync.GetPolicies()[0].GetPolicyVersion())
366366
}
367367

368+
func (s *testSuite) TestGetPolicySyncMsgFromPoliciesStripsExcludeByKind() {
369+
kindExclusion := &storage.Exclusion{
370+
Name: "skip-cronjobs",
371+
Matcher: &storage.Exclusion_ExcludeByKind_{
372+
ExcludeByKind: &storage.Exclusion_ExcludeByKind{
373+
Kinds: []storage.Exclusion_WorkloadKind{storage.Exclusion_CRON_JOB},
374+
},
375+
},
376+
}
377+
nameExclusion := &storage.Exclusion{
378+
Name: "skip-web",
379+
Matcher: &storage.Exclusion_Deployment_{
380+
Deployment: &storage.Exclusion_Deployment{Name: "web"},
381+
},
382+
}
383+
imageExclusion := &storage.Exclusion{
384+
Name: "skip-image",
385+
Image: &storage.Exclusion_Image{Name: "stackrox.io"},
386+
}
387+
imageAndKindExclusion := &storage.Exclusion{
388+
Name: "skip-image-and-jobs",
389+
Image: &storage.Exclusion_Image{Name: "example.io"},
390+
Matcher: &storage.Exclusion_ExcludeByKind_{
391+
ExcludeByKind: &storage.Exclusion_ExcludeByKind{
392+
Kinds: []storage.Exclusion_WorkloadKind{storage.Exclusion_JOB},
393+
},
394+
},
395+
}
396+
imageAndDeploymentExclusion := &storage.Exclusion{
397+
Name: "skip-image-and-web",
398+
Image: &storage.Exclusion_Image{Name: "nginx"},
399+
Matcher: &storage.Exclusion_Deployment_{
400+
Deployment: &storage.Exclusion_Deployment{Name: "web-image"},
401+
},
402+
}
403+
policy := &storage.Policy{
404+
Name: "test-policy",
405+
Exclusions: []*storage.Exclusion{
406+
kindExclusion,
407+
nameExclusion,
408+
imageExclusion,
409+
imageAndKindExclusion,
410+
imageAndDeploymentExclusion,
411+
},
412+
}
413+
414+
s.Run("without capability strips kind items and keeps others", func() {
415+
original := policy.CloneVT()
416+
msg, err := (&sensorConnection{}).getPolicySyncMsgFromPolicies([]*storage.Policy{policy})
417+
s.Require().NoError(err)
418+
419+
synced := msg.GetPolicySync().GetPolicies()
420+
s.Require().Len(synced, 1)
421+
got := synced[0].GetExclusions()
422+
s.Require().Len(got, 3)
423+
s.Equal("skip-web", got[0].GetName())
424+
s.Equal("web", got[0].GetDeployment().GetName())
425+
s.Equal("skip-image", got[1].GetName())
426+
s.Equal("stackrox.io", got[1].GetImage().GetName())
427+
s.Equal("skip-image-and-web", got[2].GetName())
428+
s.Equal("nginx", got[2].GetImage().GetName())
429+
s.Equal("web-image", got[2].GetDeployment().GetName())
430+
protoassert.Equal(s.T(), original, policy)
431+
})
432+
433+
s.Run("with capability keeps kind items", func() {
434+
conn := &sensorConnection{
435+
capabilities: set.NewSet(centralsensor.WorkloadKindExclusionCap),
436+
}
437+
msg, err := conn.getPolicySyncMsgFromPolicies([]*storage.Policy{policy})
438+
s.Require().NoError(err)
439+
440+
got := msg.GetPolicySync().GetPolicies()[0].GetExclusions()
441+
s.Require().Len(got, 5)
442+
s.NotNil(got[0].GetExcludeByKind())
443+
s.NotNil(got[3].GetExcludeByKind())
444+
s.Equal(storage.Exclusion_JOB, got[3].GetExcludeByKind().GetKinds()[0])
445+
s.Equal("example.io", got[3].GetImage().GetName())
446+
s.Equal("web-image", got[4].GetDeployment().GetName())
447+
})
448+
449+
s.Run("later capable sensor still sees original kind items", func() {
450+
// PreparePoliciesAndBroadcast reuses the same slice across connections.
451+
input := []*storage.Policy{policy}
452+
_, err := (&sensorConnection{}).getPolicySyncMsgFromPolicies(input)
453+
s.Require().NoError(err)
454+
455+
capable := &sensorConnection{
456+
capabilities: set.NewSet(centralsensor.WorkloadKindExclusionCap),
457+
}
458+
msg, err := capable.getPolicySyncMsgFromPolicies(input)
459+
s.Require().NoError(err)
460+
s.NotNil(msg.GetPolicySync().GetPolicies()[0].GetExclusions()[0].GetExcludeByKind())
461+
})
462+
}
463+
368464
func (s *testSuite) TestSendsAuditLogSyncMessageIfEnabledOnRun() {
369465
ctx := context.Background()
370466
clusterID := "this-cluster"

0 commit comments

Comments
 (0)