合规性检查:
许多行业标准和法规要求对软件进行安全合规性检查。静态分析工具可以帮助组织确保其软件产品符合这些标准和法规要求。
参考:一图看懂软件缺陷检查涉及的内容
漏洞检测:
静态分析工具可以在代码编写阶段检测潜在的安全漏洞,如SQL注入、跨站脚本攻击(XSS)、缓冲区溢出等。
参考:2023年最具威胁的25种安全漏洞(CWE TOP 25)
减少开发成本:
通过在开发早期阶段发现问题,静态分析可以减少后期修复的成本和时间,因为后期修复通常成本更高。
参考:构建DevSecOps中的代码三层防护体系
2. 静态分析工具的业务痛点
随着现在工程项目的代码量越来越大,同时开发框架的快速迭代和出现。静态分析工具所需要覆盖的场景也随之快速的增加,但静态分析工具所提供的是通用的检查能力,以及静态分析工具有限的迭代速度,无法满足客户不断出现的各种差异化需求。
目前静态分析工具的主要痛点:
2.1. 无法开发自定义规则
大多数静态分析工具由于设计之初多是为了解决特定的编码问题,所以没有考虑到后期的扩展和由用户完成规则的开发。如果需要提供自定义开发能力,需要从架构上重新设计,或者因为检查效率的问题,无法提供通用的检查配置和自定义能力。这将导致无法快速提供客户特定的需求的问题检查。用户只能通过需求反馈的方式,等待工具下个版本的发布,需要的闭环周期很长。
2.2. 对误报和漏报的规则无法快速修改
静态分析工具由于是对代码的静态分析,输入存在不确定性,这些不确定性导致工具在分析策略在上近似(Over-approximation)、下近似(Under-approximation)以及检查效率三者之间寻求某种平衡,这三个因素互相影响、互相制约。
- 上近似是指分析工具可能将一些实际上不会发生的程序行为错误地识别为可能发生的。换句话说,它可能导致分析结果过于宽泛,将一些安全的状态或行为错误地标记为不安全的。这也就导致了误报(false positives),即错误地将安全的代码标记为有问题。
- 下近似是指分析工具可能未能识别出实际上会发生的程序行为。这意味着分析结果可能过于保守,遗漏了一些潜在的问题。这就导致了漏报(false negatives),即未能发现实际存在的安全问题或错误。
- 效率是所有使用者一直追求的因素,快了还想快。但哪里有又想马儿跑得快,又想马儿不吃草的好事情。
由于这些原因,静态分析工具通常提供的是一种通用的检查规则,往往不能覆盖特定的场景,或覆盖场景不适合特定用户的使用条件,这也造成检查工具无法避免误报和漏报。比如说:从文件读对有的用户是危险,但对有的用户是安全的,工具无法识别用户读文件的实际场景,只能将所有从文件读设置为危险的。如果用户无法快速对工具规则进行修改,就会被检查结果中的误报或漏报造成困扰。
2.3. 开发自定义规则有一定的难度
分析引擎提供的自定义开发包,但也需要自定义规则的开发人员掌握静态分析的相关技术,用户上手的难度较大。且由于引擎对API的封装能力,对外提供的检查能力有限,在很大程度上限制了用户自定义规则的实现能力。
基于这些痛点,需要寻找一种适合编写静态分析规则的语言,来降低自定义规则的难度,使用户能够直接开发满足自己需求的规则,用户可以自己在很大程度上来控制和解决误报和漏报。
那么什么才是适合用户的编写静态分析规则的语言呢?
3. 寻找适合编写静态分析规则的语言
为了寻找适合用户的编写静态分析规则的语言,我们来看下我们常见的两种编程范式:声明式语言(Declarative Language)和命令式语言(Imperative Language)。这两者语言在如何描述程序行为和解决问题的方法上存在根本差异, 但同时各有优势和适用场景, 许多现代编程语言支持这两种范式, 允许程序员根据具体问题选择最合适的方法。
问题:
从一个人群中挑出成年人;
具体条件:
选出的人年龄大于等于 18 岁。
命令式语言 – Java 语言
public List<Person> selectAdults(List<Person> persons){ List<Person> result = new ArrayList<>(); for (Person person : persons) { if (person.getAge() >= 18) { result.add(person); } } return result; }
SELECT * FROM Persons WHERE Age >= 18;
从这个例子可以看出来,声明式语言更适合用户的使用,这也是为什么 SQL 语言在短时间内能够迅速的被推广和使用。
声明式语言的特点,也正是我们正在寻找的适合编写静态分析规则的语言。用户只需要关注:“做什么”(What to do),即描述期望的结果或目标状态,而不指定如何达到这个结果的具体步骤或过程。
我们也可以把这个检查语言称为一种领域特定语言(Domain Specific Language,DSL),为特定领域或问题域定制的语言,专注于解决特定类型的问题。这个语言只专注于 – 编写程序静态分析的规则。
这里没有直接使用自然语言,主要是自然语言存在表述上的差异和描述的准确性的问题。当然随着大模型的越来越成熟,直接通过自然语言完成规则的编写,也离我们越来越近了。但不管怎样,在识别到检查条件后,还是需要有一个引擎将这些约束条件转换成具体查询的程序语言,完成问题代码的搜索,这就像 SQL 语言负责描述条件,还需要一个 SQL 的查询引擎,完成 SQL 语言的解析和实施查询。
4. DSL 在程序静态分析中的应用举例
4.1. 编写检查规则
检查问题:
- 生产环境中不应该有调试代码。
问题检查条件:
- 查找所有函数声明
- 并且(And):函数名以"debug"开头
- 并且(And):函数只有一个参数
- 并且(And):参数类型为"java.util.List"
package com.dsl; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.List; /** * 检查问题:生产环境中不应该有调试代码。 * 问题检查条件: * - 查找所有函数声明; * - 并且(And):函数名以"debug"开头; * - 并且(And):函数只有一个参数; * - 并且(And):参数类型为"java.util.List"。 */ public class CheckDebug { private static final Logger LOG = LogManager.getLogger(CheckDebug.class); // 应检查出的问题函数 public void debugFunction(List<String> msgs) { for (String msg : msgs) { LOG.error("print debug info: {}", msg); } } }
编写检查规则
DSL 写的检查规则
/** * 检查问题:生产环境中不应该有调试代码。 * 问题检查条件: * - 查找所有函数声明; * - 并且(And):函数名以"debug"开头; * - 并且(And):函数只有一个参数; * - 并且(And):参数类型为"java.util.List"。 */ functionDeclaration fd where and( fd.name startWith "debug", fd.parameters.size() == 1, fd.parameters[0].type.name == "java.util.List" );
4.1.1. 规则的解读
程序是由空格分隔的字符串组成的序列。在程序分析中,这一个个的字符串被称为"token",是源代码中的最小语法单位,是构成编程语言语法的基本元素。
Token可以分为多种类型,常见的有关键字(如if、while)、标识符(变量名、函数名)、字面量(如数字、字符串)、运算符(如+、-、*、/)、分隔符(如逗号,、分号;)等。
程序在编译过程中,词法分析器(Lexer)读取源代码并将其分解成一系列的token。语法分析器(Parser)会使用这些 token 来构建一个抽象语法树(Abstract Syntax Tree, AST),这个树结构表示了代码的语法结构。这个时候每个 token 也可以称为抽象语法树的节点,树上某个节点的分支就是这个节点的子节点。每个节点都会有节点类型、属性、值。
下面来描述下规则中使用的 DSL 和需求之间的对应关系。
节点类型、属性、值
在规则中,需要查找的是函数声明。这里使用:functionDeclaration 为代码的函数声明节点。在这个节点下有许多的属性,可以通过“.”的方式获取这些属性。
例如函数节点有:函数名(name)、函数的参数(parameters)等子节点。同时每个属性有自己的类型,以及值。例如:函数名(name)为字符串类型,函数的参数(parameters)一个集合类型;