Security Command Center の IaC スキャン結果を GitHub Actions Summary に集計するステップを追加してみた
google-github-actions/analyze-code-security-scc@v1 の出力 iac_scan_result は passed / failed / error の3値しか持ちません。
違反の詳細は SARIF(artifact)に入りますが、毎回 artifact をダウンロードして件数や重大度を確認するのは手間がかかります。
そこで SARIF を jq で集計して $GITHUB_STEP_SUMMARY に書くステップを1つ足してみました。run の Summary ページに重大度別の件数が並ぶようになります。
前回、このアクションを GitHub Actions に組み込んで PR で違反を止めるところまで書きました。
前提
前回構築した検証用ワークフロー(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/erroriac_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[].ruleId と rules[].id が同じ文字列なので、両者を ID で突き合わせて、severity を取得します。
集計ステップを足す
Analyze Code Security ステップの直後に、このステップを追加します。
- 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 |
出力イメージ

違反あり(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 に組み込んでいる方は、このステップを足してみてください。




