在数据库中运行 CodeQL

已完成

将代码提取到数据库后,现在可以使用 CodeQL 查询对其进行分析。 GitHub 专家、安全研究人员和社区参与者编写和维护默认 CodeQL 查询。 还可以编写自己的查询。

可以在代码扫描分析中使用 CodeQL 查询来查找源代码中的问题并识别潜在的安全漏洞。 还可以编写自定义查询来识别源代码中使用的每种语言的问题。

有两种重要的查询类型:

  • 警报查询 在代码的特定位置显示问题。
  • 路径查询 描述代码中的源和接收器之间的信息流。

简单 CodeQL 查询

基本 CodeQL 查询结构具有文件扩展名 .ql 并包含子 select 句。 下面是一个示例查询结构:

/**
 *
 * Query metadata
 *
 */
import /* ... CodeQL libraries or modules ... */

/* ... Optional, define CodeQL classes and predicates ... */

from /* ... variable declarations ... */
where /* ... logical formula ... */
select /* ... expressions ... */

查询自定义

CodeQL 分析由查询驱动。 虽然可以使用GitHub提供的标准查询,但还可以通过编写自己的查询并将其组织到查询包中来自定义分析。

查询通常分组到 查询包中,这些查询是包含查询、共享库和配置文件的目录。 查询包允许为项目定义一组可重用的分析规则。 在包中,可以包括单个 .ql 文件、定义可重用逻辑的帮助程序库,以及将多个查询组合在一起的查询套件。

查询套件.qls文件)用于控制在分析期间运行的查询。 无需逐个运行查询,而是定义列出要执行的所有查询的套件。 例如:

- description: Custom security queries
- queries:
  - ./queries/hardcoded-credentials.ql
  - ./queries/insecure-config.ql

此套件将多个查询分组在一起,以便它们可以作为单个分析的一部分一起运行。

可以通过编写 .ql 文件来创建自己的查询。 查询描述要检测的代码中的模式。 它通常导入语言库、定义条件,并使用语句返回结果 select

例如,以下查询查找可能包含硬编码凭据的字符串文本:

/**
 * @name Hardcoded credential detection
 * @description Finds string literals that may contain passwords
 * @kind problem
 * @id example/hardcoded-credentials
 * @severity warning
 */

import javascript

from Literal l
where l.getValue().toString().matches("%password%")
select l, "Possible hardcoded credential"

在本查询中:

  • import 语句加载 JavaScript 的语言模型。
  • from 子句定义要分析的数据。
  • where 子句用于筛选匹配的模式。
  • select 语句定义返回的结果。

可以通过从标准查询开始并修改其条件或输出来生成自定义查询。

若要将查询用于GitHub代码扫描,必须包含查询元数据。 元数据在文件顶部的注释块中定义,并控制结果的解释和显示方式。

