package exprhelpers import ( "reflect" "strings" "testing" "github.com/antonmedv/expr" "github.com/crowdsecurity/crowdsec/pkg/types" "github.com/davecgh/go-spew/spew" log "github.com/sirupsen/logrus" ) type ExprDbgTest struct { Name string Expr string ExpectedOutputs []OpOutput ExpectedFailedCompile bool ExpectedFailRuntime bool Env map[string]interface{} LogLevel log.Level } // For the sake of testing functions with 2, 3 and N args func UpperTwo(params ...any) (any, error) { s := params[0].(string) v := params[1].(string) return strings.ToUpper(s) + strings.ToUpper(v), nil } func UpperThree(params ...any) (any, error) { s := params[0].(string) v := params[1].(string) x := params[2].(string) return strings.ToUpper(s) + strings.ToUpper(v) + strings.ToUpper(x), nil } func UpperN(params ...any) (any, error) { s := params[0].(string) v := params[1].(string) x := params[2].(string) y := params[3].(string) return strings.ToUpper(s) + strings.ToUpper(v) + strings.ToUpper(x) + strings.ToUpper(y), nil } func boolPtr(b bool) *bool { return &b } type teststruct struct { Foo string } func TestBaseDbg(t *testing.T) { defaultEnv := map[string]interface{}{ "queue": &types.Queue{}, "evt": &types.Event{}, "sample_array": []string{"a", "b", "c", "ZZ"}, "base_string": "hello world", "base_int": 42, "base_float": 42.42, "nillvar": &teststruct{}, "base_struct": struct { Foo string Bar int Myarr []string }{ Foo: "bar", Bar: 42, Myarr: []string{"a", "b", "c"}, }, } // tips for the tests: // use '%#v' to dump in golang syntax // use regexp to clear empty/default fields: // [a-z]+: (false|\[\]string\(nil\)|""), //ConditionResult:(*bool) //Missing multi parametes function tests := []ExprDbgTest{ { Name: "nill deref", Expr: "Upper('1') == '1' && nillvar.Foo == '42'", Env: defaultEnv, ExpectedFailRuntime: true, ExpectedOutputs: []OpOutput{ {Code: "Upper('1')", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"1\""}, FuncResults: []string{"\"1\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "== '1'", CodeDepth: 0, Comparison: true, Left: "\"1\"", Right: "\"1\"", StrConditionResult: "[true]", ConditionResult: boolPtr(true), Finalized: true}, {Code: "&&", CodeDepth: 0, JumpIf: true, IfFalse: true, StrConditionResult: "", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "OpCall2", Expr: "UpperTwo('hello', 'world') == 'HELLOWORLD'", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "UpperTwo('hello', 'world')", CodeDepth: 0, Func: true, FuncName: "UpperTwo", Args: []string{"\"world\"", "\"hello\""}, FuncResults: []string{"\"HELLOWORLD\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "== 'HELLOWORLD'", CodeDepth: 0, Comparison: true, Left: "\"HELLOWORLD\"", Right: "\"HELLOWORLD\"", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "OpCall3", Expr: "UpperThree('hello', 'world', 'foo') == 'HELLOWORLDFOO'", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "UpperThree('hello', 'world', 'foo')", CodeDepth: 0, Func: true, FuncName: "UpperThree", Args: []string{"\"foo\"", "\"world\"", "\"hello\""}, FuncResults: []string{"\"HELLOWORLDFOO\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "== 'HELLOWORLDFOO'", CodeDepth: 0, Comparison: true, Left: "\"HELLOWORLDFOO\"", Right: "\"HELLOWORLDFOO\"", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "OpCallN", Expr: "UpperN('hello', 'world', 'foo', 'lol') == UpperN('hello', 'world', 'foo', 'lol')", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "UpperN('hello', 'world', 'foo', 'lol')", CodeDepth: 0, Func: true, FuncName: "OpCallN", Args: []string{"\"lol\"", "\"foo\"", "\"world\"", "\"hello\""}, FuncResults: []string{"\"HELLOWORLDFOOLOL\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "UpperN('hello', 'world', 'foo', 'lol')", CodeDepth: 0, Func: true, FuncName: "OpCallN", Args: []string{"\"lol\"", "\"foo\"", "\"world\"", "\"hello\""}, FuncResults: []string{"\"HELLOWORLDFOOLOL\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "== UpperN('hello', 'world', 'foo', 'lol')", CodeDepth: 0, Comparison: true, Left: "\"HELLOWORLDFOOLOL\"", Right: "\"HELLOWORLDFOOLOL\"", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "base string cmp", Expr: "base_string == 'hello world'", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "== 'hello world'", CodeDepth: 0, Comparison: true, Left: "\"hello world\"", Right: "\"hello world\"", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "loop with func call", Expr: "count(base_struct.Myarr, {Upper(#) == 'C'}) == 1", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "count(base_struct.Myarr, {", CodeDepth: 4, BlockStart: true, ConditionResult: (*bool)(nil), Finalized: false}, {Code: "Upper(#)", CodeDepth: 4, Func: true, FuncName: "Upper", Args: []string{"\"a\""}, FuncResults: []string{"\"A\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "== 'C'})", CodeDepth: 4, Comparison: true, Left: "\"A\"", Right: "\"C\"", StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true}, {Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 4, JumpIf: true, IfFalse: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false}, {Code: "Upper(#)", CodeDepth: 4, Func: true, FuncName: "Upper", Args: []string{"\"b\""}, FuncResults: []string{"\"B\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "== 'C'})", CodeDepth: 4, Comparison: true, Left: "\"B\"", Right: "\"C\"", StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true}, {Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 4, JumpIf: true, IfFalse: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false}, {Code: "Upper(#)", CodeDepth: 4, Func: true, FuncName: "Upper", Args: []string{"\"c\""}, FuncResults: []string{"\"C\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "== 'C'})", CodeDepth: 4, Comparison: true, Left: "\"C\"", Right: "\"C\"", StrConditionResult: "[true]", ConditionResult: boolPtr(true), Finalized: true}, {Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 4, JumpIf: true, IfFalse: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: false}, {Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 0, BlockEnd: true, StrConditionResult: "[1]", ConditionResult: (*bool)(nil), Finalized: false}, {Code: "== 1", CodeDepth: 0, Comparison: true, Left: "1", Right: "1", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "loop with func call and extra check", Expr: "count(base_struct.Myarr, {Upper(#) == 'C'}) == 1 && Upper(base_struct.Foo) == 'BAR'", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "count(base_struct.Myarr, {", CodeDepth: 4, BlockStart: true, ConditionResult: (*bool)(nil), Finalized: false}, {Code: "Upper(#)", CodeDepth: 4, Func: true, FuncName: "Upper", Args: []string{"\"a\""}, FuncResults: []string{"\"A\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "== 'C'})", CodeDepth: 4, Comparison: true, Left: "\"A\"", Right: "\"C\"", StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true}, {Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 4, JumpIf: true, IfFalse: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false}, {Code: "Upper(#)", CodeDepth: 4, Func: true, FuncName: "Upper", Args: []string{"\"b\""}, FuncResults: []string{"\"B\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "== 'C'})", CodeDepth: 4, Comparison: true, Left: "\"B\"", Right: "\"C\"", StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true}, {Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 4, JumpIf: true, IfFalse: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false}, {Code: "Upper(#)", CodeDepth: 4, Func: true, FuncName: "Upper", Args: []string{"\"c\""}, FuncResults: []string{"\"C\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "== 'C'})", CodeDepth: 4, Comparison: true, Left: "\"C\"", Right: "\"C\"", StrConditionResult: "[true]", ConditionResult: boolPtr(true), Finalized: true}, {Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 4, JumpIf: true, IfFalse: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: false}, {Code: "count(base_struct.Myarr, {Upper(#) == 'C'})", CodeDepth: 0, BlockEnd: true, StrConditionResult: "[1]", ConditionResult: (*bool)(nil), Finalized: false}, {Code: "== 1", CodeDepth: 0, Comparison: true, Left: "1", Right: "1", StrConditionResult: "[true]", ConditionResult: boolPtr(true), Finalized: true}, {Code: "&&", CodeDepth: 0, JumpIf: true, IfFalse: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: false}, {Code: "Upper(base_struct.Foo)", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"bar\""}, FuncResults: []string{"\"BAR\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "== 'BAR'", CodeDepth: 0, Comparison: true, Left: "\"BAR\"", Right: "\"BAR\"", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "base 'in' test", Expr: "base_int in [1,2,3,4,42]", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "in [1,2,3,4,42]", CodeDepth: 0, Args: []string{"42", "map[1:{} 2:{} 3:{} 4:{} 42:{}]"}, Condition: true, ConditionIn: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "base string cmp", Expr: "base_string == 'hello world'", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "== 'hello world'", CodeDepth: 0, Comparison: true, Left: "\"hello world\"", Right: "\"hello world\"", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "base int cmp", Expr: "base_int == 42", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "== 42", CodeDepth: 0, Comparison: true, Left: "42", Right: "42", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "negative check", Expr: "base_int != 43", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "!= 43", CodeDepth: 0, Negated: true, Comparison: true, Left: "42", Right: "43", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "testing ORs", Expr: "base_int == 43 || base_int == 42", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "== 43", CodeDepth: 0, Comparison: true, Left: "42", Right: "43", StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true}, {Code: "||", CodeDepth: 0, JumpIf: true, IfTrue: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false}, {Code: "== 42", CodeDepth: 0, Comparison: true, Left: "42", Right: "42", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "testing basic true", Expr: "true", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "true", CodeDepth: 0, Condition: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "testing basic false", Expr: "false", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "false", CodeDepth: 0, Condition: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: true}, }, }, { Name: "testing multi lines", Expr: `base_int == 42 && base_string == 'hello world' && (base_struct.Bar == 41 || base_struct.Bar == 42)`, Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "== 42", CodeDepth: 0, Comparison: true, Left: "42", Right: "42", StrConditionResult: "[true]", ConditionResult: boolPtr(true), Finalized: true}, {Code: "&&", CodeDepth: 0, JumpIf: true, IfFalse: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: false}, {Code: "== 'hello world'", CodeDepth: 0, Comparison: true, Left: "\"hello world\"", Right: "\"hello world\"", StrConditionResult: "[true]", ConditionResult: boolPtr(true), Finalized: true}, {Code: "&& (", CodeDepth: 0, JumpIf: true, IfFalse: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: false}, {Code: "== 41", CodeDepth: 0, Comparison: true, Left: "42", Right: "41", StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true}, {Code: "||", CodeDepth: 0, JumpIf: true, IfTrue: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false}, {Code: "== 42)", CodeDepth: 0, Comparison: true, Left: "42", Right: "42", StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "upper + in", Expr: "Upper(base_string) contains Upper('wOrlD')", Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "Upper(base_string)", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"hello world\""}, FuncResults: []string{"\"HELLO WORLD\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "Upper('wOrlD')", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"wOrlD\""}, FuncResults: []string{"\"WORLD\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "contains Upper('wOrlD')", CodeDepth: 0, Args: []string{"\"HELLO WORLD\"", "\"WORLD\""}, Condition: true, ConditionContains: true, StrConditionResult: "true", ConditionResult: boolPtr(true), Finalized: true}, }, }, { Name: "upper + complex", Expr: `( Upper(base_string) contains Upper('/someurl?x=1') || Upper(base_string) contains Upper('/someotherurl?account-name=admin&account-status=1&ow=cmd') ) and base_string startsWith ('40') and Upper(base_string) == 'POST'`, Env: defaultEnv, ExpectedOutputs: []OpOutput{ {Code: "Upper(base_string)", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"hello world\""}, FuncResults: []string{"\"HELLO WORLD\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "Upper('/someurl?x=1')", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"/someurl?x=1\""}, FuncResults: []string{"\"/SOMEURL?X=1\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "contains Upper('/someurl?x=1')", CodeDepth: 0, Args: []string{"\"HELLO WORLD\"", "\"/SOMEURL?X=1\""}, Condition: true, ConditionContains: true, StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true}, {Code: "||", CodeDepth: 0, JumpIf: true, IfTrue: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false}, {Code: "Upper(base_string)", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"hello world\""}, FuncResults: []string{"\"HELLO WORLD\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "Upper('/someotherurl?account-name=admin&account-status=1&ow=cmd') )", CodeDepth: 0, Func: true, FuncName: "Upper", Args: []string{"\"/someotherurl?account-name=admin&account...\""}, FuncResults: []string{"\"/SOMEOTHERURL?ACCOUNT-NAME=ADMIN&ACCOUNT...\""}, ConditionResult: (*bool)(nil), Finalized: true}, {Code: "contains Upper('/someotherurl?account-name=admin&account-status=1&ow=cmd') )", CodeDepth: 0, Args: []string{"\"HELLO WORLD\"", "\"/SOMEOTHERURL?ACCOUNT-NAME=ADMIN&ACCOUNT...\""}, Condition: true, ConditionContains: true, StrConditionResult: "[false]", ConditionResult: boolPtr(false), Finalized: true}, {Code: "and", CodeDepth: 0, JumpIf: true, IfFalse: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: false}, {Code: "and", CodeDepth: 0, JumpIf: true, IfFalse: true, StrConditionResult: "false", ConditionResult: boolPtr(false), Finalized: true}, }, }, } logger := log.WithField("test", "exprhelpers") for _, test := range tests { if test.LogLevel != 0 { log.SetLevel(test.LogLevel) } else { log.SetLevel(log.DebugLevel) } extraFuncs := []expr.Option{} extraFuncs = append(extraFuncs, expr.Function("UpperTwo", UpperTwo, []interface{}{new(func(string, string) string)}..., )) extraFuncs = append(extraFuncs, expr.Function("UpperThree", UpperThree, []interface{}{new(func(string, string, string) string)}..., )) extraFuncs = append(extraFuncs, expr.Function("UpperN", UpperN, []interface{}{new(func(string, string, string, string) string)}..., )) supaEnv := GetExprOptions(test.Env) supaEnv = append(supaEnv, extraFuncs...) prog, err := expr.Compile(test.Expr, supaEnv...) if test.ExpectedFailedCompile { if err == nil { t.Fatalf("test %s : expected compile error", test.Name) } } else { if err != nil { t.Fatalf("test %s : unexpected compile error : %s", test.Name, err) } } if test.Name == "nill deref" { test.Env["nillvar"] = nil } outdbg, ret, err := RunWithDebug(prog, test.Env, logger) if test.ExpectedFailRuntime { if err == nil { t.Fatalf("test %s : expected runtime error", test.Name) } } else { if err != nil { t.Fatalf("test %s : unexpected runtime error : %s", test.Name, err) } } log.SetLevel(log.DebugLevel) DisplayExprDebug(prog, outdbg, logger, ret) if len(outdbg) != len(test.ExpectedOutputs) { t.Errorf("failed test %s", test.Name) t.Errorf("%#v", outdbg) //out, _ := yaml.Marshal(outdbg) //fmt.Printf("%s", string(out)) t.Fatalf("test %s : expected %d outputs, got %d", test.Name, len(test.ExpectedOutputs), len(outdbg)) } for i, out := range outdbg { if !reflect.DeepEqual(out, test.ExpectedOutputs[i]) { spew.Config.DisableMethods = true t.Errorf("failed test %s", test.Name) t.Errorf("expected : %#v", test.ExpectedOutputs[i]) t.Errorf("got : %#v", out) t.Fatalf("%d/%d : mismatch", i, len(outdbg)) } //DisplayExprDebug(prog, outdbg, logger, ret) } } }