Google 是如何落地静态代码分析的

Google 是如何落地静态代码分析的?

0x00 前言

本篇文章为我在读 Google 落地静态代码分析的 Paper 《Lessons from Building Static Analysis Tools at Google》时做的笔记,以及一些我自己的观点,主要聊一聊甲方安全建设中落地静态代码分析平台的一些痛点,以及相应的解决方案。

Paper 中提到的静态分析(Static Analysis)不单单只针对安全漏洞的静态代码分析,也包含针对程序功能 Bug 的分析,本质上是为了提高程序的安全性(Security)和可靠性(Reliability)。

全文包括如下几部分内容:

  1. Key Insights
  2. 为什么研发团队经常忽视 SAST 产生的告警?
  3. Scope
  4. 相关术语
  5. Google 是如何构建软件的?
  6. 我们在 FindBugs 中学到了什么?
  7. 如何将静态代码分析集成进编译器的工作流程中?
  8. 在 Code Review 期间展示漏洞告警
  9. 扩大静态代码分析的扫描范围
  10. 相关经验
  11. 总结

众所周知,软件 Bug(安全漏洞、功能 Bug)通常会花费研发人员和科技公司大量的时间和金钱去解决这些问题。

举例来说,2014 年,一个广泛使用的 SSL 实现(Apple 开源的 SecureTransport)中被披露存在 “goto fail” 漏洞,造成其可以接受无效的 SSL 证书 [1]。2016 年,一个日期格式相关的 Bug 造成了 Twitter 大范围的服务中断 [2]。

事实上,像这些 Bug,在静态代码阶段是完全可以被检测到的,通过代码审计或阅读文档,这些都是比较明显的问题,但却仍然被引入到了生产环境中。

0x01 Key Insights

  • Static analysis authors should focus on the developer and listen to their feedback. (负责静态代码分析工具的团队应该聚焦于研发人员并积极听取研发人员的反馈。)
  • Careful developer workflow integration is key for static tool adoption.(和软件开发工作流程的深度集成是静态代码分析工具落地的关键。)
  • Static analysis tools can scale by crowsourcing analysis development. (接受由团队外部人员贡献的扫描规则。)

0x02 为什么研发团队经常忽视 SAST 产生的告警?

目前业界已经有了许多将静态代码分析漏洞检测工具应用于生产环境软件的经验。

尽管我们也有许多将静态代码分析工具应用于开发环境的实际经历,但是仍然由于各种各样的原因,导致工程师不使用静态代码分析工具或者说忽略静态分析工具产生的漏洞告警,主要原因有如下几点:

  1. Not integrated. 未和软件开发工作流程集成或者运行时间较长。
  2. Not actionable. 产生的漏洞告警无明确意义无法处理。
  3. Not trustworthy. 由于过高的误报导致我们的客户(研发团队)不再信任相关漏洞告警。
  4. Not manifest in practice. 漏洞报告中提到的风险理论存在,但是并没有在实际环境中表现出来。
  5. Too expensive to fix. 修复成本过高。
  6. Warnings not understood. 研发团队不理解相关告警及修复方案。

我们主要来介绍一下,Google 基于以往 FindBugs(Java 静态代码分析工具) 的工作和在一些学术文献中总结出来的经验,最终如何成功构建了一个 Google 内部大多数工程师都在使用的静态代码分析基础设施。

Google 的静态代码分析工具每天检测数以千计的漏洞,由相关研发人员自行处理相关漏洞告警,避免存在已知漏洞的代码被引入到代码仓库中。

0x03 Scope

Google 目前专注建设的静态代码分析工具,已经集成进了核心软件开发工作流程(core developer workflow)中,目前被 Google 大部分的研发人员所使用。

许多静态代码分析工具部署在 Google 二十亿行代码量的代码库上是相当简单的,研究如何在一个更高规模的代码量上如何运行静态代码分析工具目前不是一个高优先级的事情。

需要注意的是,在 Google 以外的专业领域(例如航空航天和医疗设备厂商)工作的研发人员可能会使用其他的静态代码分析工具和工作流。同样,从事特定类型项目(例如内核代码和设备驱动程序)的研发人员可能会运行 ad hoc 分析。

0x04 术语

本文采用如下术语及原则描述相关工作:

  • 分析工具针对源代码运行一项或多项“检查(checks)”,识别可能造成软件故障的“问题(issues)”。
  • 如果研发人员在收到漏洞告警之后,未采取积极的响应措施,那我们将其视为“有效误报(effective false positive)”。
  • 如果分析报告中错误的标记了一个问题,但是研发人员仍然进行了 fix 以增加可读性和可维护性,那我们就认为这不是一个有效误报(effective false positive)。
  • 如果分析报告中报告了一个有效漏洞,但是由于开发者不理解相关告警而没有采取任何响应措施,那我们认为他是一个有效误报(effective false positive)。

我们之所以采用这种区分是为了强调开发者反馈体验的重要性。我们会基于开发者的实际使用情况来确定静态分析工具的误报率,而不是工具的作者。

0x05 Google 是如何构建软件的?

Google 构建软件的关键流程如下,在 Google 几乎所有的开发者工具都是集中化和标准化的(开发环境除外),基础设施的许多部分都是由内部团队从头开始构建构建并维护,因此可以非常灵活的进行一些实验。

