使用 SAST 工具时,为什么我们必须对编译语言(例如 C/C++)使用 "build wrapper"?

When using a SAST tool, why do we have to use a "build wrapper" for compiled languages (e.g. C/C++)?

我是 SAST 工具的新手。 运行 这些工具并找出有时很明显但我们没有注意到的错误,真是太棒了。

虽然我知道如何 运行 这些工具,但我仍然对这些令人难以置信的工具是如何工作的有很多疑问。

例如,在使用 SonarQube 或 Coverity 扫描 C/C++ 源代码时,我们必须使用构建包装器,以便该工具可以监控构建过程。但是,对于其他解释性语言,这些工具只需要看一下代码,仍然可以很好地发挥作用。

我可以设想这些工具正在构建源代码(函数 calls/variables/memory alloc 或 dealloc)之间的关系,对于编译语言,工具必须介入构建过程的原因是什么?

静态分析工具需要知道代码的含义。对于编译型语言,代码的含义通常取决于编译器是如何被调用的。对于 C/C++,这包括 -D(宏定义)和 -I(包含路径)选项,因为前者通常控制 #ifdef 和后者的行为用于为 third-party 库(以及其他内容)查找 headers。对于 Java,编译命令包含 -classpath 选项,这也是找到 third-party 依赖项的方式。其他编译语言类似。

找到正确的依赖关系很重要,因为这会影响代码的解析方式和行为。作为前者的一个例子,考虑一下,在 Java 中,表达式 a.b.c.d.e.f 可能有很多含义,因为 . 运算符既用于在包层次结构中导航,也用于取消引用一个包object 访问字段。如果 a 来自类路径,则该工具在不检查该类路径中的 类 的情况下无法知道这意味着什么。作为后者的示例,请考虑接受 object 引用的 third-party 库中的函数。该函数是否允许传递 null 引用?除非它是工具已知的 well-known 函数,否则唯一的判断方法是让分析器检查该函数的字节码。

现在,一个工具可以在调用分析器时直接要求用户提供编译信息。例如,clang-tidy 就是采用这种方法。这在概念上很简单,但维护起来可能是一个挑战。在一个大项目中,可能有许多文件集是用不同的选项编译的,这使得设置起来很麻烦。可能更糟的是,没有简单通用的方法来确保传递给分析器的选项和要分析的文件集与实际构建保持同步。

因此,一些工具提供了一个 "build monitor" 可以包装通常的构建,检查它执行的所有编译,并收集要分析的源文件集和编译它们所需的选项。完成后,可以开始主要分析。使用这种方法,正常构建中的任何内容都不必随着时间的推移进行修改或维护。然而,这并非完全没有潜在问题。该工具可能需要被告知,例如,您的编译器可执行文件的名称是什么(在 cross-compile 场景中可能会有很大差异),并且您必须确保正常构建从 "clean"状态,否则可能会漏掉一些文件。

解释型语言通常是不同的,因为它们通常具有由分析器可以看到的环境变量指定的依赖项。如果不是这种情况,分析器通常会接受额外的配置选项。例如,如果 PATH 上的 python 可执行文件不是用于正在分析的 运行 Python 脚本的,通常可以告诉分析器模拟不同的脚本.

切线:在你的问题的最后,你开玩笑地把这个过程称为"meddling"。事实上,这些工具非常努力 而不是 对正常构建产生任何可观察到的影响。论文 A Few Billion Lines of Code Later(我是其中的作者之一)有一些有趣的失败案例。