CodeQL を Docker と GitHub Actions で実行し、コードの脆弱性を検出する

## はじめに

:::note
2023/9/1 更新
:::

CodeQL は静的解析ツールの一種で、コードのセキュリティチェックを行います

https://github.com/github/codeql

[pre-commit] や [Super-Linter] を使えばコードの文法や保守性・可読性のチェックはできますが、

CodeQL ではそれだけでなく、クロスサイトスクリプティングや SQL インジェクションの原因となる記述を検出してくれるのです

CodeQL を活用すれば、プロジェクトメンバーの経験不足によるセキュリティホールの発生を防げます

今回は CodeQL を Docker （ローカル上）と GitHub Actions で実行してみましょう

実装例はこちら

https://github.com/RyoWakabayashi/ci-example

## 処理の流れ

CodeQL では、まずコードを解析してデータベース化します

その上で、構築したデータベースに対して QL という言語で書かれたクエリを発行し、脆弱性に該当するコードがないか探すわけです

どのようなクエリが発行されるかは[後述します](#チェック項目の詳細)

CodeQL は CLI が提供されており、ローカルで実行する場合は以下のように実行します

ただ、ローカルにインストールしたりするのが面倒なので、今回は Docker と GitHub Actions で実行します

### データベースの作成

```shell
codeql database create \
  --language=<解析対象の言語> \
  <データベースの作成先> \
  -s <コードのルートパス>
```

解析対象の言語には以下のものが指定可能です

- cpp: C++
- csharp: C#
- go: Go
- java: Java
- javascript: JavaScript, TypeScript
- python: Python
- ruby: Ruby
- swift: Swift

### 解析の実行

```shell
codeql database analyze \
  <データベースの作成先> \
  <クエリスイート> \
  --format <結果の出力フォーマット> \
  --output <結果の出力先>
```

指定できるクエリスイート（実行するチェックの一式） は以下のいずれかです

- javascript-code-scanning.qls
- javascript-lgtm-full.qls
- javascript-lgtm.qls
- javascript-security-and-quality.qls
- javascript-security-experimental.qls
- javascript-security-extended.qls

## Docker で実行

Microsoft が CodeQL Container というものを作ってくれているので、こちらを使います

https://github.com/microsoft/codeql-container

README に実行コマンドやシェルを用意してくれていますが、毎回長々オプションを指定したくないので、 docker-compose で定義してしまいます

[処理の流れ](#処理の流れ)で説明したように、データベースの作成と解析の実行、２段階あるため、それぞれ定義します

### データベースの作成定義

今回は JavaScript の解析のみを行う場合です

[codeql/docker-compose-create-db.yml](https://github.com/RyoWakabayashi/ci-example/blob/main/codeql/docker-compose-create-db.yml)

```yaml
version: '3'
services:
  linter:
    build:
      context: .
      dockerfile: Dockerfile.patch
    environment:
      - CODEQL_CLI_ARGS=database create --language=javascript /opt/results/source_db -s /opt/src
    volumes:
      - ../:/opt/src
      - ./results:/opt/results
```

### 解析の実行定義

クエリスイートには `javascript-security-and-quality.qls` を指定しています

セキュリティ以外にも保守性などをチェックします

また、過度にリソースを食わないように threads は 4 にしていますが、ここは環境に合わせて調整してください

[codeql/docker-compose-analyze.yml](https://github.com/RyoWakabayashi/ci-example/blob/main/codeql/docker-compose-analyze.yml)

```yaml
version: '3'
services:
  linter:
    build:
      context: .
      dockerfile: Dockerfile.patch
    environment:
      - CODEQL_CLI_ARGS=database analyze /opt/results/source_db javascript-security-and-quality.qls --format csv --output /opt/results/codeql.csv --threads 4
    volumes:
      - ./results:/opt/results
```

コンテナのイメージをそのまま実行すると setup.py に実行権限がないというエラーが発生するため、パッチをあてます

```dockerfile
# hadolint ignore=DL3007
FROM mcr.microsoft.com/cstsectools/codeql-container:latest

USER root

RUN chmod +x /usr/local/startup_scripts/setup.py

USER codeql
```

### コンテナの実行

上記ファイルを準備して、以下のように実行すれば CodeQL を動かすことができます

実行にはそこそこ時間がかかるのと、 Docker にそれなりにメモリを割り当てていないとメモリ不足で落ちることもあります

- データベースの作成

```shell
$ docker compose -f codeql/docker-compose-create-db.yml up
Recreating codeql_linter_1 ... done
Attaching to codeql_linter_1
linter_1  | Initializing database at /opt/results/source_db.
linter_1  | Running command [/usr/local/codeql-home/codeql/javascript/tools/autobuild.sh] in /opt/src.
linter_1  | Finalizing database at /opt/results/source_db.
linter_1  | Successfully created database at /opt/results/source_db.
linter_1  | [2021-08-25 05:56:28] [build] Single-threaded extraction.
linter_1  | [2021-08-25 05:57:50] [build] Extracting /opt/src/webpack-sample/dist/index.html

...

linter_1  | [2021-08-25 05:57:58] [build] Extracting /usr/local/codeql-home/codeql/javascript/tools/data/externs/lib/should.js
linter_1  | [2021-08-25 05:57:58] [build] Done extracting /usr/local/codeql-home/codeql/javascript/tools/data/externs/lib/should.js (34 ms)
linter_1  | 
codeql_linter_1 exited with code 0
```

- 解析の実行

```shell
$ docker compose -f codeql/docker-compose-analyze.yml up
Recreating codeql_linter_1 ... done
Attaching to codeql_linter_1
linter_1  | Running queries.
linter_1  | Compiling query plan for /usr/local/codeql-home/codeql-repo/javascript/ql/src/LanguageFeatures/InvalidPrototype.ql.
linter_1  | [1/172] Found in cache: /usr/local/codeql-home/codeql-repo/javascript/ql/src/LanguageFeatures/InvalidPrototype.ql.
linter_1  | Compiling query plan for /usr/local/codeql-home/codeql-repo/javascript/ql/src/LanguageFeatures/WithStatement.ql.

...

linter_1  | Compiling query plan for /usr/local/codeql-home/codeql-repo/javascript/ql/src/Security/CWE-400/RemotePropertyInjection.ql.
linter_1  | [172/172] Found in cache: /usr/local/codeql-home/codeql-repo/javascript/ql/src/Security/CWE-400/RemotePropertyInjection.ql.
linter_1  | Starting evaluation of codeql-javascript/LanguageFeatures/InvalidPrototype.ql.
linter_1  | Starting evaluation of codeql-javascript/LanguageFeatures/WithStatement.ql.
linter_1  | Starting evaluation of codeql-javascript/LanguageFeatures/DeleteVar.ql.
linter_1  | Starting evaluation of codeql-javascript/LanguageFeatures/SyntaxError.ql.
linter_1  | [1/172 eval 1.3s] Evaluation done; writing results to codeql-javascript/LanguageFeatures/SyntaxError.bqrs.
linter_1  | [2/172 eval 1.3s] Evaluation done; writing results to codeql-javascript/LanguageFeatures/DeleteVar.bqrs.

...

linter_1  | [171/172 eval 2.3s] Evaluation done; writing results to codeql-javascript/Security/CWE-918/RequestForgery.bqrs.
linter_1  | [172/172 eval 2.8s] Evaluation done; writing results to codeql-javascript/Security/CWE-400/RemotePropertyInjection.bqrs.
linter_1  | Shutting down query evaluator.
linter_1  | Interpreting results.
linter_1  | 
codeql_linter_1 exited with code 0
```

codeql/results/codeql.csv に結果が出ますが、何もエラーがなければ空です

わざと以下のような悪いコード（ SQL インジェクション）を入れてみましょう

残念ながら　eslint ではこれを検出することはできませんが、　CodeQL　ならやってくれます

```js
const app = require("express")();
const pg = require("pg");
const pool = new pg.Pool();

app.get("search", function handler(req, res) {
  pool.query(
    "SELECT * FROM TABLE_A WHERE XXX='" + req.params.xxx + "' ORDER BY YYY",
    []
  );
});
```

ちなみに、悪いコードの例や、その修正例（良い例）は以下のようなパス（各分類以下の examples）に格納されているので、参考にしてください

[https://github.com/github/codeql/blob/main/javascript/ql/src/Security/CWE-089/examples/SqlInjection.js](https://github.com/github/codeql/blob/main/javascript/ql/src/Security/CWE-089/examples/SqlInjection.js)

悪いコードがある状態で実行すると、 codeql/results/codeql.csv に以下のように悪かった箇所が出てきます

```text
"Database query built from user-controlled sources","Building a database query from user-controlled sources is vulnerable to insertion of malicious code by the user.","error","This query depends on [[""a user-provided value""|""relative:///webpack-sample/src/hello.js:7:43:7:56""]].","/webpack-sample/src/hello.js","7","5","7","75"
"Missing rate limiting","An HTTP request handler that performs expensive operations without restricting the rate at which operations can be carried out is vulnerable to denial-of-service attacks.","error","This route handler performs [[""a database access""|""relative:///webpack-sample/src/hello.js:6:3:9:3""]], but is not rate-limited.","/webpack-sample/src/hello.js","5","19","5","46"
```

### 実行の簡易化

コードを変更して再実行する場合、毎回データベースを一旦削除して再作成しなければなりません

https://github.com/github/securitylab/discussions/219

面倒なのでスクリプトにしてしまいましょう

[codeql/run.sh](https://github.com/RyoWakabayashi/ci-example/blob/main/codeql/run.sh)

```shell
#!/bin/bash
rm -rf codeql/results/source_db

docker-compose -f codeql/docker-compose-create-db.yml up
docker-compose -f codeql/docker-compose-analyze.yml up
```

ついでに npm のスクリプトにしてしまいましょう

[package.json](https://github.com/RyoWakabayashi/ci-example/blob/main/package.json)

```json
  "scripts": {
    "codeql": "codeql/run.sh"
  },
```

これで、以下のようにすれば簡単に CodeQL が実行できるようになりました

```shell
npm run codeql
```

## GitHub Actions で実行

GitHub 公式が用意してくれています

https://github.com/github/codeql-action

自分で定義ファイルを用意しても良いですし、

リポジトリーの設定から **Security & analysis** -> **Code scanning** の **Set up** をクリックしてブラウザから簡単に設定することも可能です

![repository.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b9eeea3f-3611-9685-7177-a4978c1cf32e.png)

デフォルトの設定だと保守性等のチェックはしてくれないので、今回はクエリスイートを `security-and-quality` に変えてみましょう

https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs

### GitHub Actions 設定の定義

CodeQL の設定ファイルを作成します

[.github/codeql/codeql-config.yml](https://github.com/RyoWakabayashi/ci-example/blob/main/.github/codeql/codeql-config.yml)

```yaml
name: "Security and Quality"

queries:
  - uses: security-and-quality
```

そして、それを参照するように GitHub Actions の定義を作ります

[.github/workflows/codeql-analysis.yml](https://github.com/RyoWakabayashi/ci-example/blob/main/.github/workflows/codeql-analysis.yml)

```yaml
---
name: CodeQL

on: [pull_request]

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    strategy:
      fail-fast: false
      matrix:
        language: ['javascript']

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
      - name: Initialize CodeQL
        uses: github/codeql-action/init@v2
        with:
          languages: ${{ matrix.language }}
          config-file: ./.github/codeql/codeql-config.yml
      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v2
```

### GitHub Actions の実行

プルリクエストすると、 CodeQL が動きます

![actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/0072bc66-2f8d-6c2a-801c-380b3eed1153.png)

## チェック項目の詳細

自分で QL を習得してクエリを書くことも可能ですが、一般的な脆弱性については既に定義が作られているため、これを使えば簡単にセキュリティチェックができます

例えば、 JavaScript のセキュリティ関連のチェック定義は以下の場所に [CWE] （共通脆弱性タイプ）毎に別れて格納されています

https://github.com/github/codeql/tree/main/javascript/ql/src/Security

![queries.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/7d30aa29-56f3-cac8-e673-c19a1efe032a.png)

CWE-89 の SQL インジェクションは以下のパスに定義されています

[javascript/ql/src/Security/CWE-089/SqlInjection.ql](https://github.com/github/codeql/blob/main/javascript/ql/src/Security/CWE-089/SqlInjection.ql)

クエリを一つ一つ指定することもできますが、チェック項目は大量にあるため、一括指定できるようにクエリスイートとしてまとめられています

[javascript/ql/src/codeql-suites](https://github.com/github/codeql/tree/main/javascript/ql/src/codeql-suites)

![suites.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b04f2af4-b1f7-ec0e-292a-7a06d55f9938.png)

例えば、 `javascript-security-and-quality.qls` は以下のように定義されています

```yaml
- description: Security-and-quality queries for JavaScript
- qlpack: codeql-javascript
- apply: security-and-quality-selectors.yml
  from: codeql/suite-helpers
```

で、 selectors は以下のパスで定義されています

[misc/suite-helpers](https://github.com/github/codeql/tree/main/misc/suite-helpers)

security-and-quality-selectors.yml

```yaml
- description: Selectors for selecting the security-and-quality queries for a language
- include:
    kind:
    - problem
    - path-problem
    precision:
    - high
    - very-high
- include:
    kind:
    - problem
    - path-problem
    precision: medium
    problem.severity:
      - error
      - warning
- include:
    kind:
    - diagnostic
- include:
    kind:
    - metric
    tags contain:
    - summary
- exclude:
    deprecated: //
- exclude:
    query path: /^experimental\/.*/
```

先程の SQL インジェクションの定義の先頭コメントを見ると以下のようになっていて、 selectors はここを見て対象を集めていることが分かります

```
/**
 * @name Database query built from user-controlled sources
 * @description Building a database query from user-controlled sources is vulnerable to insertion of
 *              malicious code by the user.
 * @kind path-problem
 * @problem.severity error
 * @security-severity 8.8
 * @precision high
 * @id js/sql-injection
 * @tags security
 *       external/cwe/cwe-089
 */
```

つまり、 `javascript-security-and-quality.qls` を指定すると、 `javascript/ql/src/` 配下の以下のいずれかの条件を満たすクエリが実行される、ということです

- `kind` が `problem` または `path-problem` かつ、 `precision` が `high` または `very-high`
- `kind` が `problem` または `path-problem` かつ、 `precision` が `medium` かつ、 `problem.severity` が `error` または `warning`
- `kind` が `diagnostic`
- `kind` が `metric` かつ、 `tags` に `summary` を含む
- ただし、 `deprecated` （非推奨）ではなく、パスに `experimental` （実験的）を含まない

SQL インジェクション は `kind` が `path-problem` で `precision` が `high` なので実行されます

実行時のログには以下のように実行したクエリが表示されます

```text
[170/172 eval 2s] Evaluation done; writing results to codeql-javascript/Security/CWE-200/FileAccessToHttp.bqrs.
[171/172 eval 1.4s] Evaluation done; writing results to codeql-javascript/Security/CWE-400/RemotePropertyInjection.bqrs.
[172/172 eval 2.8s] Evaluation done; writing results to codeql-javascript/Security/CWE-400/RemotePropertyInjection.bqrs.
```

## まとめ

CodeQL を使うことで、コードの単なる形式チェックだけでなく、脆弱性までチェックすることができました

Public リポジトリーであれば GitHub Actions で無償利用できるため、どんどん使っていきましょう

また、 Docker を使えばローカルでも簡単に実行できるので、既存のコードなどにもどんどん使ってみましょう

[pre-commit]: https://pre-commit.com/
[super-linter]: https://github.com/github/super-linter
[cwe]: https://www.ipa.go.jp/security/vuln/CWE.html