Source control and code ownership. Google 开发并使用了一个单一的源代码版本控制系统,以及一个单独的庞大的源代码仓库,几乎覆盖了 Google 私有的全部源代码。(一个例外,像 Google 的大型开源项目 Android、Chrome 采用单独的基础设施和自定义的工作流。)

开发者采用基于主干(trunk-based)的方式进行开发,分支(branches)通常用于发布(releases),而不是功能开发。任何一个工程师均有权限去修改任何的代码片段,但是需要得到代码 owner 的批准(approval)。

代码仓库的权限是基于路径的,当前目录的所有者也隐式拥有子目录的管理权限。

Build system. Google 代码仓库中的所有代码均由一个二次开发的 Bazel build system 进行构建。构建过程必须是封闭的,也就是说所有的输入都必须明确声明并存储于源码控制系统中,以方便并行和分布式构建。

在 Google 的构建系统中,Java rules 依赖 checked into 到源码控制的 JDK 和 Java 编译器(compiler)。对于这类二进制文件,只需要 checking-in 新版本即可为所有的用户进行更新。大多数情况下,build 是基于源代码的,很少有二进制的 artifacts 被引入到代码仓库中。

由于所有的开发者采用相同的构建系统,因此它是任何代码片段编译是否有错误的真实来源。

Analysis tools. Google 采用的静态代码分析工具并不复杂。Google 没有在 Google 范围内运行过程间(interprocedural)或整个程序(whole-program)分析,没有大范围的采用诸如 separation logic 这类的高级静态代码分析技术。但是,即使是最简单的检查(check)也需要静态代码分析基础设施支持软件开发工作流程集成(workflow integration)以使他们可以成功落地。

作为一般软件开发工作流程中的一部分,已经部署的静态代码分析类型包括如下几类工具:

  1. Style checkers – 代码风格检查器,比如说 Checkstyle、Pylint、Golint
  2. Bug-finding tools that may extend the compiler – 可以扩展编译器的漏洞发现工具,比如说 Error Prone、ClangTidy、Clang Thread Safety Analysis、Govet 以及 Checker Framework 包括但不限于 abstract-syntax-tree pattern-match tools、type-based checks、unused variable analysis
  3. Analyzers that make calls to production services – 调用生产环境的服务的分析器,例如检查代码注释中提到的一个员工是否仍然在 Google 工作等
  4. Analyzers that examine properties of build outputs – 检查 build 输出的分析器,比如说二进制文件的大小

前文中提到的“goto fail”漏洞,可以被 Google 的 C++ linter 捕获,它会检查 if 语句后面是否包含大括号。

前文中提到的由于代码中日期格式错误导致 Twitter 大范围服务中断,其也无法在 Google 编译,因为 Error Prone compiler error,a pattern-based check 可以识别出数据格式的错误使用,进而导致不解决该问题,无法编译成功。

Google 的开发者也会使用诸如 AddressSanitizer 工具在内的这类动态分析工具来检查缓冲区溢出漏洞,采用 ThreadSanitizer 检查条件竞争问题。这些工具通常会在测试阶段运行,同时也会在生产环境中的流量中运行。

IDE. 为了在整个软件开发生命周期的早起阶段(安全左移),抛出通过静态代码分析发现的漏洞,一个显而易见的工作流集成点是在 IDE 中进行集成。

但是由于 Google 的研发人员采用各种个样的 IDE,因此很难在调用构建工具之前,一致的检测出所有研发人员的代码错误。尽管 Google 确实采用了与内部流行的 IDE 进行集成的静态代码分析工具,但是将所有的 IDE 均启动静态代码分析漏洞扫描是不现实的。

Testing. 几乎所有的 Google 代码均包含测试,从单元测试到大规模的集成测试。测试作为一流的概念集成在构建系统中,并且像构建一样是密封和分布式的。

对于大多数项目,研发人员为他们的代码编写和维护测试用例,项目通常没有测试或单独的质量保证团队,Google 的持续构建和测试系统会在每次提交代码时执行测试,并在研发人员的提交的代码更改中断了了构建流程或导致测试失败时通知研发人员。

Developers, not tool authors, will determine and act on a tool’s perceived false-positive rate.

研发人员,而不是静态代码分析工具的作者,去感知误报率,并采取行动。

Code review. 对 Google 代码库的每一次提交都要经过 Code review。尽管任何研发人员都可以对 Google 代码仓库中的任何代码进行更改,但代码 owner 必须在提交更改之前 review 并批准更改。此外,即使是 owner 也必须在提交更改之前 review 他们的代码。Code review 通过一个与其他开发基础设施紧密集成的集中式、基于 Web 的工具进行。静态代码分析结果在代码审查中浮出水面。

Releasing code. Google 团队的 release 是非常频繁的,大部分的 release 都是通过 “push on green” 的方式实现 release 验证和自动化的部署。这也意味这一个艰巨的、manual-release-validation 的过程是不可能的。如果 Google 的工程师在生产环境中发现了 bug,可以以相对较低的成本发布新版本并将其部署到生产服务器中。

0x06 我们在 FindBugs 中学到了什么?

在早期的研究工作中,从 2008 年到 2010 年,在静态代码分析领域,Google 主要聚焦在采用 FindBugs 进行 Java 分析。这是马里兰大学的 William Pugh 和宾夕法尼亚约克学院 David Hovemeyer 创造的一个独立工具。主要用于分析编译后的 Java 类文件,识别导致潜在 Bug 的代码模式(patterns of code)。

