Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 17 additions & 15 deletions pkg/rulemanager/cel/cel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions pkg/rulemanager/cel/cel_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
14 changes: 14 additions & 0 deletions pkg/rulemanager/cel/libraries/parse/parse.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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:])
}
9 changes: 9 additions & 0 deletions pkg/rulemanager/cel/libraries/parse/parselib.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
),
},
}
}

Expand Down Expand Up @@ -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)}}
}
Expand Down
20 changes: 20 additions & 0 deletions pkg/rulemanager/cel/libraries/parse/parsing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
65 changes: 22 additions & 43 deletions pkg/rulemanager/rule_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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))
}
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions pkg/rulemanager/rulecreator/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions pkg/rulemanager/types/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading