Nov 12, 2026
はじめに
OWASP Dependency-Track v5.2を使って、脆弱性情報の自動収集から、Slackへの通知、Pleasanterの脆弱性対応台帳への自動登録までを、二日がかりでつなげました。この記事では、その設定手順と、実際につまずいたところをまとめます。
狙いは、EUサイバーレジリエンス法(CRA)が求める脆弱性対応を、手作業に頼らず回せる状態にすることです。CRAでは、積極的に悪用されている脆弱性を知ってから24時間以内に早期警告を出す必要があります。そのため「気づく」と「期限を管理する」を自動化しました。
本記事は、Pleasanterを使った「CRA報告対応シリーズ」の第2回です。テーマは、KEVとEPSSを手がかりに「積極的な悪用」の検知を自動化することです。
この記事でできることは次の4つです。
- 6つの脆弱性情報源(GitHub Advisories・OSV・JVN iPedia・NVD・KEV・EPSS)を毎日自動で取り込む
- 新しい脆弱性が見つかったら、Critical・High・KEV掲載のものをSlackに即時通知する
- 毎朝9時に、新規検出のまとめをSlackに送る
- Webhookと小さな中継サーバーで、Pleasanterの期限付きテーブルに対応レコードを自動で作る(対応期限とCRA報告期限つき)