截止到 2018 年 1 月,只剩下 Google 的少部分工程师将其作为命令行工具进行使用。一个 Google 内部,名为 “BugBot” 的小团队,和 Pugh 进行了多次合作,尝试将 FindBugs 集成进 Google 的研发流程(developer workflow),但最终均已失败告终。

因此,在这些尝试的过程中,我们总结出一些有价值的经验:

Attempt 1. Bug dashboard. 最初,在 2006 年,FindBugs 集成作为一个集中式的工具,每晚均会在整个 Google 代码仓库中运行,并生成一个 finding(漏洞) 数据库,工程师可以通过 Dashboard review 每一个漏洞并逐一解决。

尽管 FindBugs 在 Google 的 Java 代码仓库中发现了数以百计的漏洞,但是 Bug Dashboard 几乎没起到什么作用,因为其超出了现有的软件研发工作流程,区分新的和以往的静态代码分析漏洞将会分散研发人员的注意力。

Attempt 2. Filing bugs. BugBot Team 在第二次实验中,尝试手动创建漏洞工单,基于每晚周期性的运行 FindBugs 发现的漏洞(finding),在这些漏洞中选择较高优先级的漏洞,创建漏洞报告,反馈至研发团队。

在 2009 年 5 月,数以百计的 Google 工程师参与了公司范围的 “Fixit” 周,重点解决 FindBugs 产生的告警。

工程师总共 review 了 3,954 个告警(漏洞总量 9,473 个的 42%),但实际上只有 16%(640 个)的漏洞得到了修复,尽管 44% 的漏洞(1,746)已经通过漏洞报告的形式反馈至了研发团队。

尽管 Fixit 验证了通过 FindBugs 发现的许多漏洞是有效漏洞,但是很大一部分都是比较小的问题,不足以在实际环境中进行修复。

Attempt 3. Code review integration. BugBot Team 开发了一个系统,当一个 proposed 改变,触发 review 时,FindBugs 将会自动运行,并且会将扫描结果以评论的形式发送到 code-review thread(页面) 中。

Google 的研发人员可以减少误报,应用 FindBugs 对结果的置信度来过滤评论。后来该工具进一步尝试,仅列出新的 FindBugs 告警,但有时会将老漏洞错误的归类为新漏洞。

当 code-review 工具在 2011 年被替换之后,这种集成也停止了,主要有如下两个原因:

  1. 较高的有效误报率导致开发者对该工具失去信心
  2. 研发人员定制化导致分析结果不一致(developer customization resulted in an inconsistent view of analysis results.)

0x07 将静态代码分析集成进编译器的工作流程中

在进行 FindBugs 项目实验的同时,Google 的 C++ 工作流也在通过向 Clang 编译器添加新的检查(checks)规则来改进代码质量。

Clang Team 实现了新的编译器检查(compiler checks),以及相关漏洞修复建议,在整个 Google 的代码仓库中,采用 ClangMR 以分布式的方式运行更新之后的编译器。

细化检查(refine checks),并且以编程的方式修复代码仓库中已经存在的安全漏洞。

一旦代码仓库中清除了这些已有的安全问题,Clang Team 将启动一个新的诊断(diagnostic)作为一个编译器错误(compiler error)(不是 warning,Clang Team 发现如果是 warning 的话,研发人员通常会忽略处理),当新的代码存在安全问题时,将会中断 build 流程(通过这种方式,研发人员通常没有办法忽略。)Clang Team 采用这种方式,非常成功的提高了代码质量(减少了功能性 Bug、安全漏洞,提高了代码质量)。

Google 团队遵循这种设计,并在 Java 编译器 javac 之上构建了一个基于模式匹配的静态分析工具,我们称之为 Error Prone。

第一个检查规则推出,我们称之为 PreconditionsCheckNotNull,检测由于方法调用中的参数被调换位置,而导致 runtime precondition check trivially succeeds 的情况。举例来说,checkNotNull("uid was null", uid) 代替 checkNotNull(uid, "uid was null")

为了避免 PreconditionsCheckNotNull 检查规则中断已有的持续构建(continuous builds)项目,Error Prone 团队采用基于 javac 的 MapReduce 程序对整个代码仓库进行了此类规则的扫描,类似与 ClangMR,我们采用了 FlumeJava,并将其称之为 JavacFlume。

JavacFlume 提供了相关的修复建议、通过 diff 展示修复前后的代码,然后采用这些修复代码应用于整个代码仓库进行修改。

Error Prone 团队使用内部工具 Rosie ,将大规模更改拆分为每个影响单个项目的小更改,测试这些更改,并将它们发送给适当的团队进行 code review。

这些团队只 review 应用于他们代码的修复,当他们批准时,Rosie 提交这些更改。

所有更改最终都得到批准,现有的问题得到了修复,团队将启用编译器错误(compiler error),用于在编译期间静态分析代码,当存在不合规代码时 break build 流程,避免非预期的代码引入代码仓库中。

当我们针对收到 patch 请求的开发者进行问卷调查时,57% 的开发者表示开心,当他们收到了针对 check-in code 的修复建议时,41% 保持中立态度,只有 2% 的消极回复,说“It just created busywork for me.”。

