Security Command Center の IaC スキャン結果を GitHub Actions Summary に集計するステップを追加してみた

Security Command Center の IaC スキャン結果を GitHub Actions Summary に集計するステップを追加してみた

2026.06.14

google-github-actions/analyze-code-security-scc@v1 の出力 iac_scan_resultpassed / failed / error の3値しか持ちません。

違反の詳細は SARIF(artifact)に入りますが、毎回 artifact をダウンロードして件数や重大度を確認するのは手間がかかります。

そこで SARIF を jq で集計して $GITHUB_STEP_SUMMARY に書くステップを1つ足してみました。run の Summary ページに重大度別の件数が並ぶようになります。

前回、このアクションを GitHub Actions に組み込んで PR で違反を止めるところまで書きました。

https://dev.classmethod.jp/articles/gcp-scc-iac-validation-github-actions/

前提

前回構築した検証用ワークフロー(analyze-code-security-scc@v1 で Terraform plan を検証する構成)に、集計ステップを1つ追加します。WIF やサービスアカウントの設定は変わりません。

GitHub ホストの ubuntu-latest には jq が標準で入っているので、追加インストールは不要です。

アクション google-github-actions/analyze-code-security-scc@v1 の出力

README の Outputs はこの2つ。

  • iac_scan_result: passed / failed / error
  • iac_scan_result_sarif_path: SARIF のファイルパス

iac_scan_result は状態が1つ返るだけで、件数も重大度の内訳も持ちません。件数を出したいなら SARIF を自分で読む必要があります。

README には「iac_scan_result_sarif_path は違反検出時のみ生成される」とありますが、実際には違反0件でも SARIF は生成され、results が空配列になりました。集計ステップは0件でも問題なく動きます。

SARIF のどこに重大度があるか

results の各要素に重大度のフィールドはありません。SARIF 標準の level も設定されておらず、jq.level を引いても null が返るだけです。

重大度は runs[0].tool.driver.rules[].properties.severity に入っています。

// results 側(違反ごとに1件。ruleId はあるが重大度のフィールドが無い)
{
  "ruleId": "projects/<PROJECT_NUMBER>/locations/global/...",
  "message": { "text": "..." },
  "locations": [ /* 違反したリソース */ ]
}

// rules 側(severity はこちら)
{
  "id": "projects/<PROJECT_NUMBER>/locations/global/...",
  "properties": {
    "severity": "HIGH",
    "policyType": "SECURITY_HEALTH_ANALYTICS_CUSTOM_MODULE"
  }
}

results[].ruleIdrules[].id が同じ文字列なので、両者を ID で突き合わせて、severity を取得します。

集計ステップを足す

Analyze Code Security ステップの直後に、このステップを追加します。