至少应包括元数据:

  • 唯一标识符 (@id
  • 名称(@name
  • 说明 (@description
  • 结果类型(@kindproblempath-problem

其他属性(例如 @severity@precision)有助于确定警报在 GitHub 中的显示方式。

与代码扫描集成时需要元数据。 如果存在元数据,结果会显示为存储库中的警报。 如果缺少元数据,则 CodeQL 仍会运行查询,但结果仅显示为原始输出,不会显示为代码扫描警报。

定义查询或查询套件后,即可将其包含在分析配置中。 在GitHub Actions中,在初始化步骤中指定查询:

- name: Initialize CodeQL
  uses: github/codeql-action/init@v3
  with:
    queries: ./path/to/query-suite.qls

在工作流程期间:

  1. CodeQL 创建数据库。
  2. 运行所选查询。
  3. 以 SARIF 格式生成结果。
  4. 将结果上传到GitHub。

自定义查询结果显示在“安全”选项卡中的标准 CodeQL 结果旁边。这样,就可以使用特定于代码库的检查来扩展默认分析,同时仍受益于GitHub的维护查询集。

查询元数据

在上一部分中,你向查询添加了元数据,以便可以在代码扫描中使用元数据。 本部分介绍元数据的使用方式以及它如何影响查询结果。

查询元数据在文件的顶部的 .ql 注释块中定义。 它提供有关查询的信息,并控制如何解释和显示结果。

CodeQL 和 GitHub 代码扫描使用元数据以:

  • 识别查询及其目的。
  • 确定结果分类方式(例如, problempath)。
  • 分配严重性和精度级别。
  • 设置存储库中显示的结果的格式。

例如,查询可能包含如下所示的元数据:

/**
 * @name Hardcoded credential detection
 * @description Finds string literals that may contain passwords
 * @kind problem
 * @id example/hardcoded-credentials
 * @severity warning
 */

此元数据存在时:

  • 结果转换为 SARIF 格式。
  • 警报显示在 GitHub 代码扫描中。
  • 发现结果包含相关上下文信息,例如严重程度和描述。

缺少元数据时:

  • 查询仍在运行。
  • 结果不显示为警报。
  • 输出仅显示为原始表。

元数据还确定如何跨扫描对结果进行分组和跟踪。 例如,查询 @id 用于匹配不同运行批次之间的告警。

GitHub 具有查询元数据的建议样式指南。 可以在 CodeQL 文档中找到它。

此示例显示了其中一个标准 Java 查询的元数据:

标准Java CodeQL 查询的查询元数据的屏幕截图。

CodeQL 不解释没有元数据的查询。 它将这些结果显示为表,并且不会在源代码中显示它们。

编写、测试和运行查询

创建自定义查询后,下一步是测试它们,在工作流中运行它们,并在一段时间内对其进行维护。

编写查询时,将定义 CodeQL 应在代码库中检测到的模式。 开发查询的最有效方法,是先在本地反复迭代,然后再将其添加到代码库中。

在本地测试查询

可以使用 CodeQL CLI 或 Visual Studio Code 扩展测试查询。

使用 CodeQL CLI,针对已创建的数据库运行查询:

codeql database analyze <database> <query.ql>

此命令运行查询并生成结果,可以使用 SARIF 或其他输出格式进行查看。

还可以运行:

codeql query run <query.ql> --database=<database>

通过本地测试,可以:

  • 验证查询是否返回预期结果。
  • 优化查询逻辑。
  • 识别误报或缺失情况。

Visual Studio Code扩展提供了更交互式的体验。 您可以:

  • 打开数据库。
  • 直接从编辑器运行查询。
  • 与源代码一起查看结果。

这样,可以更轻松地了解查询的行为方式并快速调整查询。

在GitHub代码扫描中运行查询

查询生成预期结果后,即可将其包含在代码扫描工作流中。

在GitHub Actions中,查询在初始化步骤中配置:

- name: Initialize CodeQL
  uses: github/codeql-action/init@v3
  with:
    queries: ./path/to/query-suite.qls

工作流运行时:

  1. CodeQL 为存储库创建数据库。
  2. 执行所选查询。
  3. 将结果转换为 SARIF。
  4. 将结果上传到GitHub。

结果在 “安全 ”选项卡中显示为警报,以及标准 CodeQL 结果。

在工作流中运行查询可确保:

  • 在拉取请求和分支上自动运行分析。
  • 在代码更改时检测到新问题。
  • 结果对团队成员可见。

维护和更新查询

将自定义查询添加到工作流后,你可能会注意到结果并不总是预期的结果。

例如:

  • 查询可能会返回过多的结果(误报结果)。
  • 它可能会错过预期检测到的情况。
  • 存储库中的新代码模式可能未涵盖。

在这些情况下,请更新查询以提高其准确性。

首先在本地运行查询并查看结果。 查看标记的代码位置,并确定它们是否表示实际问题。 否则,请优化子句中的 where 条件以缩小结果范围。

例如,你可能:

  • 添加其他条件以排除安全模式。
  • 调整字符串匹配或数据流逻辑。
  • 重用现有库中的谓词以提高准确性。

更新查询后,针对数据库运行该查询,以确认结果已得到改进。

提交更新的查询时,它会在代码扫描工作流中自动运行。 这意味着:

  • 现有警报可能会更新或删除。
  • 可能会根据更新的逻辑显示新警报。

随着时间的推移,随着代码库的发展,将重复此过程。 维护查询是一项持续的任务,可帮助确保分析保持准确且相关。

QL 语法

QL 是一种声明性、面向对象的查询语言。 它经过优化,可以高效分析分层数据结构,特别是表示软件项目的数据库。

QL 的语法类似于 SQL,但 QL 的语义基于 Datalog。 Datalog 是一种声明性逻辑编程语言,通常用作查询语言。 由于 QL 主要是逻辑语言,因此 QL 中的所有作都是逻辑作。 QL 还从 Datalog 中继承了递归谓词。 QL 添加了对聚合的支持,使复杂的查询简洁简单。

QL 语言由逻辑公式组成。 它使用常见的逻辑连接,例如 andornot,以及限定符,如 forallexists。 由于 QL 继承递归谓词,因此还可以使用基本的 QL 语法和聚合(例如 countsumaverage编写复杂的递归查询。

有关 QL 语言的详细信息,请参阅 CodeQL 文档。

路径查询

信息流经程序的方式很重要。 看似良性的数据可能以意外的方式流动,从而允许其恶意使用。

创建路径查询有助于通过代码库可视化信息流。 查询可以跟踪数据从其可能起点()到其可能终结点(接收器)的路径。 若要为路径建模,查询必须提供有关链接路径的源、接收器和数据流步骤的信息。

开始编写自己的路径查询的最简单方法是使用现有查询之一作为模板。 若要获取这些受支持语言的查询,请参阅 CodeQL 文档。

路径查询需要某些元数据、查询谓词和 select 语句结构。 CodeQL 中的许多内置路径查询都遵循基本结构。 结构取决于 CodeQL 如何对要分析的语言进行建模。

下面是路径查询的示例模板:

/**
 * ...
 * @kind path-problem
 * ...
 */

import <language>

// For some languages (Java/C++/Python/Swift), you need to explicitly
// import the data-flow library, such as:
// import semmle.code.java.dataflow.DataFlow
// import codeql.swift.dataflow.DataFlow

...

module Flow = DataFlow::Global<MyConfiguration>;
import Flow::PathGraph

from Flow::PathNode source, Flow::PathNode sink
where Flow::flowPath(source, sink)
select sink.getNode(), source, sink, "<message>"

在该模板中:

  • MyConfiguration 是一个模块,其中包含定义数据源和接收器之间的数据流方式的谓词。
  • Flow 是基于 MyConfiguration 的数据流计算结果。
  • Flow::PathGraph 是需要导入的数据流图形模块,以便在查询中包含路径说明。
  • source 并且 sink 是配置中定义的图形中的节点,并且 Flow::PathNode 是它们的类型。
  • DataFlow::Global<..> 是数据流的调用。 可以改用 TaintTracking::Global<..> 来包含一组默认的污点步骤。

如何编写路径查询

查询需要计算路径图才能生成路径说明。 为此,请定义名为edges的查询谓词。 查询谓词是带有查询注释的非成员谓词。 查询注释返回谓词计算的所有元组。

edges谓词定义要计算的图形的边缘关系。 它用于计算与查询生成的每个结果相关的路径。 还可以从其中一个标准数据流库中的路径图模块导入预定义 edges 的谓词。

数据流库包含数据流分析中常用的其他类、谓词和模块,以及路径图模块。 CodeQL 数据流库通过对数据流图建模或实现数据流分析来运行。 普通数据流库用于分析信息流,其中数据值在每个步骤中得以保留。

下面是一个示例语句,该语句从数据流库(PathGraph)导入DataFlow.qll模块,其中edges定义了:

import DataFlow::PathGraph

可以导入 CodeQL 附带的许多其他库。 还可以导入专为在各种常见框架和环境中实现数据流分析而设计的库。

该类 PathNode 旨在实现数据流分析。 它是一个增加了调用上下文(接收器除外)、访问路径和配置的 Node。 仅生成可从源访问的 PathNode 值。

下面是导入路径的示例:

import semmle.code.cpp.ir.dataflow.internal.DataFlowImpl

可以选择定义查询 nodes 谓词,该谓词指定所有语言的路径图的节点。 定义 nodes时,所选节点仅定义具有终结点的边缘。 如果未定义 nodes,则需要选择所有可能的边缘终结点。

数据库分析

使用查询分析 CodeQL 数据库时,会在源代码的上下文中收到有意义的结果。 结果采用 SARIF 或其他解释格式的警报或路径样式。

下面是 CodeQL 数据库命令的示例,该命令通过针对数据库运行所选查询并解释结果来分析数据库:

codeql database analyze \
  --format=<format> \
  --output=<output> \
  [--threads=<num>] \
  [--ram=<MB>] \
  <options>... \
  -- <database> <query|dir|suite>...

此命令结合了codeql database run-queriescodeql database interpret-results管道命令的效果。

或者,可以运行不符合解释为源代码警报要求的查询。 为此,请使用:

  • codeql database run-queries
  • codeql query run

然后使用:

codeql bqrs decode

将原始结果转换为可读表示法。

可以在 CodeQL CLI 手册中获取可用 CodeQL CLI 命令的完整列表。

使用具有类别的 SARIF 文件

CodeQL 支持 SARIF 共享静态分析结果。 SARIF 旨在表示各种静态分析工具的输出。

使用 SARIF 输出进行 CodeQL 分析时,需要指定 类别 。 类别可以区分对同一提交存储库和不同语言或代码的不同部分执行的多个分析。 但是,具有相同类别的 SARIF 文件会相互覆盖。

当分析运行之间的类别值一致时,可以使用 CodeQL 扫描每个 SARIF 输出文件以分析同一代码库中的不同语言。 建议使用扫描的语言作为类别的标识符。

例如,category 值显示(如果尚不存在,则附加尾部斜杠)为:

  • <run>.automationId 在 SARIF v1 中
  • SARIF v2 中的 <run>.automationLogicalId
  • <run>.automationDetails.id 在 SARIF v2.1.0 中

将 SARIF 结果发布到 GitHub

数据库准备就绪后,可以交互方式对其进行查询。 或者,可以运行一组查询,以 SARIF 格式生成一组结果,并将结果上传到 GitHub.com 的目标存储库:

codeql github upload-results \
  --sarif=<file> \
  [--github-auth-stdin] \
  [--github-url=<url>] \
  [--repository=<repository-name>] \
  [--ref=<ref>] \
  [--commit=<commit>] \
  [--checkout-path=<path>] \
  <options>...

若要将结果上传到 GitHub,请确保每个持续集成(CI)服务器都有一个 GitHub 应用或个人访问令牌,供 CodeQL CLI 使用。 必须使用具有写入权限的访问令牌或具有 security_events 写入权限的 GitHub 应用。

如果 CI 服务器已经使用具有此范围的令牌从 GitHub 签出存储库,则可能允许 CodeQL CLI 使用相同的令牌。 否则,请使用 security_events 写入权限创建新的令牌,并将此令牌添加到 CI 系统的机密存储中。

作为安全最佳做法,请使用 --github-auth-stdin 标志并通过标准输入将令牌传递给命令。

上传 SARIF 结果

为了使代码扫描能够在您的 GitHub 存储库中显示来自非 Microsoft 静态分析工具的结果,您的结果必须存储在支持 SARIF 2.1.0 JSON 架构特定子集的 SARIF 文件中。 可以使用代码扫描 API 或 CodeQL CLI 上传结果。

每次上传新代码扫描的结果时,CodeQL 都会处理结果并将警报添加到存储库。 为防止出现相同问题的重复警报,代码扫描使用 SARIF partialFingerprints 属性来匹配各运行的结果,以便它们仅在所选分支的最新运行中出现一次。

消除重复项使得在编辑文件时,可以将警报与正确的代码行匹配。

在不同分析中,结果的规则 ID 必须保持相同。 指纹数据自动包含在通过 CodeQL 分析工作流或 CodeQL 运行程序创建的 SARIF 文件中。

SARIF 规范使用 JSON 属性名称 partialFingerprints,这是从命名指纹类型到指纹的字典。 该属性至少包含 primaryLocationLineHash 的值,该值根据主要位置的上下文提供指纹。

GitHub 会尝试在通过partialFingerprints操作上传 SARIF 文件时,从源文件填充缺少的upload-sarif字段。

此外,如果使用 API 终结点上传没有指纹数据的 /code-scanning/sarifs SARIF 文件,则当处理和显示代码扫描警报时,用户可能会看到重复的警报。

若要避免使用静态分析工具时出现重复的警报,请计算指纹数据并在上传 SARIF 文件之前填充 partialFingerprints 属性。 一个有用的起点是使用与 upload-sarif 操作相同的脚本。