Value of compiler checks. 编译器错误(Compiler errors)检查的价值。编译器错误(Compiler errors)可以集成到开发流程中,并且更早的将相关漏洞暴露出来。

我们发现扩展编译器检查集可以有效提高 Google 的代码质量。

因为 Error Prone 中的检查是自包含(self-contained)的,并且是针对 javac 抽象语法树编写的,而不是字节码(与 FindBugs 不同),所以团队外的开发人员贡献检查规则相对容易。

利用这些 Team 外部的贡献对于提高 Error Prone 的整体影响力(impact)是至关重要。截至 2018 年 1 月,已有 162 位作者提交了 733 份检查规则。

**Reporting issues sooner is better. ** 越早报告问题越好。Google 的集中式构建系统会记录所有 build 和 build 结果,因此我们确定了在给定时间窗口内可以看到其中指定错误消息(error messages )的所有用户。

我们向最近遇到编译器错误(Compiler errors)的研发人员和收到修复同一问题的 patch 的开发人员发送了一份调查问卷。

Google 研发人员认为在编译时标记的问题(与 patches for checked-in code 相反)会捕获更严重的漏洞。举例来说,调查参与者将 74% 的问题在编译时标记为“真正的问题”,而在 checked-in code 中发现的问题则为 21%。

此外,调查参与者认为在编译时发现的问题中有 6%(checked-in code 中为 0%)“严重”级别。

这个结果可以用“幸存者效应”来解释,即在提交代码时,相关 bug 很可能已经被更昂贵的手段(例如测试和 code review)捕获。

将尽可能多的检查规则移植到编译器中是一种行之有效的方法。

Criteria for compiler checks. 编译器检查的标准,为了扩大我们的工作,我们定义了在编译器中启用检查的标准,将标准设置得很高,因为中断编译流程往往是一个比较大的问题。

Google 的编译器检查提供的告警应该是易于理解的,可操作且易于修复(如果可能的话,触发编译错误时应该提供可以拿来即用的漏洞修复建议),不产生有效的误报(静态代码分析工具不应终止正确代码的编译工作),并报告仅影响正确性而非代码风格或最佳实践的问题。

满足这些标准的静态代码分析工具的主要目标不仅仅是检测漏洞,而是自动修复整个代码仓库中预期可能会导致编译器错误的所有已知安全漏洞。但是,此类标准限制了 Error Prone 团队在编译代码时启用的检查规则的范围,比如说需要放行比较严重,但是无法立即修复的漏洞(修复过程比较复杂,修复周期相对较长的漏洞)。

0x08 在 Code Review 期间展示漏洞告警

Error Prone 团队构建了在代码编译时进行静态代码分析漏洞扫描的基础设施,并且验证了该思路的有效性。我们需要让其针对在 Java 和 C++ 之外提供更多编程语言的支持,同时列出没有达到编译器错误(compiler errors)级别但是仍然有较高优先级的漏洞。

静态代码分析工具扫描结果的第二个展示位置是 Google 的 code review 工具 Critique。扫描结果通过利用 Google 的程序分析平台 Tricorder 在 Critique 中展示。

截至 2018 年 1 月,Google 对 C++ 和 Java 项目的构建有一个无编译器告警(compiler warnings-free)的默认设置,所有扫描结果要么被定性为编译器错误(compiler errors),要么在 code review 中展示。

Criteria for code-review checks. Code review 期间扫描误报率的标准,与编译时扫描是不一样的,code review 期间列出的扫描结果允许包含高达 10% 的有效误报。在 code review 期间列出的扫描告警是可以存在少量误报的,代码作者可以在应用其修复建议之前进行适当的分析和评估。Google 的 code review 期间的静态代码扫描结果应满足以下几个条件:

  1. Be understandable. 可以理解,任何工程师都容易理解其告警的含义。
  2. Be actionable and easy to fix. 可以执行,并且容易被修复。与编译器检查相比,code review 期间报出的漏洞修复可能需要更多的时间、思考或努力,并且扫描结果应包含漏洞修复的解决方案。
  3. Produce less than 10% effective false positives. 产生不到 10% 的有效误报。研发人员应该感觉到全部扫描规则产生的结果中至少在 90% 的情况下指出了真实存在的漏洞。
  4. Have the potential for significant impact on code quality. 部分告警可以对代码质量产生重大影响。这些告警可能不会直接影响代码的正确性,但是研发人员仍然应该认真对待这些告警,并对其进行修复。

有些漏洞虽然严重到可以在编译器中标记出来,但开发出自动修复程序是不可行的。例如,修复一个漏洞可能需要对代码进行大量重构。启用这些作为编译器错误(compiler errors)检查规则需要手动修复历史遗留漏洞,Google 拥有庞大的代码仓库,因此这是不现实的。

静态代码分析工具在 code review 中列出这些告警可以防止新漏洞的产生,允许研发人员自行决定如何修复这些漏洞。

Code review  也是报告相对低优先级问题(如代码风格问题或简化代码的机会)的良好环境。

根据我们的经验,在编译时报告这些低优先级的问题会让研发人员感到沮丧,并使快速迭代和调试变得更加困难。但是,在 Code review 期间,研发人员是将自己的代码提交给其他人查看,处于批判的心态,更愿意看到可读性和风格细节这类问题,并加以更正。

Tricorder. Tricorder 被设计成为一个容易扩展的,支持不同种类的分析工具,包括静态分析和动态分析。

