checkovをreviewdogに対応させた話


タイトルの通りですがcheckovは出力結果がreviewdogの入力形式に対応していないので、そのままだと組み合わせて使うことができません。そこで以下のようにスクリプトを書いて対応しました。

やりたい事

terraformを管理するGithubのリポジトリでPRを作成した際にterraformコードの静的解析を行い、問題があればPRにコメントを投稿してくれる、という形を目指します。

ここではcheckovで静的解析の結果を標準出力しreviewdogがパイプで結果を受け取ってPRにコメントします。

実際には以下のような表示になります。

各ツールについて

実装

まずreviewdogが受け取り可能な形式については以下に記載があります。

そのなかで今回はerrorformatという形式に合わせていこうと思います。errorformatは行単位で指定した形式の標準出力を受け取ることができます。

例えば標準出力が{file}:{line number}:{column number}: {message}のような形式だとすると、errorformat%f:%l:%c: %mとなります。

$ golint ./...
comment_iowriter.go:11:6: exported type CommentWriter should have comment or be unexported
$ golint ./... | reviewdog -efm="%f:%l:%c: %m" -diff="git diff FETCH_HEAD"

出力形式の変換

reviewdogでどのように受け取るかが分かったので次はcheckovの出力を調整します。

checkovの出力形式は以下に記されている通りです。

これらの出力形式からどれかを選びreviewdogの受け取り形式に合うよう変更していきます。選択可能なかで形式変更が最も容易なのはJSONなので、一旦JSON形式で出力した結果をパイプで受け取り、自作のスクリプトを通してerrorformatに変換していきます。

コマンドとしては以下のようになります。

$ checkov -d . -o json | python3 parse.py

ここではpythonを利用していますが言語は何でも構いません。

pythonのコードは以下のようになります。
(結構雑に書いているので細かいバグがありそうですが...)

parse.py

import json, sys

def main():
    data = json.load(sys.stdin)
    failed_checks = data["results"].get("failed_checks")
    if failed_checks is None:
        exit(0)

    for failed_check in failed_checks:
        file_name = failed_check["file_path"].replace('/', '')
        line_number = failed_check["file_line_range"][0]
        error_message = failed_check["check_name"]
        print('{}:{}: {}'.format(file_name, line_number, error_message))

    exit(1)


if __name__ == "__main__":
    main()

テスト実行

ここまで出来たらローカル環境で実際にtfファイルを作成してテストしてみます。

gke.tf

resource "google_service_account" "default" {
  account_id   = "service-account-id"
  display_name = "Service Account"
  project      = var.project_id
}

resource "google_container_cluster" "primary" {
  name     = "my-gke-cluster"
  location = var.region

  # We can't create a cluster with no node pool defined, but we want to only use
  # separately managed node pools. So we create the smallest possible default
  # node pool and immediately delete it.
  remove_default_node_pool = true
  initial_node_count       = 1
}

resource "google_container_node_pool" "primary_preemptible_nodes" {
  name       = "my-node-pool"
  location   = var.region
  cluster    = google_container_cluster.primary.name
  node_count = 1

  node_config {
    preemptible  = true
    machine_type = "e2-medium"

    # Google recommends custom service accounts that have cloud-platform scope and permissions granted via IAM Roles.
    service_account = google_service_account.default.email
    oauth_scopes = [
      "https://www.googleapis.com/auth/cloud-platform"
    ]
  }
}

ローカルでテスト実行すると以下のような表示になります。

$ checkov -d . -o json | python3 parse.py | reviewdog -efm="%f:%l: %m" -name="checkov" -reporter=local -filter-mode=nofilter
gke.tf:7: Ensure PodSecurityPolicy controller is enabled on the Kubernetes Engine Clusters
gke.tf:7: Ensure Kubernetes Cluster is created with Private cluster enabled
gke.tf:7: Ensure Kubernetes Cluster is created with Alias IP ranges enabled
gke.tf:7: Ensure master authorized networks is set to enabled in GKE clusters
gke.tf:7: Ensure Kubernetes Clusters are configured with Labels
gke.tf:7: Ensure a client certificate is used by clients to authenticate to Kubernetes Engine Clusters
gke.tf:7: Ensure GKE basic auth is disabled
gke.tf:7: Ensure Network Policy is enabled on Kubernetes Engine Clusters
gke.tf:18: Ensure 'Automatic node upgrade' is enabled for Kubernetes Clusters
gke.tf:18: Ensure Container-Optimized OS (cos) is used for Kubernetes Engine Clusters Node image
gke.tf:18: Ensure 'Automatic node repair' is enabled for Kubernetes Clusters

後はGithub Actionsのyamlに設定ファイルとして書いてあげればCIとして利用することが出来ます。

以上、「checkovをreviewdogに対応させた話」でした。