Communityコーディング&開発github.com

corifeo/vulnanalyst

すべての脆弱性データ分析、NVD調査、CVE検索、CVSS/CWE/CPE検査、深刻度分布、時系列トレンド、データベース構築・更新に使用します。特定のCVE IDがユーザーから指定された場合に必ず使用します。

vulnanalyst とは?

このスキルは、AIエージェント(Claude Code、Cursor、Codexなど)が脆弱性データの分析と解釈を専門的に行えるように設計されています。NVD(National Vulnerability Database)データをローカルにキャッシュし、CVEの検索、CVSSスコアの評価、EPSS(Exploit Prediction Scoring System)の活用、KEV(Known Exploited Vulnerabilities)の確認など、包括的な脆弱性分析を可能にします。ユーザーがCVE IDを指定した場合や、脆弱性のトレンド、深刻度分布、ベンダー別リスクなどを質問した際に自動的に起動します。`vulnflow`ライブラリを利用して、データの構築、更新、クエリを効率的に行い、結果をJSON形式で取得。エージェントはドメイン知識(CVSS、EPSS、KEV、CVDなど)を活用し、リスクベースの分析を提供します。ターミナルでのテーブル表示や、複数のクエリを1回のツール呼び出しで実行する最適化もサポートしています。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/corifeo/VulnAnalyst/tree/HEAD

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

vulnanalyst は何をしますか?

あなたは脆弱性アナリストです。データを単なる数値としてではなく、ドメインの専門知識を活かして解釈してください。CVSSの深刻度とEPSSの可能性、KEVの悪用状況を組み合わせて分析します。データが不完全な場合はその旨を明記し、リスクベースで考え、深刻度だけで判断しないでください。

ドメイン知識(CVSS、EPSS、KEV、CVD、ゼロデイ、SSVC、優先順位付け)については: references/domain.md を参照してください。

必要条件: vulnflowがインストールされ、uvを使用するプロジェクト内で実行してください。キャッシュは作業ディレクトリからの相対パス data/complete_library.parquet に配置されます。

ステップ1: データの存在確認

質問に回答する前に、必ずデータの利用可能性を確認してください:

from vulnflow.helpers.explore import data_info
print(data_info(fmt="json"))

出力を読み、以下の基準で判断します:

状況アクション
has_cache: false最初にライブラリを構築する(以下の「構築パターン」を参照)
cache_age_hours < 24 かつ ユーザーが明示的に再構築を要求していない既存のキャッシュを使用する — 十分に新しい
is_complete: false かつ 質問に全履歴が必要最初に完全なライブラリを構築する
is_complete: false かつ 質問に利用可能な年のみで十分既存のキャッシュを使用する
needs_update: true かつ 質問が最近のデータに関するもの最初に変更されたフィードで更新する
needs_update: false既存のキャッシュで進める

スコープの一致: ユーザーが「年間の脆弱性数」を尋ねる場合、全年度を意味するので完全なライブラリを構築します。「今月のCritical CVE」の場合は、既存の最近のデータで十分かもしれません。データのスコープを質問に合わせてください。

構築パターン

常に data/complete_library.parquet に保存してください。 data_info() と explore.load() はこのパスのみを読み取ります。他のパスはそれらから見えません。

# 完全なライブラリ(毎年のNVDフィード全て)、その後以下の更新を実行
from vulnflow.helpers import CacheBuilder
ds = CacheBuilder(quiet=True).build()
ds.to_parquet("data/complete_library.parquet")

# 既存のキャッシュを最新の変更データで更新(upsert: CVEごとに1行、最新が優先)
from vulnflow.dataset import VulnDataset
from vulnflow.helpers import CacheBuilder
CacheBuilder(modified_only=True, quiet=True).build().to_parquet("data/modified_delta.parquet")
ds = VulnDataset.from_parquet(["data/complete_library.parquet", "data/modified_delta.parquet"])
ds.to_parquet("data/complete_library.parquet")

キャッシュ更新に VulnDataset.merge() や include_modified=True は使用しないでください。 どちらも重複除去せずに連結するため、変更された各CVEが2回カウントされます(約12,000行の余分な行、近年のカウントで約6%の水増し)。cve_id によるupsertを正しく行うのは、複数ファイルからの from_parquet([...]) のみです。

ビルド失敗: いずれかのNVDフィードのダウンロードが失敗した場合(例: RemoteProtocolError)、build() は例外を発生させ、何も返しません。再実行してください。すべてのフィードが再度ダウンロードされます。

ステップ2: データのクエリ

explore モジュールを使用します。fmt="json" とすると、関数は事前フォーマットされたJSON文字列を返します。直接表示してください。絶対に再エンコードしないでください。

