Code QL

現在DevOps関係の仕事をしているが、そこで各種ツールをCI/CDパイプラインに組み込み、自動化する環境の構築をしている。
ここでは、コードの脆弱性を検出することができる、CodeQLについて実際に使ってみたので備忘録として残すことにする。

CodeQLを学ぶ上でのリファレンス

CodeQLを学ぶ上でどこを参照すればよいか、初めはよくわからない。
というのもCodeQLについて専門で書かれている書籍は見当たらなかったからだ。
そこで、ネットで検索してみるとCodeQLの公式documentがgithubから提供されていることがわかった。
CodeQL documentation
一応ここにCodeQLについてのすべてが記載されている。
ただ、私のような新米エンジニアにとってはなかなかハードルが高く、読んでもイマイチピンとこないことが多い。(ベースが英語なので、英語に苦手意識がある人はさらにきついかも。googleの翻訳機能を使ってもイマイチ理解できないところが多々ある。)
ネットで探すと「CodeQLを使ってみた」的なブログがあり、それはそれで私にもわかりやすい形で書かれていて理解を助けてくれるが、網羅的ではないのと、本当に正しいか不安に思うこともある。
最近見つけたのが、Microsoftが公式に無料提供している学習サイトで、こちらは日本語にも対応していてかつわかりやすい表現で書かれている。
GitHub CodeQLを使用したコードスキャン
CodeQLを使用してコードベースのセキュリティの脆弱性を特定する
今回はこちらをベースに学んだことを記載していきたいと思う。

CodeQLとは

ソフトウェアを開発する上で、品質を担保するためにコーディングルール等が定められている。自動車や飛行機などに搭載されるソフトウェアでは、特にその安全性への要求が高いことから多くの標準ルールがあり、メーカーはそのルールをベースにした社内ルール等(以下、コーディング規約と呼ぶ)を作成している。
CodeQLは、ソースコードがそのようなコーディング規約に準拠しているかどうかを解析することができるツールである。
CodeQLを使用した解析では、まずソースコードからクエリ可能なCodeQLデータベースを抽出する。
そして、コーディング規約のルールに則ったクエリをデータベースに対して実行し、ソースコードのチェックを行う。
毎回手動で実行することは、ソースコードの量やコーディングルールの量からみて現実的ではない。Githubには、CodeQLとの連携がしやすくなる機能が備わっており、GitHubActionsと併用することで自動化の仕組みを構築することができる。

GitHub構成

解析対象

Bazelのビルドシステムを利用して今回は解析をしたいと思う。
以下リンク先に、Bazelの公式ページにC++のサンプルが公開されているので、こちらを今回は用いることにする。
https://docs.bazel.build/versions/main/tutorial/cpp.html

GitHub構成

GitHubの構成とファイルの中身は以下の通りである。

|--.github
|  |--workflows
|  |  |--codeql-task.yml
|--main
|  |--BUILD
|  |--hello-world.cc
|--README.md
|--WORKSPACE
各ファイル中身
---
name: codeQL-task
on:
  workflow_dispatch:
  push:
    branches:
      - "develop"
      - "main"