[embedded content: 全体構成 · 情報源からSlackとPleasanterまで]
Dependency-Trackが情報源を毎日取り込み、新しい検出をSlackと中継サーバーに送ります。中継サーバーがPleasanterのAPIで台帳に登録します。
環境
3台のマシンをLAN内で使いました。IPアドレスは例です。
| 役割 | ホスト | ソフトウェア |
|---|---|---|
| 脆弱性管理 | 192.168.0.10 | Dependency-Track v5.2.0(Docker Compose、サービス名 apiserver) |
| 中継サーバー | 192.168.0.20 | Python 3(標準ライブラリのみ)、systemd |
| 台帳 | 192.168.0.30:50001 | Pleasanter 1.5.9.0 |
| 通知先 | (クラウド) | SlackのIncoming Webhook |
管理対象のプロジェクトは、Pleasanter 1.5.9.1のSBOM(CycloneDX 1.7、356コンポーネント)です。
Dependency-Track v5はコードネームHyadesとして作り直された版で、v4からはインプレースでアップグレードできません。管理画面の構成もv4と違い、認証情報は「シークレット」に登録してから選ぶ方式になっています。
脆弱性情報源の設定
管理画面の 管理 → 脆弱性情報源 で、ソースごとに有効化します。最終的に次の状態になりました。
| ソース | 取込件数 | 照合キー | 備考 |
|---|---|---|---|
| GitHub Advisories | 37,742 | PURL | 初回ミラーリングは約23分。PATが必要 |
| OSV | 約27.7万 | PURL | エコシステムを選ばないと動かない |
| JVN iPedia | 254,000 | CPE | 1998年以降を約7分で取り込み |
| NVD | 約42.5万 | CPE | v5.2はJSON 2.0フィード方式。APIキー欄なし |
| KEV(CISA・ENISA) | 1,779/39 | CVE | 悪用が確認された脆弱性 |
| EPSS | — | CVE | 悪用される確率の予測 |
GitHub Advisories:PATは「シークレット」に登録する
v5では、トークンを設定画面に直接入力しません。
- GitHubでPATを発行する(スコープは不要。fine-grainedとclassicのどちらでも動く)
-
管理 → シークレット で、
github-patという名前で登録する -
脆弱性情報源 → GitHub Advisories でEnabledをONにし、API Token のプルダウンで
github-patを選ぶ - Save を押してから Mirror now を押す
注意点が1つあります。同じ画面の GitHub App Private Key にもシークレットを選べますが、API TokenとGitHub Appの認証情報はどちらか一方しか使えません。PATを使うなら空欄のままにします。
進捗はログで確認できます。初回は updated since null と出て全件を取得します。
docker compose logs -f apiserver 2>&1 | grep -iE "github"
# Starting mirror [vulnDataSourceName=github]
# Downloading and processing GitHub advisories updated since null ...
# Completed mirror; processed 37742 vulnerabilities in PT23M22.6S
OSVとJVN iPedia
OSVは、対象のエコシステムを1つ以上選ばないとミラーリングされません。今回はPyPI・NuGet・Maven・Go・npmを選んでいます。コンテナイメージのOSパッケージ(Debian・Ubuntu・Alpine)も判定したい場合は、それらも追加してください。NVDはバージョン範囲をOSリリース別に持たないためです。
JVN iPediaは、v5で追加された国内向けのソースです。EnabledをONにして、URL(https://jvndb.jvn.jp/ja/feed)とBackfill start year(1998)は既定のままで大丈夫です。
KEVとEPSS:EUのKEVも標準で見られる
KEV対応は、Dependency-Track 5.1.0の目玉機能です。管理画面の KEV Sources で、次のカタログを取り込めます。
- CISA KEV(米国):標準でミラーされる
- ENISA EU KEV(EU):標準でミラーされる
- VulnCheck KEV:無料のAPIトークンを設定すると取り込める
EUのKEVが標準で有効なので、「米国のKEVだけでなく、EUのKEVも見ている」と説明できます。CRA対応の根拠として使いやすい点です。
KEVの列とフィルターは、ポートフォリオの脆弱性一覧、プロジェクトのFindings、EPSSビュー、監査ビューに表示されます。5.1.0では、KEVミラーリングの状況確認と実行を行うREST APIも追加されています。
EPSSもネイティブに対応していて、REST APIでは epssScore として取得できます。筆者の環境では、EPSSが毎日10時、KEVが毎日11時(JST)に更新されていました。
照合キーの違いに注意
NVDとJVNはCPE、GitHub AdvisoriesとOSVはPURLでコンポーネントを照合します。npmやNuGetなど、PURLしか持たないSBOMでは、NVDからは検出されません。今回の10件も、すべてGitHub Advisories由来でした。
ハマりどころ:取り込み編
NVDのフィードが途中で切れる
NVDは毎日のように失敗とリトライを繰り返していました。例外のメッセージだけを拾うと、原因が分かります。
docker compose logs apiserver --since 48h 2>&1 \
| grep -iE "exception|caused by|status code" | grep -v "^\s*at "
# Failed to download feed file from .../nvdcve-2.0-2026.json.gz
# Caused by: java.io.IOException: Received RST_STREAM: Internal error
# GET .../nvdcve-2.0-2003.meta responded with status code 503
設定の問題ではなく、NVD側がダウンロードの途中で接続を切っていました。特に件数の多い2025年・2026年のフィードで起きやすいようです。ただし差分取得なので、失敗した日の分は次に成功したときにまとめて反映されます。筆者の環境では、3回目のリトライで全年分を取り切れた日もありました。
どうしても安定しない場合は、社内のWebサーバーにNVDフィードを中継して、CVE Feeds URL をそちらに向ける方法があります。
JVN由来の検出は誤検知を疑う
JVNの取り込みでは、次の警告が大量に出ます。
WARN [BovModelConverter] BOV for JVNDB-2026-042249 reports CPE
'cpe:2.3:o:google:android:*:...' (version: '*') alongside a diverging
exact version '15.0'; using the CPE's version.
JVNのデータでは、バージョンを *(全バージョン)としたCPEと、具体的なバージョンが同時に書かれていることがあります。食い違う場合、Dependency-Trackは * を採用します。そのため影響範囲が広く判定されます。
実際に、axios@1.20.0 や lodash@4.18.1 に2020年・2021年のJVNDBがヒットしました。JVNだけが出している検出は、GitHub AdvisoriesやNVDで影響バージョンを確かめてから判断するのが安全です。
無視してよい警告
-
Unrecognized ecosystem ACTIONS:GitHub Actionsの脆弱性に対応するPURL型がないため、generic型として扱っているだけです。 -
Invalid Composer version formatやMalformedPackageURLException:取込元のデータに含まれる不正な形式を、1件ずつスキップしたものです。
Slack通知:即時と毎朝のまとめ
管理 → 通知 → アラート で、アラートを2つ作ります。パブリッシャーはどちらもSlackで、Destination URLにSlackのIncoming WebhookのURLを入れます。
| アラート | トリガー | グループ | 内容 |
|---|---|---|---|
| CRA-new-vuln | イベント | NEW_VULNERABILITY、NEW_VULNERABLE_DEPENDENCY | Critical・High・KEV掲載を即時に通知 |
| 日次-新規脆弱性まとめ | スケジュール(0 0 * * *) |
NEW_VULNERABILITIES_SUMMARY | 毎朝9時(JST)に新規検出をまとめて送る |
アラートは作成して一度保存してから開き直すと、グループや送り先を設定できます。
使う通知グループの意味は次のとおりです。
-
NEW_VULNERABILITY:登録済みのコンポーネントに、新しい脆弱性が見つかったとき -
NEW_VULNERABLE_DEPENDENCY:SBOMのアップロード時に、既知の脆弱性を持つコンポーネントが含まれていたとき -
VULNERABILITY_RETRACTED:脆弱性情報が撤回されたとき。今回は使っていませんが、CRA報告を「報告対象外」に戻したり、見直しを促したりするのに使えそうです
5.2.0の通知先(パブリッシャー)は、Console・Email・Jira・Kafka・Mattermost・Microsoft Teams・Slack・Webex・Webhookの9種類です。
Slackアプリの作り方
通知先のIncoming Webhookは、Slackアプリを作って発行します。
- api.slack.com/apps で Create New App を押す
- From scratch → Pick features を選ぶ
- 権限は
incoming-webhookだけにし、イベントは購読しない。Create and Install で、通知先のチャンネル(例:#cra-alerts)を選ぶ - 発行されたWebhook URLを、アラートの Destination URL に貼る
2026年11月の時点で、アプリ作成画面は「From a template/From scratch(Paste a manifest/Pick features)/With a coding agent」の構成に変わっています。古い解説記事とは画面が違うので注意してください。
また、使わないApp-Level Token(connections:write)が自動で作られていました。不要なので削除しておきましょう。Webhook URLは知っていれば誰でもそのチャンネルに投稿できるので、スクリーンショットに写さないようにし、漏れたらWebhookを削除して作り直します。
即時通知のフィルターはグループで分岐させる
v5のアラートには、CELで書く Filter Expression があります。最初は次の式を入れました。
subject.vulnerability.severity in ["CRITICAL", "HIGH"] || subject.vulnerability.is_kev
これだと、NEW_VULNERABLE_DEPENDENCY の通知で次のエラーが出ます。このグループの通知には subject.vulnerability がないからです。
WARN [NotificationRouter] Failed to evaluate filter expression for rule CRA-new-vuln; Failing open
field 'vulnerability' is not declared in message '...NewVulnerableDependencySubject'
「Failing open」なので通知は止まらずに全件送られますが、フィルターは効いていません。グループで分岐させる式にします。
group == Group.GROUP_NEW_VULNERABLE_DEPENDENCY
|| subject.vulnerability.severity in ["CRITICAL", "HIGH"]
|| subject.vulnerability.is_kev
KEV掲載を条件に入れておくと、CRAの「積極的に悪用されている脆弱性」の判断にそのまま使えます。
日次まとめは「限定」が必須
スケジュール型のアラートは、限定 でプロジェクトを1つ以上指定しないと動きません。CronはUTCで指定するので、0 0 * * * が日本時間の毎朝9時になります。
「変更されていない場合はパブリックをスキップします」をONにすると、新しい検出がない日は送られません。翌朝通知が来なかったときは、ログでスキップされたことを確認できました。
INFO [ProcessScheduledNotificationRuleActivity] No new findings since rule was last processed
[notificationRuleName=日次-新規脆弱性まとめ]
注意点として、限定にバージョン付きのプロジェクトを1つだけ指定すると、新しいバージョンを別プロジェクトとして登録したときに対象外になります。タグで限定するか、親子構成にして「アクティブな子プロジェクトを含める」を使うと、この手間を省けます。
Pleasanter連携:Webhookと中継サーバー
Dependency-TrackのWebhookを小さな中継サーバーで受け、PleasanterのAPIでレコードを作ります。PleasanterのAPIは、本文にAPIキーと独自形式のJSONを求めるので、Dependency-Trackから直接は送れません。中継サーバーを置くと、次の3つができます。
- 署名を検証して、なりすましを防ぐ
- 同じ脆弱性を重複登録せず、手入力した項目も上書きしない
- 深刻度やKEV掲載から、対応期限とCRA報告期限を計算する
Webhookで届くJSON
v5のWebhookが送るJSONは、公式ドキュメントに具体例がありません。そこで、最初に受け取って保存するだけのサーバーを立てて、テスト通知の中身を確認しました(押粋)。
{
"notification": {
"group": "GROUP_NEW_VULNERABILITY",
"timestamp": "2026-11-12T11:55:29.352Z",
"title": "[TEST] New Vulnerability Identified on Project: ...",
"subject": {
"component": { "uuid": "...", "name": "componentName", "version": "componentVersion" },
"project": { "uuid": "...", "name": "projectName", "version": "projectVersion", "tags": ["tag1"] },
"vulnerability": {
"vulnId": "INT-001", "source": "INTERNAL",
"aliases": [{ "vulnId": "OSV-001", "source": "OSV" }],
"description": "...", "recommendation": "...",
"cvssv3": 6.6, "cvssv4": 7.7, "severity": "MEDIUM",
"cwes": [{ "cweId": 666, "name": "..." }],
"isKev": false
}
}
}
}
EPSSはこの通知には入っていません。また、スコアのない脆弱性では cvssv4 が 0 で届くので、未入力として扱う必要があります。
台帳は「期限付きテーブル」で作る
脆弱性対応は、期限までに終わらせる業務です。開始・完了・状況・担当者を標準で持ち、期限切れを強調表示できる期限付きテーブルが向いています。列は次のように割り当てました。
| 列 | 表示名 | 入れる値 |
|---|---|---|
| タイトル | タイトル |
GHSA-846f-f764-76mx System.Security.Cryptography.Pkcs@9.0.13 の形式 |
| 開始 | 開始 | 検出日時(JST) |
| 完了 | 完了 | 対応期限:Critical 7日、High 14日、Medium 30日、Low 90日 |
| 状況 | 状況 | 未着手(100) |
| ClassA | 一意キー |
脆弱性ID|プロジェクトUUID|コンポーネントUUID(読み取り専用) |
| ClassB、ClassC、ClassH | 脆弱性ID、ソース、別名 | GHSAとCVEの対応が一目で分かるように並べる |
| ClassD | 深刻度 | CRITICAL・HIGHなど |
| ClassE、ClassF | コンポーネント、プロジェクト | PURL、プロジェクト名とバージョン |
| ClassG | CWE |
CWE-407 など |
| NumA、NumB | CVSS v3、CVSS v4 | スコア |
| CheckA | KEV | KEV掲載かどうか |
| DateB | CRA報告期限 | KEV掲載なら検出から24時間後(目安) |
| 内容 | 内容 | 説明、推奨対応、Dependency-Trackへのリンク |
完了とCRA報告期限は、書式を「年月日時分」にします。24時間の期限は、時刻まで見えないと管理できないからです。
中継サーバーの仕組み
Pythonの標準ライブラリだけで書いた約270行のスクリプト(dtrack_relay.py)です。要点は3つです。
1. 署名の検証:v5.2のWebhookは、Signing Secretを設定すると X-Webhook-Signature: sha256=<HMAC-SHA256> を付けて送ってきます。本文を解析する前に検証します。
def signature_ok(body, header):
if not header or not header.startswith("sha256="):
return False
expected = hmac.new(SECRET.encode("utf-8"), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header[7:].strip().lower())
2. 重複させない登録:一意キーで既存のレコードを探し、あれば脆弱性情報だけを更新します。状況・開始・完了・担当者は新規登録のときだけ入れるので、人が進めた対応を壊しません。Pleasanterのupsert APIを使わないのは、この使い分けができないためです。
def find_existing(key):
res = pl_post(f"{PL_SITE}/get", {"View": {
"ColumnFilterHash": {"ClassA": key},
"ColumnFilterSearchTypes": {"ClassA": "ExactMatch"},
}})
rows = (res.get("Response") or {}).get("Data") or []
rows = [r for r in rows if (r.get("ClassHash") or {}).get("ClassA") == key]
return rows[0] if rows else None
def push_to_pleasanter(n):
key, data, initial = to_record(n)
row = find_existing(key)
if row:
rid = row.get("IssueId")
pl_post(f"{rid}/update", data) # 脆弱性情報だけ更新
return f"更新 #{rid}"
rec = {**initial, **data} # 新規のときだけ状況・期限を入れる
rec["ClassHash"] = {"ClassA": key, **data["ClassHash"]}
res = pl_post(f"{PL_SITE}/create", rec)
return f'新規 #{res.get("Id")}'
同じ脆弱性がもう一度通知され、そのときにKEV掲載になっていた場合は、CRA報告期限が未入力のときだけ、その時点から24時間後を入れます。Pleasanterの未入力の日付は 1899-12-30 で返ってくるので、それを未入力として判定しています。ただし、後述のとおりDependency-TrackはKEVへの追加では通知しないので、この処理だけでは「後からKEVに載ったもの」を拾い切れません。
CRA報告期限はあくまで目安です。KEVへの掲載は、法的な「認知」と同じではありません。自社製品への影響と悪用の事実を人が判定した時点で認知日時を入力し、そこから正式な期限を求める前提です。
3. 失敗しても取りこぼさない:受け取ったJSONは先に payloads/ へ保存し、Dependency-Trackにはすぐ200を返します。Pleasanterへの登録に失敗しても、後から再送できます。
python3 dtrack_relay.py --replay payloads/20261112_205529_496754_GROUP_NEW_VULNERABILITY.json
# 再送 ...: INT-001 MEDIUM → 新規 #815
Dependency-Track側の設定
-
管理 → シークレット で、
openssl rand -hex 32で作った文字列をwebhook-signingとして登録する -
アラートを作成:名前
Pleasanter連携、パブリッシャー Webhook、トリガー イベント - 保存後に開いて、グループ
NEW_VULNERABILITY、Destination URLhttp://192.168.0.20:8090/dtrack、Signing Secretwebhook-signing、限定に対象プロジェクトを設定する
Destination URLには、コンテナから届くLAN上のIPアドレスを指定します。コンテナの中から見ると、localhost はコンテナ自身を指すからです。
systemdで常駐させる
設定は .env に書き、chmod 600 で自分以外が読めないようにします。systemdの EnvironmentFile は KEY='値' の書き方をそのまま読めます。実行ユーザー(dtrack)と配置先(/opt/dtrack-relay)は例です。
# /opt/dtrack-relay/.env
DT_WEBHOOK_SECRET='...'
PLEASANTER_URL=http://192.168.0.30:50001
PLEASANTER_SITE_ID=123
PLEASANTER_API_KEY='...'
DT_FRONTEND_URL=http://192.168.0.10:8081
# /etc/systemd/system/dtrack-relay.service
[Unit]
Description=Dependency-Track to Pleasanter relay
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=dtrack
WorkingDirectory=/opt/dtrack-relay
EnvironmentFile=/opt/dtrack-relay/.env
ExecStart=/usr/bin/python3 -u /opt/dtrack-relay/dtrack_relay.py
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/dtrack-relay/payloads
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now dtrack-relay
journalctl -u dtrack-relay -f
動作確認とつまずき:通知編
設定そのものより、「いつ通知が出るのか」で一番時間を使いました。
同じBOMを再アップロードしても通知されない
NEW_VULNERABILITY は、新しく見つかった検出だけが対象です。検出済みのプロジェクトに同じBOMを入れ直しても、再分析しても、通知は出ません。本物の通知で試すには、テスト用の新しいプロジェクトに同じBOMをアップロードします。新しいプロジェクトにとっては、すべてが新規検出になるからです。
「限定」は通知が出る前に設定する
テスト用プロジェクトで試したところ、Slackには届いたのに、中継サーバーには何も届きませんでした。ログの時刻を並べると原因が分かります。
| 時刻(JST) | 出来事 |
|---|---|
| 21:41:58 | relay-test 1にBOMをアップロードし、10件の通知が発生 |
| 21:42 | Slackに届く(限定なしのアラート) |
| 21:48:14 | Pleasanter連携の限定にrelay-test 1を追加して登録 |
| 21:53:27 | relay-test 2で、先に限定を設定してからアップロード→ 10件すべて登録 |
通知が出た時点で限定に入っていないプロジェクトは、後から追加しても送られません。正しい順番は、「プロジェクトを作る → アラートの限定に追加して登録 → BOMをアップロード」です。また、プロジェクトを削除して作り直すとUUIDが変わり、限定から外れます。
このような切り分けのためにも、アラートの 発行成功のログ はONにしておくと便利です。
後からKEVに載っても通知されない
これが一番大きな落とし穴です。5.2.0には、KEV専用の通知グループがありません。すでに検出済みの脆弱性が、後からKEVに載っても、通知は発生しません。
CRAの24時間の早期警告は、まさにこの「今まで知られていた脆弱性が、悪用され始めた」場面で効いてきます。つまり、Webhookだけでは「積極的な悪用」の検知が漏れます。対策には、Dependency-TrackのREST APIでKEV掲載とEPSSを定期的に調べ、台帳に反映するジョブが必要です。KEVの情報は自前で取得せず、Dependency-Trackがミラーしたものを使えば十分です。
検知までの遅れは24時間に収まるか
情報源の同期は1日1回です。さらに、ポートフォリオの再分析の間隔も加わるので、公開から検知までには遅れが出ます。NVDのように取得に失敗する日があれば、その分も遅れます。CRAの24時間は「認知」から数えるので、検知の遅れがそのまま期限を削るわけではありません。ただし、どれくらい遅れるのかは把握しておくべきで、今後検証します。
まとめと今後
Dependency-Track v5.2の標準機能と、標準ライブラリだけの中継サーバーで、「脆弱性情報の収集 → 通知 → 期限付きの対応台帳」までを自動化できました。Pleasanter 1.5.9.1と同じSBOMを入れたテスト用プロジェクトでは、High 6件・Medium 4件の検出が、それぞれ期限つきのレコードとして台帳に登録されました。
まだ残っている課題は次のとおりです。
-
後からKEVに載ったものとEPSSの変化を拾う定期ジョブ:Dependency-TrackのREST APIからKEV掲載と
epssScoreを取得し、台帳のKEV・EPSSを更新する。Webhookだけでは拾えない「積極的な悪用」をここで検知する - CRA報告テーブルへの自動起票:受け取ったプロジェクトUUIDから製品を突合し、CRA報告を「判定中」で起票する。認知日時は人が判定したときに入力する
- 検知までの遅れの検証:同期間隔と再分析間隔を合わせて、公開から検知までの時間を測る
- 既存の検出の初期取り込み:Webhookは新規検出しか送らないので、運用開始前の検出はREST APIから一括で取り込む
-
監査結果とVEX:台帳で「影響なし」と判断したものをDependency-Trackの監査やVEXに戻す。撤回(
VULNERABILITY_RETRACTED)の通知も取り込む - 監視対象のタグ管理:バージョンアップのたびにアラートの限定を差し替えなくて済むようにする
これらは、Pleasanterを使った「CRA報告対応シリーズ」として続けていきます。