Skip to content

静的解析ツールとreviewdogを組み合わせて、レビューを自動化する

はじめに

AWS をはじめとするクラウドや Kubernetes に触れていると、IaC を実践するために Terraform や Helm に触れる機会が多くなります。

今回は、静的解析ツールと reviewdog を組み合わせたレビューの自動化について提案したいと思います。 https://github.com/reviewdog/reviewdog

目標

以下の手順で、自動レビューの仕組みの実現を目的とします。

  1. feature ブランチにコードをコミットする
  2. CI で静的解析
  3. 解析結果を reviewdog でコメントしてもらう

静的解析ツールの紹介

使用する静的解析ツールは以下の3つです

  • Trivy
  • Kics
  • Checkov

詳しい使い方などは以下のブログで紹介しました。 https://blog.takenoko.dev/blog/2023/08/static-analysis-iac/

reviewdog

reviewdog は入力データに Reviewdog Diagnostic Format (以下 rdjson) というフォーマットをサポートしています。

reviewdog supports Reviewdog Diagnostic Format (RDFormat) as a generic diagnostic format

Reviewdog Diagnostic Format (RDFormat)

そのフォーマットは以下の通りです。

rdjson
{
"source": {
"name": "super lint",
"url": "https://example.com/url/to/super-lint"
},
"severity": "WARNING",
"diagnostics": [
{
"message": "<msg>",
"location": {
"path": "<file path>",
"range": {
"start": {
"line": 14,
"column": 15
}
}
},
"severity": "ERROR",
"code": {
"value": "RULE1",
"url": "https://example.com/url/to/super-lint/RULE1"
}
},
{
"message": "<msg>",
"location": {
"path": "<file path>",
"range": {
"start": {
"line": 14,
"column": 15
},
"end": {
"line": 14,
"column": 18
}
}
},
"suggestions": [
{
"range": {
"start": {
"line": 14,
"column": 15
},
"end": {
"line": 14,
"column": 18
}
},
"text": "<replacement text>"
}
],
"severity": "WARNING"
}
]
}

https://github.com/reviewdog/reviewdog/tree/master/proto/rdf#rdjson

静的解析ツールと reviewdog の連携

以前のブログで静的解析ツールについて紹介しました。これらのツールは、解析結果を json 形式で出力できます。
また、reviewdog は rdjson 形式の入力を受け取ることができます。
このため、解析ツールの出力結果を jq コマンドを用いて整形し、rdjson 形式に変換することで、これらのツールを組み合わせることができます。

ここでは、解析ツールの出力を jq で rdjson へ変換する方法を紹介します。

解析に使用する Helm Chart を作成します。

Terminal window
$ helm create charts/example
$ tree -L 2 charts
charts
└── example
├── Chart.yaml
├── charts
├── templates
└── values.yaml

jqのオプション

静的解析ツールとreviewdogとの連携に使用する jq のオプションを紹介します。

使用するオプションは --slurpfile-f, --from-file です。

--slurpfile

--slurpfile <変数名> <ファイル名> という形で使用します。

以下のサンプルでは、data.json ファイルにある json データを jq 内の val 変数にバインドし、jq のフィルターに渡しています。

Terminal window
$ cat data.json
{
"key": "value"
}
$ jq -n --slurpfile val data.json '{"hoge": $val}'
{
"hoge": [
{
"key": "value"
}
]
}

-f, --from-file

-f <ファイル名> / --from-file <ファイル名> という形で使用します。 ファイルにフィルターを書くことでテンプレートのような使い方もできます。

先ほどの --argjson と組み合わせると複雑なフィルターも 1 行でかけます。

Terminal window
$ tmp='{"test": "a"}'
$ cat template.jq
# コメントを書くことができます。
{
"key": $val
}
$ jq -n --argjson val $tmp -f template.jq
{
"key": {
"test": "a"
}
}

jq 1.6 Manual

解析結果のフォーマットと rdjson への変換

Trivy

変換に使用するデータ trivy-results-to-rdjson