我们在 Tricorder 展示了一套不能在编译器错误(compiler errors)中启动的 Error Prone 扫描规则。

Error Prone 还激发了一个新的 C++ 分析工具的诞生,这些分析工具(分析器和扫描规则)与 Tricorder 集成并称为 ClangTidy。

Tricorder 分析器支持 30 多种编程语言,支持简单的 syntactic analyses ,如代码风格检查工具,利用 Java、JavaScript 和 C++ 的编译器信息,可以直接与生产环境数据(例如当前正在运行的 jobs)集成。

Tricorder 在 Google 环境中得到了良好的落地。因为它是一个插件式系统可以由其他团队贡献检测插件,支持在 code review 期间列出可以执行的漏洞修复建议,并且提供了反馈渠道来改进扫描器并确保静态代码分析工具的研发人员根据反馈采取行动优化相应的静态代码分析工具。

Empower users to contribute. 允许用户贡献检测规则。截至 2018 年 1 月,Tricorder 包含了 146 个静态代码分析工具,其中 125 个来自 Tricorder 外部团队和 7 个用于数百个额外检查的插件系统(例如 ErrorProne 和 ClangTidy,它们构成了 7 个静态代码分析工具插件系统中的两个)。

Provide fixes and involve reviewers. 提供漏洞修复方案并 involve 相关 reviewers。Tricorder 可以将漏洞扫描结果提供至 code review 平台中并包含相关漏洞的修复建议。Reviewers 和代码作者都可以看到这些漏洞详情,Reviewers 可以通过单击静态代码分析结果上的“请修复(Please fix)”按钮来要求代码作者修复存在漏洞的代码。

Iterate on feedback from users. 基于用户的反馈进行持续迭代。除了“请修复(Please fix)”按钮之外,Tricorder 还提供了“无用(Not useful)”按钮,reviewers 或 proposers 可以单击该按钮以表示他们认可对应的告警。单击“无用(Not useful)”按钮后会自动在问题跟踪器(issue tracker,Google 内部的工单系统)中创建对应的 bug 追踪工单,并将其路由到对应的静态代码分析工具的研发团队。Tricorder 团队跟踪此类“无用(Not useful)”按钮的点击数量,计算“请修复(Please fix)”与“无用(Not useful)”点击的比例。如果一个静态代码分析器的比例超过 10%,Tricorder 团队将禁用该分析器,直到作者对其进行改进。虽然 Tricorder 团队很少需要永久禁用分析器,但在分析器作者正在删除和修改特别嘈杂的检查规则时,会多次禁用对应的分析器。

通过点击“无用(Not useful)”按钮创建 bug 追踪工单,通常情况下会导致静态代码分析器的持续改进,从而大大提高研发团队对该分析器的满意度。

即使在具有完整测试覆盖率和严格 code review 的成熟代码库中,也仍然存在漏掉的 bug。

Scale of Tricorder. Tricorder 的规模。截至 2018 年 1 月,Tricorder 每天分析了大约 50,000 次 code review 更改。在高峰时段,每秒运行 3 次分析。Reviewers 每天点击“请修复(Please Fix)”超过 5,000 次,代码作者每天采用自动修复约 3,000 次。Tricorder 分析器每天收到 250 次“无用(Not useful)”点击。

在 code review 展示静态代码分析结果成功得以落地,表面研发团队对此种方式还算满意。

在编译期间展示的漏洞结果必须达到更高的质量和准确性标准,这对于一些静态代码分析工具来说是不可能达到的。在 review 和代码 check in 之后,安全团队在和研发团队讨论漏洞修复时可以会遇到很多扯皮的事。因为研发人员往往不愿对已经测试和发布的代码进行额外更改,并且不大愿意解决低优先级的漏洞。

像一些其他体量比较庞大的科技公司,也是将 code review 作为呈现静态代码分析工具扫描结果的关键点。(比如说 Facebook 的 Infer 针对 Android/iOS 应用程序的分析。)

0x09 扩大静态代码分析的扫描范围

随着 Google 的开发者对 Tricorder 的分析结果愈发信任,他们希望 Tricorder 未来可以进行更进一步的分析。

Tricorder 通过两种方式解决这个问题:

  1. 允许项目级别的自定义
  2. 在软件开发工作流程的其他节点提供扫描结果

另外,在这一小节,还讨论了为什么 Google 目前仍然没有将更复杂的分析技术集成进核心软件开发工作流程的原因。

Project-level customization. 支持项目级别的自定义,不是所有的分析器均适用于全部的代码库。举例来说,有些分析器拥有较高的误报率,并且需要项目做一些特殊的配置,才能输出正确的扫描结果,显然这样的扫描器仅仅对特定的一些团队有价值。

为了满足这个需求,我们要使 Tricorder 拥有自定义的能力。基于以往的经验,FindBugs 的自定义并没有很好的实现,FindBugs 是基于用户级别的自定义,这导致了团队内部以及团队和团队之间的差异,并导致了使用该工具的人越来越少。因为每个用户看到的漏洞结果是不一致的,这样就没有办法保证参与同一个项目的人都能看到指定的问题。

为了避免此类问题的发生,Tricorder 仅允许在项目级别进行一些自定义配置,以确保针对特定项目进行更改的任何人看到的最终扫描结果均是一致的。

