静的解析ツールとreviewdogを組み合わせて、レビューを自動化する
はじめに
AWS をはじめとするクラウドや Kubernetes に触れていると、IaC を実践するために Terraform や Helm に触れる機会が多くなります。
今回は、静的解析ツールと reviewdog を組み合わせたレビューの自動化について提案したいと思います。 https://github.com/reviewdog/reviewdog
目標
以下の手順で、自動レビューの仕組みの実現を目的とします。
- feature ブランチにコードをコミットする
- CI で静的解析
- 解析結果を 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
そのフォーマットは以下の通りです。
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 を作成します。
$ helm create charts/example
$ tree -L 2 chartscharts└── example ├── Chart.yaml ├── charts ├── templates └── values.yaml
jqのオプション
静的解析ツールとreviewdogとの連携に使用する jq のオプションを紹介します。
使用するオプションは --slurpfile
と -f, --from-file
です。
--slurpfile
--slurpfile <変数名> <ファイル名>
という形で使用します。
以下のサンプルでは、data.json
ファイルにある json データを jq 内の val
変数にバインドし、jq のフィルターに渡しています。
$ cat data.json{ "key": "value"}
$ jq -n --slurpfile val data.json '{"hoge": $val}'{ "hoge": [ { "key": "value" } ]}
-f, --from-file
-f <ファイル名> / --from-file <ファイル名>
という形で使用します。
ファイルにフィルターを書くことでテンプレートのような使い方もできます。
先ほどの --argjson
と組み合わせると複雑なフィルターも 1 行でかけます。
$ tmp='{"test": "a"}'$ cat template.jq# コメントを書くことができます。{ "key": $val}
$ jq -n --argjson val $tmp -f template.jq{ "key": { "test": "a" }}
解析結果のフォーマットと rdjson への変換
Trivy
変換に使用するデータ
Trivy での解析結果を results.json
にリダイレクトします。
$ 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[].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
$ 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.json
に書き出します。
※ デフォルトのファイル名は results
です。 --output-name
オプションで名前を変更できます。
$ docker run --rm -v $(pwd):/charts checkmarx/kics:latest scan -p /charts --no-progress --report-formats json -o /charts
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
$ 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.json
にリダイレクトします。
$ checkov --framework helm -d charts/example --quiet --compact -o json > results.json
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
$ 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
でトークンを新規追加します。
project access token の設定をします。
トークンの名前は任意なのでわかりやすい名前をつけてください。スコープは api
のみで十分です。
Create project access token
を作成すると、トークンが表示されるのでコピーしておいてください。
API トークンを設定する
次にトークンを変数に設定します。
画面左のナビゲーションから Settings > CI/CD > Variables > Expand
に行き、 Add variable
ボタンから変数を追加します。
変数名は REVIEWDOG_GITLAB_API_TOKEN
にして、先ほどコピーしたトークンを Value
にペーストしてください。
Add variable
ボタンを押すと設定完了です。
Trivy
解析に使用する Helm Chart を作成します。
$ helm create charts/trivy
$ tree -L 2.├── README.md└── charts └── trivy
Trivy で紹介した jq のフィルターを .gitlab/ci/trivy.jq
に定義します。
.gitlab-ci.yml
の設定は以下の通りです。
.charts-patterns: &charts-patterns - charts/**/*
# https://docs.gitlab.com/ee/ci/jobs/job_control.html#avoid-duplicate-pipelinesworkflow: 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/1648344999variables: 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 を作成します。
$ helm create charts/kics
$ tree -L 2.├── README.md└── charts ├── kics └── trivy
Kics で紹介した jq のフィルターを .gitlab/ci/kics.jq
に定義します。
.gitlab-ci.yml
の設定は以下の通りです。
.charts-patterns: &charts-patterns - charts/**/*
# https://docs.gitlab.com/ee/ci/jobs/job_control.html#avoid-duplicate-pipelinesworkflow: 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/1648344999variables: 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 を作成します。
$ helm create charts/checkov
$ tree -L 2.├── README.md└── charts ├── checkov ├── kics └── trivy
Checkov で紹介した jq のフィルターを .gitlab/ci/checkov.jq
に定義します。
.gitlab-ci.yml
の設定は以下の通りです。
.charts-patterns: &charts-patterns - charts/**/*
# https://docs.gitlab.com/ee/ci/jobs/job_control.html#avoid-duplicate-pipelinesworkflow: 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/1648344999variables: 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
name | permission |
---|---|
Contents | Read-only |
Metadata | Read-only |
Pull requests | Read 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` が必要。  [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 とダウンロードしたプライベートキーをシークレットとして設定します。
Secrets
タブであることを確認したら、New repository secret
ボタンを押してシークレットを作成してください。
追加するシークレットは APP_ID
と APP_PRIVATE_KEY
です。
Trivy
GitLab のときとディレクトリ構成は同じです。
$ helm create charts/trivy
$ tree -L 2.├── README.md└── charts └── trivy
Trivy で紹介した jq のフィルターを .github/workflows/jq/trivy.jq
に定義します。
name: Trivy Static Analysison: 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-action は enable_comments
という変数があり、reviewdog を組み合わせなくてもプルリクエストにコメントを残してくれる機能があります。
そのため Kics では、jq と reviewdog を使いません。
GitLab のときとディレクトリ構成は同じです。
$ helm create charts/kics
$ tree -L 2.├── README.md└── charts ├── kics └── trivy
name: Kics Static Analysison: 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 のときとディレクトリ構成は同じです。
$ helm create charts/checkov
$ tree -L 2.├── README.md└── charts ├── checkov ├── kics └── trivy
Checkov で紹介した jq のフィルターを .github/workflows/jq/checkov.jq
に定義します。
name: Checkov Static Analysison: 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)'
- Integrate Checkov with GitHub Actions - checkov.io
- Integrate Checkov with GitLab CI - checkov.io
- bridgecrewio/checkov-action - github.com
以下は CI を実行した結果です。 https://github.com/kntks/helm-reviewdog/pull/3
導入に伴う課題や注意点
Checkov の MR を見ていただくとわかるのですが、ツールによっては大量の指摘を受けます。
誰が、どこまで対応するのかを決めないといけないため、人員やプロジェクトの状況が安定するまで導入するのは難しいです。
とりあえず、静的解析させてみる。でも問題ないと思いますが、具体的に検出したい内容を決め、その内容のみ検出できるようにツールのオプションを変更することをオススメします。
まとめ
静的解析ツールと reviewdog を使用したレビュー自動化の方法を GitLab CI, GitHub Actions それぞれ提案してみました。
静的解析の自動化を導入することでレビューの見逃しを削減することができ、品質の担保できるようになると思います。
設計、実装の参考になれば幸いです。