osv-scanner-mcp 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -3
- package/dist/index.js +6 -3
- package/dist/osv/runner.js +54 -12
- package/dist/osv/suggestFix.js +28 -0
- package/dist/tools/scanJavaProject.js +17 -2
- package/dist/tools/suggestFix.js +5 -2
- package/dist/utils/projectDetector.js +44 -17
- package/package.json +1 -4
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Google製 [OSV-Scanner](https://github.com/google/osv-scanner) をラップす
|
|
|
25
25
|
- 手動インストール済みのバイナリ(PATH上または `OSV_SCANNER_PATH` 指定)があればそちらを優先します
|
|
26
26
|
- 自動ダウンロードを無効化する場合は `OSV_MCP_AUTO_DOWNLOAD=0`
|
|
27
27
|
- PATH上のバイナリを使わず常に検証済み自動ダウンロードを使う場合は `OSV_MCP_PREFER_DOWNLOAD=1`(運用環境向け)
|
|
28
|
-
-
|
|
28
|
+
- スキャン時と `explain_vulnerability` 実行時にネットワークアクセスが発生します。照会先はOSVデータベース(`api.osv.dev`)ですが、**`pom.xml` のスキャンでは推移的依存を解決するため deps.dev(`api.deps.dev`)にも接続します**。詳細と無効化の方法は[通信先とプライバシー](#通信先とプライバシー)を参照してください
|
|
29
29
|
|
|
30
30
|
## セットアップ
|
|
31
31
|
|
|
@@ -123,6 +123,7 @@ npm run build
|
|
|
123
123
|
| `OSV_MCP_MAX_CONCURRENT_SCANS` | 同時実行できるスキャン数の上限(デフォルト `2`、最大 `16`)。超過したリクエストは待たずに即時エラーになります |
|
|
124
124
|
| `OSV_MCP_AUTO_DOWNLOAD` | `0` または `false` でバイナリの自動ダウンロードを無効化(デフォルト有効) |
|
|
125
125
|
| `OSV_MCP_PREFER_DOWNLOAD` | `1` または `true` 指定時、PATH上のosv-scannerを使わず、チェックサム検証済みの自動ダウンロードバイナリを常に使用します(PATH汚染による偽バイナリ実行の防止。`OSV_SCANNER_PATH` の明示指定は引き続き最優先) |
|
|
126
|
+
| `OSV_MCP_NO_REMOTE_RESOLUTION` | `1` または `true` 指定時、`pom.xml` の推移的依存を deps.dev で解決しません。**止まるのは deps.dev への送信だけで、脆弱性照会のためパッケージの名前とバージョンは引き続き `api.osv.dev` に送られます**。推移的依存の脆弱性は検出できなくなり、その旨が応答の `dependency_resolution.warning` に示されます。詳細は[通信先とプライバシー](#通信先とプライバシー) |
|
|
126
127
|
|
|
127
128
|
> **推奨**: `OSV_MCP_ALLOWED_ROOT` は未設定でも動作しますが、その場合は任意の絶対パスをスキャンできてしまいます。悪意ある指示(プロンプトインジェクション)経由で意図しないディレクトリをスキャンさせられる経路を塞ぐため、プロジェクト置き場のルート(例: `~/projects`)を設定しておくことを推奨します。各クライアントの設定で `"env": {"OSV_MCP_ALLOWED_ROOT": "/Users/you/projects"}` のように渡せます(Codex CLIのTOMLでは `[mcp_servers.osv-scanner.env]` セクション)。
|
|
128
129
|
|
|
@@ -130,6 +131,25 @@ npm run build
|
|
|
130
131
|
> - `OSV_MCP_ALLOWED_ROOT=/スキャン対象のルート` — スキャン範囲の境界を固定
|
|
131
132
|
> - `OSV_MCP_REQUIRE_ALLOWED_ROOT=1` — 境界未設定なら起動を拒否(fail-closed)
|
|
132
133
|
> - `OSV_SCANNER_PATH=/管理者所有の絶対パス` または `OSV_MCP_PREFER_DOWNLOAD=1` — PATH解決に依存せず、実行するバイナリを固定
|
|
134
|
+
>
|
|
135
|
+
> 依存の情報をどこに送るかは[通信先とプライバシー](#通信先とプライバシー)を確認してください。どの設定でも、脆弱性照会のためパッケージの名前とバージョンは `api.osv.dev` に送られます。
|
|
136
|
+
|
|
137
|
+
### 通信先とプライバシー
|
|
138
|
+
|
|
139
|
+
osv-scanner v2.4.0 で接続先を実機確認した結果です(2026-10-07)。
|
|
140
|
+
|
|
141
|
+
| 操作 | 接続先 | 送られる情報 |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `pom.xml` のスキャン(`scan_java_project` / `suggest_fix`) | `api.osv.dev`、**`api.deps.dev`** | パッケージの名前とバージョン。deps.dev には推移的依存の解決のため、`pom.xml` に宣言された依存(社内パッケージを含む)の名前とバージョンが送られます |
|
|
144
|
+
| `gradle.lockfile` のスキャン | `api.osv.dev` | パッケージの名前とバージョン(lockfileに全依存が記載済みのため、解決のための外部接続はしません) |
|
|
145
|
+
| `scan_java_artifact` / `scan_sbom` | `api.osv.dev` | 同定できたパッケージの名前とバージョン |
|
|
146
|
+
| `explain_vulnerability` | `api.osv.dev` | 指定した脆弱性ID |
|
|
147
|
+
| バイナリの自動ダウンロード(初回のみ) | GitHub(公式Releases) | なし(ピン留めしたバージョンのバイナリを取得) |
|
|
148
|
+
|
|
149
|
+
api.osv.dev と deps.dev はどちらも Google が運営するサービスです。**どの設定でも、スキャンしたパッケージの名前とバージョンは脆弱性照会のため `api.osv.dev` に送られます**(オフラインでの照会には対応していません)。
|
|
150
|
+
|
|
151
|
+
- **deps.dev への送信を止めたい場合**: `OSV_MCP_NO_REMOTE_RESOLUTION=1` を設定すると、`pom.xml` の推移的依存を解決しなくなり、接続先は `api.osv.dev` だけになります。止まるのは deps.dev への送信だけで、OSV への送信は続きます。また `pom.xml` に直接書いた依存しかスキャンされず、**推移的依存の脆弱性を見落とします**。この状態は `scan_java_project` / `suggest_fix` の応答の `dependency_resolution` に `transitive_resolution: "disabled"` と警告で示されるので、検出0件と区別できます。推移的依存も含めて deps.dev を使わずにスキャンするには、Gradleのlockfile方式(`gradle.lockfile`)を使ってください
|
|
152
|
+
- **任意の取得先には接続しません**: osv-scanner の `--data-source native` モードは、スキャン対象の `pom.xml` の `<repositories>` に書かれた任意のURLへ接続します(悪意あるpom.xmlで攻撃者のサーバーへ通信させられる)。本サーバーはこのモードを使わず、`deps.dev` を明示指定しています
|
|
133
153
|
|
|
134
154
|
## 提供ツール
|
|
135
155
|
|
|
@@ -145,12 +165,15 @@ Java(Maven)プロジェクトをスキャンし、既知の脆弱性レポート
|
|
|
145
165
|
|
|
146
166
|
> **Gradleプロジェクトについて**: 本ツールは**lockfile方式**のみ対応です(ビルド実行方式は build.gradle の任意コード実行を伴うため、セキュリティ上の理由から採用していません)。`gradle.lockfile` が無い場合は `./gradlew dependencies --write-locks` で生成してください(依存ロック未設定の場合は `build.gradle` に `dependencyLocking { lockAllConfigurations() }` の追加が必要です)。
|
|
147
167
|
|
|
168
|
+
> **スキャン範囲**: ディレクトリを指定すると、配下の `pom.xml` / `gradle.lockfile` / `buildscript-gradle.lockfile` を深さに関係なく検出し、**検出したファイルだけ**をスキャンします(応答の `manifests` がそのままスキャン範囲です)。同じディレクトリにある `package-lock.json` や `requirements.txt` などJava以外のファイルはスキャンしません。`.git`、`node_modules`、`target`、`build`、`.idea`、`.vscode` とシンボリックリンクは探索しません。探索するエントリが20万件、またはマニフェストが1,000件を超える場合は、結果を黙って省略せず `manifest_search_limit_exceeded` を返します。`pom.xml` などのマニフェストを直接指定した場合は、ディレクトリを探索せず**そのファイルだけ**をスキャンします(上限に達した場合の回避手段としても使えます)。
|
|
169
|
+
|
|
148
170
|
**出力(成功時)**
|
|
149
171
|
|
|
150
172
|
```json
|
|
151
173
|
{
|
|
152
174
|
"project_dir": "/path/to/project",
|
|
153
175
|
"manifests": ["pom.xml"],
|
|
176
|
+
"dependency_resolution": { "transitive_resolution": "enabled" },
|
|
154
177
|
"source_files": ["/path/to/project/pom.xml"],
|
|
155
178
|
"vulnerable_package_count": 4,
|
|
156
179
|
"vulnerability_count": 14,
|
|
@@ -249,6 +272,7 @@ OSV-Scanner 2.4.0の `java/archive` プラグインを使用し、ネストJAR
|
|
|
249
272
|
{
|
|
250
273
|
"project_dir": "/path/to/project",
|
|
251
274
|
"manifests": ["pom.xml"],
|
|
275
|
+
"dependency_resolution": { "transitive_resolution": "enabled" },
|
|
252
276
|
"vulnerable_package_count": 4,
|
|
253
277
|
"unfixed_vulnerability_count": 1,
|
|
254
278
|
"suggestions": [
|
|
@@ -272,6 +296,7 @@ OSV-Scanner 2.4.0の `java/archive` プラグインを使用し、ネストJAR
|
|
|
272
296
|
- MavenのOSV `ECOSYSTEM` 範囲(`introduced` / `fixed` / `last_affected` / 上限なし)を照合します。`versions` に明示された影響も確認します。範囲欠落・不正・未対応形式・`limit` による不完全な情報では安全と推定せず、候補を検証できなければ `recommended_upgrade: null`、`verification: "no_verified_candidate"` を返します。
|
|
273
297
|
- 推奨時は `verification: "verified"`、CVEごとの `recommended_status` は `affected` / `not_affected` / `unknown` です。推奨保留時は `not_evaluated` になります。`per_cve_detail.fixed_in` は各CVE単独の候補であり、最終推奨先の判定は `recommended_status` を参照してください。
|
|
274
298
|
- 現在より新しい修正版候補がないCVEは `tier: "unfixed"` として推奨の修正対象から除外します(情報欠落を含む場合があります)。除外したCVEも推奨先で判定し、その状態を表示します。全CVEがunfixedの場合も `recommended_upgrade` は `null` です。
|
|
299
|
+
- 修正版の推奨はMavenのみ対応です。Maven以外のパッケージは `verification: "unsupported_ecosystem"`、CVEごとの `tier: "unsupported"` を返し、`unfixed` には数えません(修正版の有無は判定していないため。修正版は `explain_vulnerability` で確認できます)。
|
|
275
300
|
|
|
276
301
|
### `explain_vulnerability`
|
|
277
302
|
|
|
@@ -295,7 +320,7 @@ OSV-Scanner 2.4.0の `java/archive` プラグインを使用し、ネストJAR
|
|
|
295
320
|
{
|
|
296
321
|
"error": {
|
|
297
322
|
"kind": "no_manifest_found",
|
|
298
|
-
"message": "対応マニフェスト(pom.xml / gradle.lockfile)
|
|
323
|
+
"message": "対応マニフェスト(pom.xml / gradle.lockfile)が見つかりません: /path/to/project"
|
|
299
324
|
}
|
|
300
325
|
}
|
|
301
326
|
```
|
|
@@ -305,6 +330,7 @@ OSV-Scanner 2.4.0の `java/archive` プラグインを使用し、ネストJAR
|
|
|
305
330
|
| `binary_not_found` | OSV-Scannerが見つからない(インストール案内をmessageに含む) |
|
|
306
331
|
| `project_not_found` | 指定パスが存在しない・ディレクトリ/pom.xmlでない |
|
|
307
332
|
| `no_manifest_found` | 対応マニフェスト(pom.xml / gradle.lockfile)が見つからない |
|
|
333
|
+
| `manifest_search_limit_exceeded` | マニフェスト探索が上限(20万エントリ・1,000マニフェスト)に達した。より狭いディレクトリかマニフェストを直接指定する |
|
|
308
334
|
| `binary_download_failed` | バイナリのダウンロード失敗(未対応プラットフォーム含む) |
|
|
309
335
|
| `binary_checksum_mismatch` | ダウンロードしたバイナリのチェックサム不一致(改ざん/破損の可能性) |
|
|
310
336
|
| `gradle_lockfile_missing` | Gradleプロジェクトだがgradle.lockfileが無い(生成手順をmessageで案内) |
|
|
@@ -326,9 +352,10 @@ OSV-Scanner 2.4.0の `java/archive` プラグインを使用し、ネストJAR
|
|
|
326
352
|
|
|
327
353
|
- **サプライチェーン対策**: バイナリの自動ダウンロードは公式GitHub Releasesに限定し、バージョンをピン留め。**パッケージに埋め込まれたSHA256チェックサム**で検証します(配布元のSHA256SUMSファイルは信用しないため、リリース側が改ざんされても検出可能)。検証合格まで実行権限を与えず、キャッシュ済みバイナリも使用のたびに再検証します。`OSV_MCP_PREFER_DOWNLOAD=1` でPATH上の未検証バイナリを使わない運用も選べます
|
|
328
354
|
- **コマンドインジェクション対策**: シェルを経由しない `spawn` + 引数配列で実行。OSV-Scannerへの引数は固定リストのみで、可変部は検証済み絶対パス1つだけ
|
|
329
|
-
- **パストラバーサル対策**: 入力パスは `realpath` でシンボリックリンク解決後に境界チェック。pom.xml
|
|
355
|
+
- **パストラバーサル対策**: 入力パスは `realpath` でシンボリックリンク解決後に境界チェック。pom.xml探索ではシンボリックリンクを辿りません。OSV-Scannerにはディレクトリを渡さず、検出したマニフェストだけを形式を明示して個別に渡します(ディレクトリを渡すと、OSV-Scannerが同じディレクトリの `requirements.txt` も読み、その取り込み指定 `-r ../x.txt` でスキャン範囲の外のファイルを読むため)
|
|
330
356
|
- **DoS対策**: タイムアウト・stdout上限・stderr抜粋上限を設定。スキャン結果は防御的にパースし、形式不正でも例外を投げません。同時実行スキャン数も上限(デフォルト2)を設け、並列リクエストによるプロセスの無制限起動を防ぎます
|
|
331
357
|
- **fail-closedな運用モード**: `OSV_MCP_REQUIRE_ALLOWED_ROOT=1` で、スキャン許可ルート未設定時にサーバーの起動自体を拒否できます
|
|
358
|
+
- **通信先の固定と明示**: osv-scannerの依存解決先は `deps.dev` を明示指定し、スキャン対象のpom.xmlが指定する任意のリポジトリへ接続するモード(`--data-source native`)は使いません(テストで保証)。通信先の一覧と、deps.devへの送信を止める `OSV_MCP_NO_REMOTE_RESOLUTION=1` は[通信先とプライバシー](#通信先とプライバシー)を参照
|
|
332
359
|
- **情報漏えい対策**: 想定外の例外はスタックトレース等を含めず `internal_error` に丸めます。外部由来のテキスト(脆弱性summary等)は長さ上限付きの「データ」として構造化して返します
|
|
333
360
|
- **プロンプトインジェクション対策**: OSVデータベース由来のテキスト(summary / details / ID等)とOSV-Scannerのstderrは、LLMクライアントへ返す前にサニタイズします。制御文字(ANSIエスケープ含む)・ゼロ幅文字・双方向制御文字(RLO等)・Unicodeタグ文字(不可視のテキスト密輸)・行区切り(U+2028/2029)を除去し、NFC正規化を適用。外部データの読み取りアクセサを単一のサニタイズ境界にすることで適用漏れを防いでいます
|
|
334
361
|
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - OSV_MCP_MAX_CONCURRENT_SCANS: 同時実行スキャン数の上限(デフォルト2)
|
|
10
10
|
* - OSV_MCP_AUTO_DOWNLOAD: 0/false指定時、バイナリの自動ダウンロードを無効化
|
|
11
11
|
* - OSV_MCP_PREFER_DOWNLOAD: 1/true指定時、PATH上のバイナリを使わず検証済み自動ダウンロードを優先
|
|
12
|
+
* - OSV_MCP_NO_REMOTE_RESOLUTION: 1/true指定時、pom.xmlの推移的依存をdeps.devで解決しない
|
|
12
13
|
*/
|
|
13
14
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -34,13 +35,14 @@ if (startupWarning !== null) {
|
|
|
34
35
|
// NOTE: リリース時はpackage.jsonのversionと同じ値に更新すること
|
|
35
36
|
const server = new McpServer({
|
|
36
37
|
name: "osv-scanner-mcp",
|
|
37
|
-
version: "0.3.
|
|
38
|
+
version: "0.3.4",
|
|
38
39
|
});
|
|
39
40
|
server.registerTool("scan_java_project", {
|
|
40
41
|
title: "Javaプロジェクトの脆弱性スキャン",
|
|
41
42
|
description: "Java(Maven)プロジェクトをGoogle OSV-Scannerでスキャンし、依存ライブラリの既知の脆弱性(CVE/GHSA)を深刻度順のJSONレポートで返す。" +
|
|
42
43
|
"レポートにはパッケージごとの脆弱性一覧(CVSSスコア・5段階深刻度・修正版バージョン)とサマリ集計が含まれる。" +
|
|
43
|
-
"Maven(pom.xml)とGradle(gradle.lockfile)に対応。"
|
|
44
|
+
"Maven(pom.xml)とGradle(gradle.lockfile)に対応。" +
|
|
45
|
+
"dependency_resolution.warningがある場合は推移的依存がスキャン対象外のため、検出0件でも安全とは判断しないこと。",
|
|
44
46
|
inputSchema: {
|
|
45
47
|
project_path: z
|
|
46
48
|
.string()
|
|
@@ -55,7 +57,8 @@ server.registerTool("suggest_fix", {
|
|
|
55
57
|
"(same_minor: 同一major.minor系統内 → major_internal: 同一メジャー内 → cross_major: メジャーアップグレード)で選定し、" +
|
|
56
58
|
"推奨バージョン・アップグレード距離(upgrade_tier)・CVEごとの修正版を返す。" +
|
|
57
59
|
"候補を全修正対象CVEの影響範囲と照合し、情報不足の場合は推奨を保留する。" +
|
|
58
|
-
"現在より新しい修正版候補のないCVEはunfixedとして別表示し、推奨先での判定も返す。Maven(pom.xml)とGradle(gradle.lockfile)に対応。"
|
|
60
|
+
"現在より新しい修正版候補のないCVEはunfixedとして別表示し、推奨先での判定も返す。Maven(pom.xml)とGradle(gradle.lockfile)に対応。" +
|
|
61
|
+
"dependency_resolution.warningがある場合は推移的依存の脆弱性が提案に含まれない。",
|
|
59
62
|
inputSchema: {
|
|
60
63
|
project_path: z
|
|
61
64
|
.string()
|
package/dist/osv/runner.js
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* セキュリティ設計(docs/DESIGN_TODO.md):
|
|
5
5
|
* - シェルを経由しない`spawn`+引数配列で実行(コマンドインジェクション対策)
|
|
6
6
|
* - OSV-Scannerへ渡す引数は固定リストのみ。呼び出し側から任意フラグは注入できない
|
|
7
|
-
* (
|
|
7
|
+
* (プロジェクトは検出済みマニフェストを`--lockfile <形式>:<絶対パス>`で個別に、
|
|
8
|
+
* 実体スキャンは列挙済みのJAR/WAR絶対パスだけを渡す)
|
|
9
|
+
* - プロジェクトにディレクトリ(`-r`)を渡さない。osv-scannerがディレクトリ内の
|
|
10
|
+
* requirements.txt等も読み、その`-r ../x.txt`の取り込みでスキャン範囲の外のファイルを読むため
|
|
8
11
|
* - SBOMは検証・サイズ制限済みの専用一時コピー1つだけを渡す
|
|
9
12
|
* - タイムアウトと出力サイズ上限を設ける(ハング・巨大出力によるDoS対策)
|
|
10
13
|
*
|
|
@@ -12,6 +15,7 @@
|
|
|
12
15
|
* 0 = スキャン成功・脆弱性なし / 1 = スキャン成功・脆弱性あり / 128 = 対象パッケージなし
|
|
13
16
|
*/
|
|
14
17
|
import { spawn } from "node:child_process";
|
|
18
|
+
import path from "node:path";
|
|
15
19
|
import { ScanToolError } from "../errors.js";
|
|
16
20
|
import { resolveOsvScannerBinary } from "./binaryManager.js";
|
|
17
21
|
import { parseOsvScanOutput } from "./scanReport.js";
|
|
@@ -37,8 +41,36 @@ function maxConcurrentScansFromEnv() {
|
|
|
37
41
|
const DEFAULT_MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
|
|
38
42
|
/** エラー詳細に含めるstderrの上限(外部由来テキストをそのまま膨らませない) */
|
|
39
43
|
const MAX_STDERR_DETAIL_BYTES = 8 * 1024;
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
const NO_REMOTE_RESOLUTION_ENV = "OSV_MCP_NO_REMOTE_RESOLUTION";
|
|
45
|
+
function noRemoteResolutionFromEnv() {
|
|
46
|
+
const value = process.env[NO_REMOTE_RESOLUTION_ENV]?.trim().toLowerCase();
|
|
47
|
+
return value === "1" || value === "true" || value === "yes";
|
|
48
|
+
}
|
|
49
|
+
/** 外部解決を無効にするか。スキャン引数と応答の表示が食い違わないよう、呼び出し側で一度だけ決める */
|
|
50
|
+
export function isRemoteResolutionDisabled(options = {}) {
|
|
51
|
+
return options.noRemoteResolution ?? noRemoteResolutionFromEnv();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* OSV-Scannerに渡す固定引数。ここに無いオプションは一切使わない(ホワイトリスト)
|
|
55
|
+
*
|
|
56
|
+
* pom.xml/requirements.txtの推移的依存はapi.deps.devで解決される(2.4.0で実機確認)。
|
|
57
|
+
* `--data-source native` はスキャン対象pom.xmlの<repositories>に書かれた任意のURLへ
|
|
58
|
+
* 接続するため使わない。既定値の変更に備えてdeps.devを明示する。
|
|
59
|
+
*/
|
|
60
|
+
const FIXED_SCAN_ARGS = ["scan", "source", "--format", "json", "--data-source", "deps.dev"];
|
|
61
|
+
const NO_RESOLVE_ARG = "--no-resolve";
|
|
62
|
+
/** 個別に渡せるマニフェスト。ファイル名をそのまま解析形式として明示する(2.4.0で実機確認) */
|
|
63
|
+
const PROJECT_MANIFEST_FORMATS = new Set(["pom.xml", "gradle.lockfile", "buildscript-gradle.lockfile"]);
|
|
64
|
+
/** 検出済みマニフェストを`--lockfile <形式>:<絶対パス>`の組にする。対象外のパスは渡さない */
|
|
65
|
+
export function buildProjectTargetArgs(manifestPaths) {
|
|
66
|
+
return manifestPaths.flatMap((manifestPath) => {
|
|
67
|
+
const format = path.basename(manifestPath);
|
|
68
|
+
if (!PROJECT_MANIFEST_FORMATS.has(format) || !path.isAbsolute(manifestPath)) {
|
|
69
|
+
throw new Error(`Unsupported manifest path for project scan: ${manifestPath}`);
|
|
70
|
+
}
|
|
71
|
+
return ["--lockfile", `${format}:${manifestPath}`];
|
|
72
|
+
});
|
|
73
|
+
}
|
|
42
74
|
const FIXED_ARTIFACT_ARGS = [
|
|
43
75
|
"scan", "source", "--format", "json", "--all-packages", "--no-ignore",
|
|
44
76
|
"--experimental-no-default-plugins", "--experimental-plugins", "java/archive",
|
|
@@ -48,13 +80,20 @@ const FIXED_SBOM_ARGS = [
|
|
|
48
80
|
"--experimental-no-default-plugins", "--experimental-plugins", "sbom",
|
|
49
81
|
];
|
|
50
82
|
const SCAN_ARGS = { project: FIXED_SCAN_ARGS, artifact: FIXED_ARTIFACT_ARGS, sbom: FIXED_SBOM_ARGS };
|
|
83
|
+
/** 外部解決を行うのはマニフェストを読むprojectモードだけ(artifact/sbomはOSV照会のみ) */
|
|
84
|
+
export function buildOsvScanArgs(mode, noRemoteResolution) {
|
|
85
|
+
const args = [...SCAN_ARGS[mode]];
|
|
86
|
+
if (mode === "project" && noRemoteResolution)
|
|
87
|
+
args.push(NO_RESOLVE_ARG);
|
|
88
|
+
return args;
|
|
89
|
+
}
|
|
51
90
|
const EXIT_NO_VULNS = 0;
|
|
52
91
|
const EXIT_VULNS_FOUND = 1;
|
|
53
92
|
const EXIT_NO_PACKAGES = 128;
|
|
54
|
-
function execOsvScanner(binaryPath, targetPaths, timeoutMs, maxOutputBytes,
|
|
93
|
+
function execOsvScanner(binaryPath, targetPaths, timeoutMs, maxOutputBytes, scanArgs) {
|
|
55
94
|
return new Promise((resolve, reject) => {
|
|
56
95
|
const child = spawn(binaryPath, [
|
|
57
|
-
...
|
|
96
|
+
...scanArgs, ...targetPaths,
|
|
58
97
|
], {
|
|
59
98
|
shell: false,
|
|
60
99
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -107,13 +146,16 @@ function execOsvScanner(binaryPath, targetPaths, timeoutMs, maxOutputBytes, mode
|
|
|
107
146
|
});
|
|
108
147
|
}
|
|
109
148
|
/**
|
|
110
|
-
*
|
|
149
|
+
* 検出済みマニフェストだけをOSV-Scannerでスキャンし、整形済みレポートを返す。
|
|
111
150
|
*
|
|
112
|
-
* @param
|
|
113
|
-
*
|
|
151
|
+
* @param manifestPaths **`detectJavaProject`が検出したマニフェストの絶対パス**
|
|
152
|
+
* (このレイヤーではスキャン範囲の検証を行わない。ディレクトリは渡さない)
|
|
114
153
|
*/
|
|
115
|
-
export async function runOsvScan(
|
|
116
|
-
|
|
154
|
+
export async function runOsvScan(manifestPaths, options = {}) {
|
|
155
|
+
if (manifestPaths.length === 0) {
|
|
156
|
+
throw new ScanToolError("no_manifest_found", "スキャン対象のマニフェストがありません");
|
|
157
|
+
}
|
|
158
|
+
return parseOsvScanOutput(await runScan(manifestPaths, options, "project"));
|
|
117
159
|
}
|
|
118
160
|
/** Accept only the exact absolute files enumerated by detectJavaArtifacts. */
|
|
119
161
|
export async function runOsvArtifactScan(artifactPaths, options = {}) {
|
|
@@ -141,9 +183,9 @@ async function runScan(targetPaths, options, mode) {
|
|
|
141
183
|
}
|
|
142
184
|
async function runOsvScanUnguarded(targetPaths, options, mode) {
|
|
143
185
|
const binaryPath = options.binaryPath ?? (await resolveOsvScannerBinary());
|
|
144
|
-
const result = await execOsvScanner(binaryPath, targetPaths, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES, mode);
|
|
186
|
+
const result = await execOsvScanner(binaryPath, mode === "project" ? buildProjectTargetArgs(targetPaths) : targetPaths, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES, buildOsvScanArgs(mode, isRemoteResolutionDisabled(options)));
|
|
145
187
|
if (result.exitCode === EXIT_NO_PACKAGES && mode === "project") {
|
|
146
|
-
throw new ScanToolError("no_packages_found", `OSV-Scanner
|
|
188
|
+
throw new ScanToolError("no_packages_found", `OSV-Scannerがスキャン対象のパッケージを検出できませんでした(マニフェスト${targetPaths.length}件。pom.xmlに依存関係が定義されているか確認してください)`, result.stderr);
|
|
147
189
|
}
|
|
148
190
|
if (mode !== "project" && result.exitCode === EXIT_NO_PACKAGES && result.stdout.trim() === "") {
|
|
149
191
|
return { results: [] };
|
package/dist/osv/suggestFix.js
CHANGED
|
@@ -16,6 +16,32 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { compareMavenVersions, mavenVersionSeries } from "../utils/mavenVersion.js";
|
|
18
18
|
import { candidateStatus } from "./affectedVersions.js";
|
|
19
|
+
/** バージョン比較・影響範囲の検証が対応済みのエコシステム */
|
|
20
|
+
const SUPPORTED_ECOSYSTEMS = new Set(["Maven"]);
|
|
21
|
+
/**
|
|
22
|
+
* 未対応エコシステムは推奨を出さず、CVEをunfixedにも数えない。
|
|
23
|
+
* 修正版の抽出がMaven専用のため、そのまま処理すると修正版のある脆弱性を
|
|
24
|
+
* 「修正版なし」と誤表示する(v0.3.3で確認した不具合)。
|
|
25
|
+
*/
|
|
26
|
+
function unsupportedEcosystemSuggestion(pkg) {
|
|
27
|
+
return {
|
|
28
|
+
package: pkg.name,
|
|
29
|
+
current_version: pkg.version,
|
|
30
|
+
ecosystem: pkg.ecosystem,
|
|
31
|
+
recommended_upgrade: null,
|
|
32
|
+
upgrade_tier: null,
|
|
33
|
+
upgrade_note: `${pkg.ecosystem}の修正版推奨には未対応です(修正版の有無は判定していません)。` +
|
|
34
|
+
"各脆弱性の修正版はexplain_vulnerabilityで確認してください",
|
|
35
|
+
per_cve_detail: pkg.vulnerabilities.map((vuln) => ({
|
|
36
|
+
id: vuln.id,
|
|
37
|
+
cve: vuln.cve,
|
|
38
|
+
severity: vuln.severity,
|
|
39
|
+
fixed_in: null,
|
|
40
|
+
tier: "unsupported",
|
|
41
|
+
})),
|
|
42
|
+
verification: "unsupported_ecosystem",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
19
45
|
function classifyTier(currentSeries, candidate) {
|
|
20
46
|
const candidateSeries = mavenVersionSeries(candidate);
|
|
21
47
|
if (currentSeries === null || candidateSeries === null)
|
|
@@ -73,6 +99,8 @@ function buildNote(currentSeries, recommended, tier, fixableCount, unfixedCount)
|
|
|
73
99
|
}
|
|
74
100
|
/** 1パッケージ分のアップグレード提案を組み立てる。 */
|
|
75
101
|
export function suggestUpgradeForPackage(pkg) {
|
|
102
|
+
if (!SUPPORTED_ECOSYSTEMS.has(pkg.ecosystem))
|
|
103
|
+
return unsupportedEcosystemSuggestion(pkg);
|
|
76
104
|
const currentSeries = mavenVersionSeries(pkg.version);
|
|
77
105
|
const details = [];
|
|
78
106
|
let recommended = null;
|
|
@@ -2,18 +2,33 @@
|
|
|
2
2
|
* `scan_java_project`ツールのハンドラ。
|
|
3
3
|
* レスポンス形式(成功/エラー)は`toolResult.ts`参照。
|
|
4
4
|
*/
|
|
5
|
-
import { runOsvScan } from "../osv/runner.js";
|
|
5
|
+
import { isRemoteResolutionDisabled, runOsvScan } from "../osv/runner.js";
|
|
6
6
|
import { detectJavaProject } from "../utils/projectDetector.js";
|
|
7
7
|
import { errorResult, jsonResult } from "./toolResult.js";
|
|
8
|
+
const TRANSITIVE_OMITTED_WARNING = "OSV_MCP_NO_REMOTE_RESOLUTIONの設定により、マニフェスト(pom.xml)からの推移的依存の解決を省略しています。" +
|
|
9
|
+
"lockfile(gradle.lockfile)に記録された依存は対象ですが、pom.xmlに直接記載された依存の先にある推移的依存の脆弱性は含まれません。" +
|
|
10
|
+
"検出0件でも推移的依存の安全性は確認できていません";
|
|
11
|
+
/**
|
|
12
|
+
* 推移的依存の解決状態。無効時に「検出0件」を安全と誤読されないよう応答の先頭付近に置く。
|
|
13
|
+
* マニフェスト一覧の探索深さはosv-scannerの`-r`と一致しないため、一覧からpom.xmlの有無を
|
|
14
|
+
* 判定せず、無効時は条件付きの警告を常に返す。
|
|
15
|
+
*/
|
|
16
|
+
export function dependencyResolution(noRemoteResolution) {
|
|
17
|
+
return noRemoteResolution
|
|
18
|
+
? { transitive_resolution: "disabled", warning: TRANSITIVE_OMITTED_WARNING }
|
|
19
|
+
: { transitive_resolution: "enabled" };
|
|
20
|
+
}
|
|
8
21
|
export async function handleScanJavaProject(args, options = {}) {
|
|
9
22
|
try {
|
|
10
23
|
const project = await detectJavaProject(args.project_path, {
|
|
11
24
|
allowedRoot: options.allowedRoot,
|
|
12
25
|
});
|
|
13
|
-
const
|
|
26
|
+
const noRemoteResolution = isRemoteResolutionDisabled(options);
|
|
27
|
+
const report = await runOsvScan(project.manifestPaths, { ...options, noRemoteResolution });
|
|
14
28
|
return jsonResult({
|
|
15
29
|
project_dir: project.projectDir,
|
|
16
30
|
manifests: project.manifests,
|
|
31
|
+
dependency_resolution: dependencyResolution(noRemoteResolution),
|
|
17
32
|
...report,
|
|
18
33
|
});
|
|
19
34
|
}
|
package/dist/tools/suggestFix.js
CHANGED
|
@@ -3,21 +3,24 @@
|
|
|
3
3
|
* scan_java_projectと同じスキャンを実行し、脆弱なパッケージごとの
|
|
4
4
|
* 推奨アップグレードバージョン(3段階Tier)を返す。
|
|
5
5
|
*/
|
|
6
|
-
import { runOsvScan } from "../osv/runner.js";
|
|
6
|
+
import { isRemoteResolutionDisabled, runOsvScan } from "../osv/runner.js";
|
|
7
7
|
import { suggestUpgrades } from "../osv/suggestFix.js";
|
|
8
8
|
import { detectJavaProject } from "../utils/projectDetector.js";
|
|
9
|
+
import { dependencyResolution, } from "./scanJavaProject.js";
|
|
9
10
|
import { errorResult, jsonResult } from "./toolResult.js";
|
|
10
11
|
export async function handleSuggestFix(args, options = {}) {
|
|
11
12
|
try {
|
|
12
13
|
const project = await detectJavaProject(args.project_path, {
|
|
13
14
|
allowedRoot: options.allowedRoot,
|
|
14
15
|
});
|
|
15
|
-
const
|
|
16
|
+
const noRemoteResolution = isRemoteResolutionDisabled(options);
|
|
17
|
+
const report = await runOsvScan(project.manifestPaths, { ...options, noRemoteResolution });
|
|
16
18
|
const suggestions = suggestUpgrades(report.packages);
|
|
17
19
|
const unfixedVulnerabilities = suggestions.reduce((sum, s) => sum + s.per_cve_detail.filter((d) => d.tier === "unfixed").length, 0);
|
|
18
20
|
return jsonResult({
|
|
19
21
|
project_dir: project.projectDir,
|
|
20
22
|
manifests: project.manifests,
|
|
23
|
+
dependency_resolution: dependencyResolution(noRemoteResolution),
|
|
21
24
|
vulnerable_package_count: suggestions.length,
|
|
22
25
|
unfixed_vulnerability_count: unfixedVulnerabilities,
|
|
23
26
|
suggestions,
|
|
@@ -11,14 +11,18 @@
|
|
|
11
11
|
* `project_path`はLLM・ユーザー由来の信頼できない入力として扱う:
|
|
12
12
|
* - `realpath`で正規化し、シンボリックリンクを解決した実体パスで判定する
|
|
13
13
|
* - `allowedRoot`指定時は、解決後のパスがその配下にあることを検証する(パストラバーサル対策)
|
|
14
|
-
* -
|
|
14
|
+
* - マニフェスト探索はエントリ数・件数・深さに上限を設け、シンボリックリンクのディレクトリは辿らない
|
|
15
|
+
*
|
|
16
|
+
* 検出したマニフェストが、そのままOSV-Scannerのスキャン範囲になる(ディレクトリは渡さない)。
|
|
17
|
+
* そのため探索を深さで打ち切らず(Javaのソースツリーは深い)、上限に達した場合は
|
|
18
|
+
* 結果を黙って欠落させずにエラーにする。
|
|
15
19
|
*/
|
|
16
20
|
import { readdir, realpath, stat } from "node:fs/promises";
|
|
17
21
|
import path from "node:path";
|
|
18
22
|
import { ScanToolError } from "../errors.js";
|
|
19
|
-
const DEFAULT_MAX_DEPTH =
|
|
20
|
-
|
|
21
|
-
const
|
|
23
|
+
const DEFAULT_MAX_DEPTH = 64;
|
|
24
|
+
const DEFAULT_MAX_ENTRIES = 200_000;
|
|
25
|
+
const DEFAULT_MAX_MANIFESTS = 1_000;
|
|
22
26
|
/** ビルド成果物・VCS等、マニフェスト探索でスキップするディレクトリ */
|
|
23
27
|
const SKIPPED_DIRS = new Set([".git", "node_modules", "target", "build", ".idea", ".vscode"]);
|
|
24
28
|
/** OSV-Scannerがスキャンできるマニフェスト(実機確認済み) */
|
|
@@ -48,12 +52,19 @@ function assertInsideAllowedRoot(resolvedDir, allowedRootReal) {
|
|
|
48
52
|
throw new ScanToolError("path_outside_allowed_root", `指定されたパスは許可されたディレクトリ(${allowedRootReal})の外にあります`);
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
function searchLimitError(reason) {
|
|
56
|
+
return new ScanToolError("manifest_search_limit_exceeded", `マニフェスト探索が上限(${reason})に達したため、スキャンを中止しました。` +
|
|
57
|
+
"結果の欠落を避けるため途中までの結果は返しません。より狭いディレクトリ、またはpom.xml / gradle.lockfileを直接指定してください");
|
|
58
|
+
}
|
|
59
|
+
/** 上限付きでマニフェストを探索する。シンボリックリンクは辿らない。上限到達はエラー。 */
|
|
60
|
+
async function findManifests(rootDir, limits) {
|
|
53
61
|
const manifests = [];
|
|
54
62
|
let gradleBuildFileFound = false;
|
|
63
|
+
let visited = 0;
|
|
55
64
|
let currentLevel = [rootDir];
|
|
56
|
-
for (let depth = 1;
|
|
65
|
+
for (let depth = 1; currentLevel.length > 0; depth++) {
|
|
66
|
+
if (depth > limits.maxDepth)
|
|
67
|
+
throw searchLimitError(`深さ${limits.maxDepth}`);
|
|
57
68
|
const nextLevel = [];
|
|
58
69
|
for (const dir of currentLevel) {
|
|
59
70
|
let entries;
|
|
@@ -64,10 +75,12 @@ async function findManifests(rootDir, maxDepth) {
|
|
|
64
75
|
continue; // 読めないディレクトリはスキップ(権限不足等)
|
|
65
76
|
}
|
|
66
77
|
for (const entry of entries) {
|
|
78
|
+
if (++visited > limits.maxEntries)
|
|
79
|
+
throw searchLimitError(`${limits.maxEntries}エントリ`);
|
|
67
80
|
if (entry.isFile() && MANIFEST_FILENAMES.has(entry.name)) {
|
|
81
|
+
if (manifests.length >= limits.maxManifests)
|
|
82
|
+
throw searchLimitError(`マニフェスト${limits.maxManifests}件`);
|
|
68
83
|
manifests.push(path.relative(rootDir, path.join(dir, entry.name)));
|
|
69
|
-
if (manifests.length >= MAX_MANIFESTS)
|
|
70
|
-
return { manifests, gradleBuildFileFound };
|
|
71
84
|
}
|
|
72
85
|
else if (entry.isFile() && GRADLE_BUILD_FILENAMES.has(entry.name)) {
|
|
73
86
|
gradleBuildFileFound = true;
|
|
@@ -93,14 +106,21 @@ export async function detectJavaProject(inputPath, options = {}) {
|
|
|
93
106
|
}
|
|
94
107
|
const resolved = await resolveExistingPath(inputPath);
|
|
95
108
|
const stats = await stat(resolved);
|
|
109
|
+
// マニフェストの直接指定は、そのファイル1件だけをスキャン範囲にする。
|
|
110
|
+
// 親ディレクトリを探索しないため、探索上限エラーの回避手段として使える
|
|
111
|
+
if (stats.isFile() && MANIFEST_FILENAMES.has(path.basename(resolved))) {
|
|
112
|
+
const projectDir = path.dirname(resolved);
|
|
113
|
+
if (options.allowedRoot !== undefined) {
|
|
114
|
+
assertInsideAllowedRoot(projectDir, await resolveExistingPath(options.allowedRoot));
|
|
115
|
+
}
|
|
116
|
+
return { projectDir, manifests: [path.basename(resolved)], manifestPaths: [resolved] };
|
|
117
|
+
}
|
|
96
118
|
let projectDir;
|
|
97
119
|
if (stats.isDirectory()) {
|
|
98
120
|
projectDir = resolved;
|
|
99
121
|
}
|
|
100
|
-
else if (stats.isFile() &&
|
|
101
|
-
|
|
102
|
-
GRADLE_BUILD_FILENAMES.has(path.basename(resolved)))) {
|
|
103
|
-
// build.gradle等の直接指定も受け付け、ディレクトリとして解決する
|
|
122
|
+
else if (stats.isFile() && GRADLE_BUILD_FILENAMES.has(path.basename(resolved))) {
|
|
123
|
+
// build.gradle等の直接指定は、lockfileを探すためディレクトリとして解決する
|
|
104
124
|
// (lockfileが無ければ後段でgradle_lockfile_missingの案内になる)
|
|
105
125
|
projectDir = path.dirname(resolved);
|
|
106
126
|
}
|
|
@@ -110,13 +130,20 @@ export async function detectJavaProject(inputPath, options = {}) {
|
|
|
110
130
|
if (options.allowedRoot !== undefined) {
|
|
111
131
|
assertInsideAllowedRoot(projectDir, await resolveExistingPath(options.allowedRoot));
|
|
112
132
|
}
|
|
113
|
-
const
|
|
114
|
-
|
|
133
|
+
const { manifests, gradleBuildFileFound } = await findManifests(projectDir, {
|
|
134
|
+
maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
135
|
+
maxEntries: options.maxEntries ?? DEFAULT_MAX_ENTRIES,
|
|
136
|
+
maxManifests: options.maxManifests ?? DEFAULT_MAX_MANIFESTS,
|
|
137
|
+
});
|
|
115
138
|
if (manifests.length === 0) {
|
|
116
139
|
if (gradleBuildFileFound) {
|
|
117
140
|
throw new ScanToolError("gradle_lockfile_missing", GRADLE_LOCKFILE_GUIDANCE);
|
|
118
141
|
}
|
|
119
|
-
throw new ScanToolError("no_manifest_found", `対応マニフェスト(pom.xml / gradle.lockfile)
|
|
142
|
+
throw new ScanToolError("no_manifest_found", `対応マニフェスト(pom.xml / gradle.lockfile)が見つかりません: ${projectDir}`);
|
|
120
143
|
}
|
|
121
|
-
return {
|
|
144
|
+
return {
|
|
145
|
+
projectDir,
|
|
146
|
+
manifests,
|
|
147
|
+
manifestPaths: manifests.map((manifest) => path.join(projectDir, manifest)),
|
|
148
|
+
};
|
|
122
149
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "osv-scanner-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "MCP server that wraps Google's OSV-Scanner to scan Java projects for known vulnerabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -50,8 +50,5 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
52
52
|
"zod": "^4.4.3"
|
|
53
|
-
},
|
|
54
|
-
"overrides": {
|
|
55
|
-
"@hono/node-server": "^2.0.11"
|
|
56
53
|
}
|
|
57
54
|
}
|