写点什么

从典型工具看 SAST 工具的可扩展性实现

作者:maijun
  • 2023-08-29
    新加坡
  • 本文字数:7713 字

    阅读完需:约 25 分钟

可扩展性,是 SAST 工具非常重要的评价指标。一款工具自身提供的检查能力,正常都无法满足一家企业的全部需求,因此必然就涉及到规则和能力的扩展。本文从几种典型的 SAST 工具(Coverity、Fortify、SonarQube、Klocwork、CodeQL)的扩展方式入手,分析典型的 SAST 工具的扩展方式,并分析什么样的扩展能力更有价值。

通过对部分典型的 SAST 工具进行梳理,SAST 的主要的扩展方式有如下五种:

  • 基于 SDK 扩展,主要用于规则开发

  • 基于 DSL 扩展,主要用于规则开发,函数行为描述

  • 基于代码模型扩展,主要用于第三方库行为描述,和基于 SDK 有些类似

  • 基于配置文件扩展,主要用于规则开发和第三方库行为描述

  • 基于 XPath 扩展,主要用于结构化规则开发

下面将对上面的 5 种类型的 SAST 工具的扩展方式进行介绍。

1. 基于 SDK 的扩展方式


基于 SDK 的扩展方式,主要就是对外暴露 SAST 工具原生的检查引擎的 API,让外部的检查工具通过调用这些 API,从而实现能力的扩展。当前支持该方式的检查工具有:Coverity、Klocwork、SonarQube、Clang Static Analyzer 等。下面将对部分工具的 SDK 扩展规则方式进行介绍。

1.1 基于 SDK 的扩展方式示例

其实这一部分,我最想拿 Coverity 来举例,但是我在公开资料上,没有搜索到相关的资料,担心构成信息安全风险,所以这里选择 Klocwork 和 SonarQube 来举例。

1.1.1 Klocwork Path 规则的扩展示例[1]

如下,我们直接举一个 Klocwork 的 Path 自定义规则的例子的代码(参考资料中的源代码)来说明:

/// 创建了一个 Source 点,将 malloc 函数定义为 Sourceclass MallocTrigger : public Trigger {  ...  void extract(node_t node, TriggerResult *res) {    if (expr_isCallTo(node_getReadExpression(node), "malloc") &&        node_getWrittenExpression(node)) {      res->add(node_getWrittenExpression(node), threadContext);    }  }};
/// 定义了一个 Sink 点,将 delete 函数定义为 Sinkclass DeleteTrigger : public Trigger { ... void extract(node_t node, TriggerResult *res) { if (expr_isCallTo(node_getReadExpression(node), "delete") && expr_getCallArgument(node_getReadExpression(node), 1)) { res->add(expr_getCallArgument(node_getReadExpression(node), 1), threadContext); } }};
/// 创建了一个分析器,并分别设置了 Source 和 Sink,然后执行分析static void processFunction(function_t function, ThreadContext &threadContext) { // Create analyzer SourceSinkAnalyzerPtr a = getConditionalSourceSinkChecker(KPA_PLUGIN_NAME_STR, threadContext); // Register triggers a->addSourceTrigger(new MallocTrigger(threadContext), getEvent("Put message here (use ’%v’)", threadContext)); a->addSinkTrigger (new DeleteTrigger(threadContext), getEvent("Put message here (use ’%v’)", threadContext)); // Run analysis a->analyze(function);}

复制代码

关于上面 Klocwork Path 规则的三个类,已经加了简单的注释。我们可以看到,Klocwork 在创建自定义规则时,调用了原生的 SDK,例如:在设置 Source 和 Sink 时,使用了统一的父类 Trigger,并都使用了 extract 方法,在执行分析任务时,使用的检查引擎 SDK 是 SourceSinkAnalyzerPtr,通过 getConditionalSourceSinkChecker 方法创建。

1.1.2 SonarQube AST 规则的扩展示例[2]

如下,我们直接举一个 SonarQube 创建 Java 自定义规则的例子的代码(参考资料中的源代码)来说明:

