Skip to content

Commit

Permalink
Add Common Expression Language filter
Browse files Browse the repository at this point in the history
Add Common Expression Language [^1] filter in GetEventRequest. See unit
tests for sample usages. This is largely based on Hubble CEL expression
filter [^2].

[^1]: https://cel.dev/
[^2]: cilium/cilium#32147

Signed-off-by: Michi Mutsuzaki <[email protected]>
  • Loading branch information
michi-covalent committed Nov 12, 2024
1 parent 54e452c commit 05e4231
Show file tree
Hide file tree
Showing 11 changed files with 733 additions and 493 deletions.
1 change: 1 addition & 0 deletions api/v1/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

340 changes: 176 additions & 164 deletions api/v1/tetragon/events.pb.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions api/v1/tetragon/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ message Filter {
CapFilter capabilities = 11;
// Filter parent process' binary using RE2 regular expression syntax.
repeated string parent_binary_regex = 12;
// Filter using CEL expressions.
repeated string cel_expression = 13;
}

// Filter over a set of Linux process capabilities. See `message Capabilities`
Expand Down

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/content/en/docs/reference/grpc-api.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/containerd/cgroups v1.1.0
github.com/deckarep/golang-set/v2 v2.6.0
github.com/fatih/color v1.18.0
github.com/google/cel-go v0.21.0
github.com/google/go-cmp v0.6.0
github.com/google/gops v0.3.28
github.com/google/uuid v1.6.0
Expand Down Expand Up @@ -97,7 +98,6 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/cel-go v0.21.0 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
Expand Down
129 changes: 129 additions & 0 deletions pkg/filters/cel_expression.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Tetragon

package filters

import (
"context"
"fmt"
"reflect"

v1 "github.com/cilium/cilium/pkg/hubble/api/v1"
hubbleFilters "github.com/cilium/cilium/pkg/hubble/filters"
"github.com/cilium/tetragon/api/v1/tetragon"
"github.com/cilium/tetragon/api/v1/tetragon/codegen/helpers"
"github.com/google/cel-go/cel"
"github.com/sirupsen/logrus"
)

// compile will parse and check an expression `expr` against a given
// environment `env` and determine whether the resulting type of the expression
// matches the `exprType` provided as input.
// Copied from
// https://github.com/google/cel-go/blob/338b3c80e688f7f44661d163c0dbc02eb120dcb7/codelab/solution/codelab.go#LL385C1-L399C2
// with modifications
func compile(env *cel.Env, expr string, celType *cel.Type) (*cel.Ast, error) {
ast, iss := env.Compile(expr)
if iss.Err() != nil {
return nil, iss.Err()
}
// Type-check the expression for correctness.
checked, iss := env.Check(ast)
// Report semantic errors, if present.
if iss.Err() != nil {
return nil, iss.Err()
}
if checked.OutputType() != celType {
return nil, fmt.Errorf(
"got %q, wanted %q result type",
checked.OutputType(), celType)
}
return ast, nil
}

func (t *CELExpressionFilter) filterByCELExpression(ctx context.Context, log logrus.FieldLogger, exprs []string) (hubbleFilters.FilterFunc, error) {
var programs []cel.Program
for _, expr := range exprs {
// we want filters to be boolean expressions, so check the type of the
// expression before proceeding
ast, err := compile(t.celEnv, expr, cel.BoolType)
if err != nil {
return nil, fmt.Errorf("error compiling CEL expression: %w", err)
}

prg, err := t.celEnv.Program(ast)
if err != nil {
return nil, fmt.Errorf("error building CEL program: %w", err)
}
programs = append(programs, prg)
}

return func(ev *v1.Event) bool {
if ev == nil {
return false
}
response, ok := ev.Event.(*tetragon.GetEventsResponse)
if !ok {
return false
}
for _, prg := range programs {
out, _, err := prg.ContextEval(ctx, helpers.ProcessEventMap(response))
if err != nil {
log.Errorf("error running CEL program %s", err)
return false
}

v, err := out.ConvertToNative(t.boolType)
if err != nil {
log.Errorf("invalid conversion in CEL program: %s", err)
return false
}
b, ok := v.(bool)
if ok && b {
return true
}
}
return false
}, nil
}

