はじめに
- 第1回: 生成AIで開発は速くなった。では、脆弱性管理は誰がやるのか?
- 第2回: IBM Concert Protect でソフトウェア部品とリスクを可視化してみた
- 第3回: CodeQL・Trivy・SBOM変換スクリプトでセキュリティ成果物を生成してみた
第2回では Concert Protect に手動で成果物を投入した。第3回では、その成果物が GitHub Actions でどのように生成されるかを追いかけた。
手動投入の問題は明確だ。毎回 GitHub Actions から Artifacts をダウンロードし、Concert の UI を開いて、ファイルを選択してアップロードする。main ブランチに push されるたびにこれを繰り返すのは現実的ではない。
今回は、この手動手順を REST API 経由で自動化するまでの過程を記録する。API 仕様の調査から始まり、Python の Upload Client を実装し、実際に HTTP 202 が返ってくるまでを追いかける。
まず API の仕様を調べた
自動化のための第一歩は、Concert Protect に REST API が存在するかどうかの確認だった。
結果として、IBM Docs に API ドキュメントが公開されており、GitHub にも OpenAPI 仕様書が存在することを確認した。
POST /ingestion/api/v1/upload_files
すべての成果物投入に共通するエンドポイントはこの1つだ。data_type フィールドで投入種別を切り替える設計になっている。
| 成果物 | data_type |
|---|---|
concert-package-sbom.json |
package_sbom |
python.sarif |
static_code_scan |
trivy-image.json |
image_scan |
第2回で「投入種別」ごとに画面が違うと感じていた理由はここにあった。Concert は入力経路を data_type で分けて管理している。
認証方式
認証は API Key 方式だ。Concert UI でキーを生成し、HTTP ヘッダーに付与する。
Authorization: C_API_KEY <API_key>
InstanceId: <instance_id>
Content-Type: multipart/form-data
API Key は生成時にしか表示されない。紛失した場合は再生成が必要になる。
metadata の設計
リクエストは multipart/form-data で構成される。data_type と filename(ファイル本体)に加え、metadata を JSON 文字列として付与する。
curl -X POST \
"https://<concert_host>/ingestion/api/v1/upload_files" \
-H "Authorization: C_API_KEY ${CONCERT_API_KEY}" \
-H "InstanceId: ${CONCERT_INSTANCE_ID}" \
-H "Content-Type: multipart/form-data" \
-F "data_type=package_sbom" \
-F 'metadata={"repo_url": "https://github.com/...", "repo_name": "..."}' \
-F "filename=@concert-package-sbom.json"
IBM Docs の File Upload Matrix では package_sbom と static_code_scan で repo_url と repo_name が必須と記載されている。今回の検証でもこれらを付与した上で投入している。image_scan は metadata がすべてオプションになっている。Application との関連付けには application_name / application_version を付与する。
この時点では実機での動作は未確認だった。「仕様として存在する」ことを確認しただけだ。
Upload Client を実装した
API 仕様が確認できたため、scripts/concert_upload.py として Upload Client を実装した。
設計の方針は2点だ。
-
外部ライブラリを使わない — Python 標準ライブラリ(
urllib・json・argparse)のみで実装する。GitHub Actions 上で pip install なしで動作する - dry-run モードを先に作る — API 接続情報がなくても送信内容を確認できるようにする
UploadType Enum
投入種別は Enum で管理する。文字列のタイプミスを防ぐためだ。
class UploadType(enum.Enum):
PackageSBOM = "package_sbom"
StaticCodeScan = "static_code_scan"
ImageScan = "image_scan"
metadata 生成
投入種別に応じて必要な metadata を組み立てる。
def build_metadata(upload_type: UploadType, config: ConcertConfig) -> dict:
base = {
"application_name": config.application_name,
"application_version": config.application_version,
}
if upload_type is UploadType.PackageSBOM:
return {"repo_url": config.repo_url, "repo_name": config.repo_name, **base}
if upload_type is UploadType.StaticCodeScan:
return {"repo_url": config.repo_url, "repo_name": config.repo_name,
"scanner_name": "CodeQL", **base}
return {"scanner_name": "Trivy", **base}
multipart/form-data を stdlib だけで構築
requests ライブラリを使えば簡単だが、外部依存を避けるために stdlib の urllib で multipart ボディを組み立てた。
def _build_multipart(fields, file_field, file_bytes, upload_name, boundary):
crlf = b"\r\n"
parts = []
for name, value in fields.items():
parts.append(
f"--{boundary}".encode() + crlf
+ f'Content-Disposition: form-data; name="{name}"'.encode() + crlf
+ crlf + value.encode() + crlf
)
# ファイルパート
parts.append(
f"--{boundary}".encode() + crlf
+ f'Content-Disposition: form-data; name="{file_field}"; filename="{upload_name}"'.encode() + crlf
+ b"Content-Type: application/octet-stream" + crlf
+ crlf + file_bytes + crlf
)
parts.append(f"--{boundary}--".encode() + crlf)
return b"".join(parts)
CLI インターフェース
python scripts/concert_upload.py --type package_sbom --file concert-package-sbom.json
python scripts/concert_upload.py --type static_code_scan --file python.sarif
python scripts/concert_upload.py --type image_scan --file trivy-image.json
python scripts/concert_upload.py --type package_sbom --file concert-package-sbom.json --dry-run
実機疎通で出てきた問題
実装が完了した後、実際の Concert インスタンスで疎通確認を行った。
結果は3成果物すべてで HTTP 202 が返ってきた。しかし、その前にいくつかの問題に当たった。
問題1:.sarif 拡張子を Concert が拒否する
python.sarif をそのままアップロードしたところ、HTTP 400 が返ってきた。
❌ Upload failed (HTTP 400) | type=static_code_scan | file=python.sarif
SARIF は JSON ベースの形式であり、ファイルの中身自体は問題なかった。しかし今回の検証環境では、.sarif のままでは HTTP 400 となり、.json に変更すると受け付けられた。
対処として、アップロード時に拡張子を自動変換する処理を追加した。ファイル本体は変更しない。送信時のファイル名だけを .json に変換する。
def _resolve_upload_filename(file_path: Path) -> tuple[bytes, str]:
file_bytes = file_path.read_bytes()
upload_name = file_path.name
if upload_name.endswith(".sarif"):
upload_name = upload_name[:-len(".sarif")] + ".json"
logger.info("Renaming %s → %s for Concert upload", file_path.name, upload_name)
return file_bytes, upload_name
問題2:検証環境が自己署名証明書を使っていた
検証環境の Concert インスタンスは自己署名証明書を使っていたため、TLS 検証で失敗した。
--insecure フラグを追加して対処した。本番環境では使用しない。
if insecure:
ssl_ctx = ssl.create_default_context()
ssl_ctx.check_hostname = False
ssl_ctx.verify_mode = ssl.CERT_NONE
実機疎通確認結果
3成果物すべてで HTTP 202 が返ってくることを確認した。
| 成果物 | data_type | HTTP | 結果 |
|---|---|---|---|
concert-package-sbom.json |
package_sbom |
202 | ✅ |
python.sarif |
static_code_scan |
202 | ✅ |
trivy-image.json |
image_scan |
202 | ✅ |
各投入のレスポンスには event_ids が含まれており、Concert の Event Log で追跡できる。
✅ Upload accepted (HTTP 202) | type=package_sbom | file=concert-package-sbom.json
Response: {"record_paths":["<instance_id>/roja/package_sbom/2026/9/20/<id>_concert-package-sbom.json"],
"event_ids":["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"],
"job_data":{"message":"202 Accepted"}}
Concert 側の Event Log を確認すると、python.json・trivy-image.json・concert-package-sbom.json が daihiraoka 1.0.0 に紐付いて「処理完了」として記録されていた。

