目次
はじめに
CVSS での脆弱性管理では自組織での状況を反映させた優先度付けができないため、検出される脆弱性が多すぎると何から手をつけて良いかわからなくなり本当に対応しなければいけない脆弱性に迅速に対応できなくなったりします。
SSVC を使うことで優先度付けを行い、どれから優先して対応すればいいかを迅速に判断するためのサポートができます。
公開されている Python モジュールを使って、自動的に優先度付けをしてみようと思います。
SSVC とは
SSVC(Stakeholder-Specific Vulnerability Categorization)とは、脆弱性管理のための対応優先度を判断するフレームワークです。
Decision Tree という決定木を使って分岐点ごとに値を選択することで脆弱性に対する対応方法を算出します。
Github にて対応方法を算出するソースコードが公開 されていますのでこちらを使って対応方法を算出してみたいと思います。
ステークホルダー(Stakeholders)
以下の 3 つがあります。
| Stakeholders | Description |
|---|---|
| サプライヤー | ソフトウェアを提供する組織 |
| デプロイヤー | サプライヤーから提供されたパッチなどを評価し、適用する組織 |
| コーディネーター | 脆弱性を取りまとめる組織 |
分岐点(Decision Points)
| Decision Points | Description |
|---|---|
| Exploitation | 攻撃実績があるのか、PoC か公開されているかなどで判断します |
| System Exposure | システムが公開されているかなどで判断します |
| Utility | 自動化が可能か、攻撃の有効性などで判断します |
| Impact | 影響の大きさなどで判断します |
https://certcc.github.io/SSVC/howto/deployer_tree/#deployer-decision-points
Utility is a combination of Automatable and Value Density
とありますが モデル や ソースコード では「Automatable」のみになっていました。
本記事ではひとまずモデルとソースコードに合わせて「Automatable」のみで判別してみます。
優先度(Decision Points)
算出される優先度は以下の通りになります。
| Deployer Priority | Description |
|---|---|
| Defer | 現時点では対応しない |
| Scheduled | 定期メンテナンスで対応する |
| Out-of-cycle | 次回のサイクルを待たず、迅速に緩和策/修正策を適用する |
| Immediate | 全てのリソースを使って、必要であれば通常業務を止めてでも対応する |
作ってみる
Docker を使って動作環境を構築します。
- Dockerfile
FROM python:3.14
RUN apt-get update
RUN mkdir /app
WORKDIR /app
COPY requirements.txt /app/
RUN pip install -r requirements.txt
- requirements.txt
certcc-ssvc
pydantic
構築します。
$ tree
.
├── app
├── Dockerfile
└── requirements.txt
$ docker build . -t ssvc:latest
$ docker run -v ./app:/app -it --rm ssvc:latest bash
動作確認
まずはデプロイヤーツリーを表示してみます。
- app/tree.py
from ssvc.decision_tables.ssvc.deployer_dt import LATEST as DeployerDT
from ssvc.decision_tables.helpers import ascii_tree
def main():
print(ascii_tree(DeployerDT))
if __name__ == "__main__":
main()
$ python tree.py
Exploitation.. | System Expos.. | Automatable .. | Human Impact.. | Defer, Sched.. |
-------------------------------------------------------------------------------------
├── none
│ ├── small
│ │ ├── no
│ │ │ ├── low
│ │ │ │ └── [defer]
│ │ │ ├── medium
│ │ │ │ └── [defer]
│ │ │ ├── high
│ │ │ │ └── [scheduled]
│ │ │ └── very high
│ │ │ └── [scheduled]
│ │ └── yes
│ │ ├── low
│ │ │ └── [defer]
│ │ ├── medium
│ │ │ └── [scheduled]
│ │ ├── high
│ │ │ └── [scheduled]
│ │ └── very high
│ │ └── [scheduled]
│ ├── controlled
│ │ ├── no
│ │ │ ├── low
│ │ │ │ └── [defer]
│ │ │ ├── medium
│ │ │ │ └── [scheduled]
│ │ │ ├── high
│ │ │ │ └── [scheduled]
│ │ │ └── very high
│ │ │ └── [scheduled]
│ │ └── yes
│ │ ├── low
│ │ │ └── [scheduled]
│ │ ├── medium
│ │ │ └── [scheduled]
│ │ ├── high
│ │ │ └── [scheduled]
│ │ └── very high
│ │ └── [scheduled]
│ └── open
│ ├── no
│ │ ├── low
│ │ │ └── [defer]
│ │ ├── medium
│ │ │ └── [scheduled]
│ │ ├── high
│ │ │ └── [scheduled]
│ │ └── very high
│ │ └── [scheduled]
│ └── yes
│ ├── low
│ │ └── [scheduled]
│ ├── medium
│ │ └── [scheduled]
│ ├── high
│ │ └── [scheduled]
│ └── very high
│ └── [out-of-cycle]
├── public poc
│ ├── small
│ │ ├── no
│ │ │ ├── low
│ │ │ │ └── [defer]
│ │ │ ├── medium
│ │ │ │ └── [scheduled]
│ │ │ ├── high
│ │ │ │ └── [scheduled]
│ │ │ └── very high
│ │ │ └── [scheduled]
│ │ └── yes
│ │ ├── low
│ │ │ └── [scheduled]
│ │ ├── medium
│ │ │ └── [scheduled]
│ │ ├── high
│ │ │ └── [scheduled]
│ │ └── very high
│ │ └── [scheduled]
│ ├── controlled
│ │ ├── no
│ │ │ ├── low
│ │ │ │ └── [defer]
│ │ │ ├── medium
│ │ │ │ └── [scheduled]
│ │ │ ├── high
│ │ │ │ └── [scheduled]
│ │ │ └── very high
│ │ │ └── [scheduled]
│ │ └── yes
│ │ ├── low
│ │ │ └── [scheduled]
│ │ ├── medium
│ │ │ └── [scheduled]
│ │ ├── high
│ │ │ └── [scheduled]
│ │ └── very high
│ │ └── [out-of-cycle]
│ └── open
│ ├── no
│ │ ├── low
│ │ │ └── [scheduled]
│ │ ├── medium
│ │ │ └── [scheduled]
│ │ ├── high
│ │ │ └── [scheduled]
│ │ └── very high
│ │ └── [out-of-cycle]
│ └── yes
│ ├── low
│ │ └── [scheduled]
│ ├── medium
│ │ └── [scheduled]
│ ├── high
│ │ └── [out-of-cycle]
│ └── very high
│ └── [out-of-cycle]
└── active
├── small
│ ├── no
│ │ ├── low
│ │ │ └── [scheduled]
│ │ ├── medium
│ │ │ └── [scheduled]
│ │ ├── high
│ │ │ └── [out-of-cycle]
│ │ └── very high
│ │ └── [out-of-cycle]
│ └── yes
│ ├── low
│ │ └── [scheduled]
│ ├── medium
│ │ └── [out-of-cycle]
│ ├── high
│ │ └── [out-of-cycle]
│ └── very high
│ └── [out-of-cycle]
├── controlled
│ ├── no
│ │ ├── low
│ │ │ └── [scheduled]
│ │ ├── medium
│ │ │ └── [scheduled]
│ │ ├── high
│ │ │ └── [out-of-cycle]
│ │ └── very high
│ │ └── [out-of-cycle]
│ └── yes
│ ├── low
│ │ └── [out-of-cycle]
│ ├── medium
│ │ └── [out-of-cycle]
│ ├── high
│ │ └── [out-of-cycle]
│ └── very high
│ └── [out-of-cycle]
└── open
├── no
│ ├── low
│ │ └── [scheduled]
│ ├── medium
│ │ └── [out-of-cycle]
│ ├── high
│ │ └── [out-of-cycle]
│ └── very high
│ └── [immediate]
└── yes
├── low
│ └── [out-of-cycle]
├── medium
│ └── [out-of-cycle]
├── high
│ └── [immediate]
└── very high
└── [immediate]
やってみた
上記のデプロイヤーツリーの Decision Point を判定して対応優先度を出力してみます。
まずは判定ロジックはなしで、固定の値を返します。
CVE 番号を渡すと下記の通り判定し、対応方針: Defer を出力してみます。
| Decision Point | Value |
|---|---|
| Exploitation | None |
| System Exposure | Small |
| Automatable | No |
| Human Impact | Low |
- main.py
import ssvc
from ssvc.decision_tables.ssvc.deployer_dt import (
LATEST as DeployerDT,
Exploitation,
Exposure,
Automatable,
HumanImpact,
)
from ssvc.outcomes.ssvc.dsoi import LATEST as Dsoi
from ssvc.decision_points.base import DecisionPointValue
def exploitation_dp(cveId) -> DecisionPointValue:
"""Exploitation の Decision Point を返す
none,public poc,active のいずれか
"""
return next((v for v in Exploitation.values if v.name == "None"))
def system_exposure_dp(cveId) -> DecisionPointValue:
"""System Exposure の Decision Point を返す
small,controlled,open のいずれか
"""
return next((v for v in Exposure.values if v.name == "Small"))
def automatable_dp(cveId) -> DecisionPointValue:
"""Automatable の Decision Point を返す
no,yes のいずれか
"""
return next((v for v in Automatable.values if v.name == "No"))
def human_impact_dp(cveId) -> DecisionPointValue:
"""Human Impact の Decision Point を返す
low,medium,high,very high のいずれか
"""
return next((v for v in HumanImpact.values if v.name == "Low"))
namespace = "ssvc"
def show_dsoi(cve) -> None:
check_dp = {
"E": exploitation_dp(cve.get("id")),
"EXP": system_exposure_dp(cve.get("id")),
"A": automatable_dp(cve.get("id")),
"HI": human_impact_dp(cve.get("id")),
}
query = {}
for dp in DeployerDT.decision_points.values():
if dp.namespace == namespace and not dp.key == "DSOI":
query[dp.id] = check_dp[dp.key].key
hit = next(
(m for m in DeployerDT.mapping if all(m.get(k) == v for k, v in query.items())),
None,
)
matched = next((v for v in Dsoi.values if v.key == hit[DeployerDT.outcome]), None)
print(f"{cve.get("id"):<20}{matched.name:<15}", end="")
print(query)
def main() -> None:
cve = [
{"id": "CVE-0000-00000"},
]
print(f"{"CVE":<20}{"DSOI":<15}{"QUERY":<5}")
for c in cve:
show_dsoi(c)
if __name__ == "__main__":
main()
$ python main.py
CVE DSOI QUERY
CVE-0000-00000 Defer {'ssvc:E:1.1.0': 'N', 'ssvc:EXP:1.0.1': 'S', 'ssvc:A:2.0.0': 'N', 'ssvc:HI:2.0.2': 'L'}
自動判定してみる
次は試しに Exploitation を自動で判定してみます。
判定ロジックは CISA KEV に掲載があるかとしました。(None は今回は返しません)
- ある → Active
- ない → Public Poc
コードを修正します。
json に対象の CVE番号の記載があるかだけを確認しています。
$ curl https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json > "app/cisa_kev/$(date "+%F").json"
$ diff -u prev.py main.py
--- prev.py 2026-01-13 18:17:14
+++ main.py 2026-01-13 18:17:01
@@ -8,15 +8,23 @@
)
from ssvc.outcomes.ssvc.dsoi import LATEST as Dsoi
from ssvc.decision_points.base import DecisionPointValue
+import json
def exploitation_dp(cveId) -> DecisionPointValue:
"""Exploitation の Decision Point を返す
none,public poc,active のいずれか
"""
- return next((v for v in Exploitation.values if v.name == "None"))
+ file = open("cisa_kev/2026-01-13.json", "r")
+ j = json.load(file)
+ cve_list = [item.get("cveID") for item in j["vulnerabilities"]]
+ if cveId in cve_list:
+ return next((v for v in Exploitation.values if v.name == "Active"))
+ return next((v for v in Exploitation.values if v.name == "Public PoC"))
+
+
def system_exposure_dp(cveId) -> DecisionPointValue:
"""System Exposure の Decision Point を返す
small,controlled,open のいずれか
@@ -68,6 +76,7 @@
def main() -> None:
cve = [
{"id": "CVE-0000-00000"},
+ {"id": "CVE-2025-8110"},
]
print(f"{"CVE":<20}{"DSOI":<15}{"QUERY":<5}")
実行してみます。
$ python main.py
CVE DSOI QUERY
CVE-0000-00000 Defer {'ssvc:E:1.1.0': 'P', 'ssvc:EXP:1.0.1': 'S', 'ssvc:A:2.0.0': 'N', 'ssvc:HI:2.0.2': 'L'}
CVE-2025-8110 Scheduled {'ssvc:E:1.1.0': 'A', 'ssvc:EXP:1.0.1': 'S', 'ssvc:A:2.0.0': 'N', 'ssvc:HI:2.0.2': 'L'}
KEV に載っている CVE 番号は Exploitation が Active 判定されました。
土台はできたのであとは判定ロジックを構築すれば自動で判別が可能になります。
まとめ
SSVC の自動判定を作成してみました。
判定ロジックは組織ごとに明確に、具体的に定義する必要があります。
一例として以下の様に考えることができるかと思います。
exploitation や automatable は外部公開されている情報で自動判定できそうですね。
- Exploitation
- https://certcc.github.io/SSVC/tutorials/ssvc_overview/#exploitation_1
- 例えばCISA KEV に記載があると Active と判断できるかと思います
- System Exposure
- https://certcc.github.io/SSVC/tutorials/ssvc_overview/#exposure
- web サーバーなどはインターネットからアクセスできるので open と判断できると思います
ただし対策が講じられていると Controlled と判断することも可能です
- Automatable
- https://certcc.github.io/SSVC/tutorials/ssvc_overview/#automatable
- コマンドインジェクションが可能な脆弱性などは yes と判断できると思います
- Human Impact
- https://certcc.github.io/SSVC/tutorials/ssvc_overview/#human-impact
- 開発環境などは影響が限定的なので low になることが多いかもしれません
SSVC でのトリアージは意思決定の参考にするためのものであり、最終的には現場の判断などが不可欠です。
こちらの公開ソースコードを利用すればパイプラインに組み込んでトリアージ結果を自動通知、といったこともできそうですね。