// CELExpressionFilter implements filtering based on CEL (common expression
// language) expressions
type CELExpressionFilter struct {
log logrus.FieldLogger
celEnv *cel.Env
boolType reflect.Type
}

func NewCELExpressionFilter(log logrus.FieldLogger) *CELExpressionFilter {
responseTypeMap := helpers.ResponseTypeMap()
options := []cel.EnvOption{
cel.Container("tetragon"),
}
for key, val := range responseTypeMap {
name := string(val.ProtoReflect().Descriptor().FullName())
options = append(options, cel.Variable(key, cel.ObjectType(name)))
options = append(options, cel.Types(val))
}
celEnv, err := cel.NewEnv(options...)
if err != nil {
panic(fmt.Sprintf("error creating CEL env %s", err))
}
return &CELExpressionFilter{
log: log,
celEnv: celEnv,
boolType: reflect.TypeOf(false),
}
}

// OnBuildFilter builds a CEL expression filter.
func (t *CELExpressionFilter) OnBuildFilter(ctx context.Context, f *tetragon.Filter) ([]hubbleFilters.FilterFunc, error) {
if exprs := f.GetCelExpression(); exprs != nil {
filter, err := t.filterByCELExpression(ctx, t.log, exprs)
if err != nil {
return nil, err
}
return []hubbleFilters.FilterFunc{filter}, nil
}
return []hubbleFilters.FilterFunc{}, nil
}
67 changes: 67 additions & 0 deletions pkg/filters/cel_expression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Tetragon

package filters

import (
"context"
"testing"

v1 "github.com/cilium/cilium/pkg/hubble/api/v1"
"github.com/cilium/tetragon/api/v1/tetragon"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/wrapperspb"
)

func TestInvalidFilter(t *testing.T) {
log := logrus.New()
f := tetragon.Filter{CelExpression: []string{"process_exec.process.bad_field_name == 'curl'"}}
celFilter := NewCELExpressionFilter(log)
_, err := celFilter.OnBuildFilter(context.Background(), &f)
assert.Error(t, err)
}

func TestProcessExecFilter(t *testing.T) {
log := logrus.New()
f := []*tetragon.Filter{{CelExpression: []string{"process_exec.process.pid > uint(1)"}}}
fl, err := BuildFilterList(context.Background(), f, []OnBuildFilter{NewCELExpressionFilter(log)})
assert.NoError(t, err)
ev := v1.Event{
Event: &tetragon.GetEventsResponse{
Event: &tetragon.GetEventsResponse_ProcessExec{
ProcessExec: &tetragon.ProcessExec{Process: &tetragon.Process{Pid: wrapperspb.UInt32(1)}},
},
},
}
assert.False(t, fl.MatchOne(&ev))
ev = v1.Event{
Event: &tetragon.GetEventsResponse{
Event: &tetragon.GetEventsResponse_ProcessExec{
ProcessExec: &tetragon.ProcessExec{Process: &tetragon.Process{Pid: wrapperspb.UInt32(2)}},
},
},
}
assert.True(t, fl.MatchOne(&ev))
}

func TestProcessKprobeFilter(t *testing.T) {
log := logrus.New()
f := []*tetragon.Filter{{CelExpression: []string{"process_kprobe.function_name == 'security_file_permission'"}}}
fl, err := BuildFilterList(context.Background(), f, []OnBuildFilter{NewCELExpressionFilter(log)})
assert.NoError(t, err)
ev := v1.Event{
Event: &tetragon.GetEventsResponse{
Event: &tetragon.GetEventsResponse_ProcessKprobe{
ProcessKprobe: &tetragon.ProcessKprobe{FunctionName: "security_file_permission"}},
},
}
assert.True(t, fl.MatchOne(&ev))
ev = v1.Event{
Event: &tetragon.GetEventsResponse{
Event: &tetragon.GetEventsResponse_ProcessKprobe{
ProcessKprobe: &tetragon.ProcessKprobe{FunctionName: "something_else"}},
},
}
assert.False(t, fl.MatchOne(&ev))
}
Loading

0 comments on commit 05e4231

Please sign in to comment.