jobs:
  codeql-job:
    name: codeql-job
    runs-on: ubuntu-latest
    steps:
      - name: Ceckout repository
        uses: actions/checkout@v3

      - name: Setup CodeQL
        run: |
          wget https://github.com/github/codeql-cli-binaries/releases/download/v2.11.4/codeql-linux64.zip
          unzip codeql-linux64.zip
          rm -f codeql-linux64.zip
          git clone -b codeql-cli/v2.11.4 https://github.com/github/codeql.git ql

      - name: Setup Bazel
        run: |
          sudo apt-get install openjdk-8-jdk
          sudo add-apt-repository ppa:webupd8team/java
          sudo apt-get update && sudo apt-get install oracle-java8-installer
          echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
          curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add -
          sudo apt-get update && sudo apt-get install bazel
          sudo apt-get upgrade bazel

      - name: Run codeQL-task
        run: |
          rm -rf codeql_work/codeql-database
          git clone ${codeql-rulepack-path}/codeql-rule-packs.git
          mkdir -p  codeql_work
          codeql/codeql database create codeql_work/codeql-database --language=cpp --command='bazel build //main:hello-world'
          codeql/codeql database analyze codeql_work/codeql-database codeql-rule-packs/qls/test.qls \
            --format=sarif-latest \
            --output=codeql_work/codeql-analysis.sarif
          echo "success"

      - name: Upload SARIF file
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: codeql_work/codeql-analysis.sarif

      - name: Upload artifact
        uses: actions/upload-artifact@v3
        with:
          name: SARIF file
          path: codeql_work/codeql-analysis.sarif

      - name: Clean workspace
        if: ${{ always() }}
        run: |
          chmod +w ${{github.workspace}} -Rf
          rm -rf ${{github.workspace}}/*
load("@rules_cc//cc:defs.bzl", "cc_binary")

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
)
#include <ctime>
#include <string>
#include <iostream>

std::string get_greet(const std::string& who) {
  return "Hello " + who;
}

void print_localtime() {
  std::time_t result = std::time(nullptr);
  std::cout << std::asctime(std::localtime(&result));
}

int main(int argc, char** argv) {
  std::string who = "world";
  if (argc > 1) {
    who = argv[1];
  }
  std::cout << get_greet(who) << std::endl;
  print_localtime();
  return 0;
}

WORKSPACEファイルは空でよい。

CodeQlルールパック

クエリを実行する前に、どのルールをチェックするかのルールパックを取得する必要がある。
共通で使用できるルールから、カスタマイズして作成できるルールもある。
ルールパックのファイル構成は以下の通りである。

|--ql
|  |--commentout.ql
|  |--cyclomatic-complexity.ql
|  |--source-line-number.ql
|--qlpack.yml
|--qls
|  |--test.qls
|--README.md
各ファイル中身

qlディレクトリ配下には、クエリのルールが格納されている。今回はAV ruleという公開されている3つのクエリを配置した。

/**
 * @name AV Rule 127
 * @description Code that is not used (commented out) shall be deleted.
 * @kind problem
 * @id cpp/jsf/av-rule-127
 * @problem.severity recommendation
 * @tags maintainability
 *       external/jsf
 */

import cpp
import external.ExternalArtifact

from DefectExternalData d
where d.getQueryPath() = "jsf/4.14 Comments/AV Rule 127.ql"
select d, d.getMessage()
/**
* @name cycloatic complexity
* @description All functions shall have a cyclomatic complexity number of 10 or less
* @kind problem
* @id cpp/jsf/av-rule-3
* @problem.severity error
* @tags maintainability
*/

import cpp

from Function f, int c
where
c = f.getMetrics().getCyclomaticComplexity() and
c >= 0
select f, "All functions shall have a cyclomatic complexity number of 10 or less"
/**
 * @name AV Rule 1
 * @description Any one function (or method) will contain no more than 200 logical source lines of code.
 * @kind problem
 * @id cpp/jsf/av-rule-1
 * @problem.severity warning
 * @tags maintainability
 *       external/jsf
 */

import cpp

from Function f, int n
where
  n = f.getMetrics().getNumberOfLinesOfCode() and
  n > 5
select f,
  "AV Rule 1: any one function (or method) will contain no more than 200 logical source lines of code. Function '"
    + f.toString() + "' contains " + n.toString() + " lines of code."

rule-packのrootディレクトリには、以下のようなqlpack.ymlファイルを置く必要があり、このファイルにCodeQL分析で使用するメタデータを記載する。

name: test-local
version: 0.0.0
libraryPathDependencies: codeql-cpp

qlsディレクトリ配下には、CodeQLのクエリスイートを定義する。クエリースイートを定義すると、どのルール(今回の例では、qlディレクトリ配下に置いてあるどのルールか)を適用するかを定義することができる。qlpackとしては、qlpack.ymlで定義した名前を用いて、この下に3つのルールのうちのどのルールを適用するかも選択し記載することができる。また、別のqlpackからもルールを指定することが可能である。
クエリ実行時に、下記のtest.qlsを指定すると、ql配下の3つのクエリが実行される。

- description: test-suite
- qlpack: test-local

実行の流れ

ワークフローの定義ファイル(codeql-task.yml)を見ていただくとわかるが、基本的な流れとしては、環境Setup(CodeQL、Bazel)、CodeQLの実行、結果のUploadという流れで行われている。
CodeQLの実行は、以下の二つのステップで行える。

①DataBaseの作成

codeql database create codeql_work/codeql-database --language=cpp --command='bazel build //main:hello-world'

②クエリの実行

codeql database analyze codeql_work/codeql-database codeql-rule-packs/qls/test.qls \
            --format=sarif-latest \
            --output=codeql_work/codeql-analysis.sarif

動作結果

クエリが実行されているログがでているが、期待する動作が得られていないため、引き続き検証を行う。

Test

Posted by okuribito