diff --git a/pkg/rulemanager/cel/cel.go b/pkg/rulemanager/cel/cel.go index d179e015b..4b6bb05bc 100644 --- a/pkg/rulemanager/cel/cel.go +++ b/pkg/rulemanager/cel/cel.go @@ -42,8 +42,16 @@ func NewCEL(objectCache objectcache.ObjectCache, cfg config.Config) (*CEL, error eventObj, eventTyp := xcel.NewObject(&utils.CelEventImpl{}) xcel.RegisterObject(ta, tp, eventObj, eventTyp, utils.CelFields) + // Register the nested request accessor type + requestObj, requestTyp := xcel.NewObject(utils.HttpRequestAccessor{}) + xcel.RegisterObject(ta, tp, requestObj, requestTyp, utils.HttpRequestFields) + + // Set the request field's type now that requestTyp is available + utils.CelFields["request"].Type = requestTyp + envOptions := []cel.EnvOption{ cel.Variable("event", eventTyp), // All events accessible via "event" variable + cel.Variable("http", eventTyp), // HTTP events also accessible via "http" variable cel.Variable("eventType", cel.StringType), cel.CustomTypeAdapter(ta), cel.CustomTypeProvider(tp), diff --git a/pkg/utils/cel.go b/pkg/utils/cel.go index 5795caa0d..58ebea686 100644 --- a/pkg/utils/cel.go +++ b/pkg/utils/cel.go @@ -1,7 +1,10 @@ package utils import ( + "bytes" "fmt" + "io" + "net/http" celtypes "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" @@ -12,6 +15,7 @@ type CelEvent interface { CapabilitiesEvent DNSEvent ExecEvent + HttpEvent IOUring LinkEvent NetworkEvent @@ -27,6 +31,12 @@ type CelEventImpl struct { CelEvent } +// HttpRequestAccessor provides access to HTTP request fields +// It's a lightweight wrapper around CelEvent that avoids allocations +type HttpRequestAccessor struct { + HttpEvent CelEvent +} + var isSet = ref.FieldTester(func(target any) bool { x := target.(*xcel.Object[CelEvent]) if x.Raw == nil { @@ -35,6 +45,27 @@ var isSet = ref.FieldTester(func(target any) bool { return true }) +var requestIsSet = ref.FieldTester(func(target any) bool { + x := target.(*xcel.Object[HttpRequestAccessor]) + if x.Raw.HttpEvent == nil { + return false + } + req := x.Raw.HttpEvent.GetRequest() + return req != nil +}) + +var urlIsSet = ref.FieldTester(func(target any) bool { + x := target.(*xcel.Object[HttpRequestAccessor]) + if x.Raw.HttpEvent == nil { + return false + } + req := x.Raw.HttpEvent.GetRequest() + if req == nil || req.URL == nil { + return false + } + return req.URL.String() != "" +}) + var CelFields = map[string]*celtypes.FieldType{ "args": { Type: celtypes.ListType, @@ -388,4 +419,139 @@ var CelFields = map[string]*celtypes.FieldType{ return x.Raw.GetUid(), nil }), }, + // HTTP request nested object (no allocation - just wraps the event) + "request": { + Type: nil, // Will be set during registration + IsSet: isSet, + GetFrom: ref.FieldGetter(func(target any) (any, error) { + x := target.(*xcel.Object[CelEvent]) + if x.Raw == nil { + return nil, fmt.Errorf("celval: object is nil") + } + // Return a wrapped accessor - xcel.NewObject is lightweight (just pointer wrapping) + accessor := HttpRequestAccessor{HttpEvent: x.Raw} + obj, _ := xcel.NewObject(accessor) + return obj, nil + }), + }, + "direction": { + Type: celtypes.StringType, + IsSet: isSet, + GetFrom: ref.FieldGetter(func(target any) (any, error) { + x := target.(*xcel.Object[CelEvent]) + if x.Raw == nil { + return nil, fmt.Errorf("celval: object is nil") + } + return string(x.Raw.GetDirection()), nil + }), + }, +} + +// HttpRequestFields defines CEL fields for the nested http.request object +var HttpRequestFields = map[string]*celtypes.FieldType{ + "headers": { + Type: celtypes.MapType, + IsSet: requestIsSet, + GetFrom: ref.FieldGetter(func(target any) (any, error) { + x := target.(*xcel.Object[HttpRequestAccessor]) + if x.Raw.HttpEvent == nil { + return nil, fmt.Errorf("celval: object is nil") + } + req := x.Raw.HttpEvent.GetRequest() + if req != nil { + return req.Header, nil + } + return http.Header{}, nil + }), + }, + "host": { + Type: celtypes.StringType, + IsSet: requestIsSet, + GetFrom: ref.FieldGetter(func(target any) (any, error) { + x := target.(*xcel.Object[HttpRequestAccessor]) + if x.Raw.HttpEvent == nil { + return nil, fmt.Errorf("celval: object is nil") + } + req := x.Raw.HttpEvent.GetRequest() + if req != nil { + return req.Host, nil + } + return "", nil + }), + }, + "method": { + Type: celtypes.StringType, + IsSet: requestIsSet, + GetFrom: ref.FieldGetter(func(target any) (any, error) { + x := target.(*xcel.Object[HttpRequestAccessor]) + if x.Raw.HttpEvent == nil { + return nil, fmt.Errorf("celval: object is nil") + } + req := x.Raw.HttpEvent.GetRequest() + if req != nil { + return req.Method, nil + } + return "", nil + }), + }, + "url": { + Type: celtypes.StringType, + IsSet: urlIsSet, + GetFrom: ref.FieldGetter(func(target any) (any, error) { + x := target.(*xcel.Object[HttpRequestAccessor]) + if x.Raw.HttpEvent == nil { + return nil, fmt.Errorf("celval: object is nil") + } + req := x.Raw.HttpEvent.GetRequest() + if req != nil && req.URL != nil { + return req.URL.String(), nil + } + return "", nil + }), + }, + "path": { + Type: celtypes.StringType, + IsSet: urlIsSet, + GetFrom: ref.FieldGetter(func(target any) (any, error) { + x := target.(*xcel.Object[HttpRequestAccessor]) + if x.Raw.HttpEvent == nil { + return nil, fmt.Errorf("celval: object is nil") + } + req := x.Raw.HttpEvent.GetRequest() + if req != nil && req.URL != nil { + return req.URL.Path, nil + } + return "", nil + }), + }, + "body": { + Type: celtypes.StringType, + IsSet: requestIsSet, + GetFrom: ref.FieldGetter(func(target any) (any, error) { + x := target.(*xcel.Object[HttpRequestAccessor]) + if x.Raw.HttpEvent == nil { + return nil, fmt.Errorf("celval: object is nil") + } + // Try GetBuf() first (for eBPF events) + buf := x.Raw.HttpEvent.GetBuf() + if len(buf) > 0 { + return string(buf), nil + } + // Fallback to reading from Request.Body (for test events) + req := x.Raw.HttpEvent.GetRequest() + if req != nil && req.Body != nil { + // Read with size limit (10MB) and restore body for downstream readers + const maxBodySize = 10 * 1024 * 1024 // 10MB + limitedReader := io.LimitReader(req.Body, maxBodySize) + bodyBytes, err := io.ReadAll(limitedReader) + req.Body.Close() // Close the original body + req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) // Restore for downstream + if err != nil { + return "", err + } + return string(bodyBytes), nil + } + return "", nil + }), + }, }