はじめに
第2回では、Package Risk・SAST Exposure・CVE Dashboard が同じ Application に集約されることを確認した。
では、その情報はどこから来ているのだろうか。
Concert Protect が魔法のように生成しているわけではない。SBOM・SARIF・脆弱性スキャン結果という3種類の成果物があり、それぞれ別のジョブで生成されている。
今回は、その生成過程を追いかけてみる。
あわせて、第2回で積み残した問いにも戻る。「なぜ標準出力の SBOM はそのまま使えなかったのか」——その答えはコードの中にあった。
CI パイプラインの全体構造
.github/workflows/ci.yml は main ブランチへの push と PR 作成をトリガーに動作する。
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
ジョブの依存関係は次の通りだ。

図:ci.yml のジョブ実行グラフ。第2回で取り込んだ Package SBOM・SARIF・Image Scan は、この3系統のジョブから生成されている。Concert Ingestion に収束し、CI Gate で全ジョブの成否を判定する。
セキュリティ成果物を生成する3ジョブ(sbom・codeql・trivy-image)が完了した後に concert-ingest が起動し、Concert Protect へ自動投入する。左下の Lint・テスト・Trivy Filesystem Scan は Concert 投入には関与しない。
SBOM 生成ジョブ
標準 SBOM の生成
sbom ジョブは cyclonedx-bom を使って Python 実行環境の依存関係一覧を生成する。
- name: Generate Standard SBOM
run: cyclonedx-py environment --of json -o sbom.json
出力された sbom.json は CycloneDX 1.6 形式だ。しかしこのファイルは第2回で確認したように、Concert Protect にそのまま投入するとフォーマット不一致エラーになる。
Concert 向け変換
sbom.json を Concert 向けに変換するのが次のステップだ。
- name: Generate Concert Package SBOM
run: python scripts/convert_sbom_for_concert.py -i sbom.json -o concert-package-sbom.json
scripts/convert_sbom_for_concert.py が行う変換は3点に絞られる。
-
metadata.component.typeをapplicationに設定する -
metadata.componentに VCS 参照(リポジトリ URL)を追加する - 依存コンポーネントの
typeをframeworkに変換する
この変換は、Python パッケージの一覧だった SBOM に「Application」というルート要素を与える作業だ。Concert Protect はアプリケーション単位でリスクを管理するため、「誰の依存パッケージなのか」を SBOM 内に表現する必要がある。
両ファイルは同じ Artifact としてアップロードされ、90日間保存される。
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: |
sbom.json
concert-package-sbom.json
retention-days: 90
CodeQL SAST ジョブ
codeql ジョブは GitHub が提供する CodeQL Action を使い、Python コードを静的解析する。
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: python
queries: security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
output: codeql-results
upload: false
queries: security-and-quality は CodeQL のセキュリティルールと品質ルールを両方適用するオプションだ。upload: false にしているのは、SARIF を GitHub の Security タブへ直接アップロードせず、Concert Protect に送るためだ。
出力された codeql-results/python.sarif は Artifact として保存される。
第2回で確認したように、この SARIF ファイルは変換不要で Concert Protect に取り込める。取り込み後は py/missing-call-to-init や py/import-and-import-from などのルールごとに Exposure が表示された。
Trivy スキャンジョブ
Trivy のジョブは2つある。両方とも aquasecurity/trivy-action@v0.36.0(Trivy 0.70.0)を使うが、スキャン種別が異なり、Concert への投入可否も分かれた。
trivy-fs:ファイルシステムスキャン
- name: Run Trivy filesystem scan
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: fs
scan-ref: .
format: json
output: trivy-fs.json
リポジトリ内のソースコードをスキャンする。生成される trivy-fs.json の ArtifactType は repository になり、Results フィールドが欠落している場合がある。
第2回で確認したように、このファイルは Concert Protect に投入できなかった。concert-ingest ジョブのアップロード対象から除外している。Artifact としては 90日間保存されるが、Concert への投入は行わない。
trivy-image:コンテナイメージスキャン
- name: Run Trivy image scan
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: image
image-ref: inference-sustainability-service:ci
format: json
output: trivy-image.json
docker-build ジョブでビルドしたコンテナイメージをスキャンする。生成される trivy-image.json の ArtifactType は container_image となり、OS パッケージ・Python パッケージそれぞれの脆弱性情報が Results に含まれる。
このファイルは Concert Protect の image_scan として正常に取り込まれ、CVE Dashboard が生成された。
同じツール、異なる結果
2つの Trivy ジョブは Action のバージョンも Trivy のバージョンも同じだ。違いはスキャン対象だけだ。
| ジョブ | scan-type | ArtifactType | Concert 投入 |
|---|---|---|---|
trivy-fs |
fs |
repository |
❌ 除外 |
trivy-image |
image |
container_image |
✅ image_scan
|
今回の検証では ArtifactType: container_image の出力は取り込みに成功した一方、ArtifactType: repository の出力は取り込みに失敗した。IBM Docs では Trivy のスキャン形式がサポート対象として記載されているが、今回利用した Filesystem Scan の JSON 出力が対象となるのかまでは確認できていない。
concert-ingest ジョブ:成果物を Concert へ渡す
3ジョブが完了した後、concert-ingest ジョブが各 Artifact をダウンロードして Concert Protect へ投入する。
concert-ingest:
needs:
- sbom
- codeql
- trivy-image
最初に Secrets の設定を確認し、未設定の場合(フォーク PR など)はジョブ全体をスキップする。
- name: Check secrets availability
run: |
if [ -z "${CONCERT_BASE_URL:-}" ] || [ -z "${CONCERT_API_KEY:-}" ]; then
echo "configured=false" >> "$GITHUB_OUTPUT"
else
echo "configured=true" >> "$GITHUB_OUTPUT"
fi
Secrets が設定されている場合は、3成果物を順番に投入する。
- name: Upload package_sbom to Concert
run: |
python scripts/concert_upload.py \
--type package_sbom \
--file artifacts/sbom/concert-package-sbom.json
- name: Upload static_code_scan to Concert
run: |
python scripts/concert_upload.py \
--type static_code_scan \
--file artifacts/codeql/python.sarif
- name: Upload image_scan to Concert
run: |
python scripts/concert_upload.py \
--type image_scan \
--file artifacts/trivy-image/trivy-image.json
投入順序は package_sbom → static_code_scan → image_scan だ。Concert Protect のデータモデルは Application 登録を前提とするため、SBOM を先に送る設計にしている。
Trivy FS スキャンを除外した理由
trivy-fs ジョブはパイプラインに残っており、成果物も生成される。しかし concert-ingest のアップロード対象には含まれていない。
第2回の手動投入検証で trivy-fs.json の Concert 投入が失敗し、構造差異を確認した。
-
ArtifactTypeがrepository(trivy-image.jsonはcontainer_image) - 今回の検証環境では
Resultsフィールドが欠落していた
これを受けて、concert-ingest ジョブのアップロード対象から trivy-fs.json を除外した。IBM Docs では Trivy のスキャン形式がサポート対象として記載されているが、Filesystem Scan の出力が対象となるのかまでは確認できていない。現時点ではファイルとして保存するだけにとどめている。
検証結果のまとめ
| ジョブ | 生成物 | Concert 投入 | 投入経路 |
|---|---|---|---|
sbom |
concert-package-sbom.json |
✅ | package_sbom |
codeql |
python.sarif |
✅ | static_code_scan |
trivy-image |
trivy-image.json |
✅ | image_scan |
trivy-fs |
trivy-fs.json |
❌ | 除外 |
3ジョブの成果物はそれぞれ異なる経路で Concert Protect に取り込まれ、Package Inventory・SAST Exposure・CVE Dashboard を構成する。trivy-fs.json はファイルとして保存されるが、Concert への投入は行わない。
第2回では Concert Protect の画面から結果を見ていた。しかし今回パイプラインを追いかけてみると、Package Risk・SAST Exposure・CVE Dashboard は Concert が独自に生成しているわけではなく、SBOM・SARIF・Trivy の成果物をもとに構成されていることが分かった。
リスク管理の出発点は Concert Protect ではない。品質チェックやセキュリティスキャンを CI の中で継続的に実行し、その結果を蓄積することだった。
同じ構成を再現するために
同じパイプラインを構築する場合に確認が必要な点を整理する。
SBOM は検証が必要:
今回の検証環境では、cyclonedx-py の標準出力は取り込めず、変換後の SBOM のみ取り込みに成功した。どの差異が原因かまでは特定できていないため、変換スクリプトを利用している。
CodeQL は upload: false にする: デフォルトでは SARIF が GitHub Security タブへ自動アップロードされる。Concert へ送りたい場合は upload: false で出力先を Artifact に切り替える。
Trivy はスキャン種別で出力構造が変わる: scan-type: image と scan-type: fs は同じ Action でも出力 JSON の構造が異なる。今回の検証環境では、Concert に投入できたのは ArtifactType: container_image の出力だった。
フォーク PR では Concert 投入をスキップ: Secrets にアクセスできないフォーク PR では concert-ingest ジョブ全体をスキップする設計が必要だ。ci-gate はスキップを成功として扱う。
次回予告
今回は成果物の生成ロジックを整理した。
では、その Concert Protect への投入はどのように自動化されているのか。
第4回では Concert Protect の REST API 仕様を調べ、concert_upload.py がどのように実装されているかを追いかける。API の構造、HTTP 202 の意味、.sarif 拡張子問題など、実際に疎通確認した過程を記録する。
連載一覧
| 回 | タイトル |
|---|---|
| 第1回 | 生成AIで開発は速くなった。では、脆弱性管理は誰がやるのか? |
| 第2回 | IBM Concert Protect でソフトウェア部品とリスクを可視化してみた |
| 第3回(本記事) | CodeQL・Trivy・SBOM変換スクリプトでセキュリティ成果物を生成してみた |
| 第4回 | REST API で IBM Concert への登録を自動化してみた |
| 第5回 | コード変更からリスク可視化までを自動化してみた |
検証環境
- GitHub Actions: ubuntu-latest
- Python: 3.12
-
cyclonedx-bom(cyclonedx-py7.4.0 /cyclonedx-python-lib11.12.0) - CodeQL:
github/codeql-action/analyze@v3(queries: security-and-quality) - Trivy: 0.70.0(
aquasecurity/trivy-action@v0.36.0) - IBM Concert: 3.0.x