.github/workflows/iac-scan.yml
      - name: 'Summarize scan result'
        if: always()
        env:
          SCAN_RESULT: '${{ steps.analyze-code-security-scc.outputs.iac_scan_result }}'
          SARIF_PATH: '${{ steps.analyze-code-security-scc.outputs.iac_scan_result_sarif_path }}'
        run: |
          {
            echo "## IaC scan result: ${SCAN_RESULT:-unknown}"
            if [ -n "$SARIF_PATH" ] && [ -f "$SARIF_PATH" ]; then
              echo ""
              echo "Total violations: $(jq '.runs[0].results | length' "$SARIF_PATH")"
              echo ""
              echo "| Severity | Count |"
              echo "|---|---|"
              jq -r '
                (.runs[0].tool.driver.rules // []) as $rules
                | ($rules | map({(.id): (.properties.severity // "UNKNOWN")}) | add // {}) as $sev
                | {"CRITICAL":0,"HIGH":1,"MEDIUM":2,"LOW":3,"UNKNOWN":4} as $rank
                | [ (.runs[0].results // [])[] | $sev[.ruleId] // "UNKNOWN" ]
                | group_by(.)
                | sort_by($rank[.[0]] // 99)
                | map("| \(.[0]) | \(length) |")[]
              ' "$SARIF_PATH"
            fi
          } | tee -a "$GITHUB_STEP_SUMMARY"

if: always() を入れているのは、違反があると前段の Analyze ステップが failure_criteria に引っかかって失敗するからです。

これがないと集計ステップがスキップされてしまい、失敗時に見たい件数が出ません。

env: でアクションの出力を環境変数に渡しているのは、run:${{ }} を直書きするとクォート崩れやインジェクションが起きやすいためです。

{ ...; } | tee -a "$GITHUB_STEP_SUMMARY" は、Summary ファイルへの追記とジョブのログへの出力を同時に行います。

[ -n "$SARIF_PATH" ] && [ -f "$SARIF_PATH" ] は、パスが空のときやファイルがないときに jq を叩かないための安全ガードです。

jq の流れ

違反あり(5件)の SARIF を例に、中間出力を追います。

rules{id: severity} のオブジェクト配列に変換します。(.id) のように括弧で囲むと、キーが固定文字列ではなく式として評価され、その値がキー名になります。

map({(.id): (.properties.severity // "UNKNOWN")})
→ [{"<id1>":"HIGH"}, {"<id2>":"HIGH"}, {"<id3>":"HIGH"}, {"<id4>":"MEDIUM"}, {"<id5>":"LOW"}]

add でオブジェクト配列を1つのオブジェクト($sev)にまとめます。これが ruleId → severity の早見表になります。

| add // {}
→ {"<id1>":"HIGH", "<id2>":"HIGH", "<id3>":"HIGH", "<id4>":"MEDIUM", "<id5>":"LOW"}

results を走査して、各 ruleId の severity を引きます。$sev[.ruleId] は早見表を ruleId で添字アクセスする書き方で、// "UNKNOWN" は早見表にない ruleId が来たときの保険です。違反件数分の severity が並びます。

| [ (.runs[0].results // [])[] | $sev[.ruleId] // "UNKNOWN" ]
→ ["HIGH", "HIGH", "HIGH", "MEDIUM", "LOW"]

group_by で重大度ごとにグループを作ります。group_by はキーの昇順(アルファベット順)でグループを並べるので、このままだと HIGH → LOW → MEDIUM になってしまいます。

| group_by(.)
→ [["HIGH","HIGH","HIGH"], ["LOW"], ["MEDIUM"]]

重大度順(CRITICAL → HIGH → MEDIUM → LOW)に並べたいので、重大度に重みを振った早見表 $rank を使い、sort_by で並べ替えます。$rank[.[0]] はグループの先頭要素(そのグループの重大度)から重みを引く書き方です。// 99 は、早見表にない重大度が来たときに末尾へ送るための保険です。

| sort_by($rank[.[0]] // 99)
→ [["HIGH","HIGH","HIGH"], ["MEDIUM"], ["LOW"]]

最後に各グループを Markdown テーブルの行にします。

| map("| \(.[0]) | \(length) |")[]
→ | HIGH | 3 | / | MEDIUM | 1 | / | LOW | 1 |

出力イメージ

Introduce_policy_violations__mt…lidation-github-actions_923b2fa.png

違反あり(5件)の run で確認した、Summary への出力です。

IaC scan result: failed

Total violations: 5

| Severity | Count |
|---|---|
| HIGH | 3 |
| MEDIUM | 1 |
| LOW | 1 |

$GITHUB_STEP_SUMMARY に書いた Markdown は、run の Summary ページに「<ジョブ名> summary」セクションとして GFM レンダリングされます。今回は security-scan summary として表示されました。

違反0件の run では Total violations: 0 になり、テーブルは空になります。results が空配列でも // [] // {} // "UNKNOWN" のフォールバックが効くので、jq はエラーになりません。

おわりに

SARIF を jq で集計して Summary に出すステップを試してみました。iac_scan_result は合否しか返さないので、件数や重大度を実行画面で確認したいときに便利でした。

重大度が results ではなく rules 側にある点を押さえておくと良さそうです。

SCC の IaC 検証を CI に組み込んでいる方は、このステップを足してみてください。

この記事をシェアする

関連記事