写点什么

一种基于 Kotlin DSL 的静态代码分析 AST 规则扩展实现

用户头像
maijun
关注
发布于: 刚刚

本文介绍一种基于 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) 常量传播问题

看下面的三组代码:

public class ConstantUse {    private static final int NUM = -1;    public static void main(String[] args) {        use(-1);        use(NUM);                int x = 0;        int y = x - 1;        use(y);    }
public static void use(int x) { // sensitive usage of x, when x < 0, there will be some problem }}
复制代码

如上面的代码,我们关注方法 use() 使用,只要入参 < 0,我们就需要报个告警。上面,第 4, 5, 9 三行,都有使用 use() 方法,此时,根据我们的分析,三个都应该报告警。但是如果我们只在 AST 上面分析,第 4 行,肯定可以报告警,第 5 行,还是应该可以识别出来,因为 NUM 是个 final 的 static 数据,但是,第 9 行的缺陷,无法分析出来。

(2) 函数调用(指向分析)问题

public class FunctionCallUse {    public static void main(String[] args) {        FunctionCallUse use = new FunctionCallUse();        use.a().b();                FunctionCallUse use1 = new FunctionCallUse();        FunctionCallUse use2 = use1.a();        use2.b();    }        public FunctionCallUse a() {        return this;    }        public void b() {        // if a() is called first, then call this method will occur some security problem    }}
复制代码

如上,我们在 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 上面,例如下面的代码:

val Node.empty: Boolean    get() {        if (this is NodeWithBlockStmt<*>) {            val body = this.body            if (body == null || body.isEmptyStmt || CollectionUtils.isEmpty(body.statements)) {                return true            }            return false        }
throw UnSupportedParameterException("empty", this) }
复制代码

该参数用来识别节点是否为空,主要针对的是存在 block 的节点,其 block 是否为空,如果有需要,可以进一步扩展到其他的节点上面。不过,这里我将 empty 属性放到了 基类 Node 上面,对于不支持该属性的 AST 节点,则会直接抛出异常。

这样,我们所有的操作,都简化到了基类上面的匹配操作,而且规避掉了各类节点的函数或方法调用,只需要记住需要的属性即可,使 自定义规则的扩展 更加容易。

2.3 匹配方法扩展

匹配的方法,主要如下:

infix fun Node.match(block: Node.() -> Boolean): Boolean {    return block(this)}
infix fun Node.contain(block: Node.() -> Boolean): Boolean { if (this is NodeWithBlockStmt<*>) { this.body.statements.forEach { if (block(it)) { return true } } }
return false}
复制代码

可以看到,针对匹配,只需要使用一个 Node 上面的扩展方法,不需要再对 每种类型的 AST 都定义 match 方法,简化实现。

2.4 AST 类型的匹配

实现中,将 AST 类型,也抽象为 Node 的一个属性,如下面的代码所示:

val Node.simpleName: String    get() = this.javaClass.simpleName
val Node.qualifiedName: String get() = this.javaClass.name
复制代码

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="java">        <id>EC377C0A-3C95-4D91-B4F9-15CE5318FFA1</id>        <name>leftover debug code</name>        <description>leftover debug code</description>        <matcher>            <![CDATA[                MethodDeclaration md: md match {                    (methodName startWith "debug") and                    (parameterTypes.size == 1) and                    (parameterTypes[0].resolveType matches "java.util.List<.*?>")                }            ]]>        </matcher>    </rule>
复制代码

如上,根节点是 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 类型,泛型是任意的。

上面的几个属性的定义如下:

val Node.methodName: String    get() {        if (this is MethodDeclaration) {            return this.name.asString()        }
throw UnSupportedParameterException("methodName", this) }
val Node.parameterTypes: MutableList<Type> get() { if (this is MethodDeclaration) { val parameterTypes = mutableListOf<Type>() this.parameters.forEach { parameterTypes.add(it.type) } return parameterTypes }
throw UnSupportedParameterException("parameterTypes", this) }
val Type.resolveType: String get() = resolveType(this)
fun resolveType(type: Type): String { val resolvedType = type.resolve() if (resolvedType != null) { return resolvedType.describe() } return ""}
复制代码

整体来说,可以做的,都可以做了。

3.2 匹配嵌套及需要记录参数的场景

这里,在 Fortify 官方文档的 6 个例子里面,没有一个合适的例子,之前也没有想起来,刚刚写的时候,想起来了,就写个方法,知道怎么扩展。

node match {    prop1 match {        val node2 = this        prop2 match {            // can use node and node2 here         }    }}
复制代码

如上的代码,node 需要匹配的条件中,prop1 是抽象到 Node 中的属性,可以表示子节点或者父节点等,因此也可以直接适用 match 方法的嵌套。同时,如果在再下面的匹配中(例如 prop2 的匹配中,需要使用到 prop1 表示的节点),只需要在 匹配的最前面添加 val node2 = this,就给当前节点创建了一个别名,这样在 prop2 的匹配中就可以使用了,这里,明确命名的有 node 和 node2 两个。

4. 总结

本文实现了一种基于 Kotlin DSL + JavaParser 的 面向静态代码分析工具的 AST 规则 的扩展方式。主要参考了 Fortify 的自定义规则的定制方式。而且在实现上,也在很大程度上,实现了能够类似于 缺陷描述 的规则定制方式,规避了 方法调用 类型的语法的出现,可读性和可扩展性更好。


发布于: 刚刚阅读数: 2
用户头像

maijun

关注

还未添加个人签名 2019.09.20 加入

关注基于源码的静态代码分析,缺陷模式识别,Java白盒审计等

评论

发布
暂无评论
一种基于Kotlin DSL的静态代码分析AST规则扩展实现