一、基础用法

前段时间学了点golang的基础语法,就来学习cel-go模块的使用,网上有用的教程倒是比较少,cel-go的github官方页面倒是给了点示例代码可以学习下。

1.变量使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package examples

import (
"fmt"
"log"

"github.com/google/cel-go/cel"
)
// 入口函数
func ExampleSimple() {
// 通过NewEnv创建一个程序环境,并且该程序环境内声明了一个String类型的变量name
env, err := cel.NewEnv(cel.Variable("name", cel.StringType))
if err != nil {
log.Fatalf("environment creation error: %v\n", err)
}
// 编译所给出的CEL表达式字符串,并返回一个AST语法树,可以理解为CEL可读的格式
ast, iss := env.Compile(`"Hello world! I'm " + name + "."`)
// Check iss for compilation errors.
if iss.Err() != nil {
log.Fatalln(iss.Err())
}
// 通过AST语法树,创建一个可执行的程序
prg, err := env.Program(ast)
if err != nil {
log.Fatalln(err)
}
// 传入参数并计算结果
out, _, err := prg.Eval(map[string]any{
"name": "CEL",
})
if err != nil {
log.Fatalln(err)
}
fmt.Println(out)
// 输出:Hello world! I'm CEL.
}

注释也按照自己的理解在对应代码上方写出,其他示例代码不会对上述代码进行注释。

2.函数使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package examples

import (
"fmt"
"log"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)

// 入口函数
func ExampleCustomInstanceFunction() {
// 创建一个程序环境,其中调用cel.Lib时,会将customLib内的CompileOptions和ProgramOptions更新到程序环境中
env, err := cel.NewEnv(cel.Lib(customLib{}))
if err != nil {
log.Fatalf("environment creation error: %v\n", err)
}
// Check iss for error in both Parse and Check.
ast, iss := env.Compile(`i.greet(you)`)
if iss.Err() != nil {
log.Fatalln(iss.Err())
}
prg, err := env.Program(ast)
if err != nil {
log.Fatalf("Program creation error: %v\n", err)
}

out, _, err := prg.Eval(map[string]any{
"i": "CEL",
"you": "world",
})
if err != nil {
log.Fatalf("Evaluation error: %v\n", err)
}

fmt.Println(out)
// 输出:Hello world! Nice to meet you, I'm CEL.
}

// 创建一个结构体,继承Library接口
type customLib struct{}

// 实现Library接口的CompileOptions方法,用于程序环境的变量声明和函数定义
func (customLib) CompileOptions() []cel.EnvOption {
return []cel.EnvOption{
// 声明字符串类型的变量i和you
cel.Variable("i", cel.StringType),
cel.Variable("you", cel.StringType),
// 定义并实现函数greet,并将函数的实现功能与string_greet_string进行绑定
cel.Function("greet",
// 通过MemberOverload重载的函数,可被环境内声明的变量以点的方式调用,例如i.greet(),但不能直接通过greet()方式调用
// string_greet_string可以理解为该函数唯一的重载Id
cel.MemberOverload("string_greet_string",
// 定义函数传入的数据类型
[]*cel.Type{cel.StringType, cel.StringType},
// 定义函数返回的数据类型
cel.StringType,
// 使用BinaryBinding代表该函数是有两个传入参数
// 该函数所有的返回值都是ref.Val
cel.BinaryBinding(func(lhs, rhs ref.Val) ref.Val {
// 返回字符串类型
return types.String(
fmt.Sprintf("Hello %s! Nice to meet you, I'm %s.\n", rhs, lhs))
}),
),
),
}
}

// 实现Library接口的ProgramOptions方法,用于配置程序内的一些参数,暂时可以置空
func (customLib) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}

第三个示例代码就不展示了,因为和第二个也大差不差,总结下来主要是以下不同:

  • 使用的重载方法不同:示例2的代码采用MemberOverload进行重载,调用方式为已声明的变量.funcName(),而示例3的代码采用Overload进行重载,调用方式可直接通过正常方式funcName()即可完成调用;
  • 另外还有一点是在编写CEL函数是,根据参数的需要来选择是用BinaryBindingUnaryBinding,前者可以传入两个参数,后者只能传入一个参数;

二、动态Env

在实际的开发中,我们可能需要多种不同的程序环境,但如果为每一种类型去写一种Env,那属实会造成代码冗余,不妨我们整一个动态Env,并根据需要动态加载对应的变量和函数。

首先我们正常定义一个SimpleLib结构体

1
2
3
4
5
type SimpleLib struct {
Variables map[string]any
GlobalVariable []cel.EnvOption
GlobalFunction []cel.EnvOption
}
  • Variables用于存储变量与对应的值,例如{“name”:”王刚”};
  • GlobalVariable用于存储Env环境创建所需要的变量类型;
  • GlobalFunction用于存储Env环境创建所需要的函数类型;