Tricorder 项目级别的自定义支持如下操作:

  1. Produce dichotomous results. 产生差异化的扫描结果。举例来说,Tricorder 集成了协议缓冲区定义分析器(analyzer for protocol buffer definitions),用于检测不向后兼容的更改。研发团队使用该分析器来确保协议缓冲区中的信息以序列化形式持久化存储,这对于不采用此类方式进行数据存储的团队来说,该告警是没有价值的。另一个例子是代码风格检查的分析器,该分析器建议使用 Guava 或者 Java 7 的编程风格,但是对于不采用这个编程语言或库的特性的研发团队来说,此类告警价值并不大。
  2. Need a particular setup or in-code annotations. 需要特殊配置或者代码内注释以开启或关闭对特定扫描器的支持。举例来说,如果团队内的代码被适当注释,则团队只能使用 Checker Framework 的 nullness analysis。另一个例子是,在经过特定的配置之后,将检查特定 Android 二进制文件的大小和方法数量,并提示研发人员是否有一个明显的增加或者大小、数量接近 hard limit。
  3. Support custom domain-specific languages (DSLs) and team-specific coding guidelines.  支持 DSL 或者 Team 自定义的编程规范。部分 Google 软件研发团队开发了小型的 DSL,他们希望静态代码分析可以支持他们的 DSL,还有一些团队已经制定了符合团队内部的代码可读性和可维护性最佳编程规范,并希望静态代码分析可以支持针对此类规范进行检查。
  4. Are highly resource-intensive. 一个典型的例子是支持包含动态分析的混合分析结果。这类扫描结果对于部分团队来说是有很高的价值的,但是对于其他团队而言可能成本稍高,并且扫描速度过慢。

截至 2018 年 1 月,Google 内部大约有 70 项可以选择的静态代码分析器,2,500 个项目至少启用了其中一项静态代码分析。

Additional workflow integration points. 将静态代码分析工具集成进软件开发工作流程中的其他环节。随着研发团队对该工具的扫描结果愈发信任,其希望进一步的将静态代码分析工具集成进软件开发工作流程的其他环节。

Tricorder 现在支持通过命令行工具、持续集成系统和代码浏览工具提供扫描结果。

Command line support. 命令行支持,Tricorder 团队为研发人员提供了命令行支持,这些研发人员实际上是团队代码管理员,这些管理员会定期检查和清理针对他们的代码库各种扫描器产生的告警。这些管理员熟悉每个分析器生成的修复类型,并且对指定的分析器高度信任。因此这些研发人员可以采用命令行工具基于相关漏洞告警进行自动化的漏洞修复。

Gating commits. 阻止提交,一些研发团队拥有较高的安全标准,希望特定的扫描器产生的漏洞告警后会阻止其代码被 commit 至代码库中,而不仅仅是将告警结果展示在 code review 工具中。这一般适用于高度自定义的检测规则并且没有误报,这样的场景下相关团队会需要阻止提交的能力,通常适用于自定义的 DSL 或库。

Results in code browsing. 支持在代码浏览的过程中展示扫描结果。代码浏览比较适合在大型项目或整个代码库级别展示相关漏洞的规模。比如说,浏览已弃用 API 的代码时,通过静态分析工具的扫描结果我们可以知道迁移至新的 API 还需要多少工作。

再比如说,一些涉及到安全和隐私相关的问题是全局性的,需要专门的团队去 review 漏洞是否真实存在。

默认情况下,是不显示扫描结果的,如果有此类需求的团队可以启用该功能,并扫描整个代码仓库,而且不会对其他研发团队造成干扰。对于部分扫描结果直接提供了修复方案,研发人员可以通过在代码浏览工具中单击进行修复。

Sophisticated analyses. 更加复杂的分析器。在 Google 广泛部署的所有静态分析程序均相对简单,尽管一些团队致力于为进行过程间分析(interprocedural analysis)的有限领域(例如 Android 应用程序)开发特定于项目的分析框架。

像 Google 这个体量,在技术上落地过程间分析是可行的,但是实施这样的分析器其实是非常具有挑战性的。如前所述,Google 的所有代码都存储在一个源码仓库中,因此从概念上讲,代码仓库中的任何代码均有可能是二进制文件的一部分。

因此可以想象这样一个场景,针对任何特定部分的代码审查的扫描结果均需要扫描整个代码仓库。尽管 Facebook 的 Infer 专注于组合分析(compositional analysis),以便将基于分离逻辑(separation-logic-based)的分析扩展到数百万行的代码库,但将此类分析扩展到 Google 的数十亿行的代码库中仍然是很大的工程量。

截至 2018 年 1 月,开发一套系统来进行更复杂的静态分析在 Google 一直不具备较高的优先级,主要原因如下:

  1. 资源投入过大,前期基础设施的资源投入令人望而却步。
  2. 需要降低误报率,静态代码分析团队必须想办法显著降低许多研究性质的静态代码分析器的误报率和或严格限制展示哪些漏洞。
  3. 目前还有很多其他的事情可以做,静态代码分析团队还有更多相对“简单”的静态代码分析器来实现和集成。(搞静态代码分析的工程师必须采用数据支撑以证明所做事情的影响力(impact)和潜在价值)
  4. 前期成本高,我们发现这种“简单”静态代码分析器的实用性很高,这是 FindBugs 的核心动机。相比之下,即使为更复杂的扫描规则或者分析器确实可以产生很大的价值,但是也需要考虑前期高昂的成本投入。

