想定読者
- 日々たくさんの脆弱性情報を確認しておられる情シスやセキュリティ担当の方々
- NVDやKEVのサイトを巡回することに飽き飽きされている方々
スクリプト
#!/usr/bin/env bash
set -euo pipefail
# Usage:
# ./cve_enrich.sh /path/to/cve.json
# cat cve.json | ./cve_enrich.sh
#
# Input: CVE JSON 5.1 (CVE Programの公式スキーマ)
# Output: JSON {cve, published, cvss{baseScore, baseSeverity, vectorString}, kev{dateAdded, reference}, epss, epss_percentile}
# 依存: jq, curl
INPUT="${1:-/dev/stdin}"
INPUT_JSON="$(cat "$INPUT")"
# まずJSONから CVSS/KEV と、もしあれば EPSS を抽出(EPSSはADPに載っている場合のみ)
BASE_JSON="$(
jq -c '
{
cve: .cveMetadata.cveId,
published: (.cveMetadata.datePublished // .cveMetadata.dateReserved // null),
# CVSS v3.1(containers.cna.metrics[].cvssV3_1 の最初を採用)
cvss: (
[
.containers.cna.metrics[]?
| select(has("cvssV3_1"))
| .cvssV3_1
| {baseScore, baseSeverity, vectorString}
] | first
),
# KEV(ADP: other.type == "kev" の最初を採用)
kev: (
[
.containers.adp[]?
| .metrics[]?
| select(.other?.type=="kev")
| .other.content
] | first
),
# EPSS(もしADPに格納されている場合を想定:type=="epss")
epss_block: (
[
.containers.adp[]?
| .metrics[]?
| select(.other?.type=="epss")
| .other.content
] | first
)
}
' <<<"$INPUT_JSON"
)"
CVE_ID="$(jq -r '.cve' <<<"$BASE_JSON")"
# 既にEPSSがJSON内にあるか(score/epss と percentileの両方またはいずれか)
EPSS_SCORE="$(jq -r '.epss_block.epss // .epss_block.score // empty' <<<"$BASE_JSON" || true)"
EPSS_PCTL="$(jq -r '.epss_block.percentile // empty' <<<"$BASE_JSON" || true)"
# JSONにEPSSが無ければ FIRST 公式APIから取得
if [[ -z "${EPSS_SCORE}" || -z "${EPSS_PCTL}" ]]; then
if [[ -n "$CVE_ID" ]]; then
EPSS_API_JSON="$(curl -s "https://api.first.org/data/v1/epss?cve=${CVE_ID}&page_size=1000" || true)"
# 期待: {"data":[{"cve":"CVE-XXXX-YYYY","epss":"0.12345","percentile":"0.98765"}], ...}
EPSS_SCORE_API="$(jq -r --arg c "$CVE_ID" '.data[]? | select(.cve==$c) | .epss // empty' <<<"$EPSS_API_JSON" || true)"
EPSS_PCTL_API="$(jq -r --arg c "$CVE_ID" '.data[]? | select(.cve==$c) | .percentile // empty' <<<"$EPSS_API_JSON" || true)"
# 取得できた方を採用(既にJSON内にEPSSがあればそちらを優先)
EPSS_SCORE="${EPSS_SCORE:-$EPSS_SCORE_API}"
EPSS_PCTL="${EPSS_PCTL:-$EPSS_PCTL_API}"
fi
fi
# 最終JSONの整形(数値化できる値は数値化)
jq -n \
--argjson base "$BASE_JSON" \
--arg epss_score "${EPSS_SCORE:-}" \
--arg epss_pctl "${EPSS_PCTL:-}" '
{
cve: $base.cve,
published: $base.published,
cvss: ($base.cvss // null),
kev: (
if $base.kev then
{dateAdded: $base.kev.dateAdded, reference: $base.kev.reference}
else null end
),
epss: (
if ($epss_score|length) > 0 and ($epss_score|tonumber? != null)
then ($epss_score|tonumber)
else null end
),
epss_percentile: (
if ($epss_pctl|length) > 0 and ($epss_pctl|tonumber? != null)
then ($epss_pctl|tonumber)
else null end
)
}'
入力:CVE-ID
出力:CVSSのBase Score、KEVの掲載有無(有の場合は掲載された日付)、EPSSのscoreとpercentile
使いかた
curl https://vulnerability.circl.lu/api/cve/{CVE-ID} | ./cve_search.sh
上記のスクリプトを適当な名前で保存して、Vulnerability-Lookup
1のAPIを叩いて実行します。
{CVE-ID}の箇所を任意の値に変更してください。
(APIを叩く処理をスクリプトの内部に埋め込むと403になってしまったのでパイプで渡しています)
例
curl https://vulnerability.circl.lu/api/cve/CVE-2025-0282 | ./cve_search.sh
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 4497 100 4497 0 0 20979 0 --:--:-- --:--:-- --:--:-- 21014
{
"cve": "CVE-2025-0282",
"published": "2025-01-08T22:15:09.386Z",
"cvss": {
"baseScore": 9,
"baseSeverity": "CRITICAL",
"vectorString": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H"
},
"kev": {
"dateAdded": "2025-01-08",
"reference": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2025-0282"
},
"epss": 0.93263,
"epss_percentile": 0.99801
}
おわりに
CVEの情報を集めるのはCVE_Priorivizer
2などの便利なツールがすでに存在しているので、Pythonが動く環境であればそちらを利用されたほうがよいかと思います。
シェルスクリプトした動かない環境でサクッと調査したいときに使ってみてください。