一种基于 Kotlin DSL 的静态代码分析 AST 规则扩展实现
本文介绍一种基于 Kotlin DSL 的静态代码分析工具的,AST 类型规则的扩展方法实现。其中 AST 为 JavaParser (介绍可以参考本人之前的博客:Java Parser应用介绍 - InfoQ 写作平台)构造的。本文的代码 demo 参考链接:zhangmaijun/java-sca-ast-dsl (github.com)
1. 结构化规则
1.1 什么是结构化规则
结构化规则是针对代码的结构进行检查的一类规则。通常意义上来讲,不需要进行数据流和控制流的分析。例如,非常火的,Java 的静态代码分析工具 PMD,就是这样的一类规则的检查工具。这类规则一般检查速度快,在 缺陷模式 设置准确的情况下,误报率还是比较低的。
1.2 结构化规则和 AST 检查是一致的吗?
很多时候,我们在做这类检查引擎的时候,都是在 AST 上面开发,例如:Java 会有 JavaParser,JDT(还有 PMD 和 SonarQube 等自己生成了 AST 结构)等,C/C++ 会有 CDT,CPPCheck,Clang AST 等。因此,很多时候,将 结构化规则 和 AST 检查规则 等同起来。
但是,很多时候,并不是这样的,结构化规则,是针对代码结构的规则,但是并不是说,不需要基于一定基础的检查。 下面看两个例子:
(1) 常量传播问题
看下面的三组代码:
如上面的代码,我们关注方法 use()
使用,只要入参 < 0,我们就需要报个告警。上面,第 4, 5, 9 三行,都有使用 use()
方法,此时,根据我们的分析,三个都应该报告警。但是如果我们只在 AST 上面分析,第 4 行,肯定可以报告警,第 5 行,还是应该可以识别出来,因为 NUM
是个 final 的 static 数据,但是,第 9 行的缺陷,无法分析出来。
(2) 函数调用(指向分析)问题
如上,我们在 b() 中增加了注释,如果在 b() 之前调用的是 a() 获取的,那么就报告警。例如第 4 行的使用方式,就会报个缺陷。但是,在第 8 行,同样调用了 b(),而且根据分析,虽然是 use2 调用了方法 b(),但是 use2 是通过 a() 获取到的,此时,其实在 第 8 行,也应该报缺陷。但是仅基于 AST 层面,也很难报出来。就是说,仅依赖 AST 检查,结构化规则检查是不充分的,会存在漏报。
1.3 结构化规则和 IR 分析
针对上面,在 AST 上面,直接进行检查的缺陷,Fortify 工具的结构化规则,是在 IR 层面进行的分析,而且在 IR 上面,首先完成 常量传播、指向分析、类型分析 等基础分析之后,再进行上面的结构化规则分析,相对漏报会更少。
当然,也存在一些问题,就是 IR 的语法信息,毕竟不如 原始的 AST 丰富,因为部分的语法结构,没有在 IR 中体现出来,例如,for 语句 在 IR 中,被处理成 while 语句,因此,结构化规则,就不支持 for 检查,而只能处理成可以识别的 while 语句的检查。
2. 基于 Kotlin DSL 的 AST 规则扩展实现机制
首先,如标题所示,本文基于 AST 进行扩展,因此,没有提前进行基础的 常量传播、指向分析、类型分析 等,仅仅是纯 AST 层面的分析。下面简单介绍该 demo 中实现的机制,还有已经扩展的部分规则。
2.1 Kotlin DSL 的原理介绍
具体可以参考我之前的博客:Kotlin DSL实现原理介绍 - InfoQ 写作平台
2.2 对 AST 节点基础类扩展
为了更好地进行匹配,我将所有的需要的参数,都提到了 JavaParser 的 AST 节点的 根节点 Node 上面,例如下面的代码:
该参数用来识别节点是否为空,主要针对的是存在 block 的节点,其 block 是否为空,如果有需要,可以进一步扩展到其他的节点上面。不过,这里我将 empty 属性放到了 基类 Node 上面,对于不支持该属性的 AST 节点,则会直接抛出异常。
这样,我们所有的操作,都简化到了基类上面的匹配操作,而且规避掉了各类节点的函数或方法调用,只需要记住需要的属性即可,使 自定义规则的扩展 更加容易。
2.3 匹配方法扩展
匹配的方法,主要如下:
可以看到,针对匹配,只需要使用一个 Node 上面的扩展方法,不需要再对 每种类型的 AST 都定义 match 方法,简化实现。
2.4 AST 类型的匹配
实现中,将 AST 类型,也抽象为 Node 的一个属性,如下面的代码所示:
2.5 and 和 or 及 && 和 || 的使用
在 逻辑运算符 进行连接时,不同的条件的连接,在 Kotlin DSL 中,可以使用上面两类操作,当然,面向 DSL,肯定是 and 和 or 来得更直观,但是,在 Kotlin DSL 中,and 和 or 没有短路策略,只有 && 和 || 才有,因此,两种方式都行,但是理论上来说,&& 和 || 应该会更好。
具体这部分的内容,可以参考我之前的博客:Kotlin中逻辑运算符操作分析 - InfoQ 写作平台
3. 基于 Kotlin DSL 的 AST 规则扩展示例
3.1 基础 Kotlin DSL 的 AST 规则扩展示例
已经实现的 demo,参考了 Fortify 的自定义规则,也以 Fortify 帮助文档里面的几个例子来进行实现,其实在 自定义规则的 格式上,也参考了 Fortify 的自定义规则(参考链接:https://tech.esvali.com/mf_manuals/html/sca_ssc/hpe_security_fortify_static_code_analyzer-custom_rules_guide.htm)。
Fortify 的自定义规则,可以参考上面的链接的内容(只有 Fortify 17.xx 的版本),下面仅介绍一个 leftover debug 的规则例子,其他的 例子,可以参考代码(完整的规则,可以参考 github demo 的 doc/rule_template.xml)。
规则定制如下:
如上,根节点是 rule,有一个属性是 language,下面是四个子节点:id,name,description,matcher。其中 id 是 UUID 转大写,name 是规则名,description 是规则描述,matcher 是 规则的 dsl 写法。规则的 dsl 都放在了 <![CDATA[]]> 里面。
在上面的规则里面,主要有两部分(以最前面的冒号分隔):
(1) MethodDeclaration md,有两个字符串,第一个表示我们关心的 节点类型,此处表示的是 方法定义,第二个表示我们在 后面的 dsl 中,使用的 根节点的 别名;
(2) 右边部分,就是结构匹配部分:
① md match {},都是这样的形式,所有要匹配的条件,都在 match 后面的大括号里面写,但是整体,都应该符合这样的写法;
② 这里包含了三个条件,并且三个条件是 and 的关系,就是需要 全部满足。第一个条件表示属性 methodName 需要以 debug 开头,第二个条件表示 属性 parameterTypes.size 只有一个,第三个条件表示 属性 parameterTypes 的第一个元素,是 java.util.List 类型,泛型是任意的。
上面的几个属性的定义如下:
整体来说,可以做的,都可以做了。
3.2 匹配嵌套及需要记录参数的场景
这里,在 Fortify 官方文档的 6 个例子里面,没有一个合适的例子,之前也没有想起来,刚刚写的时候,想起来了,就写个方法,知道怎么扩展。
如上的代码,node 需要匹配的条件中,prop1 是抽象到 Node 中的属性,可以表示子节点或者父节点等,因此也可以直接适用 match 方法的嵌套。同时,如果在再下面的匹配中(例如 prop2 的匹配中,需要使用到 prop1 表示的节点),只需要在 匹配的最前面添加 val node2 = this,就给当前节点创建了一个别名,这样在 prop2 的匹配中就可以使用了,这里,明确命名的有 node 和 node2 两个。
4. 总结
本文实现了一种基于 Kotlin DSL + JavaParser 的 面向静态代码分析工具的 AST 规则 的扩展方式。主要参考了 Fortify 的自定义规则的定制方式。而且在实现上,也在很大程度上,实现了能够类似于 缺陷描述 的规则定制方式,规避了 方法调用 类型的语法的出现,可读性和可扩展性更好。
版权声明: 本文为 InfoQ 作者【maijun】的原创文章。
原文链接:【http://xie.infoq.cn/article/08e2dba13ead0f3bb86b48580】。文章转载请联系作者。
评论