需要注意的是,对于在其他特定专业领域(例如航空航天和医疗设备)或特定项目(例如设备驱动程序和手机应用程序)工作的 Google 以外的研发人员而言,这种成本收益分析的计算方式可能会存在很大的差异。简单粗暴的理解就是对于一些不计较成本的行业而言,并且对安全性和稳定性的追求相当高,任何前期巨大的投入只要可以解决问题,那都是值得的。

0x10 相关经验

Google 通过历时数年、多次尝试最终成功将静态代码分析工具集成进入软件开发工作流程,总结出如下经验:

  1. **Finding bugs is easy. ** 相较而言,发现漏洞是一件容易的事情。当企业的代码库足够庞大时,这其中包含了任何我们可以想象的代码模式(code pattern),即使项目中拥有完整的测试覆盖率和严格的 code review 流程,也仍然充斥这各种安全漏洞。有的时候,在局部检查(local inspection)中并不明显(这其实是一个很常见的问题,很多漏洞都是在组件与组件耦合时才能满足漏洞利用条件,所以在针对 commit 的 code review 和单元测试时并不足以发现此类问题),有的时候安全漏洞是由看似无害的代码重构引入的。因为发现漏洞是容易的,因此可以采用简单的规则匹配 bug patterns,然后再根据针对代码仓库的扫描结果优化调整相关扫描规则。
  2. Most developers will not go out of their way to use static analysis tools. 大多数研发人员并不会特意使用静态代码分析工具。参考了业界常见的商业化扫描工具,Google 最初为 FindBugs 开发了一套集中的 Dashboard,工程师需要通过访问 Dashboard 来 review 静态代码分析工具产生的告警,这样造成的结果是只有很少的一部分工程师会 follow 这个流程。等到代码被引入到代码库中再去进行扫描可能已经来不及了,因为这些代码可能已经部署并运行在了生产环境中(伴随着代码的部署和运行,漏洞也被引入进了生产环境)。为了确保大部分的工程师都可以看到静态代码分析的告警,静态代码分析工具必须集成进软件开发的工作流程中,并且默认启用。拿 Error Prone 来说,其未提供统一的漏洞 Dashboard,但是将其集成进入了编译器,并提供了额外的静态代码扫描,以及在 code review 平台上也会展示静态分析的结果。
  3. Developer happiness is key. 开发人员用的爽是关键。根据 Google 的实际经验和相关的参考文献,很多公司在尝试将静态代码分析工具集成带软件开发流程中,最后都失败了。在 Google,管理层面也并未要求工程师都必须使用静态分析工具。因此,负责静态代码分析研发和运营的团队必须通过数据支撑证明其提供的扫描器、检测规则的影响力(impact)。要想使静态代码分析项目得到推广,研发团队必须能从中收益并乐于使用它。Tricorder 团队仔细分析了历史修复漏洞,并基于此调查研发人员的情绪,基于上述数据支撑,验证了持续投资资源持续改进静态代码分析工具的价值,保证研发人员对工具告警的信任,避免因为大量的误报和较多低优先级的漏洞浪费研发人员的时间,进而导致研发人员对扫描结果失去信息并忽略相关告警这类事情发情。
  4. Do not just find bugs, fix them. 既要报告漏洞,又要提供解决方案。为了推广一个静态代码分析工具,一个典型的做法是扫描代码库中的代码并输出大量的漏洞告警。目的是让利益相关者知道,我们需要解决扫描到的漏洞,并且应该在未来预防此类漏洞再次发生。但是如果研发人员不采取下一步行动(分析漏洞、修复漏洞),我们就没有办法达到这个目的。如果一个静态代码分析工具仅仅是通过其识别出来的漏洞数量而衡量该工具的有效性这是非常片面的,因为它并没有解决该漏洞,也没有进行任何有效操作来预防同类漏洞。Google 的静态代码分析团队即负责发现漏洞,又负责修复漏洞(提供代码级别的漏洞修复方案)。聚焦于漏洞修复并确保其可以提供可执行性的漏洞修复建议并最大限度的减少误报。在很多场景中,自动化的漏洞修复和漏洞发现一样简单(emm,看到这里我只想说,不愧是 Google!这个逼装的多了很多细节,复杂漏洞修复场景可以看参考链接中的几篇 paper)。
  5. Crowdsource analysis development. 鼓励其他团队的研发人员贡献检测规则。由于市面上很多静态代码分析工具的检测规则的编写学习成本比较高,需要对静态代码分析工具比较有研究的人才能写对来对应的扫描器和检测规则,但是因为这类专员人士是比较稀缺的,而且由于这类专家缺少对相关知识背景的了解,没有办法衡量哪些检测规则更有价值可以产生最大的影响力(impact)。举例来说一个对静态代码分析比较有研究的专家并不一定熟悉安全技术、项目的编程语言、相关的 API等。在 FindBugs 集成项目中,由于其规则编写的复杂性,只要少数的 Google 员工知道如何才能编写相应的扫描规则,因此小型的 BugBot 团队必须独立完成所有规则。这样限制了新的扫描规则集成的速度,并且也阻止了其他人贡献他们的领域知识(domain knowledge)。而像 Tricorder 团队其目前专注于降低研发人员提供扫描规则的门槛,不需要有静态代码分析的相关经验背景即可实现行之有效的扫描规则。