1.动态添加变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 传入一个Map类型
func (s *SimpleLib) AddVariable(varMap map[string]any) {
var env []cel.EnvOption
s.Variables = make(map[string]any)
// 遍历varMap,根据对应值的数据类型创建对应的EnvOption类型
for k, v := range varMap {
if reflect.TypeOf(v).String() == "int" {
env = append(env, cel.Variable(k, types.IntType))
s.Variables[k] = v
} else if reflect.TypeOf(v).String() == "string" {
env = append(env, cel.Variable(k, types.StringType))
s.Variables[k] = v
} else if reflect.TypeOf(v).Elem().String() == "otherType" {
// 其他类型的判断添加
} else {
fmt.Println(fmt.Sprintf("UnSupport Variable Type %s", reflect.TypeOf(v).String()))
}
}
s.GlobalVariable = append(s.GlobalVariable, env...)
}

通过遍历varMap内值的类型去创建对应Env环境创建所需要的EnvOption类型,若有自定义可在条件判断后继续添加判断和对应操作。

2.动态添加函数

动态添加函数相对来说就比较简单,因为不需要像变量一样需要判断数据类型

1
2
3
func (s *SimpleLib) AddGlobalFunction(funcArray []cel.EnvOption) {
s.GlobalFunction = append(s.GlobalFunction, funcArray...)
}

这边我采用函数的方式去获取对应的CEL函数,可根据CEL函数的不同类型和作用去创建对应的获取函数,如下代码所示为获取的Instance类型的函数,只作用于变量。

1
2
3
4
5
6
7
func GetAllInstanceFunction() []cel.EnvOption {
return []cel.EnvOption{
cel.Function("contains", containsFunction),
cel.Function("bContains", bContainsFunction),
cel.Function("iContains", iContainsFunction),
}
}

containsFunction等也都是定义好的FunctionOpt类型,如下为其中一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var containsFunction = cel.MemberOverload("contains_string",
[]*cel.Type{cel.StringType, cel.StringType}, cel.BoolType,
cel.BinaryBinding(func(lhs ref.Val, rhs ref.Val) ref.Val {
// 判断传入的第一个参数是否为字符串
v1, ok := lhs.(types.String)
if !ok {
return types.ValOrErr(lhs, "unexpected type '%v' passed to contains_string", lhs.Type())
}
// 判断传入的第二个参数是否为字符串
v2, ok := rhs.(types.String)
if !ok {
return types.ValOrErr(rhs, "unexpected type '%v' passed to contains_string", rhs.Type())
}
return types.Bool(strings.Contains(string(v1), string(v2)))
}))

3.CompileOption

CompileOption方法内是可以配置我们环境内的自定义变量和函数,故逻辑也是在该函数内实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (s *SimpleLib) CompileOptions() []cel.EnvOption {
env := []cel.EnvOption{
// 如果有自定义类型的话,可以提前声明好,没有则无需添加下面的Types代码
cel.Types(
&http.UrlType{},
&http.Request{},
&http.Response{},
)}
// 新增自定义变量,如果GlobalVariable内不为空,则添加到env中
if s.GlobalVariable != nil {
env = append(env, s.GlobalVariable...)
}
// 新增自定义函数,如果GlobalFunction不为空,则添加到env中
if s.GlobalFunction != nil {
env = append(env, s.GlobalFunction...)
}
// 最后返回我们定制的CEL程序环境
return env
}

三、使用案例

以下是对上述代码的一个简单使用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var sayFunction = cel.MemberOverload(
"saying",
[]*cel.Type{cel.StringType},
cel.StringType,
cel.UnaryBinding(func(value ref.Val) ref.Val {
val, ok := value.(types.String)
if !ok {
return types.ValOrErr(value, "unexpected type '%v'", value.Type())
}
return types.String(fmt.Sprintf("我叫%s", string(val)))
}))

func main() {
var funcArray = []cel.EnvOption{
cel.Function("say", sayFunction),
}

simple := SimpleLib{}
simple.AddVariable(map[string]any{
"name": "王刚",
})
simple.AddGlobalFunction(funcArray)
out, err := simple.Execute("name.say()")
if err != nil {
panic(err)
}
fmt.Println(out)
}
// output: 我叫王刚

我自己也写好了一套通过yaml文件去进行漏洞检测的功能,函数和功能都是参考xray扫描器上的,后续是决定完全按照xray上的解析规则实现还是怎样暂时还没想好,这个项目写好后也会发到github上进行开源。以下是yaml模板和最后执行的结果输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
set:
test1: base64("hello")
test2: md5("hello")
rules:
r1:
path: /
method: GET
headers:
Content-Type: application/json
User-Agent: "{{test1}}"
expression: response.body.bContains(bytes('baidu'))
r2:
path: /
method: POST
headers:
Content-Type: application/json
User-Agent: "{{test2}}"
body: '{"admin": "admin"}'
expression: response.status_code == 200

expression: r1() || r2()

下图是运行结果

image-20231207133135944