From 4942af12c6b023bfc8404ebe31c415431331ae37 Mon Sep 17 00:00:00 2001 From: Khuswant Rajpurohit Date: Sat, 22 Aug 2026 03:07:24 +0530 Subject: [PATCH 1/2] fix: filter runtime rules by context Signed-off-by: Khuswant Rajpurohit --- pkg/rulemanager/rule_manager.go | 3 +- pkg/rulemanager/rulecreator/context_match.go | 35 ++++ .../rulecreator/context_match_test.go | 157 +++++++++++++++ pkg/rulemanager/rulecreator/factory.go | 15 ++ .../rulecreator/factory_context_test.go | 181 ++++++++++++++++++ .../rulecreator/ruleengine_interface.go | 2 + .../rulecreator/ruleengine_mock.go | 11 ++ pkg/rulemanager/rulepolicy.go | 30 +-- pkg/rulemanager/rulepolicy_test.go | 102 ++++++++++ 9 files changed, 506 insertions(+), 30 deletions(-) create mode 100644 pkg/rulemanager/rulecreator/context_match.go create mode 100644 pkg/rulemanager/rulecreator/context_match_test.go create mode 100644 pkg/rulemanager/rulecreator/factory_context_test.go create mode 100644 pkg/rulemanager/rulepolicy_test.go diff --git a/pkg/rulemanager/rule_manager.go b/pkg/rulemanager/rule_manager.go index db0933f00a..dbfe241a7c 100644 --- a/pkg/rulemanager/rule_manager.go +++ b/pkg/rulemanager/rule_manager.go @@ -297,8 +297,7 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) if enrichedEvent.SourceContext == nil || enrichedEvent.SourceContext.Context() == contextdetection.Kubernetes { rules = rm.ruleBindingCache.ListRulesForPod(namespace, pod) } else { - // TODO: rule filtering based on context - rules = rm.ruleBindingCache.GetRuleCreator().CreateAllRules() + rules = rm.ruleBindingCache.GetRuleCreator().CreateRulesForContext(enrichedEvent.SourceContext.Context()) } if len(rules) == 0 { diff --git a/pkg/rulemanager/rulecreator/context_match.go b/pkg/rulemanager/rulecreator/context_match.go new file mode 100644 index 0000000000..f658046f7c --- /dev/null +++ b/pkg/rulemanager/rulecreator/context_match.go @@ -0,0 +1,35 @@ +package rulecreator + +import ( + "strings" + + "github.com/kubescape/node-agent/pkg/contextdetection" + typesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" +) + +// RuleMatchesContext is the single source of truth for context-based rule matching. +func RuleMatchesContext(rule *typesv1.Rule, currentContext contextdetection.EventSourceContext) bool { + isContainerContext := currentContext == contextdetection.Kubernetes || + currentContext == contextdetection.Standalone || + currentContext == contextdetection.Container || + currentContext == contextdetection.ECS + + var hasContextTags bool + for _, tag := range rule.Tags { + if ctx, found := strings.CutPrefix(tag, "context:"); found { + if ctx == string(currentContext) { + return true + } + if ctx == string(contextdetection.Container) && isContainerContext { + return true + } + hasContextTags = true + } + } + + if !hasContextTags { + return currentContext == contextdetection.Kubernetes + } + + return false +} diff --git a/pkg/rulemanager/rulecreator/context_match_test.go b/pkg/rulemanager/rulecreator/context_match_test.go new file mode 100644 index 0000000000..23b9bb00ef --- /dev/null +++ b/pkg/rulemanager/rulecreator/context_match_test.go @@ -0,0 +1,157 @@ +package rulecreator + +import ( + "testing" + + "github.com/kubescape/node-agent/pkg/contextdetection" + typesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" +) + +func TestRuleMatchesContext(t *testing.T) { + tests := []struct { + name string + rule typesv1.Rule + context contextdetection.EventSourceContext + expected bool + }{ + { + name: "host tag matches host context", + rule: typesv1.Rule{Tags: []string{"context:host"}}, + context: contextdetection.Host, + expected: true, + }, + { + name: "host tag does not match kubernetes context", + rule: typesv1.Rule{Tags: []string{"context:host"}}, + context: contextdetection.Kubernetes, + expected: false, + }, + { + name: "kubernetes tag matches kubernetes context", + rule: typesv1.Rule{Tags: []string{"context:kubernetes"}}, + context: contextdetection.Kubernetes, + expected: true, + }, + { + name: "kubernetes tag does not match host context", + rule: typesv1.Rule{Tags: []string{"context:kubernetes"}}, + context: contextdetection.Host, + expected: false, + }, + { + name: "standalone tag matches standalone context", + rule: typesv1.Rule{Tags: []string{"context:standalone"}}, + context: contextdetection.Standalone, + expected: true, + }, + { + name: "ecs tag matches ecs context", + rule: typesv1.Rule{Tags: []string{"context:ecs"}}, + context: contextdetection.ECS, + expected: true, + }, + { + name: "container tag matches kubernetes context", + rule: typesv1.Rule{Tags: []string{"context:container"}}, + context: contextdetection.Kubernetes, + expected: true, + }, + { + name: "container tag matches standalone context", + rule: typesv1.Rule{Tags: []string{"context:container"}}, + context: contextdetection.Standalone, + expected: true, + }, + { + name: "container tag matches ecs context", + rule: typesv1.Rule{Tags: []string{"context:container"}}, + context: contextdetection.ECS, + expected: true, + }, + { + name: "container tag matches container context", + rule: typesv1.Rule{Tags: []string{"context:container"}}, + context: contextdetection.Container, + expected: true, + }, + { + name: "container tag does not match host context", + rule: typesv1.Rule{Tags: []string{"context:container"}}, + context: contextdetection.Host, + expected: false, + }, + + { + name: "no context tags defaults to kubernetes", + rule: typesv1.Rule{Tags: []string{"some-other-tag"}}, + context: contextdetection.Kubernetes, + expected: true, + }, + { + name: "no context tags rejects host", + rule: typesv1.Rule{Tags: []string{"some-other-tag"}}, + context: contextdetection.Host, + expected: false, + }, + { + name: "no context tags rejects standalone", + rule: typesv1.Rule{Tags: []string{}}, + context: contextdetection.Standalone, + expected: false, + }, + { + name: "nil tags defaults to kubernetes", + rule: typesv1.Rule{}, + context: contextdetection.Kubernetes, + expected: true, + }, + { + name: "nil tags rejects host", + rule: typesv1.Rule{}, + context: contextdetection.Host, + expected: false, + }, + + { + name: "multiple context tags: host+kubernetes matches host", + rule: typesv1.Rule{Tags: []string{"context:host", "context:kubernetes"}}, + context: contextdetection.Host, + expected: true, + }, + { + name: "multiple context tags: host+kubernetes matches kubernetes", + rule: typesv1.Rule{Tags: []string{"context:host", "context:kubernetes"}}, + context: contextdetection.Kubernetes, + expected: true, + }, + { + name: "multiple context tags: host+kubernetes rejects standalone", + rule: typesv1.Rule{Tags: []string{"context:host", "context:kubernetes"}}, + context: contextdetection.Standalone, + expected: false, + }, + + { + name: "mixed tags with context:host matches host", + rule: typesv1.Rule{Tags: []string{"severity:high", "context:host", "category:network"}}, + context: contextdetection.Host, + expected: true, + }, + { + name: "mixed tags with context:host rejects kubernetes", + rule: typesv1.Rule{Tags: []string{"severity:high", "context:host", "category:network"}}, + context: contextdetection.Kubernetes, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := RuleMatchesContext(&tt.rule, tt.context) + if result != tt.expected { + t.Errorf("RuleMatchesContext() = %v, want %v (rule tags: %v, context: %s)", + result, tt.expected, tt.rule.Tags, tt.context) + } + }) + } +} diff --git a/pkg/rulemanager/rulecreator/factory.go b/pkg/rulemanager/rulecreator/factory.go index 9a685e1b90..ba2444c73e 100644 --- a/pkg/rulemanager/rulecreator/factory.go +++ b/pkg/rulemanager/rulecreator/factory.go @@ -4,6 +4,7 @@ import ( "slices" "sync" + "github.com/kubescape/node-agent/pkg/contextdetection" "github.com/kubescape/node-agent/pkg/rulemanager/prefilter" typesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" "github.com/kubescape/node-agent/pkg/utils" @@ -101,6 +102,20 @@ func (r *RuleCreatorImpl) CreateAllRules() []typesv1.Rule { return rules } +func (r *RuleCreatorImpl) CreateRulesForContext(ctx contextdetection.EventSourceContext) []typesv1.Rule { + var rules []typesv1.Rule + for i := range r.Rules { + if !RuleMatchesContext(&r.Rules[i], ctx) { + continue + } + if r.Rules[i].Prefilter == nil { + r.Rules[i].Prefilter = prefilter.ParseWithDefaults(r.Rules[i].State, nil) + } + rules = append(rules, r.Rules[i]) + } + return rules +} + // SyncRules replaces the current rules with the new set of rules // It removes rules that are no longer present and adds/updates existing ones func (r *RuleCreatorImpl) SyncRules(newRules []typesv1.Rule) { diff --git a/pkg/rulemanager/rulecreator/factory_context_test.go b/pkg/rulemanager/rulecreator/factory_context_test.go new file mode 100644 index 0000000000..dd9f696c12 --- /dev/null +++ b/pkg/rulemanager/rulecreator/factory_context_test.go @@ -0,0 +1,181 @@ +package rulecreator + +import ( + "testing" + + "github.com/kubescape/node-agent/pkg/contextdetection" + typesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" +) + +func testRules() []typesv1.Rule { + return []typesv1.Rule{ + {ID: "host-only", Name: "Host Rule", Enabled: true, Tags: []string{"context:host"}}, + {ID: "k8s-only", Name: "K8s Rule", Enabled: true, Tags: []string{"context:kubernetes"}}, + {ID: "standalone-only", Name: "Standalone Rule", Enabled: true, Tags: []string{"context:standalone"}}, + {ID: "ecs-only", Name: "ECS Rule", Enabled: true, Tags: []string{"context:ecs"}}, + {ID: "container-meta", Name: "Container Rule", Enabled: true, Tags: []string{"context:container"}}, + {ID: "no-context", Name: "Legacy Rule", Enabled: true, Tags: []string{"severity:high"}}, + {ID: "multi-context", Name: "Multi Context", Enabled: true, Tags: []string{"context:host", "context:standalone"}}, + } +} + +func TestCreateRulesForContext(t *testing.T) { + tests := []struct { + name string + context contextdetection.EventSourceContext + expectedIDs []string + }{ + { + name: "host context returns host-only and multi-context rules", + context: contextdetection.Host, + expectedIDs: []string{ + "host-only", + "multi-context", + }, + }, + { + name: "kubernetes context returns k8s-only, container-meta, and no-context (backward compat)", + context: contextdetection.Kubernetes, + expectedIDs: []string{ + "k8s-only", + "container-meta", + "no-context", + }, + }, + { + name: "standalone context returns standalone-only, container-meta, and multi-context", + context: contextdetection.Standalone, + expectedIDs: []string{ + "standalone-only", + "container-meta", + "multi-context", + }, + }, + { + name: "ecs context returns ecs-only and container-meta", + context: contextdetection.ECS, + expectedIDs: []string{ + "ecs-only", + "container-meta", + }, + }, + { + name: "container context returns container-meta only", + context: contextdetection.Container, + expectedIDs: []string{ + "container-meta", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creator := &RuleCreatorImpl{Rules: testRules()} + rules := creator.CreateRulesForContext(tt.context) + + gotIDs := make(map[string]bool) + for _, r := range rules { + gotIDs[r.ID] = true + } + + expectedSet := make(map[string]bool) + for _, id := range tt.expectedIDs { + expectedSet[id] = true + } + + for _, id := range tt.expectedIDs { + if !gotIDs[id] { + t.Errorf("expected rule %q to be included for context %s, but it was not", id, tt.context) + } + } + + for _, r := range rules { + if !expectedSet[r.ID] { + t.Errorf("unexpected rule %q included for context %s", r.ID, tt.context) + } + } + + if len(rules) != len(tt.expectedIDs) { + t.Errorf("got %d rules, want %d", len(rules), len(tt.expectedIDs)) + } + + }) + } +} + +func TestCreateRulesForContext_PrefilterInitialized(t *testing.T) { + creator := &RuleCreatorImpl{ + Rules: []typesv1.Rule{ + { + ID: "host-rule", + Enabled: true, + Tags: []string{"context:host"}, + State: map[string]any{ + "ports": []uint16{443}, + }, + }, + }, + } + + rules := creator.CreateRulesForContext(contextdetection.Host) + if len(rules) != 1 { + t.Fatalf("expected 1 rule, got %d", len(rules)) + } + + if rules[0].Prefilter == nil { + t.Error("expected Prefilter to be initialized, got nil") + } +} + +func TestCreateRulesForContext_EmptyRules(t *testing.T) { + creator := &RuleCreatorImpl{Rules: []typesv1.Rule{}} + rules := creator.CreateRulesForContext(contextdetection.Host) + if len(rules) != 0 { + t.Errorf("expected 0 rules for empty creator, got %d", len(rules)) + } +} + +func TestCreateRulesForContext_NoMatchingRules(t *testing.T) { + creator := &RuleCreatorImpl{ + Rules: []typesv1.Rule{ + {ID: "k8s-only", Tags: []string{"context:kubernetes"}}, + }, + } + rules := creator.CreateRulesForContext(contextdetection.Host) + if len(rules) != 0 { + t.Errorf("expected 0 rules for non-matching context, got %d", len(rules)) + } +} + +func TestCreateRulesForContext_ConsistentWithRuleMatchesContext(t *testing.T) { + allContexts := []contextdetection.EventSourceContext{ + contextdetection.Kubernetes, + contextdetection.Host, + contextdetection.Standalone, + contextdetection.Container, + contextdetection.ECS, + } + + allRules := testRules() + creator := &RuleCreatorImpl{Rules: allRules} + + for _, ctx := range allContexts { + t.Run(string(ctx), func(t *testing.T) { + filteredRules := creator.CreateRulesForContext(ctx) + filteredIDs := make(map[string]bool) + for _, r := range filteredRules { + filteredIDs[r.ID] = true + } + + for i := range allRules { + expected := RuleMatchesContext(&allRules[i], ctx) + got := filteredIDs[allRules[i].ID] + + if expected != got { + t.Errorf("inconsistency for rule %q in context %s: RuleMatchesContext=%v, CreateRulesForContext included=%v", + allRules[i].ID, ctx, expected, got) + } + } + }) + } +} diff --git a/pkg/rulemanager/rulecreator/ruleengine_interface.go b/pkg/rulemanager/rulecreator/ruleengine_interface.go index 75f8bc5cb4..f09b808ef0 100644 --- a/pkg/rulemanager/rulecreator/ruleengine_interface.go +++ b/pkg/rulemanager/rulecreator/ruleengine_interface.go @@ -1,6 +1,7 @@ package rulecreator import ( + "github.com/kubescape/node-agent/pkg/contextdetection" typesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" "github.com/kubescape/node-agent/pkg/utils" @@ -25,6 +26,7 @@ type RuleCreator interface { CreateRulesByEventType(eventType utils.EventType) []typesv1.Rule CreateRulePolicyRulesByEventType(eventType utils.EventType) []typesv1.Rule CreateAllRules() []typesv1.Rule + CreateRulesForContext(ctx contextdetection.EventSourceContext) []typesv1.Rule GetAllRuleIDs() []string // Dynamic rule management methods for CRD sync diff --git a/pkg/rulemanager/rulecreator/ruleengine_mock.go b/pkg/rulemanager/rulecreator/ruleengine_mock.go index a56f82f8b0..a004e0fa2f 100644 --- a/pkg/rulemanager/rulecreator/ruleengine_mock.go +++ b/pkg/rulemanager/rulecreator/ruleengine_mock.go @@ -1,6 +1,7 @@ package rulecreator import ( + "github.com/kubescape/node-agent/pkg/contextdetection" typesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" "github.com/kubescape/node-agent/pkg/utils" ) @@ -49,6 +50,16 @@ func (r *RuleCreatorMock) CreateAllRules() []typesv1.Rule { return []typesv1.Rule{} } +func (r *RuleCreatorMock) CreateRulesForContext(ctx contextdetection.EventSourceContext) []typesv1.Rule { + var rules []typesv1.Rule + for i := range r.Rules { + if RuleMatchesContext(&r.Rules[i], ctx) { + rules = append(rules, r.Rules[i]) + } + } + return rules +} + func (r *RuleCreatorMock) GetAllRuleIDs() []string { var ids []string for _, rule := range r.Rules { diff --git a/pkg/rulemanager/rulepolicy.go b/pkg/rulemanager/rulepolicy.go index b14b0fa70a..542d7f5a9a 100644 --- a/pkg/rulemanager/rulepolicy.go +++ b/pkg/rulemanager/rulepolicy.go @@ -2,10 +2,10 @@ package rulemanager import ( "slices" - "strings" "github.com/kubescape/node-agent/pkg/contextdetection" "github.com/kubescape/node-agent/pkg/objectcache" + "github.com/kubescape/node-agent/pkg/rulemanager/rulecreator" typesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" ) @@ -33,7 +33,6 @@ func (v *RulePolicyValidator) Validate(ruleId string, process string, pcp *objec return false, nil } -// RuleAppliesToContext checks if a rule should execute in the given context func RuleAppliesToContext(rule *typesv1.Rule, contextInfo contextdetection.ContextInfo) bool { var currentContext contextdetection.EventSourceContext if contextInfo == nil { @@ -41,30 +40,5 @@ func RuleAppliesToContext(rule *typesv1.Rule, contextInfo contextdetection.Conte } else { currentContext = contextInfo.Context() } - - // container is a meta-context matching any containerized workload (kubernetes, standalone, container, ecs) - isContainerContext := currentContext == contextdetection.Kubernetes || - currentContext == contextdetection.Standalone || - currentContext == contextdetection.Container || - currentContext == contextdetection.ECS - - var hasContextTags bool - for _, tag := range rule.Tags { - if ctx, found := strings.CutPrefix(tag, "context:"); found { - if ctx == string(currentContext) { - return true - } - if ctx == string(contextdetection.Container) && isContainerContext { - return true - } - hasContextTags = true - } - } - - // No context specified in tags: default to kubernetes only (backward compatible) - if !hasContextTags { - return currentContext == contextdetection.Kubernetes - } - - return false + return rulecreator.RuleMatchesContext(rule, currentContext) } diff --git a/pkg/rulemanager/rulepolicy_test.go b/pkg/rulemanager/rulepolicy_test.go new file mode 100644 index 0000000000..8f0319c27a --- /dev/null +++ b/pkg/rulemanager/rulepolicy_test.go @@ -0,0 +1,102 @@ +package rulemanager + +import ( + "testing" + + "github.com/kubescape/node-agent/pkg/contextdetection" + typesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1" +) + +type mockContextInfo struct { + ctx contextdetection.EventSourceContext + workloadID string +} + +func (m *mockContextInfo) Context() contextdetection.EventSourceContext { return m.ctx } +func (m *mockContextInfo) WorkloadID() string { return m.workloadID } + +func TestRuleAppliesToContext(t *testing.T) { + tests := []struct { + name string + rule typesv1.Rule + contextInfo contextdetection.ContextInfo + expected bool + }{ + { + name: "nil contextInfo: rule with no tags defaults to kubernetes", + rule: typesv1.Rule{Tags: []string{}}, + contextInfo: nil, + expected: true, + }, + { + name: "nil contextInfo: rule with host tag rejected", + rule: typesv1.Rule{Tags: []string{"context:host"}}, + contextInfo: nil, + expected: false, + }, + { + name: "nil contextInfo: rule with kubernetes tag accepted", + rule: typesv1.Rule{Tags: []string{"context:kubernetes"}}, + contextInfo: nil, + expected: true, + }, + { + name: "nil contextInfo: rule with container tag accepted (kubernetes is container-type)", + rule: typesv1.Rule{Tags: []string{"context:container"}}, + contextInfo: nil, + expected: true, + }, + { + name: "host context: host tag matches", + rule: typesv1.Rule{Tags: []string{"context:host"}}, + contextInfo: &mockContextInfo{ctx: contextdetection.Host}, + expected: true, + }, + { + name: "host context: kubernetes tag rejected", + rule: typesv1.Rule{Tags: []string{"context:kubernetes"}}, + contextInfo: &mockContextInfo{ctx: contextdetection.Host}, + expected: false, + }, + { + name: "host context: no tags rejected (backward compat = k8s only)", + rule: typesv1.Rule{Tags: []string{"other"}}, + contextInfo: &mockContextInfo{ctx: contextdetection.Host}, + expected: false, + }, + + { + name: "standalone context: standalone tag matches", + rule: typesv1.Rule{Tags: []string{"context:standalone"}}, + contextInfo: &mockContextInfo{ctx: contextdetection.Standalone}, + expected: true, + }, + { + name: "standalone context: container meta-tag matches", + rule: typesv1.Rule{Tags: []string{"context:container"}}, + contextInfo: &mockContextInfo{ctx: contextdetection.Standalone}, + expected: true, + }, + { + name: "ecs context: ecs tag matches", + rule: typesv1.Rule{Tags: []string{"context:ecs"}}, + contextInfo: &mockContextInfo{ctx: contextdetection.ECS}, + expected: true, + }, + { + name: "ecs context: host tag rejected", + rule: typesv1.Rule{Tags: []string{"context:host"}}, + contextInfo: &mockContextInfo{ctx: contextdetection.ECS}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := RuleAppliesToContext(&tt.rule, tt.contextInfo) + if result != tt.expected { + t.Errorf("RuleAppliesToContext() = %v, want %v", result, tt.expected) + } + }) + } +} From 6d7406d74646400e5c86f32e261161e4db88da30 Mon Sep 17 00:00:00 2001 From: Khuswant Rajpurohit Date: Sat, 22 Aug 2026 03:21:07 +0530 Subject: [PATCH 2/2] fix: synchronize rule creation Signed-off-by: Khuswant Rajpurohit --- pkg/rulemanager/rulecreator/factory.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/rulemanager/rulecreator/factory.go b/pkg/rulemanager/rulecreator/factory.go index ba2444c73e..8c99ebb089 100644 --- a/pkg/rulemanager/rulecreator/factory.go +++ b/pkg/rulemanager/rulecreator/factory.go @@ -92,6 +92,9 @@ func (r *RuleCreatorImpl) GetAllRuleIDs() []string { } func (r *RuleCreatorImpl) CreateAllRules() []typesv1.Rule { + r.mutex.Lock() + defer r.mutex.Unlock() + var rules []typesv1.Rule for i := range r.Rules { if r.Rules[i].Prefilter == nil { @@ -103,6 +106,9 @@ func (r *RuleCreatorImpl) CreateAllRules() []typesv1.Rule { } func (r *RuleCreatorImpl) CreateRulesForContext(ctx contextdetection.EventSourceContext) []typesv1.Rule { + r.mutex.Lock() + defer r.mutex.Unlock() + var rules []typesv1.Rule for i := range r.Rules { if !RuleMatchesContext(&r.Rules[i], ctx) {