はじめに
- 第1回: 生成AIで開発は速くなった。では、脆弱性管理は誰がやるのか?
- 第2回: IBM Concert Protect でソフトウェア部品とリスクを可視化してみた
- 第3回: CodeQL・Trivy・SBOM変換スクリプトでセキュリティ成果物を生成してみた
- 第4回: REST API で IBM Concert への登録を自動化してみた
第4回では concert_upload.py を実装し、ローカルから REST API 経由で3成果物すべての投入に成功した。最後にこう書いた。「残るのは、それらを GitHub Actions の中で結び付けることだ。」
今回はそれを実行した記録だ。
concert-ingest ジョブを GitHub Actions ワークフローに追加することで、main ブランチへの push をトリガーに SBOM・SAST・CVE が IBM Concert Protect に自動投入される。この記事では、その設計の判断と実装の詳細を記録する。
Before / After
まず、このジョブを追加する前後の変化を整理する。
Before(第4回まで):
GitHub Actions
↓ 成果物を Artifacts に保存
手動でダウンロード
↓
concert_upload.py をローカルで実行
↓
IBM Concert Protect に投入
After(今回):
main への push
↓
GitHub Actions
├── sbom → concert-package-sbom.json ─┐
├── codeql → python.sarif ├──► concert-ingest ──► IBM Concert Protect
└── trivy-image → trivy-image.json ┘
人手を介する工程がなくなった。
ジョブ依存グラフの設計
concert-ingest は既存の3ジョブが完了した後に動く。
sbom ──────────────┐
codeql ────────────┤──► concert-ingest ──► ci-gate
trivy-image ───────┘
needs に3ジョブを列挙することで、GitHub Actions が並列完了を待ってから concert-ingest を起動する。
concert-ingest:
name: Concert Ingestion (package_sbom / static_code_scan / image_scan)
runs-on: ubuntu-latest
needs:
- sbom
- codeql
- trivy-image
この構造は意図的だ。成果物の生成ジョブが失敗した状態では concert-ingest が起動しない。Concert に古いデータや不完全なデータが投入されるリスクを GitHub Actions の依存グラフ側で排除している。
フォーク PR ではスキップする設計
concert-ingest ジョブは5つの GitHub Secrets を参照する。フォークからの Pull Request では、セキュリティ上の理由から GitHub Actions が Secrets にアクセスできない。concert-ingest ジョブをそのまま実行すると、Secrets が空文字列になり upload が失敗する。
この挙動に対処するため、ジョブの最初に Secrets の設定状態を確認し、未設定の場合は後続のすべてのステップをスキップする設計にした。
- name: Check secrets availability
id: check_secrets
run: |
if [ -z "${CONCERT_BASE_URL:-}" ] || [ -z "${CONCERT_API_KEY:-}" ]; then
echo "Concert secrets not configured. Skipping upload steps."
echo "configured=false" >> "$GITHUB_OUTPUT"
else
echo "configured=true" >> "$GITHUB_OUTPUT"
fi
後続のすべてのステップには if: steps.check_secrets.outputs.configured == 'true' を付与する。Secrets が未設定の場合、concert-ingest ジョブはスキップ状態で終了する。
ci-gate 側でも concert-ingest のスキップを「成功」として扱うように条件を設定している。
if [[ "${{ needs.concert-ingest.result }}" != "success" && "${{ needs.concert-ingest.result }}" != "skipped" ]]; then
echo "concert-ingest failed"
exit 1
fi
success または skipped のいずれかであれば ci-gate を通過する。Secrets が設定されていない環境(フォーク PR、ローカルテスト用ブランチ等)でも CI が失敗扱いにならない。
Secrets の一覧
| Secret 名 | 用途 |
|---|---|
CONCERT_BASE_URL |
Concert インスタンスの URL |
CONCERT_API_KEY |
Concert API キー |
CONCERT_INSTANCE_ID |
インスタンス ID |
CONCERT_APPLICATION_NAME |
アプリケーション名 |
CONCERT_APPLICATION_VERSION |
バージョン文字列 |
これらは Concert UI で生成・確認し、GitHub リポジトリの Settings → Secrets and variables → Actions から登録する。
投入ステップの実装
Secrets の確認が通過した場合、3ジョブの成果物をダウンロードして順番に投入する。
Artifacts の取得
- name: Download sbom artifact
if: steps.check_secrets.outputs.configured == 'true'
uses: actions/download-artifact@v4
with:
name: sbom
path: artifacts/sbom
- name: Download codeql-results artifact
if: steps.check_secrets.outputs.configured == 'true'
uses: actions/download-artifact@v4
with:
name: codeql-results
path: artifacts/codeql
- name: Download trivy-image artifact
if: steps.check_secrets.outputs.configured == 'true'
uses: actions/download-artifact@v4
with:
name: trivy-image
path: artifacts/trivy-image
Concert への投入
第4回で実装した concert_upload.py をそのまま呼び出す。新しいスクリプトは書いていない。
- name: Upload package_sbom to Concert
id: upload_sbom
if: steps.check_secrets.outputs.configured == 'true'
run: |
set -euo pipefail
echo "::group::Upload package_sbom — concert-package-sbom.json"
python scripts/concert_upload.py \
--type package_sbom \
--file artifacts/sbom/concert-package-sbom.json
echo "::endgroup::"
- name: Upload static_code_scan to Concert
id: upload_sast
if: steps.check_secrets.outputs.configured == 'true'
run: |
set -euo pipefail
echo "::group::Upload static_code_scan — python.sarif"
python scripts/concert_upload.py \
--type static_code_scan \
--file artifacts/codeql/python.sarif
echo "::endgroup::"
- name: Upload image_scan to Concert
id: upload_image
if: steps.check_secrets.outputs.configured == 'true'
run: |
set -euo pipefail
echo "::group::Upload image_scan — trivy-image.json"
python scripts/concert_upload.py \
--type image_scan \
--file artifacts/trivy-image/trivy-image.json
echo "::endgroup::"
投入順序は package_sbom → static_code_scan → image_scan の順に固定している。Application との関連付けを先に確立するため、パッケージ情報を最初に送る設計にした。
Job Summary で結果を可視化する
GitHub Actions の Summary タブに投入結果を表示するステップを追加した。
- name: Write Job Summary
if: always()
run: |
if [ "${{ steps.check_secrets.outputs.configured }}" != "true" ]; then
{
echo "## Concert Ingestion Summary"
echo ""
echo "⚠️ **Concert secrets not configured.** Ingestion steps were skipped."
echo "To enable automatic ingestion, configure \`CONCERT_BASE_URL\` and \`CONCERT_API_KEY\` in GitHub Secrets."
} >> "$GITHUB_STEP_SUMMARY"
else
{
echo "## Concert Ingestion Summary"
echo ""
echo "| Upload Type | File | Status |"
echo "|-------------|------|--------|"
echo "| \`package_sbom\` | \`concert-package-sbom.json\` | ${{ steps.upload_sbom.outcome == 'success' && '✅ Accepted' || '❌ Failed' }} |"
echo "| \`static_code_scan\` | \`python.sarif\` | ${{ steps.upload_sast.outcome == 'success' && '✅ Accepted' || '❌ Failed' }} |"
echo "| \`image_scan\` | \`trivy-image.json\` | ${{ steps.upload_image.outcome == 'success' && '✅ Accepted' || '❌ Failed' }} |"
echo ""
echo "> Event IDs are visible in the individual step logs above."
echo "> Check Concert UI: Package Inventory / SAST Exposure / CVE Dashboard"
} >> "$GITHUB_STEP_SUMMARY"
fi
if: always() を指定しているため、投入が失敗した場合でも Summary は出力される。個別の event_id は各ステップのログに出力される。
Summary タブには次のようなテーブルが表示される。
| Upload Type | File | Status |
|---|---|---|
package_sbom |
concert-package-sbom.json |
✅ Accepted |
static_code_scan |
python.sarif |
✅ Accepted |
image_scan |
trivy-image.json |
✅ Accepted |
エラー発生時の挙動
投入が失敗した場合の挙動は次のとおりだ。
| 条件 | 挙動 |
|---|---|
| HTTP 400(形式不正) |
exit 1 → ステップ失敗 → concert-ingest ジョブ失敗 |
| HTTP 403(認証エラー) |
exit 1 → ステップ失敗 → concert-ingest ジョブ失敗 |
| HTTP 409(重複) |
exit 1 → ステップ失敗 → concert-ingest ジョブ失敗 |
| HTTP 500(Concert 側) |
exit 1 → ステップ失敗 → concert-ingest ジョブ失敗 |
| Artifacts 未発見 |
download-artifact ステップ失敗 |
| Secrets 未設定 | ジョブ全体をスキップ(失敗扱いにしない) |
各ステップが独立しているため、どのステップで失敗したかはログで特定できる。
同じ構成を再現するために
1. GitHub Secrets の登録
リポジトリの Settings → Secrets and variables → Actions から次の5つを登録する。
| Secret 名 | 値の取得方法 |
|---|---|
CONCERT_BASE_URL |
Concert インスタンスの URL |
CONCERT_API_KEY |
Concert UI → API Keys → Generate |
CONCERT_INSTANCE_ID |
Concert UI → Settings → Instance |
CONCERT_APPLICATION_NAME |
任意の文字列(アプリ名) |
CONCERT_APPLICATION_VERSION |
任意のバージョン文字列(例: 1.0.0) |
2. ワークフローファイルへの追加
.github/workflows/ci.yml に concert-ingest ジョブを追加する。既存ジョブに変更は不要だ。needs に列挙したジョブ(sbom / codeql / trivy-image)が完了すれば自動的に起動する。
また、ci-gate ジョブの needs に concert-ingest を追加し、合否判定条件を success または skipped の両方を通過扱いにする。
3. concert_upload.py の配置
scripts/concert_upload.py はリポジトリに含まれていれば追加のインストールは不要だ。Python 標準ライブラリのみで動作する。
4. 動作確認
main への push 後、GitHub Actions の Actions タブ から最新の実行を開き、concert-ingest ジョブを確認する。各ステップのログに event_id が出力される。Summary タブで投入結果のテーブルを確認できる。
Concert の Event Log で event_id を検索すると、投入の処理状態を追跡できる。
検証で確認したこと
第4回でローカルから投入して HTTP 202 を確認した3成果物が、GitHub Actions のジョブからも同様に HTTP 202 で受け付けられることを確認した。
Concert の Event Log では、concert-package-sbom.json・python.json(.sarif から自動変換)・trivy-image.json が処理完了として記録される。
python.sarif から python.json への自動リネームは concert_upload.py の _resolve_upload_filename() が処理する。GitHub Actions 側では何も追加しなくてよい。
連載を通じて構築したパイプライン
この連載で構築したパイプラインの全体像を整理する。
main への push
│
▼
GitHub Actions
├── sbom ジョブ
│ cyclonedx-py でSBOM生成
│ convert_sbom_for_concert.py でConcert形式に変換
│ → concert-package-sbom.json (Artifacts に保存)
│
├── codeql ジョブ
│ CodeQL SAST 解析
│ → python.sarif (Artifacts に保存)
│
├── docker-build ジョブ
│ Docker イメージをビルド
│
├── trivy-image ジョブ (needs: docker-build)
│ Trivy でイメージスキャン
│ → trivy-image.json (Artifacts に保存)
│
└── concert-ingest ジョブ (needs: sbom, codeql, trivy-image)
Artifacts を取得
concert_upload.py で3成果物を順番に投入
package_sbom → HTTP 202
static_code_scan → HTTP 202
image_scan → HTTP 202
Job Summary に結果を出力
│
▼
IBM Concert Protect
Package Inventory(パッケージ一覧とリスク)
SAST Exposure(静的解析の指摘)
CVE Dashboard(コンテナイメージの脆弱性)
第1回で「継続的なリスク管理」という言葉を使った。コード変更のたびに最新の状態が Concert に反映される仕組みが、ここで動いている。
第1回の問いに戻る
第1回では次のように書いた。
ソフトウェアの構成要素と脆弱性を、継続的に把握し続ける仕組みが必要になっている。
この連載で構築したのは、その「仕組み」の一形態だ。
- 第2回:Concert Protect に手動で投入し、何が可視化されるかを確認した
- 第3回:成果物を GitHub Actions で自動生成できることを確認した
- 第4回:REST API 経由でプログラムから投入できることを確認した
- 第5回(今回):それを GitHub Actions に組み込み、push のたびに自動で動くようにした
各回で確認した要素が、今回のパイプラインで接続されている。
すべての環境で同じ構成が正解とは限らない。しかし、「生成する」「登録する」「可視化する」を自動でつなぐことで、脆弱性管理を人の記憶や手作業に依存しない仕組みにできることは確認できた。
おわりに
第1回を書いたとき、正直なところ答えを持っていたわけではなかった。
生成AIで開発は速くなった。
しかし、その先にある脆弱性管理をどう回し続けるべきかは、自分でも確認したかった。
連載を通じて見えてきたのは、特別な魔法ではない。
SBOM を生成し、CodeQL と Trivy でスキャンし、その結果を IBM Concert Protect に集約する。
そして、それらをコード変更のたびに自動で繰り返す。
やっていることは地味だ。
しかし、「その時だけ確認する」から「継続的に把握する」へ変えるには、この地味な仕組みが必要だった。
生成AIがコードを書く時代になった今、私自身は以前よりもこの部分の重要性を強く感じている。
同じ環境を再現する場合に必要なもの
| 要素 | 内容 |
|---|---|
| GitHub リポジトリ | GitHub Actions が使えるリポジトリ |
| IBM Concert Protect | Concert インスタンスへのアクセスと API キー |
| 5つの GitHub Secrets |
CONCERT_BASE_URL / CONCERT_API_KEY / CONCERT_INSTANCE_ID / CONCERT_APPLICATION_NAME / CONCERT_APPLICATION_VERSION
|
concert_upload.py |
scripts/ 配下に配置(外部依存なし) |
convert_sbom_for_concert.py |
scripts/ 配下に配置(SBOMの Concert 形式変換用) |
.github/workflows/ci.yml |
concert-ingest ジョブを追加したワークフロー定義 |
Concert インスタンスが自己署名証明書を使っている検証環境の場合は CONCERT_INSECURE=true の Secret も追加する。本番環境では使用しない。
連載一覧
| 回 | タイトル |
|---|---|
| 第1回 | 生成AIで開発は速くなった。では、脆弱性管理は誰がやるのか? |
| 第2回 | IBM Concert Protect でソフトウェア部品とリスクを可視化してみた |
| 第3回 | CodeQL・Trivy・SBOM変換スクリプトでセキュリティ成果物を生成してみた |
| 第4回 | REST API で IBM Concert への登録を自動化してみた |
| 第5回(本記事) | GitHub Actions から IBM Concert Protect にセキュリティ成果物を自動投入してみた |
検証環境
- IBM Concert: 3.0.x
- GitHub Actions: ubuntu-latest
- Python: 3.12(標準ライブラリのみ使用)
- Trivy: 0.70.0(GitHub Actions 側)
-
concert_upload.py: stdlib のみ(urllib・json・argparse)