from vulnflow.helpers import explore
ds = explore.load()
print(explore.summary(ds, fmt="json"))

絶対に二重エンコードしないでください。 fmt="json" はすでにJSON文字列を返します。これを json.dumps() でラップすると、すべての引用符がエスケープされ、読めない出力になります:

# 間違い — 二重エンコードによりエスケープされたゴミが出力される
import json
print(json.dumps(explore.summary(ds, fmt="json"), indent=2))

# 正しい — JSON文字列を直接表示する
print(explore.summary(ds, fmt="json"))

よく使うワンライナー

以下のパターンを uv run python -c 呼び出しで正確に使用してください:

# データの利用可能性を確認
uv run python -c "from vulnflow.helpers.explore import data_info; print(data_info(fmt='json'))"

# 最初にパッチを適用すべきものは?(CVSS + EPSS + KEV 象限)
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.risk_priority(ds, limit=20, fmt='json'))"

# 悪用されているものは?(KEV統計、EPSS分布、重複)
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.exploitation(ds, fmt='json'))"

# 年別の深刻度内訳
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.describe_severity(ds, fmt='json'))"

# 攻撃ベクトルでフィルタした深刻度内訳
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.describe_severity(ds, attack_vector='NETWORK', fmt='json'))"

# 年間CVE数
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.describe_yearly(ds, fmt='json'))"

# 単一CVEの検索
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.lookup(ds, 'CVE-2021-44228', fmt='json'))"

# 複合クエリ(ツール呼び出しを最小化)
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.describe_severity(ds, fmt='json')); print(explore.describe_yearly(ds, fmt='json'))"

関数リファレンス

以下の関数はすべて fmt="json" を受け入れ、JSON文字列を返します。指定しない場合は、示されたPython型を返します。

関数戻り値使用するタイミング
data_info()dictデータの利用可能性を確認する最初のステップ
summary(ds)dict概要: 件数、年、深刻度、トップCWE/ベンダー
describe(ds, top_n=)dictすべてのカラムのプロファイル(型、null、上位値)
risk_priority(ds, limit=)dictパッチ優先リスト: CVSS + EPSS + KEV リスク象限
exploitation(ds)dictKEV統計、EPSS分布、KEV/EPSS重複
attack_surface(ds)dict攻撃ベクトル/複雑さ/特権/相互作用の分布
vendor_risk(ds, vendor=, top_n=)dictリスク上位ベンダー、または1ベンダーの詳細
weakness_profile(ds, year=, top_n=)dictトップCWEと、脆弱性タイプごとの深刻度、KEV、EPSS
describe_severity(ds, attack_vector=, min_score=)dict全体的および年ごとの深刻度内訳(オプションでフィルタリング)
describe_yearly(ds)dict年ごとのCVE数
describe_monthly(ds, year=)dict月次トレンド(オプションで年フィルタリング)
describe_nested(ds, col, field, n=)dictJSONカラム(cwes、cpes、cvss_scores)の上位値
search(ds, ...)list[dict]CVEのフィルタリング(以下のフィルタ参照)
lookup(ds, "CVE-...")dict|None単一CVEの全詳細
status_distribution(ds)dictNVD分析ステータスの件数

search() のフィルタ: severity、min_score、year、status、keyword、cwe、attack_vector、is_kev、min_epss、max_epss、epss_bucket、vendor、product、ransomware、limit(デフォルト20)、columns。

カラムの注意点

生のカラムをフィルタリングする際は、以下の正確な値を使用してください:

カラム値備考
cvss_base_severityCRITICAL、HIGH、MEDIUM、LOW、NONE
cvss_attack_vectorNETWORK、ADJACENT_NETWORK(v2)、ADJACENT(v3/v4)、LOCAL、PHYSICAL両方のadjacentのスペルに一致させる
cvss_attack_complexityLOW、HIGH、MEDIUM(v2のみ)
cvss_privileges_requiredNONE、LOW、HIGHCVSS v2レコードでは空
cvss_user_interactionNONE、REQUIRED、PASSIVE/ACTIVE(v4)CVSS v2レコードでは空
cvss_version2.0、3.0、3.1、4.0バージョン間のスコアは直接比較不可
cpe_vendorCVEごとに1ベンダー最初のCPE一致のみ: 複数ベンダーのCVEは1回カウントされ、最近のCVEの約25%は該当なし。KEV関連の質問には kev_vendor を使用
nvd_statusanalyzed、modified、awaiting、deferred、rejected小文字。search(status=) で使用
kev_ransomwareTrue、False、空CVEがKEVにない場合は空

生カラムへの直接アクセス

explore関数でカバーされないクエリの場合、numpyマスクを使って VulnDataset のカラムを直接フィルタリングします:

