diff --git a/pkg/rulemanager/cel/cel.go b/pkg/rulemanager/cel/cel.go index 28201bde4..2c9fb2230 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,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{ @@ -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,16 @@ 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.Event) + return c.EvaluateRuleWithContext(evalContext, expressions) +} + +func (c *CEL) EvaluateExpression(event *events.EnrichedEvent, expression string) (string, error) { + evalContext := c.CreateEvalContext(event.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..b7353677c 100644 --- a/pkg/rulemanager/cel/cel_interface.go +++ b/pkg/rulemanager/cel/cel_interface.go @@ -8,9 +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) + + 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/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..ba07fac95 100644 --- a/pkg/rulemanager/cel/libraries/parse/parsing_test.go +++ b/pkg/rulemanager/cel/libraries/parse/parsing_test.go @@ -42,6 +42,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 { diff --git a/pkg/rulemanager/rule_manager.go b/pkg/rulemanager/rule_manager.go index a5bebb2ec..f4b2bae8c 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.Event) + 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 @@ -320,24 +320,25 @@ 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) + evalContext := rm.celEvaluator.CreateEvalContext(event) + 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 +370,12 @@ 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,31 +385,19 @@ 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)) hashString := fmt.Sprintf("%x", hash) 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..c3633f754 100644 --- a/pkg/rulemanager/types/v1/types.go +++ b/pkg/rulemanager/types/v1/types.go @@ -32,6 +32,22 @@ 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() { + 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) + } } type RuleExpressions struct {