図:Concert Protect の Event Log 画面。パッケージ SBOM・静的コード・スキャン・イメージ・スキャンの3種類が「処理完了」として記録されている。python.json は .sarif から自動変換された結果だ。
なお、ICSC_SAST_daihiraoka_trivy_packages_... というファイル名の「コード・スキャン」が「エラー処理」として記録されている。ファイル名のプレフィックスから、IBM Concert Secure Coder が Trivy Package Scan 結果を code_scan として投入した経路である可能性が高い。第2回で触れた失敗経路がここでも確認できる。
HTTP 202 は「受け付けた」であって「処理が完了した」ではない。そのため、疎通確認では HTTP 202 の取得だけでなく、Event Log 上で「処理完了」まで確認することにした。Concert 側での非同期処理が完了するまで、ダッシュボードへの反映には少し時間がかかる。
dry-run で動作確認する
API 接続情報がない環境でも、--dry-run で送信内容を確認できる。
python scripts/concert_upload.py \
--type package_sbom \
--file concert-package-sbom.json \
--dry-run
出力:
[INFO] === DRY RUN — no API call will be made ===
[INFO] Endpoint : POST https://concert.example.com/ingestion/api/v1/upload_files
[INFO] InstanceId: 0000-0000-0000-0000
[INFO] Auth : C_API_KEY <REDACTED>
[INFO] data_type : package_sbom
[INFO] metadata : {"repo_url": "...", "repo_name": "...", "application_name": "..."}
[INFO] file : concert-package-sbom.json (12345 bytes)
[INFO] === END DRY RUN ===
同じ構成を再現するために
環境変数の設定: 最低限 CONCERT_BASE_URL と CONCERT_API_KEY が必要だ。GitHub Actions では Secrets に格納する。
CONCERT_BASE_URL=https://your-concert-host.example.com
CONCERT_API_KEY=<Concert UIで生成したAPIキー>
CONCERT_INSTANCE_ID=0000-0000-0000-0000
CONCERT_APPLICATION_NAME=your-application-name
CONCERT_APPLICATION_VERSION=1.0.0
.sarif 拡張子に注意: CodeQL の出力ファイルは .sarif だが、Concert は .json 拡張子しか受け付けない。アップロード時に拡張子を変換する処理が必要になる。
HTTP 202 は非同期受付: 202 が返っても、Concert のダッシュボードへの反映は即時ではない。Event Log で event_id を確認し、処理完了を待つ。
自己署名証明書を使っている検証環境では --insecure が必要な場合がある: TLS 検証をスキップする必要がある。本番環境では使用しない。
検証結果のまとめ
手動投入から REST API 投入に移行するために確認が必要だったことは3点だった。
- API が存在すること — IBM Docs と OpenAPI 仕様で確認した
-
3成果物すべてに対応する
data_typeがあること — File Upload Matrix で確認した - 実機で HTTP 202 が返ること — 疎通確認で確認した
API 仕様の調査段階では「実現可能性が高い」という評価にとどめていた。実機疎通で初めて「できる」と確認できた。
第2回では手動でファイルを投入していた。第3回では成果物を自動生成した。そして今回、API を使って投入そのものを自動化できることが確認できた。
残るのは、それらを GitHub Actions の中で結び付けることだ。
次回予告
今回は Upload Client をローカルで手動実行して API 疎通を確認した。
では、これを GitHub Actions に組み込んで main push のたびに自動実行するにはどうすればよいか。
第5回では concert-ingest ジョブを GitHub Actions ワークフローに追加し、コード変更からリスク可視化までを完全自動化した過程を記録する。
連載一覧
| 回 | タイトル |
|---|---|
| 第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
- Python: 3.12(標準ライブラリのみ使用)
-
concert_upload.py: stdlib のみ(urllib・json・argparse) - Trivy: 0.70.0(GitHub Actions 側)