0x11 总结

Google 通过构建这套静态代码分析的基础设施,每天在代码编译过程中和 code review 期间成功避免了数百个安全漏洞被引入进 Google 的代码仓库。(2018 年)

很重要的一点,和研发团队现有的工作流程集成,是静态代码分析工具落地的关键。

搭建一个用于推动工作流集成的平台。如果条件允许的话,将扫描规则作为 compiler errors 启用,保证存在安全问题的代码没有办法通过编译器的编译。

这里有一个问题,也是甲方安全团队在推进安全策略时经常遇到的一个问题,就是增量和存量的问题。由于目前代码仓库中积累了各种个样的代码模式,所以会存在很多不符合规范的历史遗留代码,因此,作为负责静态代码分析工具研发和推广的安全团队来说,我们需要先着手解决这些历史遗留问题,修复代码仓库中已经存在的安全漏洞,避免在持续集成的过程中,由于这些历史遗留漏洞触发了新的检测规则,能导致编译失败,影响正常业务的运行。小步快走,避免同类问题重复出现。

如果我们将检测规则集成进编译器中,可以通过 compiler errors 的方式输出告警,研发人员在编码完成进行编译时可以立即遇到他们,此时便可以根据报错提示以及相应的修复建议开展漏洞修复工作。

静态代码分析团队开发了一套基础设施,针对整个代码仓库运行静态代码分析扫描规则,并生成修复后的代码,从而解决这些已经存在与代码仓库中的历史遗留漏洞。

当然,为了完全解决掉这些历史遗留的漏洞,Google 的乐于对历史代码进行优化的工程文化起到了至关重要的作用,Google 的工程文化是乐于对这些历史代码进行优化(嗯,这点很有意思,很多公司研发团队就是代码写完就动不得了,毕竟改完之后业务挂了,这个锅谁也背不动,索性多一事不如少一事,放着算了。)

还有一点就是 Google 的 code review 平台是支持自动化提交修改并且可以一次更改数百个代码问题。

作为负责静态代码分析工具(SAST)研发和运营的安全团队,我们通常需要将已检出并修复的漏洞数量及质量,以及其他安全工具(如 DAST)、安全团队(如内部 Red Team、外部提供 Pentest 服务的 vendor、搞 Bug Bounty 的白帽黑客等)发现但 SAST 未捕获的安全漏洞的比例未作为衡量团队成功的标准,而不仅仅是我们的静态代码分析平台推广到了多少个业务部门,目前有多少研发人员在使用该平台,也不是 SAST 本身发现了多少安全漏洞(注意这里提到的发现漏洞,和发现并修复的漏洞的区别)。这也便意味着,我们团队的职责远远超出了静态代码分析平台(SAST)本身。

代码质量、可靠性、安全性都是静态代码分析追求的目标。

Code riview 期间是在代码提交至仓库之前展示静态代码分析告警的最佳位置之一。为了确保研发团队可以查看并处理静态代码分析产生的漏洞告警,Tricorder (Google 自研的静态代码分析工具) 会在研发人员在修改代码时、提交代码之前展示这些安全告警,同时 Tricorder 团队也制定了一个标准,来展示哪些告警(有 SAST 运营经验的师傅应该会比较有体会,很多 SAST 的扫描规则拥有较高的误报率,而 review 这些高误报率的告警则会导致研发团队对工具本身失去信心,因此根据漏洞等级区分好优先级,并保证展示出来的漏洞告警误报率在我们可以接受的范围内是至关重要的)。

同时,如前所述,研发团队的反馈是至关重要的,Tricorder 在 code review 平台中展示漏洞告警时也会进一步的收集用户的数据,统计研发人员没有积极处理的漏洞,以及分析背后潜在的原因(是误报还是修复过程比较复杂,还是研发人员不理解修复建议等)。禁用掉不能提供准确信息的告警规则,保证误报率在研发人员可以接受的范围内。

研发工程师对静态代码分析工具的扫描结果信任是关键,较高的误报率会让研发人员有意识的忽略静态代码分析工具产生的告警,误报或者未提供明确修复建议的告警都会导致研发人员接收到告警之后不进行任何整改措施。

因此,作为静态代码分析工具的研发和运营团队,很重要的一点就是在添加新的检测规则时一定要足够谨慎,确保误报率在可以接受的范围内,避免研发团队被淹没在海量的误报中。

问卷调查和有效的反馈渠道是可以保证静态代码分析工具质量的重要方法之一。当研发团队对扫描结果足够信任之后,接收研发团队的反馈,基于这些 request 排好优先级,保证静态代码分析工具可以在未来集成进软件开发工作流程中的更多环节。

0x12 碎碎念

我们在读这些行业最佳实践时,不是为了完全照搬,因地制宜,取长补短,适合自己的才是最好的。

0x13 References

  1. Lessons Learned from Apple’s GoToFail Bug – https://www.infoq.com/news/2014/02/apple_gotofail_lessons/
  2. Twitter outage report – https://news.ycombinator.com/item?id=8810157
  3. Lessons from Building Static Analysis Tools at Google – https://cacm.acm.org/magazines/2018/4/226371-lessons-from-building-static-analysis-tools-at-google/fulltext

发表评论

登录后才能评论
服务中心
服务中心
联系客服
联系客服
投诉举报
返回顶部