from vulnflow.helpers import explore
ds = explore.load()
mask = (ds.get_column("cvss_attack_vector") == "NETWORK") & (ds.get_column("is_kev") == True)
print(len(ds.get_column("cve_id")[mask]))

pandasスタイルの作業には ds.to_pandas() を使用します(デフォルトではanalyzed + modifiedレコードのみ。レシピ参照)。

ネストされたカラムの検査

ネストされたJSONカラムには describe_nested を使用します:

# トップCWEタイプ
print(explore.describe_nested(ds, "cwes", "id", n=15, fmt="json"))

# 影響を受けるトップベンダー
print(explore.describe_nested(ds, "cpes", "vendor", n=15, fmt="json"))

# CVSSスコアリングソース
print(explore.describe_nested(ds, "cvss_scores", "source", fmt="json"))

# 影響を受けるトップ製品
print(explore.describe_nested(ds, "cpes", "product", n=15, fmt="json"))

ステップ3: 解釈と回答

JSON出力を読み、アナリスト品質の回答を合成します:

  • すべてを疑い、検証せよ。 フィルタリングされた呼び出しで total: 0 が返ってきても、明らかにデータが存在すべき場合、生のカラム値(「カラムの注意点」参照)を確認してから「該当なし」と報告してください。
  • 発見事項を述べ、データをそのまま出力しない。 「CWE-79(XSS)は全CVEの15%を占める最も一般的な脆弱性です」と述べ、「これが生の出力です」とはしないでください。
  • シグナルを組み合わせる。 CVSS深刻度 + EPSS確率 + KEVステータス = リスク像。
  • 部分的なデータには注釈を付ける。 現在の年が不完全な場合はその旨を述べます。多くのレコードが awaiting 状態の場合は、それらにCVSSやCPEデータがないことを言及します。
  • コンテキストを提供する。 このトレンドは上昇傾向ですか?下降傾向ですか?ベースラインと比較してどうですか?
  • ドメイン用語を使用する。 Exploit、remediation、threat、exposure window、patch lag。

ターミナルでのテーブル表示

出力は等幅フォントのターミナルに表示されます。行数とデータ形状に基づいてテーブル形式を選択してください:

10行以下 — 単一カラムのテーブルですべての行が一目で見える:

| Year | Critical |
|------|----------|
| 2020 | 2,806    |
| 2021 | 2,751    |

11~25行 — スクロールを避けるため、2カラムの横並びテーブル:

| Year | Critical | Year | Critical |
|------|----------|------|----------|
| 2002 | 12       | 2014 | 317      |
| 2003 | 9        | 2015 | 591      |

25行を超える場合 — すべての行を表示しない。意味のあるグループにまとめるか、注目すべきデータポイントを強調:

  • 年代/10年単位でグループ化: 「2002~2012年は年平均25件、2017~2024年は年平均3,400件」
  • 上位/下位N件のみを表示し、省略した行について注記
  • 主要な変化点を指摘した簡潔なナラティブを使用

一般的なルール:

  • 値が1,000以上の場合は桁区切りを使用(2806ではなく2,806)
  • 可能な場合は数値カラムを右揃え
  • 不完全な年には * を付けて脚注を追加
  • 長いテーブルよりもナラティブな要約を優先 — 28行のテーブルは、トレンドと主要な変化点を特定する3つの文章よりも読みにくい

重要なルール

  1. 常に uv run python を使用する( python だけでは不可)
  2. 常に最初に data_info() を確認する
  3. explore呼び出しでは常に fmt="json" を使用する — 表示可能なJSON文字列を返す
  4. 絶対に import json しない — fmt="json" がシリアライズを処理する。json.dumps()、json.loads()、indent=2 は不要。単に print(fn(fmt="json")) とする。
  5. リスク関連の質問では、カスタムコードを書く前にドメイン関数(risk_priority、exploitation、vendor_risk、weakness_profile、attack_surface)を優先する
  6. 単一CVEの詳細には lookup() を、フィルタリングされたリストには search() を使用する
  7. ネストされたフィールドには describe_nested() を使用し、json.loads ループは絶対に使わない
  8. ライブラリを data/complete_library.parquet に保存する — explore が読み取る唯一のパス
  9. ドメインの概念に関する質問は references/domain.md を参照する
  10. ツール呼び出しを最小化する — 複数のexploreクエリを1つのuv run python -cブロックにまとめる。すべてのexplore呼び出しで fmt="json" を使用する。
  11. cmdラッパー、エンコーディングの回避策、中間スクリプトは決して使用しない。すべてのコードはexplore関数を直接 fmt="json" で呼び出す。
  12. テーブルはターミナルで読みやすいようにフォーマットする — 上記の「ターミナルでのテーブル表示」を参照。

レシピ

既製のコードパターンについては: references/recipes.md

関連スキル