静的解析ツールとreviewdogを組み合わせて、レビューを自動化する
AWS をはじめとするクラウドや Kubernetes に触れていると、IaC を実践するために Terraform や Helm に触れる機会が多くなります。
今回は、静的解析ツールと reviewdog を組み合わせたレビューの自動化について提案したいと思います。 https://github.com/reviewdog/reviewdog
以下の手順で、自動レビューの仕組みの実現を目的とします。
- feature ブランチにコードをコミットする
 - CI で静的解析
 - 解析結果を reviewdog でコメントしてもらう
 
静的解析ツールの紹介
Section titled “静的解析ツールの紹介”使用する静的解析ツールは以下の3つです
- Trivy
 - Kics
 - Checkov
 
詳しい使い方などは以下のブログで紹介しました。 https://blog.takenoko.dev/blog/2023/08/static-analysis-iac/
reviewdog
Section titled “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 の連携
Section titled “静的解析ツールと reviewdog の連携”以前のブログで静的解析ツールについて紹介しました。これらのツールは、解析結果を json 形式で出力できます。
また、reviewdog は rdjson 形式の入力を受け取ることができます。
このため、解析ツールの出力結果を jq コマンドを用いて整形し、rdjson 形式に変換することで、これらのツールを組み合わせることができます。
ここでは、解析ツールの出力を jq で rdjson へ変換する方法を紹介します。
解析に使用する Helm Chart を作成します。
$ helm create charts/example
$ tree -L 2 chartscharts└── example    ├── Chart.yaml    ├── charts    ├── templates    └── values.yamljqのオプション
Section titled “jqのオプション”静的解析ツールとreviewdogとの連携に使用する jq のオプションを紹介します。
使用するオプションは --slurpfile と -f, --from-file です。
--slurpfile
Section titled “--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
Section titled “-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 への変換
Section titled “解析結果のフォーマットと rdjson への変換”変換に使用するデータ

Trivy での解析結果を results.json にリダイレクトします。
$ trivy conf --severity CRITICAL,HIGH,MEDIUM charts/example -f json > results.jsonresults.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 での解析結果を results.json に書き出します。
※ デフォルトのファイル名は results です。 --output-name オプションで名前を変更できます。
$ docker run --rm -v $(pwd):/charts checkmarx/kics:latest scan -p /charts --no-progress --report-formats json -o /chartsjq で使用するフィルターは以下の通りです。
$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
Section titled “Checkov”変換に使用するデータ

Checkov での解析結果を results.json にリダイレクトします。
$ checkov --framework helm -d charts/example --quiet --compact -o json > results.jsonjq で使用するフィルターは以下の通りです。
$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 で実行する
Section titled “GitLab CI で実行する”API トークンを作成する
Section titled “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 トークンを設定する
Section titled “API トークンを設定する”次にトークンを変数に設定します。
画面左のナビゲーションから Settings > CI/CD > Variables > Expand に行き、 Add variable ボタンから変数を追加します。

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

解析に使用する Helm Chart を作成します。
$ helm create charts/trivy
$ tree -L 2.├── README.md└── charts    └── trivyTrivy で紹介した 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
解析に使用する Helm Chart を作成します。
$ helm create charts/kics
$ tree -L 2.├── README.md└── charts    ├── kics    └── trivyKics で紹介した 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
Section titled “Checkov”解析に使用する Helm Chart を作成します。
$ helm create charts/checkov
$ tree -L 2.├── README.md└── charts    ├── checkov    ├── kics    └── trivyCheckov で紹介した 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 で実行する
Section titled “GitHub Actions で実行する”GitHub Appsを作成する
Section titled “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)
シークレットを設定する
Section titled “シークレットを設定する”作成した App の ID とダウンロードしたプライベートキーをシークレットとして設定します。

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

GitLab のときとディレクトリ構成は同じです。
$ helm create charts/trivy
$ tree -L 2.├── README.md└── charts    └── trivyTrivy で紹介した 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 -teeaquasecurity/trivy-action - github.com
以下は CI を実行した結果です。 https://github.com/kntks/helm-reviewdog/pull/1
kics-github-action は enable_comments という変数があり、reviewdog を組み合わせなくてもプルリクエストにコメントを残してくれる機能があります。
そのため Kics では、jq と reviewdog を使いません。
GitLab のときとディレクトリ構成は同じです。
$ helm create charts/kics
$ tree -L 2.├── README.md└── charts    ├── kics    └── trivyname: 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: trueGithub Actions - docs.kics.io Checkmarx/kics-github-action - github.com
以下は CI を実行した結果です。 https://github.com/kntks/helm-reviewdog/pull/2
Checkov
Section titled “Checkov”GitLab のときとディレクトリ構成は同じです。
$ helm create charts/checkov
$ tree -L 2.├── README.md└── charts    ├── checkov    ├── kics    └── trivyCheckov で紹介した 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 -teeInfo: 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
導入に伴う課題や注意点
Section titled “導入に伴う課題や注意点”Checkov の MR を見ていただくとわかるのですが、ツールによっては大量の指摘を受けます。
誰が、どこまで対応するのかを決めないといけないため、人員やプロジェクトの状況が安定するまで導入するのは難しいです。
とりあえず、静的解析させてみる。でも問題ないと思いますが、具体的に検出したい内容を決め、その内容のみ検出できるようにツールのオプションを変更することをオススメします。
静的解析ツールと reviewdog を使用したレビュー自動化の方法を GitLab CI, GitHub Actions それぞれ提案してみました。
静的解析の自動化を導入することでレビューの見逃しを削減することができ、品質の担保できるようになると思います。
設計、実装の参考になれば幸いです。