Trivy での解析結果を results.json にリダイレクトします。

Terminal window
$ trivy conf --severity CRITICAL,HIGH,MEDIUM charts/example -f json > results.json

results.jsonは以下のようになります。

results.json
{
"SchemaVersion": 2,
"ArtifactName": "charts/example",
"ArtifactType": "filesystem",
"Metadata": {
"ImageConfig": {
"architecture": "",
"created": "0001-01-01T00:00:00Z",
"os": "",
"rootfs": {
"type": "",
"diff_ids": null
},
"config": {}
}
},
"Results": [
{
"Target": "templates/deployment.yaml",
"Class": "config",
"Type": "helm",
"MisconfSummary": {
"Successes": 83,
"Failures": 3,
"Exceptions": 0
},
"Misconfigurations": [
{
"Type": "Helm Security Check",
"ID": "KSV001",
"AVDID": "AVD-KSV-0001",
"Title": "Can elevate its own privileges",
"Description": "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.",
"Message": "Container 'example' of Deployment 'example' should set 'securityContext.allowPrivilegeEscalation' to false",
"Namespace": "builtin.kubernetes.KSV001",
"Query": "data.builtin.kubernetes.KSV001.deny",
"Resolution": "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.",
"Severity": "MEDIUM",
"PrimaryURL": "https://avd.aquasec.com/misconfig/ksv001",
"References": [
"https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted",
"https://avd.aquasec.com/misconfig/ksv001"
],
"Status": "FAIL",
"Layer": {},
"CauseMetadata": {
"Provider": "Kubernetes",
"Service": "general",
"StartLine": 28,
"EndLine": 46,
"Code": {
"Lines": [
{
"Number": 28,
"Content": " - name: example",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": true,
"LastCause": false
},
{
"Number": 29,
"Content": " securityContext:",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 30,
"Content": " {}",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 31,
"Content": " image: \"nginx:1.16.0\"",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 32,
"Content": " imagePullPolicy: IfNotPresent",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 33,
"Content": " ports:",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 34,
"Content": " - name: http",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 35,
"Content": " containerPort: 80",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 36,
"Content": " protocol: TCP",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": true
},
{
"Number": 37,
"Content": "",
"IsCause": false,
"Annotation": "",
"Truncated": true,
"FirstCause": false,
"LastCause": false
}
]
}
}
},
{
"Type": "Helm Security Check",
"ID": "KSV012",
"AVDID": "AVD-KSV-0012",
"Title": "Runs as root user",
"Description": "'runAsNonRoot' forces the running image to run as a non-root user to ensure least privileges.",
"Message": "Container 'example' of Deployment 'example' should set 'securityContext.runAsNonRoot' to true",
"Namespace": "builtin.kubernetes.KSV012",
"Query": "data.builtin.kubernetes.KSV012.deny",
"Resolution": "Set 'containers[].securityContext.runAsNonRoot' to true.",
"Severity": "MEDIUM",
"PrimaryURL": "https://avd.aquasec.com/misconfig/ksv012",
"References": [
"https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted",
"https://avd.aquasec.com/misconfig/ksv012"
],
"Status": "FAIL",
"Layer": {},
"CauseMetadata": {
"Provider": "Kubernetes",
"Service": "general",
"StartLine": 28,
"EndLine": 46,
"Code": {
"Lines": [
{
"Number": 28,
"Content": " - name: example",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": true,
"LastCause": false
},
{
"Number": 29,
"Content": " securityContext:",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 30,
"Content": " {}",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 31,
"Content": " image: \"nginx:1.16.0\"",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 32,
"Content": " imagePullPolicy: IfNotPresent",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 33,
"Content": " ports:",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 34,
"Content": " - name: http",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 35,
"Content": " containerPort: 80",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": false
},
{
"Number": 36,
"Content": " protocol: TCP",
"IsCause": true,
"Annotation": "",
"Truncated": false,
"FirstCause": false,
"LastCause": true
},
{
"Number": 37,
"Content": "",
"IsCause": false,
"Annotation": "",
"Truncated": true,
"FirstCause": false,
"LastCause": false
}
]
}
}
},
{
"Type": "Helm Security Check",
"ID": "KSV104",
"AVDID": "AVD-KSV-0104",
"Title": "Seccomp policies disabled",
"Description": "Seccomp profile must not be explicitly set to 'Unconfined'.",
"Message": "container example of deployment example in default namespace should specify a seccomp profile",
"Namespace": "builtin.kubernetes.KSV104",
"Query": "data.builtin.kubernetes.KSV104.deny",
"Resolution": "Do not set seccomp profile to 'Unconfined'",
"Severity": "MEDIUM",
"PrimaryURL": "https://avd.aquasec.com/misconfig/ksv104",
"References": [
"https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline",
"https://avd.aquasec.com/misconfig/ksv104"
],
"Status": "FAIL",
"Layer": {},
"CauseMetadata": {
"Provider": "Kubernetes",
"Service": "general",
"Code": {
"Lines": null
}
}
}
]
},
{
"Target": "templates/service.yaml",
"Class": "config",
"Type": "helm",
"MisconfSummary": {
"Successes": 86,
"Failures": 0,
"Exceptions": 0
}
},
{
"Target": "templates/serviceaccount.yaml",
"Class": "config",
"Type": "helm",
"MisconfSummary": {
"Successes": 86,
"Failures": 0,
"Exceptions": 0
}
}
]
}

