eSOC项目的一个重要功能就是规则引擎,规则引擎的主要功能就是关联分析。规则引擎的最基本的功能就是计算表达的值(表达式是规则中的基础部分),为了选择一个合适的基础构件作表达式求值工具,前后比较了Drools, IKExpression,Aviator和Groovy,Drools为JBosss的开源规则引擎, IKExpression和Aviator都是轻量的Java表达式求值引擎(均为中国人开发的开源表达式求值引擎),Groovy是JVM上的动态语言,Java的官方弟弟。比较的基本要素是性能;第二是功能:支持常见的操作包括>,>=, ==, !=, <,<=,contains和正则表达式以及二元操作符&& ,括号等, ,其次是使用方便。
在如下的场景中,我们需要找出威胁程度最高的事件,也就是报警5,依靠的就是关联分析来完成,下图中所示的是交叉关联分析中的典型场景。
对应于这个场景,规则引擎需要做的事情是从收集到的IDS日志,防火墙日志,资产列表和漏洞库中发现如下的情况:
“IDS检测到Attack的行为,并且防火墙允许了这个行为,Attack的目标资产(服务器,网络设备等)非常重要,并且资产具有该攻击对应的漏洞;如果以上都匹配,并且在10分钟之内发生了一次以上,就产生一个高级别的实时报警”
为了能够分析出上述行为,需要一个或者规则来执行,那么上述文字描述可以转换为如下描述的规则,一个规则由表达式+聚合 + action三部分组成
表达式部分:
1. (Event1.devicetype == 'IDS' && Event1.eventtype=='Attack')
2. (Event2.devicetype == 'Firewall' && Event2.eventtype=='Accept')
3 ((Event1.attcaktype + Event1.destinationIP) in vulnerabilityList //事件的攻击目标地址具有该攻击类型所包括的漏洞
聚合部分:
1. (Event1.destinationIP == Event2.destinationIP) //事件1和2的目标地址是同一个
2. Event1和Event2 在10分钟内发生的次数 > 1次
action
if( Event1.destinationIP 是高价值资产) 产生报警,级别是10
else if(( Event1.destinationIP 是中价值资产) ) 产生报警,级别是8
从这里可以看出,表达式是规则的基础组成部分
下面展示一下赛门铁克SSIM的表达式样式
在这里简要介绍一下4种表达式求值方法,并对同一含义的表达式在各个引擎中的使用方法给出了示例。需要求值的表达式的文字描述为:(模块等于"system" 并且 设备ip地址包含"252") 并且 事件开始时间大于等于"2011-08-09 09:44:15"。需要进行比较的数据输入为Map或者Bean格式,内容为(groovy的语法示例,定义一个map是不是很简单呢):
Drools(JBoss Rules )具有一个易于访问企业策略、易于调整以及易于管理的开源业务规则引擎,符合业内标准,速度快、效率高。业务分析师或审核人员可以利用它轻松查看业务规则,从而检验是否已编码的规则执行了所需的业务规则。
除了应用了Rete核心算法,开源软件License和100%的Java实现之外,Drools还提供了很多有用的特性。其中包括实现了JSR94 API和创新的规则语义系统,这个语义系统可用来编写描述规则的语言。目前,Drools提供了三种语义模块――Python模块,Java模块和 Groovy模块。Drools的规则是写在drl文件中。 对于前面的表达式,在Drools的drl文件描述为:
When表示条件,then是满足条件以后,可以执行的动作,在这里可以调用任何java方法等。在drools不支持字符串的contians方法,只能采用正则表达式来代替。
IK Expression 是一个开源的(OpenSource),可扩展的(Extensible), 基于java 语言开发的一个超轻量级(Super lightweight)的公式化语言解析执行工具包。IK Expression 不依赖于任何第三方的java 库。它做为一个简单的jar,可以集成于任意的Java 应用中。这包括了JavaEE 应用(基于应用服务器的),Java桌面应用以及Java WebStart 方式的应用。 对于前面的表达式,IKExpression的写法为:
可以看到IK是通过内置的函数$CONTAINS来完成字符串是否包含另外一个字符串。
Aviator是一个高性能、轻量级的java语言实现的表达式求值引擎,主要用于各种表达式的动态求值。现在已经有很多开源可用的java表达式求值引擎,为什么还需要Avaitor呢?
Aviator的设计目标是轻量级和高性能,相比于Groovy、JRuby的笨重,Aviator非常小,加上依赖包也才450K,不算依赖包的话只有70K;当然,
Aviator的语法是受限的,它不是一门完整的语言,而只是语言的一小部分集合。
其次,Aviator的实现思路与其他轻量级的求值器很不相同,其他求值器一般都是通过解释的方式运行,而Aviator则是直接将表达式编译成Java字节码,交给JVM去执行。简单来说,Aviator的定位是介于Groovy这样的重量级脚本语言和IKExpression这样的轻量级表达式引擎之间。对于前面的表达式,Aviator的写法为:
在Aviator中,字符串是调用String函数来完成的。
Groovy的网站(http://groovy.codehaus.org)给出了groovy的最好定义:groovy是在java平台上的、具有象Python,Ruby和Smalltalk语言特性的灵活动态语言,groovy保证了这些特性象java语法一样被java开发者使用。
Groovy经常被认为是脚本语言——它也像脚本一样工作的很好。但是,把Groovy理解为脚本语言是一种误解,groovy代码被编译成java字节码,然后能集成到java应用程序中或者web应用程序,整个应用程序都可以是groovy编写的——groovy是非常灵活的。
groovy与java平台非常融合,groovy的许多代码是使用java实现的,其余部分是用groovy实现的,当你使用groovy编程的时候,许多情况下你正在写特殊的java程序,在java平台上的所有强大功能——包括大量的java类库也可以直接在groovy中使用。对于前面的表达式,Groovy的写法为:
Groovy的写法最接近于语言表达式本身需要描述的事情,我非常喜欢。
在表达式引擎介绍中,简要概述了不同的引擎的用法,那么它们的性能有什么差异呢。为了进行对比,针对同一功能的表达式,(模块等于"system" 并且 设备ip地址包含"252") 并且 事件开始时间大于等于"2011-08-09 09:44:15",分别执行100万次,每次跑三遍。运行环境为:
硬件:Core2 7250 2G双核/2G内存;
操作系统:Win7;
JDK:1.6.0.24;
Drools:5.1;
IKExpression:2.1.2;
Aviator:2.0;
Groovy:1.8
运行JVM参数为:-Xms512m -Xmx512m -XX:CompileThreshold=10000 -XX:MaxPermSize=128m。执行结果为:
execute Drools total time = 66318ms
execute Drools total time = 66303ms
execute Drools total time = 66802ms
execute IKExpresion total time = 12603ms
execute Aviator total time = 4855ms
execute Groovy total time = 1722ms
execute IKExpresion total time = 12465ms
execute Aviator total time = 5035ms
execute Groovy total time = 1841ms
execute IKExpresion total time = 12591ms。
execute Aviator total time = 4809ms
execute Groovy total time = 1784ms
从执行100万次表达式求值看,Groovy最快,Aviator其次,IKExpression然后,Drools最慢;而且Groovy的语法我非常喜欢,简单并且功能强悍,大爱Groovy。数据统计图如下
Groovy太快了,一秒钟可以执行56万次! 程序的源码见附件。
Drools是一个高性能的规则引擎,但是设计的使用场景和在本次测试中的场景并不太一样,Drools的目标是一个复杂对象比如有上百上千的属性,怎么快速匹配规则,而不是简单对象重复匹配规则,因此在这次测试中结果垫底。
IKExpression是依靠解释执行来完成表达式的执行,因此性能上来说也差强人意,和Aviator,Groovy编译执行相比,还是性能差距还是明显。
Aviator会把表达式编译成字节码,然后代入变量再执行,整体上性能做得很好
Groovy是动态语言,依靠反射方式动态执行表达式的求值,并且依靠JIT编译器,在执行次数够多以后,编译成本地字节码,因此性能非常的高。对应于eSOC这样需要反复执行的表达式,Groovy是一种非常好的选择。在学习Groovy过程中,已经深深的爱上了它,强大的功能,简洁的语法,和Java完美集成。
在执行测试的时候,使用的VM Args:-Xms512m -Xmx512m -XX:CompileThreshold=10000 -XX:MaxPermSize=128m,我突发奇想,如果把CompileThreshold调整到100W会咋样,也就是在整个测试过程中,都不要让JIT编译器把代码编译为本地代码。当调整到XX:CompileThreshold=1000000,结果数据发生了非常显著的变化。大家可以看下面的数据
VM Args:-Xms64m -Xmx64m -XX:CompileThreshold=10000
execute IKExpresion total time = 12553ms
execute Aviator total time = 4900ms
execute Groovy total time = 1782ms
VM Args:-Xms64m -Xmx64m -XX:CompileThreshold=1000000
execute IKExpresion total time = 45913ms
execute Aviator total time = 8914ms
execute Groovy total time = 18559ms
数据对比整理结果:
家可以明显的看出,Groovy执行时间变为原来的11倍左右,IKExpression也差不多慢了4倍,Aviator本身就编译成为了字节码,因此受到的影响较小,时间为原来的2倍。也就是说原来Groovy从执行1万次的解释执行以后,后面的99万次由JIT编译器编译为本地字节码后,本地字节码的性能是解释执行的11倍!
那么大家可能有个疑问,是不是JIT都是好的呢,我们什么代码都让JIT编译为本地字节码,这样不是性能就非常高了。其实不然,这个和你的应用场景是密切相关的,如果你的代码执行的次数不够,那么就没有必要编译为本地字节码,否则执行性能的提升的幅度还抵消不了JIT编译浪费的时间。JIT默认都是开启的,在JVM的 client模式CompileThreshold=1500,server模式是CompileThreshold=10000。更多一点的信息,可以参考Java关于JIT的原理和相关知识
到这里,关于表达式求值的论述就算结束了,其中还发现一些有趣的现象,比如GC的策略和堆大小对测试程序的影响,在这里就不一一写出来,人懒:)