From 325b600e97daa75795d21d8a32c9c7152147f6ac Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Tue, 10 Feb 2026 15:28:28 +0200 Subject: [PATCH 1/7] optimize CreateEventFromRequest and MakeHttpEvent performance Signed-off-by: Yakir Oren --- pkg/containerwatcher/v2/tracers/httpparse.go | 25 +++++--------------- pkg/utils/datasource_event.go | 3 +-- pkg/utils/events.go | 6 +++++ pkg/utils/struct_event.go | 10 ++++---- 4 files changed, 17 insertions(+), 27 deletions(-) diff --git a/pkg/containerwatcher/v2/tracers/httpparse.go b/pkg/containerwatcher/v2/tracers/httpparse.go index ed80e6fb3..aa7f87c71 100644 --- a/pkg/containerwatcher/v2/tracers/httpparse.go +++ b/pkg/containerwatcher/v2/tracers/httpparse.go @@ -14,20 +14,6 @@ import ( "github.com/kubescape/storage/pkg/apis/softwarecomposition/consts" ) -var writeSyscalls = map[string]bool{ - "write": true, - "writev": true, - "sendto": true, - "sendmsg": true, -} - -var readSyscalls = map[string]bool{ - "read": true, - "readv": true, - "recvfrom": true, - "recvmsg": true, -} - var ConsistentHeaders = []string{ "Accept-Encoding", "Accept-Language", @@ -37,12 +23,12 @@ var ConsistentHeaders = []string{ } func CreateEventFromRequest(bpfEvent utils.HttpRawEvent) (utils.HttpEvent, error) { - request, err := ParseHttpRequest(FromCString(bpfEvent.GetBuf())) + direction, err := GetPacketDirection(bpfEvent.GetSyscall()) if err != nil { return nil, err } - direction, err := GetPacketDirection(bpfEvent.GetSyscall()) + request, err := ParseHttpRequest(FromCString(bpfEvent.GetBuf())) if err != nil { return nil, err } @@ -68,11 +54,12 @@ func ExtractConsistentHeaders(headers http.Header) map[string][]string { } func GetPacketDirection(syscall string) (consts.NetworkDirection, error) { - if readSyscalls[syscall] { + switch syscall { + case "read", "readv", "recvfrom", "recvmsg": return consts.Inbound, nil - } else if writeSyscalls[syscall] { + case "write", "writev", "sendto", "sendmsg": return consts.Outbound, nil - } else { + default: return "", fmt.Errorf("unknown syscall %s", syscall) } } diff --git a/pkg/utils/datasource_event.go b/pkg/utils/datasource_event.go index 0c31c8b20..6c075db4b 100644 --- a/pkg/utils/datasource_event.go +++ b/pkg/utils/datasource_event.go @@ -2,7 +2,6 @@ package utils import ( "errors" - "net" "net/http" "os" "strings" @@ -870,7 +869,7 @@ func (e *DatasourceEvent) MakeHttpEvent(request *http.Request, direction consts. Datasource: e.Datasource, Direction: direction, EventType: e.EventType, - Internal: func() bool { ip := net.ParseIP(e.GetOtherIp()); return ip != nil && ip.IsPrivate() }(), + Internal: isPrivateIP(e.GetOtherIp()), Request: request, Response: e.Response, Syscall: e.Syscall, diff --git a/pkg/utils/events.go b/pkg/utils/events.go index 1b383c7e3..37c1f15f6 100644 --- a/pkg/utils/events.go +++ b/pkg/utils/events.go @@ -2,6 +2,7 @@ package utils import ( "fmt" + "net" "net/http" "path/filepath" @@ -9,6 +10,11 @@ import ( "github.com/kubescape/storage/pkg/apis/softwarecomposition/consts" ) +func isPrivateIP(ipStr string) bool { + ip := net.ParseIP(ipStr) + return ip != nil && ip.IsPrivate() +} + type HTTPDataType int const ( diff --git a/pkg/utils/struct_event.go b/pkg/utils/struct_event.go index 674af7037..054f01d34 100644 --- a/pkg/utils/struct_event.go +++ b/pkg/utils/struct_event.go @@ -1,7 +1,6 @@ package utils import ( - "net" "net/http" "time" @@ -654,11 +653,10 @@ func (e *StructEvent) IsDir() bool { } func (e *StructEvent) MakeHttpEvent(request *http.Request, direction consts.NetworkDirection) HttpEvent { - event := *e - event.Request = request - event.Direction = direction - event.Internal = func() bool { ip := net.ParseIP(e.GetOtherIp()); return ip != nil && ip.IsPrivate() }() - return &event + e.Request = request + e.Direction = direction + e.Internal = isPrivateIP(e.GetOtherIp()) + return e } func (e *StructEvent) Release() {} From ca697b16c234d18fa81be33aeb444aa472ff76f3 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Thu, 12 Feb 2026 17:04:23 +0200 Subject: [PATCH 2/7] add parse.basename CEL function and enable cel.bind macro Signed-off-by: Yakir Oren --- pkg/rulemanager/cel/cel.go | 34 ++++--- pkg/rulemanager/cel/cel_interface.go | 5 + pkg/rulemanager/cel/libraries/parse/parse.go | 14 +++ .../cel/libraries/parse/parselib.go | 9 ++ .../cel/libraries/parse/parsing_test.go | 96 +++++++++++++++++++ 5 files changed, 146 insertions(+), 12 deletions(-) diff --git a/pkg/rulemanager/cel/cel.go b/pkg/rulemanager/cel/cel.go index 28201bde4..ab79d14d5 100644 --- a/pkg/rulemanager/cel/cel.go +++ b/pkg/rulemanager/cel/cel.go @@ -56,6 +56,7 @@ func NewCEL(objectCache objectcache.ObjectCache, cfg config.Config) (*CEL, error cel.CustomTypeAdapter(ta), cel.CustomTypeProvider(tp), ext.Strings(), + ext.Bindings(), k8s.K8s(objectCache.K8sObjectCache(), cfg), applicationprofile.AP(objectCache, cfg), networkneighborhood.NN(objectCache, cfg), @@ -128,7 +129,7 @@ func (c *CEL) getOrCreateProgram(expression string) (cel.Program, error) { return program, nil } -func (c *CEL) createEvalContext(event *events.EnrichedEvent) map[string]any { +func (c *CEL) CreateEvalContext(event *events.EnrichedEvent) map[string]any { eventType := event.Event.GetEventType() // Apply event converter if one is registered, otherwise cast to CelEvent @@ -175,15 +176,8 @@ func (c *CEL) evaluateProgramWithContext(expression string, evalContext map[stri return out, nil } -func (c *CEL) EvaluateRule(event *events.EnrichedEvent, expressions []typesv1.RuleExpression) (bool, error) { - eventType := event.Event.GetEventType() - evalContext := c.createEvalContext(event) - +func (c *CEL) EvaluateRuleWithContext(evalContext map[string]any, expressions []typesv1.RuleExpression) (bool, error) { for _, expression := range expressions { - if expression.EventType != eventType { - continue - } - out, err := c.evaluateProgramWithContext(expression.Expression, evalContext) if err != nil { return false, err @@ -206,9 +200,7 @@ func (c *CEL) EvaluateRule(event *events.EnrichedEvent, expressions []typesv1.Ru return true, nil } -func (c *CEL) EvaluateExpression(event *events.EnrichedEvent, expression string) (string, error) { - evalContext := c.createEvalContext(event) - +func (c *CEL) EvaluateExpressionWithContext(evalContext map[string]any, expression string) (string, error) { out, err := c.evaluateProgramWithContext(expression, evalContext) if err != nil { return "", err @@ -226,6 +218,24 @@ func (c *CEL) EvaluateExpression(event *events.EnrichedEvent, expression string) return strVal, nil } +func (c *CEL) EvaluateRule(event *events.EnrichedEvent, expressions []typesv1.RuleExpression) (bool, error) { + evalContext := c.CreateEvalContext(event) + eventType := event.Event.GetEventType() + // Filter expressions to match event type for backward compatibility + var filtered []typesv1.RuleExpression + for _, expr := range expressions { + if expr.EventType == eventType { + filtered = append(filtered, expr) + } + } + return c.EvaluateRuleWithContext(evalContext, filtered) +} + +func (c *CEL) EvaluateExpression(event *events.EnrichedEvent, expression string) (string, error) { + evalContext := c.CreateEvalContext(event) + return c.EvaluateExpressionWithContext(evalContext, expression) +} + func (c *CEL) RegisterHelper(function cel.EnvOption) error { extendedEnv, err := c.env.Extend(function) if err != nil { diff --git a/pkg/rulemanager/cel/cel_interface.go b/pkg/rulemanager/cel/cel_interface.go index 500c3cbe1..31ea7897c 100644 --- a/pkg/rulemanager/cel/cel_interface.go +++ b/pkg/rulemanager/cel/cel_interface.go @@ -13,4 +13,9 @@ type RuleEvaluator interface { RegisterHelper(function cel.EnvOption) error RegisterCustomType(eventType utils.EventType, obj interface{}) error RegisterEventConverter(eventType utils.EventType, converter func(utils.K8sEvent) utils.K8sEvent) + + // Context-aware variants — create the eval context once and reuse across multiple evaluations + CreateEvalContext(event *events.EnrichedEvent) map[string]any + EvaluateRuleWithContext(evalContext map[string]any, expressions []typesv1.RuleExpression) (bool, error) + EvaluateExpressionWithContext(evalContext map[string]any, expression string) (string, error) } diff --git a/pkg/rulemanager/cel/libraries/parse/parse.go b/pkg/rulemanager/cel/libraries/parse/parse.go index ba82f982f..80de653dd 100644 --- a/pkg/rulemanager/cel/libraries/parse/parse.go +++ b/pkg/rulemanager/cel/libraries/parse/parse.go @@ -1,6 +1,8 @@ package parse import ( + "strings" + "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "github.com/kubescape/node-agent/pkg/rulemanager/cel/libraries/celparse" @@ -25,3 +27,15 @@ func (l *parseLibrary) getExecPath(args ref.Val, comm ref.Val) ref.Val { } return types.String(commStr) } + +func (l *parseLibrary) basename(path ref.Val) ref.Val { + s, ok := path.Value().(string) + if !ok { + return types.MaybeNoSuchOverloadErr(path) + } + idx := strings.LastIndex(s, "/") + if idx == -1 { + return types.String(s) + } + return types.String(s[idx+1:]) +} diff --git a/pkg/rulemanager/cel/libraries/parse/parselib.go b/pkg/rulemanager/cel/libraries/parse/parselib.go index 57b05be45..bd3f9876d 100644 --- a/pkg/rulemanager/cel/libraries/parse/parselib.go +++ b/pkg/rulemanager/cel/libraries/parse/parselib.go @@ -48,6 +48,12 @@ func (l *parseLibrary) Declarations() map[string][]cel.FunctionOpt { }), ), }, + "parse.basename": { + cel.Overload( + "parse_basename", []*cel.Type{cel.StringType}, cel.StringType, + cel.UnaryBinding(l.basename), + ), + }, } } @@ -76,6 +82,9 @@ func (e *parseCostEstimator) EstimateCallCost(function, overloadID string, targe case "parse.get_exec_path": // List parsing + simple array access + string comparison - O(1) operation cost = 5 + case "parse.basename": + // Single string scan for last '/' - O(n) on path length + cost = 1 } return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: uint64(cost), Max: uint64(cost)}} } diff --git a/pkg/rulemanager/cel/libraries/parse/parsing_test.go b/pkg/rulemanager/cel/libraries/parse/parsing_test.go index 5677c8b56..b15a76ec2 100644 --- a/pkg/rulemanager/cel/libraries/parse/parsing_test.go +++ b/pkg/rulemanager/cel/libraries/parse/parsing_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/google/cel-go/cel" + "github.com/google/cel-go/ext" "github.com/kubescape/node-agent/pkg/config" "github.com/stretchr/testify/assert" ) @@ -42,6 +43,26 @@ func TestParseLibrary(t *testing.T) { expr: "parse.get_exec_path(['/usr/bin/python'], 'python')", expected: "/usr/bin/python", }, + { + name: "basename with full path", + expr: "parse.basename('/usr/bin/nmap')", + expected: "nmap", + }, + { + name: "basename with just filename", + expr: "parse.basename('nmap')", + expected: "nmap", + }, + { + name: "basename with trailing slash", + expr: "parse.basename('/usr/bin/')", + expected: "", + }, + { + name: "basename with root path", + expr: "parse.basename('/nmap')", + expected: "nmap", + }, } for _, tt := range tests { @@ -76,6 +97,81 @@ func TestParseLibrary(t *testing.T) { } } +func TestCelBindWithJoinedArgs(t *testing.T) { + env, err := cel.NewEnv( + cel.Variable("event", cel.AnyType), + Parse(config.Config{}), + ext.Strings(), + ext.Bindings(), + ) + if err != nil { + t.Fatalf("failed to create env: %v", err) + } + + tests := []struct { + name string + expr string + args []string + expected bool + }{ + { + name: "cel.bind caches joined args - match", + expr: "cel.bind(joined_args, event.args.join(' '), joined_args.contains('-e') && joined_args.contains('cmd'))", + args: []string{"nc", "-e", "/bin/sh", "cmd"}, + expected: true, + }, + { + name: "cel.bind caches joined args - no match", + expr: "cel.bind(joined_args, event.args.join(' '), joined_args.contains('-e') && joined_args.contains('cmd'))", + args: []string{"ls", "-la"}, + expected: false, + }, + { + name: "cel.bind with multiple contains checks", + expr: "cel.bind(joined_args, event.args.join(' '), joined_args.contains('socket') || joined_args.contains('exec') || joined_args.contains('pty'))", + args: []string{"python", "-c", "import pty; pty.spawn('/bin/sh')"}, + expected: true, + }, + { + name: "cel.bind with empty args", + expr: "cel.bind(joined_args, event.args.join(' '), joined_args.contains('test'))", + args: []string{}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ast, issues := env.Compile(tt.expr) + if issues != nil { + t.Fatalf("failed to compile expression: %v", issues.Err()) + } + + program, err := env.Program(ast) + if err != nil { + t.Fatalf("failed to create program: %v", err) + } + + result, _, err := program.Eval(map[string]interface{}{ + "event": map[string]interface{}{ + "args": tt.args, + "comm": "test", + }, + }) + if err != nil { + t.Fatalf("failed to eval program: %v", err) + } + + actual, ok := result.Value().(bool) + if !ok { + t.Fatalf("expected bool result, got %T", result.Value()) + } + + assert.Equal(t, tt.expected, actual) + }) + } +} + func TestParseLibraryErrorCases(t *testing.T) { env, err := cel.NewEnv( cel.Variable("event", cel.AnyType), From cf7f88ffe76b05fdae5f276268b57425818649d0 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Thu, 12 Feb 2026 17:04:32 +0200 Subject: [PATCH 3/7] pre-index rules by event type to eliminate per-event scanning Signed-off-by: Yakir Oren --- pkg/rulemanager/rule_manager.go | 62 +++++++++----------------- pkg/rulemanager/rulecreator/factory.go | 8 +++- pkg/rulemanager/types/v1/types.go | 13 ++++++ 3 files changed, 41 insertions(+), 42 deletions(-) diff --git a/pkg/rulemanager/rule_manager.go b/pkg/rulemanager/rule_manager.go index a5bebb2ec..9127d5bc9 100644 --- a/pkg/rulemanager/rule_manager.go +++ b/pkg/rulemanager/rule_manager.go @@ -176,9 +176,7 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) return } - if !isSupportedEventType(rules, enrichedEvent) { - return - } + eventType := enrichedEvent.Event.GetEventType() _, apChecksum, err := profilehelper.GetContainerApplicationProfile(rm.objectCache, enrichedEvent.ContainerID) profileExists = err == nil @@ -188,12 +186,19 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) return } - eventType := enrichedEvent.Event.GetEventType() + evalContext := rm.celEvaluator.CreateEvalContext(enrichedEvent) + for _, rule := range rules { if !rule.Enabled { continue } + // Fast path: skip rules that have no expressions for this event type + ruleExpressions := rule.ExpressionsByEventType[eventType] + if len(ruleExpressions) == 0 { + continue + } + if !RuleAppliesToContext(&rule, enrichedEvent.SourceContext) { continue } @@ -203,17 +208,12 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) continue } - ruleExpressions := rm.getRuleExpressions(rule, eventType) - if len(ruleExpressions) == 0 { - continue - } - if rule.SupportPolicy && rm.validateRulePolicy(rule, enrichedEvent.Event, enrichedEvent.ContainerID) { continue } startTime := time.Now() - shouldAlert, err := rm.celEvaluator.EvaluateRule(enrichedEvent, rule.Expressions.RuleExpression) + shouldAlert, err := rm.celEvaluator.EvaluateRuleWithContext(evalContext, ruleExpressions) evaluationTime := time.Since(startTime) rm.metrics.ReportRuleEvaluationTime(rule.Name, eventType, evaluationTime) @@ -225,10 +225,10 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) if shouldAlert { state := rule.State if eventType == utils.HTTPEventType { // TODO: Manage state evaluation in a better way (this is abuse of the state map, we need a better way to pass payloads from rules.) - state = rm.evaluateHTTPPayloadState(rule.State, enrichedEvent) + state = rm.evaluateHTTPPayloadState(rule.State, evalContext) } rm.metrics.ReportRuleAlert(rule.Name) - message, uniqueID, err := rm.getUniqueIdAndMessage(enrichedEvent, rule) + message, uniqueID, err := rm.getUniqueIdAndMessage(evalContext, rule) if err != nil { logger.L().Error("RuleManager - failed to get unique ID and message", helpers.Error(err)) continue @@ -325,19 +325,21 @@ func (rm *RuleManager) EvaluatePolicyRulesForEvent(eventType utils.EventType, ev creator := rm.ruleBindingCache.GetRuleCreator() rules := creator.CreateRulePolicyRulesByEventType(eventType) + enrichedEvent := &events.EnrichedEvent{Event: event} + evalContext := rm.celEvaluator.CreateEvalContext(enrichedEvent) + for _, rule := range rules { if !rule.SupportPolicy { continue } - enrichedEvent := &events.EnrichedEvent{Event: event} - ruleExpressions := rm.getRuleExpressions(rule, eventType) + ruleExpressions := rule.ExpressionsByEventType[eventType] if len(ruleExpressions) == 0 { continue } startTime := time.Now() - shouldAlert, err := rm.celEvaluator.EvaluateRule(enrichedEvent, ruleExpressions) + shouldAlert, err := rm.celEvaluator.EvaluateRuleWithContext(evalContext, ruleExpressions) evaluationTime := time.Since(startTime) rm.metrics.ReportRuleEvaluationTime(rule.ID, eventType, evaluationTime) @@ -369,22 +371,13 @@ func (rm *RuleManager) validateRulePolicy(rule typesv1.Rule, event utils.K8sEven return allowed } -func (rm *RuleManager) getRuleExpressions(rule typesv1.Rule, eventType utils.EventType) []typesv1.RuleExpression { - var ruleExpressions []typesv1.RuleExpression - for _, expression := range rule.Expressions.RuleExpression { - if string(expression.EventType) == string(eventType) { - ruleExpressions = append(ruleExpressions, expression) - } - } - return ruleExpressions -} -func (rm *RuleManager) getUniqueIdAndMessage(enrichedEvent *events.EnrichedEvent, rule typesv1.Rule) (string, string, error) { - message, err := rm.celEvaluator.EvaluateExpression(enrichedEvent, rule.Expressions.Message) +func (rm *RuleManager) getUniqueIdAndMessage(evalContext map[string]any, rule typesv1.Rule) (string, string, error) { + message, err := rm.celEvaluator.EvaluateExpressionWithContext(evalContext, rule.Expressions.Message) if err != nil { logger.L().Error("RuleManager - failed to evaluate message", helpers.Error(err)) } - uniqueID, err := rm.celEvaluator.EvaluateExpression(enrichedEvent, rule.Expressions.UniqueID) + uniqueID, err := rm.celEvaluator.EvaluateExpressionWithContext(evalContext, rule.Expressions.UniqueID) if err != nil { logger.L().Error("RuleManager - failed to evaluate unique ID", helpers.Error(err)) } @@ -394,17 +387,6 @@ func (rm *RuleManager) getUniqueIdAndMessage(enrichedEvent *events.EnrichedEvent return message, uniqueID, err } -func isSupportedEventType(rules []typesv1.Rule, enrichedEvent *events.EnrichedEvent) bool { - eventType := enrichedEvent.Event.GetEventType() - for _, rule := range rules { - for _, expression := range rule.Expressions.RuleExpression { - if string(expression.EventType) == string(eventType) { - return true - } - } - } - return false -} func hashStringToMD5(str string) string { hash := md5.Sum([]byte(str)) @@ -412,13 +394,13 @@ func hashStringToMD5(str string) string { return hashString } -func (rm *RuleManager) evaluateHTTPPayloadState(state map[string]any, enrichedEvent *events.EnrichedEvent) map[string]any { +func (rm *RuleManager) evaluateHTTPPayloadState(state map[string]any, evalContext map[string]any) map[string]any { payloadExpression, ok := state["payload"].(string) if !ok || payloadExpression == "" { return state } - payloadValue, err := rm.celEvaluator.EvaluateExpression(enrichedEvent, payloadExpression) + payloadValue, err := rm.celEvaluator.EvaluateExpressionWithContext(evalContext, payloadExpression) if err != nil { logger.L().Error("RuleManager - failed to evaluate http payload expression", helpers.Error(err)) return state diff --git a/pkg/rulemanager/rulecreator/factory.go b/pkg/rulemanager/rulecreator/factory.go index 7ad18cfc2..5e63d72eb 100644 --- a/pkg/rulemanager/rulecreator/factory.go +++ b/pkg/rulemanager/rulecreator/factory.go @@ -51,6 +51,7 @@ func (r *RuleCreatorImpl) CreateRuleByName(name string) typesv1.Rule { } func (r *RuleCreatorImpl) RegisterRule(rule typesv1.Rule) { + rule.Init() r.Rules = append(r.Rules, rule) } @@ -105,8 +106,9 @@ func (r *RuleCreatorImpl) SyncRules(newRules []typesv1.Rule) { // Create a map of new rules by ID for quick lookup newRuleMap := make(map[string]typesv1.Rule) - for _, rule := range newRules { - newRuleMap[rule.ID] = rule + for i := range newRules { + newRules[i].Init() + newRuleMap[newRules[i].ID] = newRules[i] } // Remove rules that are no longer present @@ -148,6 +150,8 @@ func (r *RuleCreatorImpl) UpdateRule(rule typesv1.Rule) bool { r.mutex.Lock() defer r.mutex.Unlock() + rule.Init() + for i, existingRule := range r.Rules { if existingRule.ID == rule.ID { r.Rules[i] = rule diff --git a/pkg/rulemanager/types/v1/types.go b/pkg/rulemanager/types/v1/types.go index 120a162a0..b9f5235fb 100644 --- a/pkg/rulemanager/types/v1/types.go +++ b/pkg/rulemanager/types/v1/types.go @@ -32,6 +32,19 @@ type Rule struct { IsTriggerAlert bool `json:"isTriggerAlert" yaml:"isTriggerAlert"` MitreTactic string `json:"mitreTactic" yaml:"mitreTactic"` MitreTechnique string `json:"mitreTechnique" yaml:"mitreTechnique"` + + // Pre-computed index: event type → expressions for that type. + // Populated by Init(). Avoids per-event scanning and allocation. + ExpressionsByEventType map[utils.EventType][]RuleExpression `json:"-" yaml:"-"` +} + +// Init pre-computes the ExpressionsByEventType index. +// Must be called after a Rule is created or updated. +func (r *Rule) Init() { + r.ExpressionsByEventType = make(map[utils.EventType][]RuleExpression, len(r.Expressions.RuleExpression)) + for _, expr := range r.Expressions.RuleExpression { + r.ExpressionsByEventType[expr.EventType] = append(r.ExpressionsByEventType[expr.EventType], expr) + } } type RuleExpressions struct { From 973ccc24326d58f913010ef103987d29320b6057 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Thu, 12 Feb 2026 17:45:43 +0200 Subject: [PATCH 4/7] revert MakeHttpEvent and CreateEventFromRequest optimization Signed-off-by: Yakir Oren --- pkg/containerwatcher/v2/tracers/httpparse.go | 25 +++++++++++++++----- pkg/utils/datasource_event.go | 3 ++- pkg/utils/events.go | 6 ----- pkg/utils/struct_event.go | 10 ++++---- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/pkg/containerwatcher/v2/tracers/httpparse.go b/pkg/containerwatcher/v2/tracers/httpparse.go index aa7f87c71..ed80e6fb3 100644 --- a/pkg/containerwatcher/v2/tracers/httpparse.go +++ b/pkg/containerwatcher/v2/tracers/httpparse.go @@ -14,6 +14,20 @@ import ( "github.com/kubescape/storage/pkg/apis/softwarecomposition/consts" ) +var writeSyscalls = map[string]bool{ + "write": true, + "writev": true, + "sendto": true, + "sendmsg": true, +} + +var readSyscalls = map[string]bool{ + "read": true, + "readv": true, + "recvfrom": true, + "recvmsg": true, +} + var ConsistentHeaders = []string{ "Accept-Encoding", "Accept-Language", @@ -23,12 +37,12 @@ var ConsistentHeaders = []string{ } func CreateEventFromRequest(bpfEvent utils.HttpRawEvent) (utils.HttpEvent, error) { - direction, err := GetPacketDirection(bpfEvent.GetSyscall()) + request, err := ParseHttpRequest(FromCString(bpfEvent.GetBuf())) if err != nil { return nil, err } - request, err := ParseHttpRequest(FromCString(bpfEvent.GetBuf())) + direction, err := GetPacketDirection(bpfEvent.GetSyscall()) if err != nil { return nil, err } @@ -54,12 +68,11 @@ func ExtractConsistentHeaders(headers http.Header) map[string][]string { } func GetPacketDirection(syscall string) (consts.NetworkDirection, error) { - switch syscall { - case "read", "readv", "recvfrom", "recvmsg": + if readSyscalls[syscall] { return consts.Inbound, nil - case "write", "writev", "sendto", "sendmsg": + } else if writeSyscalls[syscall] { return consts.Outbound, nil - default: + } else { return "", fmt.Errorf("unknown syscall %s", syscall) } } diff --git a/pkg/utils/datasource_event.go b/pkg/utils/datasource_event.go index 6c075db4b..0c31c8b20 100644 --- a/pkg/utils/datasource_event.go +++ b/pkg/utils/datasource_event.go @@ -2,6 +2,7 @@ package utils import ( "errors" + "net" "net/http" "os" "strings" @@ -869,7 +870,7 @@ func (e *DatasourceEvent) MakeHttpEvent(request *http.Request, direction consts. Datasource: e.Datasource, Direction: direction, EventType: e.EventType, - Internal: isPrivateIP(e.GetOtherIp()), + Internal: func() bool { ip := net.ParseIP(e.GetOtherIp()); return ip != nil && ip.IsPrivate() }(), Request: request, Response: e.Response, Syscall: e.Syscall, diff --git a/pkg/utils/events.go b/pkg/utils/events.go index 37c1f15f6..1b383c7e3 100644 --- a/pkg/utils/events.go +++ b/pkg/utils/events.go @@ -2,7 +2,6 @@ package utils import ( "fmt" - "net" "net/http" "path/filepath" @@ -10,11 +9,6 @@ import ( "github.com/kubescape/storage/pkg/apis/softwarecomposition/consts" ) -func isPrivateIP(ipStr string) bool { - ip := net.ParseIP(ipStr) - return ip != nil && ip.IsPrivate() -} - type HTTPDataType int const ( diff --git a/pkg/utils/struct_event.go b/pkg/utils/struct_event.go index 054f01d34..674af7037 100644 --- a/pkg/utils/struct_event.go +++ b/pkg/utils/struct_event.go @@ -1,6 +1,7 @@ package utils import ( + "net" "net/http" "time" @@ -653,10 +654,11 @@ func (e *StructEvent) IsDir() bool { } func (e *StructEvent) MakeHttpEvent(request *http.Request, direction consts.NetworkDirection) HttpEvent { - e.Request = request - e.Direction = direction - e.Internal = isPrivateIP(e.GetOtherIp()) - return e + event := *e + event.Request = request + event.Direction = direction + event.Internal = func() bool { ip := net.ParseIP(e.GetOtherIp()); return ip != nil && ip.IsPrivate() }() + return &event } func (e *StructEvent) Release() {} From aeb4a8a961462318619f952c3e66f2dde821e58a Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Thu, 12 Feb 2026 18:35:17 +0200 Subject: [PATCH 5/7] remove cel.bind test for external library Signed-off-by: Yakir Oren --- .../cel/libraries/parse/parsing_test.go | 76 ------------------- 1 file changed, 76 deletions(-) diff --git a/pkg/rulemanager/cel/libraries/parse/parsing_test.go b/pkg/rulemanager/cel/libraries/parse/parsing_test.go index b15a76ec2..ba07fac95 100644 --- a/pkg/rulemanager/cel/libraries/parse/parsing_test.go +++ b/pkg/rulemanager/cel/libraries/parse/parsing_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/google/cel-go/cel" - "github.com/google/cel-go/ext" "github.com/kubescape/node-agent/pkg/config" "github.com/stretchr/testify/assert" ) @@ -97,81 +96,6 @@ func TestParseLibrary(t *testing.T) { } } -func TestCelBindWithJoinedArgs(t *testing.T) { - env, err := cel.NewEnv( - cel.Variable("event", cel.AnyType), - Parse(config.Config{}), - ext.Strings(), - ext.Bindings(), - ) - if err != nil { - t.Fatalf("failed to create env: %v", err) - } - - tests := []struct { - name string - expr string - args []string - expected bool - }{ - { - name: "cel.bind caches joined args - match", - expr: "cel.bind(joined_args, event.args.join(' '), joined_args.contains('-e') && joined_args.contains('cmd'))", - args: []string{"nc", "-e", "/bin/sh", "cmd"}, - expected: true, - }, - { - name: "cel.bind caches joined args - no match", - expr: "cel.bind(joined_args, event.args.join(' '), joined_args.contains('-e') && joined_args.contains('cmd'))", - args: []string{"ls", "-la"}, - expected: false, - }, - { - name: "cel.bind with multiple contains checks", - expr: "cel.bind(joined_args, event.args.join(' '), joined_args.contains('socket') || joined_args.contains('exec') || joined_args.contains('pty'))", - args: []string{"python", "-c", "import pty; pty.spawn('/bin/sh')"}, - expected: true, - }, - { - name: "cel.bind with empty args", - expr: "cel.bind(joined_args, event.args.join(' '), joined_args.contains('test'))", - args: []string{}, - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ast, issues := env.Compile(tt.expr) - if issues != nil { - t.Fatalf("failed to compile expression: %v", issues.Err()) - } - - program, err := env.Program(ast) - if err != nil { - t.Fatalf("failed to create program: %v", err) - } - - result, _, err := program.Eval(map[string]interface{}{ - "event": map[string]interface{}{ - "args": tt.args, - "comm": "test", - }, - }) - if err != nil { - t.Fatalf("failed to eval program: %v", err) - } - - actual, ok := result.Value().(bool) - if !ok { - t.Fatalf("expected bool result, got %T", result.Value()) - } - - assert.Equal(t, tt.expected, actual) - }) - } -} - func TestParseLibraryErrorCases(t *testing.T) { env, err := cel.NewEnv( cel.Variable("event", cel.AnyType), From 9cf15a14d302284fd92bb255d819a567921c7ef9 Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Thu, 12 Feb 2026 19:15:17 +0200 Subject: [PATCH 6/7] removed old functions and replace EnrichedEvent with utils.K8sEvent in CEL evaluation Signed-off-by: Yakir Oren --- pkg/rulemanager/cel/cel.go | 22 +++++++--------------- pkg/rulemanager/cel/cel_interface.go | 8 ++++++-- pkg/rulemanager/rule_manager.go | 9 +++------ 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/pkg/rulemanager/cel/cel.go b/pkg/rulemanager/cel/cel.go index ab79d14d5..2c9fb2230 100644 --- a/pkg/rulemanager/cel/cel.go +++ b/pkg/rulemanager/cel/cel.go @@ -129,15 +129,15 @@ func (c *CEL) getOrCreateProgram(expression string) (cel.Program, error) { return program, nil } -func (c *CEL) CreateEvalContext(event *events.EnrichedEvent) map[string]any { - eventType := event.Event.GetEventType() +func (c *CEL) CreateEvalContext(event utils.K8sEvent) map[string]any { + eventType := event.GetEventType() // Apply event converter if one is registered, otherwise cast to CelEvent var obj interface{} if converter, exists := c.eventConverters[eventType]; exists { - obj, _ = xcel.NewObject(converter(event.Event)) + obj, _ = xcel.NewObject(converter(event)) } else { - obj, _ = xcel.NewObject(event.Event.(utils.CelEvent)) + obj, _ = xcel.NewObject(event.(utils.CelEvent)) } evalContext := map[string]any{ @@ -219,20 +219,12 @@ func (c *CEL) EvaluateExpressionWithContext(evalContext map[string]any, expressi } func (c *CEL) EvaluateRule(event *events.EnrichedEvent, expressions []typesv1.RuleExpression) (bool, error) { - evalContext := c.CreateEvalContext(event) - eventType := event.Event.GetEventType() - // Filter expressions to match event type for backward compatibility - var filtered []typesv1.RuleExpression - for _, expr := range expressions { - if expr.EventType == eventType { - filtered = append(filtered, expr) - } - } - return c.EvaluateRuleWithContext(evalContext, filtered) + evalContext := c.CreateEvalContext(event.Event) + return c.EvaluateRuleWithContext(evalContext, expressions) } func (c *CEL) EvaluateExpression(event *events.EnrichedEvent, expression string) (string, error) { - evalContext := c.CreateEvalContext(event) + evalContext := c.CreateEvalContext(event.Event) return c.EvaluateExpressionWithContext(evalContext, expression) } diff --git a/pkg/rulemanager/cel/cel_interface.go b/pkg/rulemanager/cel/cel_interface.go index 31ea7897c..b7353677c 100644 --- a/pkg/rulemanager/cel/cel_interface.go +++ b/pkg/rulemanager/cel/cel_interface.go @@ -8,14 +8,18 @@ import ( ) type RuleEvaluator interface { + // EvaluateRule evaluates rules for a single call. For repeated evaluation of the same event, + // use CreateEvalContext once and call EvaluateRuleWithContext for each rule instead. EvaluateRule(event *events.EnrichedEvent, expressions []typesv1.RuleExpression) (bool, error) + // EvaluateExpression evaluates an expression for a single call. For repeated evaluation of the same event, + // use CreateEvalContext once and call EvaluateExpressionWithContext instead. EvaluateExpression(event *events.EnrichedEvent, expression string) (string, error) + RegisterHelper(function cel.EnvOption) error RegisterCustomType(eventType utils.EventType, obj interface{}) error RegisterEventConverter(eventType utils.EventType, converter func(utils.K8sEvent) utils.K8sEvent) - // Context-aware variants — create the eval context once and reuse across multiple evaluations - CreateEvalContext(event *events.EnrichedEvent) map[string]any + CreateEvalContext(event utils.K8sEvent) map[string]any EvaluateRuleWithContext(evalContext map[string]any, expressions []typesv1.RuleExpression) (bool, error) EvaluateExpressionWithContext(evalContext map[string]any, expression string) (string, error) } diff --git a/pkg/rulemanager/rule_manager.go b/pkg/rulemanager/rule_manager.go index 9127d5bc9..f4b2bae8c 100644 --- a/pkg/rulemanager/rule_manager.go +++ b/pkg/rulemanager/rule_manager.go @@ -186,7 +186,7 @@ func (rm *RuleManager) ReportEnrichedEvent(enrichedEvent *events.EnrichedEvent) return } - evalContext := rm.celEvaluator.CreateEvalContext(enrichedEvent) + evalContext := rm.celEvaluator.CreateEvalContext(enrichedEvent.Event) for _, rule := range rules { if !rule.Enabled { @@ -320,13 +320,12 @@ func (rm *RuleManager) IsPodMonitored(namespace, pod string) bool { } func (rm *RuleManager) EvaluatePolicyRulesForEvent(eventType utils.EventType, event utils.K8sEvent) []string { - results := []string{} + var results []string creator := rm.ruleBindingCache.GetRuleCreator() rules := creator.CreateRulePolicyRulesByEventType(eventType) - enrichedEvent := &events.EnrichedEvent{Event: event} - evalContext := rm.celEvaluator.CreateEvalContext(enrichedEvent) + evalContext := rm.celEvaluator.CreateEvalContext(event) for _, rule := range rules { if !rule.SupportPolicy { @@ -371,7 +370,6 @@ func (rm *RuleManager) validateRulePolicy(rule typesv1.Rule, event utils.K8sEven return allowed } - func (rm *RuleManager) getUniqueIdAndMessage(evalContext map[string]any, rule typesv1.Rule) (string, string, error) { message, err := rm.celEvaluator.EvaluateExpressionWithContext(evalContext, rule.Expressions.Message) if err != nil { @@ -387,7 +385,6 @@ func (rm *RuleManager) getUniqueIdAndMessage(evalContext map[string]any, rule ty return message, uniqueID, err } - func hashStringToMD5(str string) string { hash := md5.Sum([]byte(str)) hashString := fmt.Sprintf("%x", hash) From 03d2b02e6b4ef1255a7319809e9e0a1c75235b7e Mon Sep 17 00:00:00 2001 From: Yakir Oren Date: Tue, 17 Feb 2026 18:53:36 +0200 Subject: [PATCH 7/7] add guard for duplicate Init calls Signed-off-by: Yakir Oren --- pkg/rulemanager/types/v1/types.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/rulemanager/types/v1/types.go b/pkg/rulemanager/types/v1/types.go index b9f5235fb..c3633f754 100644 --- a/pkg/rulemanager/types/v1/types.go +++ b/pkg/rulemanager/types/v1/types.go @@ -41,6 +41,9 @@ type Rule struct { // Init pre-computes the ExpressionsByEventType index. // Must be called after a Rule is created or updated. func (r *Rule) Init() { + if r.ExpressionsByEventType != nil { + return + } r.ExpressionsByEventType = make(map[utils.EventType][]RuleExpression, len(r.Expressions.RuleExpression)) for _, expr := range r.Expressions.RuleExpression { r.ExpressionsByEventType[expr.EventType] = append(r.ExpressionsByEventType[expr.EventType], expr)