...@Rule(key = "AvoidAnnotation")public class AvoidAnnotationRule extends BaseTreeVisitor implements JavaFileScanner {
private static final String DEFAULT_VALUE = "Inject";
private JavaFileScannerContext context;
/** * Name of the annotation to avoid. Value can be set by users in Quality profiles. * The key */ @RuleProperty( defaultValue = DEFAULT_VALUE, description = "Name of the annotation to avoid, without the prefix @, for instance 'Override'") protected String name;
@Override public void scanFile(JavaFileScannerContext context) { this.context = context; scan(context.getTree()); }
@Override public void visitMethod(MethodTree tree) { List<AnnotationTree> annotations = tree.modifiers().annotations(); for (AnnotationTree annotationTree : annotations) { TypeTree annotationType = annotationTree.annotationType(); if (annotationType.is(Tree.Kind.IDENTIFIER)) { IdentifierTree identifier = (IdentifierTree) annotationType; if (identifier.name().equals(name)) { context.reportIssue(this, identifier, String.format("Avoid using annotation @%s", name)); } } }
// The call to the super implementation allows to continue the visit of the AST. // Be careful to always call this method to visit every node of the tree. super.visitMethod(tree); }}
复制代码

如上,是一个检查在 Java 代码中,是否有使用 Annotation 的规则,如果有使用,报告警。上面的规则,调用了 SonarQube 提供的 SDK,对源码的 AST 进行遍历,从而分析得到是否有使用 Annotation 的代码。

1.2 基于 SDK 的扩展方式实现

我们在上一节中,分别采用 SonarQube 和 Klocwork 介绍了两类规则的开发:

  • 结构化规则的开发,在 SonarQube 中,通过调用原生引擎提供的 SDK,采用 Visitor 遍历的方式,检查问题场景;

  • 数据流规则的开发,在 Klocwork Path 规则中,调用了基于污点分析的数据流引擎 SourceSinkAnalyzer,并分别定义 Source 和 Sink 来进行检查。

如果 SAST 工具支持其他的检查引擎,例如基于状态机的检查等,可以相应地曝露相关的 API 接口,在自定义规则中,可以调用相关的 API 完成规则开发。


这里,SDK 的调用,并完成规则开发并不难,主要的难点在于完成的规则如何调起来。这里我们介绍两类调用方式:

  • 自定义规则作为一个单独的可执行文件执行,例如 Klocwork 和 Coverity 就是这样的实现机制,这种机制实现起来也比较容易;

  • 自定义规则以插件的方式加载到执行引擎中,和内置规则一起加载使用。例如 SonarQube 和 Clang Static Analyzer 就是这样的实现机制。

1.3 基于 SDK 的扩展方式优缺点分析

那么,这样的实现方式有什么优缺点呢?我觉得优缺点都非常明显。

主要的优点有:

  • 可以充分利用原生 SDK 的能力,实现更加灵活的定制;

  • 使用原生的开发语言,能够实现更加深层次的复杂的规则开发。

主要的缺点有:

  • 规则定制有一定的准入门槛,比如用户如果要开发 Klocwork Path 规则,需要会 C++ 开发;

  • 规则 SDK 确定好后就无法轻易变动,否则容易造成向下不兼容。

2. 基于 DSL 的扩展方式


DSL 就是针对 SAST 工具,专门设计一种简化的检查语言。DSL 是实现难度相对比较复杂的一种扩展方式,这里简单介绍两个 Fortify 的结构化规则 和 CodeQL 的 DSL(业界标杆),Coverity 大概在 19 年左右推出 CodeXM 的 DSL(这里不用 Coverity 举例,但是其实实现的能力都类似)。

2.1 基于 DSL 的扩展方式示例

2.1.1 Fortify 结构化自定义规则举例[3]

下面是一个 Fortify 的自定义规则示例。

<RuleDefinitions>    <StructuralRule formatVersion="19.10" language="cpp">        <RuleID>224166A0-3461-4661-969E-918ED309AE34</RuleID>        <VulnKingdom>PRIVATE</VulnKingdom>        <VulnCategory>PRIVATE</VulnCategory>        <VulnSubcategory>PRIVATE</VulnSubcategory>        <DefaultSeverity>5.0</DefaultSeverity>        <Description></Description>        <Predicate><![CDATA[            FunctionCall fc:                fc.name contains "operator="                 and arguments contains [Operation op:                    op.lhs is [FunctionCall pfc:                         pfc.name contains "GetPrivateData"                    ]                ] and fc.instance is [ Operation op1:                     op1.lhs is [FieldAccess fa:                         fa.field.enclosingClass is [Class c:                            c.name contains "Response"                        ]                    ]                ]        ]]></Predicate>    </StructuralRule></RuleDefinitions>
复制代码

如上,真正的 DSL 部分是上面的第 10-22 行,实际上,这种一个针对 C/C++ 语言开发的自定义规则,表达的含义是检查某个“=”运算,这个“=”右边,应该是个函数调用,函数名是 “GetPrivateData”,“=”左边应该是个对某个变量的域的访问,并且这个变量的类型是 Response。

2.1.2 CodeQL 自定义规则举例

下面介绍一个 CodeQL 的自定义规则的例子:

// UnsafeDeserialization.ql
from DataFlow::PathNode source, DataFlow::PathNode sink, UnsafeDeserializationConfig conf where conf.hasFlowPath(source, sink) select sink.getNode().(UnsafeDeserializationSink).getMethodAccess(), source, sink, "Unsafe deserialization of $@.", source.getNode(), "user input"
复制代码

上面的这个例子,是 CodeQL 首页上面的一个例子。是一个数据流规则的告警,要求从 source 到 sink 可达,并且 sink 是 UnsafeDeserializationSink。

CodeQL 提供了我当前知道的,最丰富的 DSL 定义的 API。

2.2 基于 DSL 的扩展方式实现

一般来说,基于 DSL 的扩展方式,对面向结构化的代码检查更有用处。因为如果是面向数据流的规则,很多时候通过配置方式就可以实现,而且更加简单直观。

如果要实现一种 DSL,是一种非常复杂的功能,在这里的篇幅,是无法完全介绍清楚的。下面简单介绍两个大的方向(后面有机会,单独介绍 DSL 的扩展实现):

  • 设计一种规则扩展的语言,使用 Antlr4 等解析规则,生成对应的语法树,然后在引擎中开发代码,通过匹配节点的方式,解释执行,最后报告警;

  • 采用业界经典的 DSL 设计框架,例如 Kotlin,设计适用于 SAST 工具的 DSL,这样就不是解释执行,而是直接运行了,这种方式在当前商用工具中使用比较少,我之前写过一个简单的 demo[4],大家可以参考(我有想过把 xml 舍弃掉,做成一个更好看的 DSL 来着,就是一直没有时间)。

不建议大家考虑自己实现一个 DSL 还支持把这个 DSL 编译成可执行文件去执行,工作量太大了,如果选择把这个 DSL 编译成 class 文件,然后再解释执行,完全可以选择 Kotlin DSL。

2.3 基于 DSL 扩展方式优缺点分析

基于 DSL 的扩展方式,优缺点也非常明显:

主要的优点有:

  • 对于一个 SAST 工具中支持的所有语言,提供一个有限支持的 DSL,可以简化规则开发难度,提高用户体验;

  • 用户不需要去学习一门高级语言;

  • 让你的 SAST 工具看起来就高大上了起来。

主要的缺点有:

  • DSL 是一种有限支持的规则开发方式,检查能力跟对外暴露的接口相关,支持的语法特性有限,因此一般来说,基于 DSL 只能进行有限的规则能力支持,误报和漏报相对于基于 SDK 会较高,并且部分规则场景可能都没办法支持。

3. 基于代码模型的扩展方式

这一部分内容,在 Coverity 中有看到类似。但是其机制并不复杂,如果要做相关的开发,并不是很难。

这里的模型,其实就是一种第三方库的摘要信息。通过写代码的方式,调用 Coverity 的原语,从而对函数的行为加以定义,这就是这类扩展方式。

下面介绍一下 Coverity 中这一类扩展的例子。

3.1 Coverity 模型扩展举例

如下面的例子:

void free(void* x) {    // Do nothing.}
void my_free(void* x) { __coverity_free__(x);}
void xx_sql(char* s) { __coverity_mark_pointee_as_tainted__(s, SQLI)}
复制代码

在代码中,my_free 函数,在其中给了一个实现,是通过宏定义的,宏名是 __coverity_free__,该宏的调用会告诉 Coverity,如果代码中调用了 my_free 函数,就相当于释放了该函数的第一个参数指针,就不会有内存泄漏。基于该配置,可以避免出现相关的误报。

上面的 __coverity_free__ 在 Coverity 中被称为原语(primitive),Coverity 中,提供了非常非常多的类似的原语。

在 Coverity 中,原语的类型非常多,可以分为:

  • 资源管理相关的原语

  • 安全相关的原语

  • 并发相关的原语

3.2 模型扩展方式实现

前面提到过,模型扩展方式实现相对比较简单,但是也要看实现机制。目前看来,主要有两类实现途径(我们使用 Coverity 中的原语的概念):

  • 通过 SAST 工具内建模型原语,然后将写好的模型文件(即调用了原语的 C/C++源码)和源码一起编译,并且链接在一起,自然模型中的函数相关的实现就会取代原来的实现,从而可以直接进行进一步分析;

  • 通过 SAST 工具内建模型原语,然后将写好的模型文件(即调用了原语的 C/C++源码)单独编译,生成摘要信息,然后在分析中使用摘要。

实际上,Coverity 采用的是第 2 种方式,但是我们讲,摘要并不好建立,函数摘要没有银弹,有可能每一条规则、每一种代码结构可能都需要单独建立摘要。

4. 基于配置文件的扩展方式

基于配置文件的 SAST 工具扩展方式,在 SAST 工具中应用非常常见。下面我们介绍两种配置文件的方式,分别对应了规则相关的配置扩展和第三方库的函数行为的配置扩展。

4.1 基于配置文件的扩展方式示例

4.1.1 SonarQube 的配置文件的扩展示例(面向规则扩展)[5]

SonarQube 可以通过配置文件,给特定的规则增加 Source、Sink、Passthrough 等节点,可以通过 JSON 配置,Fortify 也有类似的机制,但是是通过 XML 格式配置的,并且配置的是 TaintFlag 相关的传递信息,整体类似。

下面是 SonarQube 官方的一个示例:

{  "S3649": {    "sources": [      {        "methodId": "my.package.ServerRequest#getQuery()Ljava/lang/String;"      }    ],    "sanitizers": [      {        "methodId": "my.package.StringUtils#stringReplace(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",        "args": [          2         ]      }    ],    "validators": [      {        "methodId": "my.package.StringUtils#equals(Ljava/lang/String;)Z",        "args": [          1        ]      }    ],    "passthroughs": [      {        "methodId": "my.package.RawUrl#<init>(Ljava/lang/String;)V",        "isWhitelist": true,        "args": [          1        ]      }    ],    "sinks": [      {        "methodId": "my.package.MySql#query(Ljava/lang/String;)V",        "args": [          1        ]      }    ]  },  "common": {    "sources": [      {        "methodId": "my.package.Input#getUserInput()Ljava/lang/String;"      }    ]  }}
复制代码

为了缩小篇幅,删掉了一部分,原生的配置,可以参考下面的链接。

如上,配置文件的信息,可以对 Source、Sink、Passthrough、Sanitizer 等进行配置,从而实现规则的误报优化,或者减少漏报。实现规则扩展。

4.1.2 Klocwork kb 文件的配置扩展示例(面向第三方库扩展)[6]

Klocwork 的 KB 文件,是对第三方库的函数行为进行建模的一种配置方式,并不是直接针对规则的配置,其目的和上一节介绍的 Coverity 的模型类似。比如对于下面的代码(该例子是从上面引用中摘下来的):

void* C::custom_alloc(unsigned int n) {    return malloc(n);}
void C::foo() { void* p = custom_alloc(10); return;}
复制代码

比如对于上面的代码,如果不希望在第 6 行报内存泄漏的问题,可以在 kb 文件中,对 custom_alloc 添加如下的配置:

C::custom_alloc - ALLOC ignore
复制代码

该配置,告诉 Klocwork 忽略掉 custom_alloc 的 ALLOC 的记录(record)。Klocwork 支持非常多的记录类型(record kind,比如前面的 ALLOC 就是一种 record kind)。

这种建模方式,跟 Coverity 的模型建模方式,目的类似,但是没有 Coverity 灵活,能实现的功能不如 Coverity 丰富,但是会比 Coverity 简单一点儿。

4.2 基于配置文件的扩展方式的优缺点

一般来说,基于配置文件的扩展方式,实现比较简单,大部分的 SAST 工具,支持的第一步,一般就是基于配置的扩展,只是看配置文件中配置的丰富程度各有不同。

基于配置文件的扩展方式的优点

  • 配置简单清晰,容易理解

  • 方便阅读,直观简单

基于配置文件的扩展的缺点

  • 能力略显不足,无法实现复杂的配置

  • 并不适合结构化规则

5. 基于 XPath 的扩展方式


基于 XPath 的规则扩展方式,就是通过 XPath 的方式,对 AST 进行遍历的一种方式,也只适用于 AST,不适用于其他场景。

5.1 基于 XPath 的扩展方式示例

PMD 就支持基于 XPath 的规则,大家可以看看,这里举一个 Klocwork KAST 的规则扩展的例子。

5.1.1 Klocwork KAST 规则举例[7]

比如针对如下代码:

//finds this functionstatic void f() {    /*code*/}
// and this onestatic void h() { /*code*/}
class C { public: // find this function as well static void m() { /*code*/ }};
复制代码

我们希望找到所有的 static 函数,只需要写如下表达式,创建 KAST 规则即可:

// FuncDeclarator [ isStatic() ]
复制代码

5.2 基于 XPath 的规则扩展方式的优缺点

基于 XPath 的规则的扩展方式非常简单,如我们之前提到的,XPath 的扩展方式只适合 AST 遍历,使用受限,实现上,大家可以参考 PMD 的实现。

主要的优点有:

  • 结构紧凑

  • 逻辑比较清晰

主要的缺点有:

  • 只支持结构化规则

  • XPath 原生在部分属性函数支持上比较弱,如果要支持需要进行更多的额外开发

6. 总结

本文介绍了部分 SAST 工具的扩展方式。从实现上面来说,可以采用多种扩展方式的组合。这里,笔者基于个人经验,给如下建议:

  • 对外提供的工具版本,建议支持 基于 DSL 的扩展 + 模型扩展

  • 基于 SDK 的方式,可以作为专业服务进行提供

  • 配置文件的方式能够提供的功能比较弱,但是实现容易,可以作为初始版本提供的能力,再慢慢迭代

  • 基于 XPath 的方式,在商用工具中并不多见,不建议采用

当然,上面是个人建议,仅供参考。

参考

[1] Klocwork Path 规则 API 参考文档(https://help.klocwork.com/2023/pdfs/Klocwork_C_Cxx_PATH_API_Reference.pdf)

[2] SonarQube Java 规则开发举例(https://github.com/SonarSource/sonar-java/blob/master/docs/java-custom-rules-example)

[3] fortify-structural-rules-guide(https://github.com/ryohare/fortify-structural-rules-guide)

[4] java-sca-ast-dsl(https://github.com/maijun-sec/java-sca-ast-dsl)

[5] Security engine custom configuration(https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/security-engine-custom-configuration/)

[6] Walk-through 3: Tuning with knowledge base records(https://help.klocwork.com/2022/en-us/concepts/walkthrough3tuningwithknowledgebaserecords.htm)

[7] Find all static functions(https://help.klocwork.com/current/en-us/concepts/findallstaticfunctions.htm)


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

maijun

关注

还未添加个人签名 2019-09-20 加入

关注SAST工具开发、应用,DevSecOps、研发效能等

评论

发布
暂无评论
从典型工具看SAST工具的可扩展性实现_SAST工具_maijun_InfoQ写作社区