jq で使用するフィルターは以下の通りです。

trivy.jq
$trivy[].Results | map(select(.Misconfigurations))[] as $Results | $Results.Misconfigurations | map({
"message": (.Title + "<br> **Severity**:" + .Severity + "<br> **Description**: " + .Description + "<br> **Message**: " + .Message + "<br> **Expected**: " + .Resolution),
"path": ["charts", $Results.Target] | join("/"),
"startLine": ( .CauseMetadata.StartLine // 1 ) ,
"endLine": ( .CauseMetadata.EndLine // 1 )
}) | {
"source": {
"name": "aquasecurity/trivy",
"url": "https://github.com/aquasecurity/trivy"
},
"severity": "WARNING",
"diagnostics": [ .[] | {
"message": .message,
"location": {
"path": .path,
"range": {
"start": {
"line": .startLine
},
"end": {
"line": .endLine
}
}
}
}]
}

jq を使って、Trivy の解析結果 (results.json) を rdjson に変換します。

jq -n —slurpfile trivy results.json -f trivy.jq
Terminal window
$ jq -n --slurpfile trivy results.json -f trivy.jq
{
"source": {
"name": "aquasecurity/trivy",
"url": "https://github.com/aquasecurity/trivy"
},
"severity": "WARNING",
"diagnostics": [
{
"message": "Can elevate its own privileges<br> **Severity**:MEDIUM<br> **Description**: A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.<br> **Message**: Container 'trivy' of Deployment 'trivy' should set 'securityContext.allowPrivilegeEscalation' to false<br> **Expected**: Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.",
"location": {
"path": "charts/templates/deployment.yaml",
"range": {
"start": {
"line": 28
},
"end": {
"line": 46
}
}
}
},
{
"message": "Runs as root user<br> **Severity**:MEDIUM<br> **Description**: 'runAsNonRoot' forces the running image to run as a non-root user to ensure least privileges.<br> **Message**: Container 'trivy' of Deployment 'trivy' should set 'securityContext.runAsNonRoot' to true<br> **Expected**: Set 'containers[].securityContext.runAsNonRoot' to true.",
"location": {
"path": "charts/templates/deployment.yaml",
"range": {
"start": {
"line": 28
},
"end": {
"line": 46
}
}
}
},
{
"message": "Seccomp policies disabled<br> **Severity**:MEDIUM<br> **Description**: Seccomp profile must not be explicitly set to 'Unconfined'.<br> **Message**: container trivy of deployment trivy in default namespace should specify a seccomp profile<br> **Expected**: Do not set seccomp profile to 'Unconfined'",
"location": {
"path": "charts/templates/deployment.yaml",
"range": {
"start": {
"line": 1
},
"end": {
"line": 1
}
}
}
}
]
}

これで Trivy の解析結果を rdjson に変換できました。
後ほど、この変換の流れを CI で実行します。

Kics

変換に使用するデータ kics-results-to-rdjson

Kics での解析結果を results.json に書き出します。
※ デフォルトのファイル名は results です。 --output-name オプションで名前を変更できます。

Terminal window
$ docker run --rm -v $(pwd):/charts checkmarx/kics:latest scan -p /charts --no-progress --report-formats json -o /charts

jq で使用するフィルターは以下の通りです。

kics.jq
$kics[].queries | map({
"message": (.query_name + " (queryId: "+ .query_id +") <br> **Severity**: " + .severity + "<br> **Description**: " + .description + "<br> **Expected**: " + .files[].expected_value),
"path": .files[].file_name,
"startLine": .files[].line
}) | {
"source": {
"name": "Checkmarx/kics",
"url": "https://github.com/Checkmarx/kics"
},
"severity": "WARNING",
"diagnostics": [ .[] | {
"message": .message,
"location": {
"path": .path,
"range": {
"start": {
"line": .startLine
}
}
}
}]
}

jq を使って、Kics の解析結果 (results.json) を rdjson に変換します。

jq -n —slurpfile kics results.json -f kics.jq
Terminal window
$ jq -n --slurpfile kics results.json -f kics.jq
{
"source": {
"name": "Checkmarx/kics",
"url": "https://github.com/Checkmarx/kics"
},
"severity": "WARNING",
"diagnostics": [
{
"message": "Privilege Escalation Allowed (queryId: 5572cc5e-1e4c-4113-92a6-7a8a3bd25e6d) <br> **Severity**: HIGH<br> **Description**: Containers should not run with allowPrivilegeEscalation in order to prevent them from gaining more privileges than their parent process<br> **Expected**: metadata.name={{kics-helm-example}}.spec.template.spec.containers.name={{example}}.securityContext.allowPrivilegeEscalation should be set and should be set to false",
"location": {
"path": "../../charts/charts/example/templates/deployment.yaml",
"range": {
"start": {
"line": 1
}
}
}
},
{
# 以下省略

これで Kics の解析結果を rdjson に変換できました。
後ほど、この変換の流れを CI で実行します。

Checkov

変換に使用するデータ checkov-results-to-rdjson

Checkov での解析結果を results.json にリダイレクトします。

Terminal window
$ checkov --framework helm -d charts/example --quiet --compact -o json > results.json

jq で使用するフィルターは以下の通りです。

checkov.jq
$checkov[].results.failed_checks | map({
"message": ("**CheckId**: "+ .check_id + "<br> **Check Name**: " + .check_name + "<br> **Guideline**: " + .guideline),
"path": ("charts" + .repo_file_path),
"startLine": .file_line_range[0],
"endLine": .file_line_range[1]
}) | {
"source": {
"name": "bridgecrewioz/checkov",
"url": "https://github.com/bridgecrewio/checkov"
},
"severity": "WARNING",
"diagnostics": [ .[] | {
"message": .message,
"location": {
"path": .path,
"range": {
"start": {
"line": .startLine
},
"end": {
"line": .endLine
}
}
}
}]
}

jq を使って、Checkov の解析結果 (results.json) を rdjson に変換します。

jq -n —slurpfile checkov results.json -f checkov.jq
Terminal window
$ jq -n --slurpfile checkov results.json -f checkov.jq
{
"source": {
"name": "bridgecrewioz/checkov",
"url": "https://github.com/bridgecrewio/checkov"
},
"severity": "WARNING",
"diagnostics": [
{
"message": "**CheckId**: CKV_K8S_37<br> **Check Name**: Minimize the admission of containers with capabilities assigned<br> **Guideline**: https://docs.paloaltonetworks.com/content/techdocs/en_US/prisma/prisma-cloud/prisma-cloud-code-security-policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-34.html",
"location": {
"path": "charts/example/templates/deployment.yaml",
"range": {
"start": {
"line": 3
},
"end": {
"line": 47
}
}
}
},
{
"message": "**CheckId**: CKV_K8S_31<br> **Check...."
# 以下省略

これで Checkov の解析結果を rdjson に変換できました。
後ほど、この変換の流れを CI で実行します。

GitLab CI で実行する

API トークンを作成する

reviewdog が GitLab と連携するためには、REVIEWDOG_GITLAB_API_TOKEN 変数を定義する必要があります。

Store REVIEWDOG_GITLAB_API_TOKEN in GitLab CI variable. https://github.com/reviewdog/reviewdog#gitlab-ci

まずはじめに、画面左のナビゲーションから Settings > Access Tokens > Add new token でトークンを新規追加します。

gitlab-project-access-tokens-page

project access token の設定をします。
トークンの名前は任意なのでわかりやすい名前をつけてください。スコープは api のみで十分です。
Create project access token を作成すると、トークンが表示されるのでコピーしておいてください。 gitlab-add-project-access-token

API トークンを設定する

次にトークンを変数に設定します。
画面左のナビゲーションから Settings > CI/CD > Variables > Expand に行き、 Add variable ボタンから変数を追加します。 gitlab-add-ci-variable

変数名は REVIEWDOG_GITLAB_API_TOKEN にして、先ほどコピーしたトークンを Value にペーストしてください。
Add variable ボタンを押すと設定完了です。 gitlab-add-project-access-token-as-ci-variable

Trivy

解析に使用する Helm Chart を作成します。

Terminal window
$ helm create charts/trivy
$ tree -L 2
.
├── README.md
└── charts
└── trivy

Trivy で紹介した jq のフィルターを .gitlab/ci/trivy.jq に定義します。

.gitlab-ci.yml の設定は以下の通りです。

.gitlab-ci.yml
.charts-patterns: &charts-patterns
- charts/**/*
# https://docs.gitlab.com/ee/ci/jobs/job_control.html#avoid-duplicate-pipelines
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
stages:
- scan
# 以下のエラーが出ないよう variableを設定する
# `reviewdog: fail to get diff: failed to get merge-base commit: exit status 128`
#
# 参考:ReviewdogをGitLabで使うときに`failed to get merge-base commit`が発生する - hatenablog
# https://kiririmode.hatenablog.jp/entry/20220327/1648344999
variables:
GIT_STRATEGY: clone
GIT_DEPTH: 0
trivy-misconf:
stage: scan
allow_failure: true
image:
name: docker.io/aquasec/trivy:latest
entrypoint: [""]
rules:
- changes: *charts-patterns
before_script:
- git fetch
- apk add --no-cache jq
- wget -O - -q https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh | sh -s v0.14.1
- CHANGED_DIR_REGEX=$(git diff --name-only $CI_COMMIT_SHA origin/main | grep -E "^charts/" | awk -F / '{print $2}' | sort | uniq | sed -e '$ ! s/$/|/g' | tr -d '\n')
- EXCLUDE_DIR_SET=$(ls charts | sed -E "/$CHANGED_DIR_REGEX/d" | sed -e '$ ! s/$/,/g' | tr -d '\n')
script:
- trivy --version
- trivy conf
--exit-code 1
--severity HIGH,CRITICAL,MEDIUM
--format json
--output results.json
--skip-dirs ${EXLUDE_DIR_SET-="tests"}
--debug
${PWD}/charts
after_script:
- jq -n --slurpfile trivy results.json -f .gitlab/ci/trivy.jq | ./bin/reviewdog -f=rdjson -reporter=gitlab-mr-commit -tee

以下は CI を実行した結果です。 https://gitlab.com/kntks/helm-reviewdog/-/merge_requests/1

Kics

解析に使用する Helm Chart を作成します。

Terminal window
$ helm create charts/kics
$ tree -L 2
.
├── README.md
└── charts
├── kics
└── trivy

Kics で紹介した jq のフィルターを .gitlab/ci/kics.jq に定義します。

.gitlab-ci.yml の設定は以下の通りです。

.gitlab-ci.yml
.charts-patterns: &charts-patterns
- charts/**/*
# https://docs.gitlab.com/ee/ci/jobs/job_control.html#avoid-duplicate-pipelines
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
stages:
- scan
# 以下のエラーが出ないよう variableを設定する
# `reviewdog: fail to get diff: failed to get merge-base commit: exit status 128`
#
# 参考:ReviewdogをGitLabで使うときに`failed to get merge-base commit`が発生する - hatenablog
# https://kiririmode.hatenablog.jp/entry/20220327/1648344999
variables:
GIT_STRATEGY: clone
GIT_DEPTH: 0
kics-scan:
stage: scan
allow_failure: true
image:
name: checkmarx/kics:latest
entrypoint: [""]
rules:
- changes: *charts-patterns
before_script:
- git fetch
- apk add --no-cache jq
- wget -O - -q https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh | sh -s v0.14.1
- CHANGED_CHARTS=$(git diff --name-only $CI_COMMIT_SHA origin/main | grep -E "^charts/" | awk -F / '{print "charts/"$2}' | sort | uniq | paste -sd "," -)
script:
- kics scan
--no-progress
-p $CHANGED_CHARTS
-o ${PWD}
--report-formats json
--output-name results
--exclude-severities low,medium
after_script:
- jq -n --slurpfile kics results.json -f .gitlab/ci/kics.jq | ./bin/reviewdog -f=rdjson -reporter=gitlab-mr-commit -tee

以下は CI を実行した結果です。 https://gitlab.com/kntks/helm-reviewdog/-/merge_requests/2

Checkov

解析に使用する Helm Chart を作成します。

Terminal window
$ helm create charts/checkov
$ tree -L 2
.
├── README.md
└── charts
├── checkov
├── kics
└── trivy

Checkov で紹介した jq のフィルターを .gitlab/ci/checkov.jq に定義します。

.gitlab-ci.yml の設定は以下の通りです。

.gitlab-ci.yml
.charts-patterns: &charts-patterns
- charts/**/*
# https://docs.gitlab.com/ee/ci/jobs/job_control.html#avoid-duplicate-pipelines
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
stages:
- scan
# 以下のエラーが出ないよう variableを設定する
# `reviewdog: fail to get diff: failed to get merge-base commit: exit status 128`
#
# 参考:ReviewdogをGitLabで使うときに`failed to get merge-base commit`が発生する - hatenablog
# https://kiririmode.hatenablog.jp/entry/20220327/1648344999
variables:
GIT_STRATEGY: clone
GIT_DEPTH: 0
checkov:
stage: scan
allow_failure: true
image:
name: bridgecrew/checkov:latest
entrypoint:
- '/usr/bin/env'
- 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
rules:
- changes: *charts-patterns
before_script:
- git fetch
- apt update && apt upgrade -y && apt install -y jq wget
- wget -O - -q https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh | sh -s v0.14.1
- CHANGED_DIR_REGEX=$(git diff --name-only $CI_COMMIT_SHA origin/main | grep -E "^charts/" | awk -F / '{print $2}' | sort | uniq | sed -e '$ ! s/$/|/g' | tr -d '\n')
- EXCLUDE_DIR_SET=$(ls charts | sed -E "/$CHANGED_DIR_REGEX/d" | awk '{print " --skip-path "$1}' | tr -d '\n' && echo " --skip-path tests")
script:
- checkov -v
- sh -c "checkov -d ./charts --framework helm --quiet --compact $EXCLUDE_DIR_SET -o json" | jq -M > results.json
after_script:
- jq -n --slurpfile checkov results.json -f .gitlab/ci/checkov.jq | ./bin/reviewdog -f=rdjson -reporter=gitlab-mr-commit -tee

以下は CI を実行した結果です。 https://gitlab.com/kntks/helm-reviewdog/-/merge_requests/3

GitHub Actions で実行する

GitHub Appsを作成する

GitHub Actions のワークフロー内で reviewdog がプルリクエストにコメントを書き込むためにはトークンが必要です。

今回は Personal Access Token ではなく、GitHub Apps を使用します。
以前のブログに GitHub Apps の作り方を書いたので、作成の流れは ↓ のブログに任せます。 https://blog.takenoko.dev/blog/2023/04/github-rest-api/#github-apps%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B

プライベートキーと App のインストールまで完了してください。

GitHub Apps に必要な権限は以下の通りです。 Repository permissions

namepermission
ContentsRead-only
MetadataRead-only
Pull requestsRead and write

info: reviewdog は [`GET /repos/{owner}/{repo}/pulls/{pull_number}`](https://docs.github.com/ja/rest/pulls/pulls#get-a-pull-request) を実行している。そのため permission に `Contents` が必要。 ![github-repository-permissions-for-pull-requests](/2023/08/static-analysis-with-reviewdog/github-repository-permissions-for-pull-requests.webp) [Repository permissions for "Pull requests"](https://docs.github.com/ja/rest/overview/permissions-required-for-github-apps?apiVersion=2022-11-28#repository-permissions-for-pull-requests)

シークレットを設定する

作成した App の ID とダウンロードしたプライベートキーをシークレットとして設定します。 github-settings-secrets-1



Secrets タブであることを確認したら、New repository secret ボタンを押してシークレットを作成してください。
追加するシークレットは APP_IDAPP_PRIVATE_KEY です。 github-settings-secrets-2

Trivy

GitLab のときとディレクトリ構成は同じです。

Terminal window
$ helm create charts/trivy
$ tree -L 2
.
├── README.md
└── charts
└── trivy

Trivy で紹介した jq のフィルターを .github/workflows/jq/trivy.jq に定義します。

.github/workflows/trivy.yaml
name: Trivy Static Analysis
on:
pull_request:
paths:
- 'charts/**'
jobs:
trivy:
name: Trivy GitHub Action
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Get Exclude Directories Set
id: get-exclude-directories-set
run: |
git fetch
CHANGED_DIR_REGEX=$(git diff --name-only HEAD origin/main | grep -E "^charts/" | awk -F / '{print $2}' | sort | uniq | sed -e '$ ! s/$/|/g' | tr -d '\n')
echo "EXCLUDE_DIR_SET=$(ls charts | sed -E "/$CHANGED_DIR_REGEX/d" | sed -e '$ ! s/$/,/g' | tr -d '\n')" >> "$GITHUB_OUTPUT"
- name: Run Trivy vulnerability scanner in config mode
uses: aquasecurity/trivy-action@master
with:
scan-type: config
scan-ref: charts/
exit-code: 0
skip-dirs: ${{ steps.get-exclude-directories-set.outputs.EXCLUDE_DIR_SET }}
format: json
output: results.json
severity: 'HIGH,CRITICAL,MEDIUM'
- name: Generate a token
id: generate-token
uses: tibdex/github-app-token@v1.7.0
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: reviewdog/action-setup@v1
with:
reviewdog_version: latest
- name: Run reviewdog
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ steps.generate-token.outputs.token }}
run: jq -n --slurpfile trivy results.json -f .github/workflows/jq/trivy.jq | reviewdog -f=rdjson -reporter=github-pr-review -tee

aquasecurity/trivy-action - github.com

以下は CI を実行した結果です。 https://github.com/kntks/helm-reviewdog/pull/1

Kics

kics-github-actionenable_comments という変数があり、reviewdog を組み合わせなくてもプルリクエストにコメントを残してくれる機能があります。
そのため Kics では、jq と reviewdog を使いません。

GitLab のときとディレクトリ構成は同じです。

Terminal window
$ helm create charts/kics
$ tree -L 2
.
├── README.md
└── charts
├── kics
└── trivy
.github/workflows/kics.yaml
name: Kics Static Analysis
on:
pull_request:
paths:
- 'charts/**'
jobs:
kics:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Get Changed Charts Set
id: get-changed-charts-set
run: |
git fetch
echo "CHANGED_CHARTS=$(git diff --name-only HEAD origin/main | grep -E "^charts/" | awk -F / '{print "charts/"$2}' | sort | uniq | paste -sd "," -)" >> "$GITHUB_OUTPUT"
- name: Generate a token
id: generate-token
uses: tibdex/github-app-token@v1.7.0
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Run Kics Scan
uses: checkmarx/kics-github-action@v1.7.0
with:
path: ${{ steps.get-changed-charts-set.outputs.CHANGED_CHARTS }}
token: ${{ steps.generate-token.outputs.token }}
output_path: results
ignore_on_exit: results
exclude_severities: 'info,low,medium'
enable_comments: true

Github Actions - docs.kics.io Checkmarx/kics-github-action - github.com

以下は CI を実行した結果です。 https://github.com/kntks/helm-reviewdog/pull/2

Checkov

GitLab のときとディレクトリ構成は同じです。

Terminal window
$ helm create charts/checkov
$ tree -L 2
.
├── README.md
└── charts
├── checkov
├── kics
└── trivy

Checkov で紹介した jq のフィルターを .github/workflows/jq/checkov.jq に定義します。

.github/workflows/checkov.yaml
name: Checkov Static Analysis
on:
pull_request:
paths:
- 'charts/**'
jobs:
checkov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Get Exclude Directories Set
id: get-exclude-directories-set
run: |
git fetch
CHANGED_DIR_REGEX=$(git diff --name-only HEAD origin/main | grep -E "^charts/" | awk -F / '{print $2}' | sort | uniq | sed -e '$ ! s/$/|/g' | tr -d '\n')
echo "EXCLUDE_DIR_SET=$(ls charts | sed -E "/$CHANGED_DIR_REGEX/d" | sed -e '$ ! s/$/,/g' | tr -d '\n')" >> "$GITHUB_OUTPUT"
- name: Checkov GitHub Action
uses: bridgecrewio/checkov-action@v12
with:
output_format: json
directory: charts/
framework: helm
quiet: true
compact: true
skip_path: ${{ steps.get-exclude-directories-set.outputs.EXCLUDE_DIR_SET }}
soft_fail: true
output_file_path: results
- name: Generate a token
id: generate-token
uses: tibdex/github-app-token@v1.7.0
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: reviewdog/action-setup@v1
with:
reviewdog_version: latest
- name: Run reviewdog
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ steps.generate-token.outputs.token }}
run: jq -n --slurpfile checkov results/results_json.json -f .github/workflows/jq/checkov.jq | reviewdog -f=rdjson -reporter=github-pr-review -tee

Info: bridgecrewio/checkov-action の with で指定できる [skip_path](https://github.com/bridgecrewio/checkov-action/blob/f2bbdaa530587b5873a295ad92d78d9227aa5178/action.yml#L118-L120) はカンマ区切りで指定できる。 > skip_path: > description: 'Path ... (comma separated)'

以下は CI を実行した結果です。 https://github.com/kntks/helm-reviewdog/pull/3

導入に伴う課題や注意点

Checkov の MR を見ていただくとわかるのですが、ツールによっては大量の指摘を受けます。
誰が、どこまで対応するのかを決めないといけないため、人員やプロジェクトの状況が安定するまで導入するのは難しいです。

とりあえず、静的解析させてみる。でも問題ないと思いますが、具体的に検出したい内容を決め、その内容のみ検出できるようにツールのオプションを変更することをオススメします。

まとめ

静的解析ツールと reviewdog を使用したレビュー自動化の方法を GitLab CI, GitHub Actions それぞれ提案してみました。
静的解析の自動化を導入することでレビューの見逃しを削減することができ、品質の担保できるようになると思います。

設計、実装の参考になれば幸いです。