osv-scanner-mcp 0.3.3 → 0.4.0
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 +92 -10
- package/dist/index.js +17 -1
- package/dist/osv/binaryDownloader.js +5 -1
- package/dist/osv/runner.js +30 -12
- package/dist/osv/scanReport.js +10 -2
- package/dist/osv/suggestFix.js +28 -0
- package/dist/tools/scanJavaProject.js +20 -5
- package/dist/tools/scanProject.js +129 -0
- package/dist/tools/suggestFix.js +3 -2
- package/dist/utils/manifestDetector.js +226 -0
- package/dist/utils/manifestFormats.js +23 -0
- package/dist/utils/pomParent.js +206 -0
- package/dist/utils/projectDetector.js +58 -63
- package/dist/utils/projectWalk.js +77 -0
- package/dist/utils/requirementsFile.js +194 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](package.json)
|
|
7
7
|
|
|
8
|
-
Google製 [OSV-Scanner](https://github.com/google/osv-scanner) をラップするMCPサーバーです。Claude等のMCP
|
|
8
|
+
Google製 [OSV-Scanner](https://github.com/google/osv-scanner) をラップするMCPサーバーです。Claude等のMCPクライアントから「このプロジェクトの脆弱性をチェックして」と自然言語で依頼するだけで、依存ライブラリの既知の脆弱性(CVE / GHSA)を深刻度順のレポートで取得できます。
|
|
9
9
|
|
|
10
|
-
> **ステータス**: [npmで公開中](https://www.npmjs.com/package/osv-scanner-mcp)(`npx -y osv-scanner-mcp`)。Maven
|
|
10
|
+
> **ステータス**: [npmで公開中](https://www.npmjs.com/package/osv-scanner-mcp)(`npx -y osv-scanner-mcp`)。Java(Maven / Gradle)、JavaScript(npm / yarn / pnpm / bun)、Python(Poetry / uv / Pipenv / PDM / requirements.txt)、Go のlockfileに対応しています(修正版の推奨はJavaのみ)。MCPクライアントは Claude Code / Claude Desktop / Codex CLI / Antigravity / VS Code(GitHub Copilot)での利用手順を用意しています。
|
|
11
11
|
|
|
12
12
|
## 特徴
|
|
13
13
|
|
|
14
|
-
-
|
|
14
|
+
- **複数言語のワンショットスキャン**: `scan_project` ツールにプロジェクトパスを渡すだけで、Java・JavaScript・Python・Goのlockfileを検出してまとめてスキャンします。lockfileが無い・バージョンが未固定などでスキャンできなかった依存は、応答先頭の `coverage` で明示します
|
|
15
|
+
- **Javaプロジェクトのスキャン**: `scan_java_project` ツールでJava(Maven / Gradle)のマニフェストだけを対象に、検出→スキャン→整形済みレポートまで一気に返します
|
|
15
16
|
- **JAR/WAR実体スキャン**: `scan_java_artifact` ツールで、lockfileが無い・shaded/fat JARしか手元にないプロジェクトでもアーカイブ内メタデータから既知の脆弱性を検出します(ベストエフォート同定であることを明示するcoverage情報付き)
|
|
16
17
|
- **SBOM入力スキャン**: `scan_sbom` ツールでCycloneDX/SPDXのJSON SBOMに記録された依存を検査します。SBOMの網羅性や実成果物との一致は未検証であることを明示します
|
|
17
18
|
- **深刻度順のレポート**: パッケージごとに脆弱性をCVSSスコア順に整理し、5段階の深刻度ラベル(critical / high / medium / low / unknown)とサマリ集計付きで返します
|
|
@@ -123,7 +124,7 @@ npm run build
|
|
|
123
124
|
| `OSV_MCP_MAX_CONCURRENT_SCANS` | 同時実行できるスキャン数の上限(デフォルト `2`、最大 `16`)。超過したリクエストは待たずに即時エラーになります |
|
|
124
125
|
| `OSV_MCP_AUTO_DOWNLOAD` | `0` または `false` でバイナリの自動ダウンロードを無効化(デフォルト有効) |
|
|
125
126
|
| `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` に示されます。詳細は[通信先とプライバシー](#通信先とプライバシー) |
|
|
127
|
+
| `OSV_MCP_NO_REMOTE_RESOLUTION` | `1` または `true` 指定時、`pom.xml` と `requirements.txt` の推移的依存を deps.dev で解決しません。**止まるのは deps.dev への送信だけで、脆弱性照会のためパッケージの名前とバージョンは引き続き `api.osv.dev` に送られます**。推移的依存の脆弱性は検出できなくなり、その旨が応答の `dependency_resolution.warning` に示されます。詳細は[通信先とプライバシー](#通信先とプライバシー) |
|
|
127
128
|
|
|
128
129
|
> **推奨**: `OSV_MCP_ALLOWED_ROOT` は未設定でも動作しますが、その場合は任意の絶対パスをスキャンできてしまいます。悪意ある指示(プロンプトインジェクション)経由で意図しないディレクトリをスキャンさせられる経路を塞ぐため、プロジェクト置き場のルート(例: `~/projects`)を設定しておくことを推奨します。各クライアントの設定で `"env": {"OSV_MCP_ALLOWED_ROOT": "/Users/you/projects"}` のように渡せます(Codex CLIのTOMLでは `[mcp_servers.osv-scanner.env]` セクション)。
|
|
129
130
|
|
|
@@ -140,19 +141,79 @@ osv-scanner v2.4.0 で接続先を実機確認した結果です(2026-10-07)。
|
|
|
140
141
|
|
|
141
142
|
| 操作 | 接続先 | 送られる情報 |
|
|
142
143
|
|---|---|---|
|
|
143
|
-
| `pom.xml` のスキャン(`scan_java_project` / `suggest_fix`) | `api.osv.dev`、**`api.deps.dev`** | パッケージの名前とバージョン。deps.dev には推移的依存の解決のため、`pom.xml` に宣言された依存(社内パッケージを含む)の名前とバージョンが送られます |
|
|
144
|
-
| `
|
|
144
|
+
| `pom.xml` のスキャン(`scan_project` / `scan_java_project` / `suggest_fix`) | `api.osv.dev`、**`api.deps.dev`** | パッケージの名前とバージョン。deps.dev には推移的依存の解決のため、`pom.xml` に宣言された依存(社内パッケージを含む)の名前とバージョンが送られます |
|
|
145
|
+
| `requirements.txt` のスキャン(`scan_project`) | `api.osv.dev`、**`api.deps.dev`** | `pom.xml` と同じく、推移的依存の解決のため記載された依存の名前とバージョンが deps.dev に送られます。`--index-url` 等に書かれた取得先へは接続しません |
|
|
146
|
+
| lockfileのスキャン(`gradle.lockfile`、`package-lock.json` 等のnpm系、`poetry.lock` 等のPython系、`go.mod`) | `api.osv.dev` | パッケージの名前とバージョン(lockfileに全依存が記載済みのため、解決のための外部接続はしません) |
|
|
145
147
|
| `scan_java_artifact` / `scan_sbom` | `api.osv.dev` | 同定できたパッケージの名前とバージョン |
|
|
146
148
|
| `explain_vulnerability` | `api.osv.dev` | 指定した脆弱性ID |
|
|
147
149
|
| バイナリの自動ダウンロード(初回のみ) | GitHub(公式Releases) | なし(ピン留めしたバージョンのバイナリを取得) |
|
|
148
150
|
|
|
149
151
|
api.osv.dev と deps.dev はどちらも Google が運営するサービスです。**どの設定でも、スキャンしたパッケージの名前とバージョンは脆弱性照会のため `api.osv.dev` に送られます**(オフラインでの照会には対応していません)。
|
|
150
152
|
|
|
151
|
-
- **deps.dev への送信を止めたい場合**: `OSV_MCP_NO_REMOTE_RESOLUTION=1` を設定すると、`pom.xml` の推移的依存を解決しなくなり、接続先は `api.osv.dev` だけになります。止まるのは deps.dev への送信だけで、OSV
|
|
153
|
+
- **deps.dev への送信を止めたい場合**: `OSV_MCP_NO_REMOTE_RESOLUTION=1` を設定すると、`pom.xml` と `requirements.txt` の推移的依存を解決しなくなり、接続先は `api.osv.dev` だけになります。止まるのは deps.dev への送信だけで、OSV への送信は続きます。また直接書いた依存しかスキャンされず、**推移的依存の脆弱性を見落とします**。この状態は `scan_project` / `scan_java_project` / `suggest_fix` の応答の `dependency_resolution` に `transitive_resolution: "disabled"` と警告で示されるので、検出0件と区別できます。推移的依存も含めて deps.dev を使わずにスキャンするには、lockfile方式(`gradle.lockfile`、`poetry.lock` 等)を使ってください
|
|
152
154
|
- **任意の取得先には接続しません**: osv-scanner の `--data-source native` モードは、スキャン対象の `pom.xml` の `<repositories>` に書かれた任意のURLへ接続します(悪意あるpom.xmlで攻撃者のサーバーへ通信させられる)。本サーバーはこのモードを使わず、`deps.dev` を明示指定しています
|
|
153
155
|
|
|
154
156
|
## 提供ツール
|
|
155
157
|
|
|
158
|
+
### `scan_project`
|
|
159
|
+
|
|
160
|
+
プロジェクト内のlockfile・マニフェストを検出し、Java / JavaScript / Python / Go の依存をまとめてスキャンします。パッケージマネージャーやビルドは実行しません。
|
|
161
|
+
|
|
162
|
+
**入力**
|
|
163
|
+
|
|
164
|
+
| パラメータ | 型 | 説明 |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `project_path` | string | スキャン対象のプロジェクトディレクトリ、または対応するlockfile・マニフェストの絶対パス(直接指定したファイルはそれ1件だけをスキャン) |
|
|
167
|
+
|
|
168
|
+
**対応ファイル**
|
|
169
|
+
|
|
170
|
+
| エコシステム | ファイル |
|
|
171
|
+
|---|---|
|
|
172
|
+
| Java(Maven) | `pom.xml`、`gradle.lockfile`、`buildscript-gradle.lockfile` |
|
|
173
|
+
| JavaScript(npm) | `package-lock.json`、`npm-shrinkwrap.json`、`yarn.lock`、`pnpm-lock.yaml`、`bun.lock`(テキスト形式) |
|
|
174
|
+
| Python(PyPI) | `poetry.lock`、`uv.lock`、`Pipfile.lock`、`pdm.lock`、`requirements.txt`(`requirements-dev.txt` 等も) |
|
|
175
|
+
| Go | `go.mod` |
|
|
176
|
+
|
|
177
|
+
`.git`、`node_modules`、`target`、`build`、`.venv`、`venv`、`site-packages`、`__pycache__`、`.tox`、`vendor` とシンボリックリンクは探索しません。検出したファイルだけを形式を明示してOSV-Scannerに渡します(応答の `coverage.manifests` がそのままスキャン範囲です)。探索上限は `scan_java_project` と同じです。
|
|
178
|
+
|
|
179
|
+
**requirements.txtは元のファイルをOSV-Scannerに渡しません**。本サーバーが解析し、解釈できた依存の行だけを `名前==版` 等の単純な形に直して専用の一時コピーに書き、それをスキャンします(スキャン後に削除)。OSV-Scannerは取り込み指定を独自に解釈してたどる(`- r ../x.txt` のような空白入りも取り込みとみなし、スキャン範囲の外のファイルを読む)ため、コピーには取り込み指定やオプションを一切含めません。取り込み(`-r` / `--requirement`)は、プロジェクトディレクトリ内の取り込み先だけを本サーバーが展開してコピーに含めます。
|
|
180
|
+
|
|
181
|
+
**出力の読み方**
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"project_dir": "/path/to/project",
|
|
186
|
+
"dependency_resolution": { "transitive_resolution": "enabled" },
|
|
187
|
+
"coverage": {
|
|
188
|
+
"complete": false,
|
|
189
|
+
"warning": "一部の依存はスキャンされていないか、版を推測してスキャンしています(…)",
|
|
190
|
+
"manifests": [{ "path": "web/package-lock.json", "ecosystem": "npm", "format": "package-lock.json" }],
|
|
191
|
+
"lockfile_missing": [{ "path": "svc/package.json", "ecosystem": "npm", "status": "missing", "hint": "lockfileがありません。…" }],
|
|
192
|
+
"unpinned_requirements": [{ "file": "py/requirements.txt", "line": 2, "name": "Jinja2", "specifier": ">=2.0", "kind": "lower_bound" }],
|
|
193
|
+
"unscannable_requirements": [
|
|
194
|
+
{ "file": "py/requirements.txt", "line": 4, "text": "-e git+https://…", "reason": "編集可能インストール(-e)はスキャンされません" },
|
|
195
|
+
{ "file": "py/requirements.txt", "line": 5, "text": "-r ../shared/base.txt", "reason": "取り込み先がプロジェクトディレクトリの外のため展開しません" }
|
|
196
|
+
],
|
|
197
|
+
"skipped_files": []
|
|
198
|
+
},
|
|
199
|
+
"ecosystem_breakdown": { "npm": { "manifests": 1, "vulnerable_package_count": 2, "vulnerability_count": 4 } },
|
|
200
|
+
"vulnerable_package_count": 2,
|
|
201
|
+
"vulnerability_count": 4,
|
|
202
|
+
"severity_breakdown": { "critical": 0, "high": 2, "medium": 2, "low": 0, "unknown": 0 },
|
|
203
|
+
"packages": [{ "name": "minimist", "version": "1.2.5", "ecosystem": "npm", "dependency_groups": ["dev"], "vulnerabilities": [] }]
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
- **`coverage` を必ず確認してください**。`complete: false` の場合、一部の依存はスキャンされていないため、検出0件でも安全とは言えません
|
|
208
|
+
- `lockfile_missing`: lockfileの無いマニフェスト(`package.json`、`pyproject.toml`、`Pipfile`、`setup.py`、`build.gradle` 等)。同じディレクトリに同じエコシステムのlockfileがあれば記録しません。上位のディレクトリのlockfileだけがある場合は、`package-lock.json`(v2以降)にそのディレクトリが収録されていることを確認できたときだけ記録しません(npm workspaces)。収録されていなければ `status: "missing"`、確認できない形式(yarn.lock、Python系等)なら `status: "unconfirmed"` として記録します。`hint` の手順でlockfileを生成してから再スキャンしてください(生成は信頼できる環境で)
|
|
209
|
+
- `unpinned_requirements`: requirements.txtのうち、版を固定していない行。`kind` は `unpinned`(版の指定なし)・`range`(`>`、`<`、`!=`、`==1.*`、範囲の組み合わせ等)・`lower_bound`(`>=`、`~=`)。`unpinned` と `range` の行はOSV-Scannerがスキャンせず、`lower_bound` の行は下限の版を使用中の版とみなしてスキャンします(該当パッケージには `version_is_lower_bound: true` が付き、実際の版とは異なる可能性があります)
|
|
210
|
+
- `unscannable_requirements`: スキャンされない行と理由。`-e`、`name @ URL`、パス指定、展開しなかった取り込み(プロジェクトディレクトリの外・存在しない・URL・上限超過)、制約ファイル(`-c`、適用しません)、解釈できないオプションや版の指定。解釈できない行は無視せず、ここに記録します
|
|
211
|
+
- `skipped_files`: スキャン対象から外したファイルと理由(requirements.txt自体が読めない・1MiBを超える場合、`pom.xml` の親POMが許可ルートの外を参照する場合。後者は[親POMの扱い](#親pomの扱い)を参照)
|
|
212
|
+
- 各一覧は200件までで、超えた分の件数を `omitted_items` に返します
|
|
213
|
+
- `ecosystem_breakdown` は、脆弱性0件のエコシステムも含めて「スキャンした」ことを示します
|
|
214
|
+
- `dependency_groups` はOSV-Scannerが付けた依存グループ(例: `dev`)の生の値です。lockfileの形式によって欠落・不正確なため(pnpmでは付かず、pdmでは `optional` になる等)、参考情報として扱ってください
|
|
215
|
+
- 修正版の推奨(`suggest_fix`)は現在Javaのみ対応です
|
|
216
|
+
|
|
156
217
|
### `scan_java_project`
|
|
157
218
|
|
|
158
219
|
Java(Maven)プロジェクトをスキャンし、既知の脆弱性レポートを返します。
|
|
@@ -165,6 +226,22 @@ Java(Maven)プロジェクトをスキャンし、既知の脆弱性レポート
|
|
|
165
226
|
|
|
166
227
|
> **Gradleプロジェクトについて**: 本ツールは**lockfile方式**のみ対応です(ビルド実行方式は build.gradle の任意コード実行を伴うため、セキュリティ上の理由から採用していません)。`gradle.lockfile` が無い場合は `./gradlew dependencies --write-locks` で生成してください(依存ロック未設定の場合は `build.gradle` に `dependencyLocking { lockAllConfigurations() }` の追加が必要です)。
|
|
167
228
|
|
|
229
|
+
> **スキャン範囲**: ディレクトリを指定すると、配下の `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` などのマニフェストを直接指定した場合は、ディレクトリを探索せず**そのファイルだけ**をスキャンします(上限に達した場合の回避手段としても使えます)。
|
|
230
|
+
|
|
231
|
+
#### 親POMの扱い
|
|
232
|
+
|
|
233
|
+
OSV-Scannerは `pom.xml` の `<parent>` が参照する親POM(`<relativePath>` の指すファイル。省略時はMavenの既定どおり `../pom.xml`)を読み、親の親もたどって、そこに書かれた依存を結果に含めます。サブモジュールだけをスキャンしても親から引き継いだ依存を検出できるのはこのためです。
|
|
234
|
+
|
|
235
|
+
`OSV_MCP_ALLOWED_ROOT` を設定している場合、親POMの連鎖のどこかが**許可ルートの外**のファイルを参照する `pom.xml` は、スキャン対象から外します(許可ルート外のファイルの内容を結果や照会先に出さないため)。外したファイルは応答の `skipped_manifests`(`scan_project` では `coverage.skipped_files`)に理由付きで示し、`scope_warning` で「検出0件でも安全とは判断しない」旨を伝えます。全件が外れた場合や、該当する `pom.xml` を直接指定した場合は `path_outside_allowed_root` を返します。
|
|
236
|
+
|
|
237
|
+
- 許可ルート内の親POMは従来どおり読みます(サブモジュールのスキャンは許可ルート内なら引き続き使えます)
|
|
238
|
+
- `<relativePath/>`(空)はローカルの親POMを参照しないため対象外です
|
|
239
|
+
- 親POMの指定は、OSV-Scanner(Go)のXMLの解釈に合わせて読みます。要素は名前空間の接頭辞に関係なく要素名で照合し(`<m:parent>` も親として扱う)、ルート要素の直下の `parent` だけを対象にし、文字参照を展開します
|
|
240
|
+
- XMLの仕様どおり、解析前に改行(CRLF・CR)をLFに正規化します
|
|
241
|
+
- 同じ解釈を保証できない場合は除外します: ルート直下の `parent` や `relativePath` が複数ある、CDATA・DOCTYPE・未知の実体参照・プロパティ参照(`${...}`)がある、タグが閉じていない、UTF-8として読めない、`relativePath` に制御文字(改行・タブ等)・通常の空白以外の空白・書式文字が含まれる(通常の空白や日本語のディレクトリ名は使えます)
|
|
242
|
+
- 親のGAVが一致しなければOSV-Scannerは読みませんが、本サーバーはGAVを確認せず、許可ルートの外に参照先のファイルがあれば安全側に除外します
|
|
243
|
+
- `OSV_MCP_ALLOWED_ROOT` が未設定の場合は任意の絶対パスをスキャンできる状態のため、この検証は行いません
|
|
244
|
+
|
|
168
245
|
**出力(成功時)**
|
|
169
246
|
|
|
170
247
|
```json
|
|
@@ -294,6 +371,7 @@ OSV-Scanner 2.4.0の `java/archive` プラグインを使用し、ネストJAR
|
|
|
294
371
|
- MavenのOSV `ECOSYSTEM` 範囲(`introduced` / `fixed` / `last_affected` / 上限なし)を照合します。`versions` に明示された影響も確認します。範囲欠落・不正・未対応形式・`limit` による不完全な情報では安全と推定せず、候補を検証できなければ `recommended_upgrade: null`、`verification: "no_verified_candidate"` を返します。
|
|
295
372
|
- 推奨時は `verification: "verified"`、CVEごとの `recommended_status` は `affected` / `not_affected` / `unknown` です。推奨保留時は `not_evaluated` になります。`per_cve_detail.fixed_in` は各CVE単独の候補であり、最終推奨先の判定は `recommended_status` を参照してください。
|
|
296
373
|
- 現在より新しい修正版候補がないCVEは `tier: "unfixed"` として推奨の修正対象から除外します(情報欠落を含む場合があります)。除外したCVEも推奨先で判定し、その状態を表示します。全CVEがunfixedの場合も `recommended_upgrade` は `null` です。
|
|
374
|
+
- 修正版の推奨はMavenのみ対応です。Maven以外のパッケージは `verification: "unsupported_ecosystem"`、CVEごとの `tier: "unsupported"` を返し、`unfixed` には数えません(修正版の有無は判定していないため。修正版は `explain_vulnerability` で確認できます)。
|
|
297
375
|
|
|
298
376
|
### `explain_vulnerability`
|
|
299
377
|
|
|
@@ -317,7 +395,7 @@ OSV-Scanner 2.4.0の `java/archive` プラグインを使用し、ネストJAR
|
|
|
317
395
|
{
|
|
318
396
|
"error": {
|
|
319
397
|
"kind": "no_manifest_found",
|
|
320
|
-
"message": "対応マニフェスト(pom.xml / gradle.lockfile)
|
|
398
|
+
"message": "対応マニフェスト(pom.xml / gradle.lockfile)が見つかりません: /path/to/project"
|
|
321
399
|
}
|
|
322
400
|
}
|
|
323
401
|
```
|
|
@@ -327,10 +405,11 @@ OSV-Scanner 2.4.0の `java/archive` プラグインを使用し、ネストJAR
|
|
|
327
405
|
| `binary_not_found` | OSV-Scannerが見つからない(インストール案内をmessageに含む) |
|
|
328
406
|
| `project_not_found` | 指定パスが存在しない・ディレクトリ/pom.xmlでない |
|
|
329
407
|
| `no_manifest_found` | 対応マニフェスト(pom.xml / gradle.lockfile)が見つからない |
|
|
408
|
+
| `manifest_search_limit_exceeded` | マニフェスト探索が上限(20万エントリ・1,000マニフェスト)に達した。より狭いディレクトリかマニフェストを直接指定する |
|
|
330
409
|
| `binary_download_failed` | バイナリのダウンロード失敗(未対応プラットフォーム含む) |
|
|
331
410
|
| `binary_checksum_mismatch` | ダウンロードしたバイナリのチェックサム不一致(改ざん/破損の可能性) |
|
|
332
411
|
| `gradle_lockfile_missing` | Gradleプロジェクトだがgradle.lockfileが無い(生成手順をmessageで案内) |
|
|
333
|
-
| `path_outside_allowed_root` | `OSV_MCP_ALLOWED_ROOT` の外を指している |
|
|
412
|
+
| `path_outside_allowed_root` | `OSV_MCP_ALLOWED_ROOT` の外を指している(マニフェストの親POMが許可ルートの外を参照し、スキャンできるマニフェストが残らない場合を含む) |
|
|
334
413
|
| `no_packages_found` | スキャン対象パッケージなし(依存関係が未定義のpom.xml等) |
|
|
335
414
|
| `scan_failed` | OSV-Scannerが異常終了(stderr抜粋を`detail`に含む) |
|
|
336
415
|
| `scan_timeout` | タイムアウト(デフォルト120秒) |
|
|
@@ -348,7 +427,7 @@ OSV-Scanner 2.4.0の `java/archive` プラグインを使用し、ネストJAR
|
|
|
348
427
|
|
|
349
428
|
- **サプライチェーン対策**: バイナリの自動ダウンロードは公式GitHub Releasesに限定し、バージョンをピン留め。**パッケージに埋め込まれたSHA256チェックサム**で検証します(配布元のSHA256SUMSファイルは信用しないため、リリース側が改ざんされても検出可能)。検証合格まで実行権限を与えず、キャッシュ済みバイナリも使用のたびに再検証します。`OSV_MCP_PREFER_DOWNLOAD=1` でPATH上の未検証バイナリを使わない運用も選べます
|
|
350
429
|
- **コマンドインジェクション対策**: シェルを経由しない `spawn` + 引数配列で実行。OSV-Scannerへの引数は固定リストのみで、可変部は検証済み絶対パス1つだけ
|
|
351
|
-
- **パストラバーサル対策**: 入力パスは `realpath` でシンボリックリンク解決後に境界チェック。pom.xml
|
|
430
|
+
- **パストラバーサル対策**: 入力パスは `realpath` でシンボリックリンク解決後に境界チェック。pom.xml探索ではシンボリックリンクを辿りません。OSV-Scannerにはディレクトリを渡さず、検出したマニフェストだけを形式を明示して個別に渡します(ディレクトリを渡すと、OSV-Scannerが同じディレクトリの `requirements.txt` も読み、その取り込み指定 `-r ../x.txt` でスキャン範囲の外のファイルを読むため)。`scan_project` でrequirements.txtをスキャンする場合は元ファイルを渡さず、解釈できた依存の行だけを正規化して書いた専用コピーをスキャンします。コピーには取り込み指定を含めないため、OSV-Scannerの取り込みの解釈と本サーバーの解析がずれても、範囲外のファイルは読まれません。`pom.xml` の親POMの連鎖が `OSV_MCP_ALLOWED_ROOT` の外を参照する場合は、その `pom.xml` をスキャン対象から外します([親POMの扱い](#親pomの扱い))
|
|
352
431
|
- **DoS対策**: タイムアウト・stdout上限・stderr抜粋上限を設定。スキャン結果は防御的にパースし、形式不正でも例外を投げません。同時実行スキャン数も上限(デフォルト2)を設け、並列リクエストによるプロセスの無制限起動を防ぎます
|
|
353
432
|
- **fail-closedな運用モード**: `OSV_MCP_REQUIRE_ALLOWED_ROOT=1` で、スキャン許可ルート未設定時にサーバーの起動自体を拒否できます
|
|
354
433
|
- **通信先の固定と明示**: osv-scannerの依存解決先は `deps.dev` を明示指定し、スキャン対象のpom.xmlが指定する任意のリポジトリへ接続するモード(`--data-source native`)は使いません(テストで保証)。通信先の一覧と、deps.devへの送信を止める `OSV_MCP_NO_REMOTE_RESOLUTION=1` は[通信先とプライバシー](#通信先とプライバシー)を参照
|
|
@@ -374,6 +453,9 @@ npm run build # dist/ へビルド
|
|
|
374
453
|
- [x] OSV-Scannerバイナリの自動ダウンロード(チェックサム検証付き)
|
|
375
454
|
- [x] Gradle対応(lockfile方式)
|
|
376
455
|
- [x] `scan_java_artifact` ツール: JAR/WAR実体スキャン(lockfileが無い・shaded/fat JARのみのプロジェクト向け)
|
|
456
|
+
- [x] `scan_project` ツール: Java / JavaScript / Python / Go のlockfileをまとめてスキャン
|
|
457
|
+
- [ ] `suggest_fix` のJavaScript / Go対応(semver)
|
|
458
|
+
- [ ] `suggest_fix` のPython対応(PEP 440)、直接/推移的依存の区別
|
|
377
459
|
|
|
378
460
|
## ライセンス
|
|
379
461
|
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { handleExplainVulnerability } from "./tools/explainVulnerability.js";
|
|
18
18
|
import { handleScanJavaProject } from "./tools/scanJavaProject.js";
|
|
19
|
+
import { handleScanProject } from "./tools/scanProject.js";
|
|
19
20
|
import { handleScanJavaArtifact } from "./tools/scanJavaArtifact.js";
|
|
20
21
|
import { handleScanSbom } from "./tools/scanSbom.js";
|
|
21
22
|
import { handleSuggestFix } from "./tools/suggestFix.js";
|
|
@@ -35,8 +36,23 @@ if (startupWarning !== null) {
|
|
|
35
36
|
// NOTE: リリース時はpackage.jsonのversionと同じ値に更新すること
|
|
36
37
|
const server = new McpServer({
|
|
37
38
|
name: "osv-scanner-mcp",
|
|
38
|
-
version: "0.
|
|
39
|
+
version: "0.4.0",
|
|
39
40
|
});
|
|
41
|
+
server.registerTool("scan_project", {
|
|
42
|
+
title: "プロジェクトの依存の脆弱性スキャン(Java / JavaScript / Python / Go)",
|
|
43
|
+
description: "プロジェクト内のlockfile・マニフェストを検出し、依存ライブラリの既知の脆弱性(CVE/GHSA)をまとめてスキャンする。" +
|
|
44
|
+
"対応: Java(pom.xml / gradle.lockfile)、JavaScript(package-lock.json / npm-shrinkwrap.json / yarn.lock / pnpm-lock.yaml / bun.lock)、" +
|
|
45
|
+
"Python(poetry.lock / uv.lock / Pipfile.lock / pdm.lock / requirements.txt)、Go(go.mod)。" +
|
|
46
|
+
"パッケージマネージャーやビルドは実行しない。" +
|
|
47
|
+
"応答先頭のcoverageを必ず確認すること: lockfileが無いマニフェスト、バージョン未固定のrequirements行、スキャン対象から外したファイルを示す。" +
|
|
48
|
+
"coverage.complete=falseの場合は、検出0件でも安全とは判断しないこと。修正版の推奨(suggest_fix)は現在Javaのみ対応。",
|
|
49
|
+
inputSchema: {
|
|
50
|
+
project_path: z
|
|
51
|
+
.string()
|
|
52
|
+
.min(1)
|
|
53
|
+
.describe("スキャン対象のプロジェクトディレクトリ、または対応するlockfile・マニフェストの絶対パス"),
|
|
54
|
+
},
|
|
55
|
+
}, async ({ project_path }) => handleScanProject({ project_path }, { allowedRoot: process.env[ALLOWED_ROOT_ENV] }));
|
|
40
56
|
server.registerTool("scan_java_project", {
|
|
41
57
|
title: "Javaプロジェクトの脆弱性スキャン",
|
|
42
58
|
description: "Java(Maven)プロジェクトをGoogle OSV-Scannerでスキャンし、依存ライブラリの既知の脆弱性(CVE/GHSA)を深刻度順のJSONレポートで返す。" +
|
|
@@ -16,7 +16,11 @@ import os from "node:os";
|
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { ScanToolError } from "../errors.js";
|
|
18
18
|
import { readResponseBytes } from "../utils/readResponseBytes.js";
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* ピン留めするOSV-Scannerのバージョン。更新時は下のチェックサムも必ず更新すること。
|
|
21
|
+
* 範囲外の読み込みを防ぐ検証はこの版の挙動に合わせているため、更新時は
|
|
22
|
+
* docs/DESIGN_TODO.md「B5」の監査と、requirements.txt・親POMの解釈の実機確認をやり直すこと
|
|
23
|
+
*/
|
|
20
24
|
export const PINNED_OSV_SCANNER_VERSION = "2.4.0";
|
|
21
25
|
/**
|
|
22
26
|
* v2.4.0公式リリースのSHA256(osv-scanner_SHA256SUMSより転記、2026-07-04取得)。
|
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,7 +15,9 @@
|
|
|
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";
|
|
20
|
+
import { isManifestFormat } from "../utils/manifestFormats.js";
|
|
16
21
|
import { resolveOsvScannerBinary } from "./binaryManager.js";
|
|
17
22
|
import { parseOsvScanOutput } from "./scanReport.js";
|
|
18
23
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
@@ -53,8 +58,17 @@ export function isRemoteResolutionDisabled(options = {}) {
|
|
|
53
58
|
* `--data-source native` はスキャン対象pom.xmlの<repositories>に書かれた任意のURLへ
|
|
54
59
|
* 接続するため使わない。既定値の変更に備えてdeps.devを明示する。
|
|
55
60
|
*/
|
|
56
|
-
const FIXED_SCAN_ARGS = ["scan", "source", "
|
|
61
|
+
const FIXED_SCAN_ARGS = ["scan", "source", "--format", "json", "--data-source", "deps.dev"];
|
|
57
62
|
const NO_RESOLVE_ARG = "--no-resolve";
|
|
63
|
+
/** 検出済みマニフェストを`--lockfile <形式>:<絶対パス>`の組にする。許可外の形式・相対パスは渡さない */
|
|
64
|
+
export function buildProjectTargetArgs(targets) {
|
|
65
|
+
return targets.flatMap((target) => {
|
|
66
|
+
if (!isManifestFormat(target.format) || !path.isAbsolute(target.path)) {
|
|
67
|
+
throw new Error(`Unsupported manifest target for project scan: ${target.format}:${target.path}`);
|
|
68
|
+
}
|
|
69
|
+
return ["--lockfile", `${target.format}:${target.path}`];
|
|
70
|
+
});
|
|
71
|
+
}
|
|
58
72
|
const FIXED_ARTIFACT_ARGS = [
|
|
59
73
|
"scan", "source", "--format", "json", "--all-packages", "--no-ignore",
|
|
60
74
|
"--experimental-no-default-plugins", "--experimental-plugins", "java/archive",
|
|
@@ -130,13 +144,16 @@ function execOsvScanner(binaryPath, targetPaths, timeoutMs, maxOutputBytes, scan
|
|
|
130
144
|
});
|
|
131
145
|
}
|
|
132
146
|
/**
|
|
133
|
-
*
|
|
147
|
+
* 検出済みマニフェストだけをOSV-Scannerでスキャンし、整形済みレポートを返す。
|
|
134
148
|
*
|
|
135
|
-
* @param
|
|
136
|
-
*
|
|
149
|
+
* @param targets **検出器(detectJavaProject / detectProject)が検出したマニフェスト**
|
|
150
|
+
* (このレイヤーではスキャン範囲の検証を行わない。ディレクトリは渡さない)
|
|
137
151
|
*/
|
|
138
|
-
export async function runOsvScan(
|
|
139
|
-
|
|
152
|
+
export async function runOsvScan(targets, options = {}) {
|
|
153
|
+
if (targets.length === 0) {
|
|
154
|
+
throw new ScanToolError("no_manifest_found", "スキャン対象のマニフェストがありません");
|
|
155
|
+
}
|
|
156
|
+
return parseOsvScanOutput(await runScan(buildProjectTargetArgs(targets), options, "project"));
|
|
140
157
|
}
|
|
141
158
|
/** Accept only the exact absolute files enumerated by detectJavaArtifacts. */
|
|
142
159
|
export async function runOsvArtifactScan(artifactPaths, options = {}) {
|
|
@@ -149,24 +166,25 @@ export async function runOsvArtifactScan(artifactPaths, options = {}) {
|
|
|
149
166
|
export async function runOsvSbomScan(snapshotPath, options = {}) {
|
|
150
167
|
return runScan([snapshotPath], options, "sbom");
|
|
151
168
|
}
|
|
152
|
-
|
|
169
|
+
/** targetArgs: projectモードは`--lockfile`の組、artifact/sbomモードは検証済みの絶対パス */
|
|
170
|
+
async function runScan(targetArgs, options, mode) {
|
|
153
171
|
const limit = options.maxConcurrentScans ?? maxConcurrentScansFromEnv();
|
|
154
172
|
if (activeScans >= limit) {
|
|
155
173
|
throw new ScanToolError("too_many_concurrent_scans", `同時実行できるスキャンは${limit}件までです(現在${activeScans}件実行中)。実行中のスキャン完了後に再試行してください`);
|
|
156
174
|
}
|
|
157
175
|
activeScans++;
|
|
158
176
|
try {
|
|
159
|
-
return await runOsvScanUnguarded(
|
|
177
|
+
return await runOsvScanUnguarded(targetArgs, options, mode);
|
|
160
178
|
}
|
|
161
179
|
finally {
|
|
162
180
|
activeScans--;
|
|
163
181
|
}
|
|
164
182
|
}
|
|
165
|
-
async function runOsvScanUnguarded(
|
|
183
|
+
async function runOsvScanUnguarded(targetArgs, options, mode) {
|
|
166
184
|
const binaryPath = options.binaryPath ?? (await resolveOsvScannerBinary());
|
|
167
|
-
const result = await execOsvScanner(binaryPath,
|
|
185
|
+
const result = await execOsvScanner(binaryPath, targetArgs, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES, buildOsvScanArgs(mode, isRemoteResolutionDisabled(options)));
|
|
168
186
|
if (result.exitCode === EXIT_NO_PACKAGES && mode === "project") {
|
|
169
|
-
throw new ScanToolError("no_packages_found",
|
|
187
|
+
throw new ScanToolError("no_packages_found", "OSV-Scannerがスキャン対象のパッケージを検出できませんでした(マニフェストに依存関係が定義されているか確認してください)", result.stderr);
|
|
170
188
|
}
|
|
171
189
|
if (mode !== "project" && result.exitCode === EXIT_NO_PACKAGES && result.stdout.trim() === "") {
|
|
172
190
|
return { results: [] };
|
package/dist/osv/scanReport.js
CHANGED
|
@@ -119,9 +119,11 @@ export function parseOsvScanOutput(raw) {
|
|
|
119
119
|
const key = `${ecosystem}:${name}@${version}`;
|
|
120
120
|
let entry = packageMap.get(key);
|
|
121
121
|
if (!entry) {
|
|
122
|
-
entry = { name, version, ecosystem, vulns: new Map() };
|
|
122
|
+
entry = { name, version, ecosystem, groups: new Set(), vulns: new Map() };
|
|
123
123
|
packageMap.set(key, entry);
|
|
124
124
|
}
|
|
125
|
+
for (const group of asStrings(pkgObj.dependency_groups))
|
|
126
|
+
entry.groups.add(group);
|
|
125
127
|
const details = asArray(pkgObj.vulnerabilities);
|
|
126
128
|
for (const groupRaw of asArray(pkgObj.groups)) {
|
|
127
129
|
const group = asRecord(groupRaw);
|
|
@@ -171,7 +173,13 @@ function buildReport(sourceFiles, packageMap) {
|
|
|
171
173
|
severityBreakdown[vuln.severity]++;
|
|
172
174
|
vulnerabilityCount++;
|
|
173
175
|
}
|
|
174
|
-
return {
|
|
176
|
+
return {
|
|
177
|
+
name: entry.name,
|
|
178
|
+
version: entry.version,
|
|
179
|
+
ecosystem: entry.ecosystem,
|
|
180
|
+
...(entry.groups.size > 0 ? { dependency_groups: [...entry.groups].sort() } : {}),
|
|
181
|
+
vulnerabilities,
|
|
182
|
+
};
|
|
175
183
|
})
|
|
176
184
|
.sort((a, b) => maxScore(b.vulnerabilities) - maxScore(a.vulnerabilities) || cmpId(a.name, b.name));
|
|
177
185
|
return {
|
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;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* レスポンス形式(成功/エラー)は`toolResult.ts`参照。
|
|
4
4
|
*/
|
|
5
5
|
import { isRemoteResolutionDisabled, runOsvScan } from "../osv/runner.js";
|
|
6
|
+
import { sanitizeExternalText } from "../utils/externalText.js";
|
|
6
7
|
import { detectJavaProject } from "../utils/projectDetector.js";
|
|
7
8
|
import { errorResult, jsonResult } from "./toolResult.js";
|
|
8
9
|
const TRANSITIVE_OMITTED_WARNING = "OSV_MCP_NO_REMOTE_RESOLUTIONの設定により、マニフェスト(pom.xml)からの推移的依存の解決を省略しています。" +
|
|
@@ -10,24 +11,38 @@ const TRANSITIVE_OMITTED_WARNING = "OSV_MCP_NO_REMOTE_RESOLUTIONの設定によ
|
|
|
10
11
|
"検出0件でも推移的依存の安全性は確認できていません";
|
|
11
12
|
/**
|
|
12
13
|
* 推移的依存の解決状態。無効時に「検出0件」を安全と誤読されないよう応答の先頭付近に置く。
|
|
13
|
-
*
|
|
14
|
-
* 判定せず、無効時は条件付きの警告を常に返す。
|
|
14
|
+
* 無効時は、どのマニフェストを含むかに依らず条件付きの警告を常に返す。
|
|
15
15
|
*/
|
|
16
|
-
export function dependencyResolution(noRemoteResolution) {
|
|
16
|
+
export function dependencyResolution(noRemoteResolution, warning = TRANSITIVE_OMITTED_WARNING) {
|
|
17
17
|
return noRemoteResolution
|
|
18
|
-
? { transitive_resolution: "disabled", warning
|
|
18
|
+
? { transitive_resolution: "disabled", warning }
|
|
19
19
|
: { transitive_resolution: "enabled" };
|
|
20
20
|
}
|
|
21
|
+
const SKIPPED_MANIFESTS_WARNING = "一部のマニフェストをスキャン対象から外しました(skipped_manifestsを参照)。" +
|
|
22
|
+
"それらの依存の脆弱性は結果に含まれないため、検出0件でも安全とは判断しないでください";
|
|
23
|
+
/**
|
|
24
|
+
* 親POMが許可ルートの外を参照するため外したマニフェスト。外したものが無ければ何も出力しない
|
|
25
|
+
* (既存の出力を変えない)。件数より前に置き、検出0件を安全と誤読させない
|
|
26
|
+
*/
|
|
27
|
+
export function skippedManifestsFields(skipped) {
|
|
28
|
+
if (skipped.length === 0)
|
|
29
|
+
return {};
|
|
30
|
+
return {
|
|
31
|
+
skipped_manifests: skipped.map((s) => ({ path: sanitizeExternalText(s.path), reason: sanitizeExternalText(s.reason) })),
|
|
32
|
+
scope_warning: SKIPPED_MANIFESTS_WARNING,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
21
35
|
export async function handleScanJavaProject(args, options = {}) {
|
|
22
36
|
try {
|
|
23
37
|
const project = await detectJavaProject(args.project_path, {
|
|
24
38
|
allowedRoot: options.allowedRoot,
|
|
25
39
|
});
|
|
26
40
|
const noRemoteResolution = isRemoteResolutionDisabled(options);
|
|
27
|
-
const report = await runOsvScan(project.
|
|
41
|
+
const report = await runOsvScan(project.targets, { ...options, noRemoteResolution });
|
|
28
42
|
return jsonResult({
|
|
29
43
|
project_dir: project.projectDir,
|
|
30
44
|
manifests: project.manifests,
|
|
45
|
+
...skippedManifestsFields(project.skipped),
|
|
31
46
|
dependency_resolution: dependencyResolution(noRemoteResolution),
|
|
32
47
|
...report,
|
|
33
48
|
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `scan_project`ツールのハンドラ: Java / JavaScript / Python / Goのlockfileをまとめてスキャンする。
|
|
3
|
+
*
|
|
4
|
+
* 誤要約対策(JAR実体スキャンと同じ原則): スキャンできなかった依存を示すcoverageを件数より前に置き、
|
|
5
|
+
* complete=falseの場合は「検出0件でも安全とは言えない」旨の警告を付ける。
|
|
6
|
+
*/
|
|
7
|
+
import { mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { isRemoteResolutionDisabled, runOsvScan } from "../osv/runner.js";
|
|
11
|
+
import { parseOsvScanOutput } from "../osv/scanReport.js";
|
|
12
|
+
import { sanitizeExternalText } from "../utils/externalText.js";
|
|
13
|
+
import { detectProject } from "../utils/manifestDetector.js";
|
|
14
|
+
import { normalizePypiName } from "../utils/requirementsFile.js";
|
|
15
|
+
import { dependencyResolution } from "./scanJavaProject.js";
|
|
16
|
+
import { errorResult, jsonResult } from "./toolResult.js";
|
|
17
|
+
const TRANSITIVE_OMITTED_WARNING = "OSV_MCP_NO_REMOTE_RESOLUTIONの設定により、マニフェスト(pom.xml / requirements.txt)からの推移的依存の解決を省略しています。" +
|
|
18
|
+
"lockfile(package-lock.json / poetry.lock / go.mod / gradle.lockfile等)に記録された依存は対象ですが、" +
|
|
19
|
+
"pom.xml・requirements.txtに直接記載された依存の先にある推移的依存の脆弱性は含まれません。" +
|
|
20
|
+
"検出0件でも推移的依存の安全性は確認できていません";
|
|
21
|
+
const INCOMPLETE_WARNING = "一部の依存はスキャンされていないか、版を推測してスキャンしています" +
|
|
22
|
+
"(lockfile_missing / unpinned_requirements / unscannable_requirements / skipped_filesを参照)。" +
|
|
23
|
+
"検出0件でも、それらの依存の安全性は確認できていません";
|
|
24
|
+
/** coverageの各一覧の上限(巨大なrequirements.txtで応答を膨らませない) */
|
|
25
|
+
const MAX_COVERAGE_ITEMS = 200;
|
|
26
|
+
function capped(items) {
|
|
27
|
+
return { items: items.slice(0, MAX_COVERAGE_ITEMS), omitted: Math.max(0, items.length - MAX_COVERAGE_ITEMS) };
|
|
28
|
+
}
|
|
29
|
+
function buildCoverage(project) {
|
|
30
|
+
const rel = (file) => sanitizeExternalText(path.relative(project.projectDir, file));
|
|
31
|
+
const lockfileMissing = capped(project.lockfileMissing);
|
|
32
|
+
const unpinned = capped(project.requirementIssues);
|
|
33
|
+
const unscannable = capped(project.requirementReferences);
|
|
34
|
+
const skipped = capped(project.skippedFiles);
|
|
35
|
+
const complete = project.lockfileMissing.length === 0 &&
|
|
36
|
+
project.requirementIssues.length === 0 &&
|
|
37
|
+
project.requirementReferences.length === 0 &&
|
|
38
|
+
project.skippedFiles.length === 0;
|
|
39
|
+
const omitted = lockfileMissing.omitted + unpinned.omitted + unscannable.omitted + skipped.omitted;
|
|
40
|
+
return {
|
|
41
|
+
complete,
|
|
42
|
+
...(complete ? {} : { warning: INCOMPLETE_WARNING }),
|
|
43
|
+
manifests: project.manifests.map((m) => ({ path: sanitizeExternalText(m.path), ecosystem: m.ecosystem, format: m.format })),
|
|
44
|
+
lockfile_missing: lockfileMissing.items.map((m) => ({
|
|
45
|
+
path: sanitizeExternalText(m.path),
|
|
46
|
+
ecosystem: m.ecosystem,
|
|
47
|
+
status: m.status,
|
|
48
|
+
hint: sanitizeExternalText(m.hint),
|
|
49
|
+
})),
|
|
50
|
+
unpinned_requirements: unpinned.items.map((issue) => ({
|
|
51
|
+
file: rel(issue.file),
|
|
52
|
+
line: issue.line,
|
|
53
|
+
name: sanitizeExternalText(issue.name),
|
|
54
|
+
specifier: sanitizeExternalText(issue.specifier),
|
|
55
|
+
kind: issue.kind,
|
|
56
|
+
})),
|
|
57
|
+
unscannable_requirements: unscannable.items.map((ref) => ({
|
|
58
|
+
file: rel(ref.file),
|
|
59
|
+
line: ref.line,
|
|
60
|
+
text: sanitizeExternalText(ref.text),
|
|
61
|
+
reason: ref.reason,
|
|
62
|
+
})),
|
|
63
|
+
skipped_files: skipped.items.map((s) => ({ path: sanitizeExternalText(s.path), reason: sanitizeExternalText(s.reason) })),
|
|
64
|
+
...(omitted > 0 ? { omitted_items: omitted } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function ecosystemBreakdown(project, packages) {
|
|
68
|
+
const breakdown = {};
|
|
69
|
+
for (const manifest of project.manifests) {
|
|
70
|
+
breakdown[manifest.ecosystem] ??= { manifests: 0, vulnerable_package_count: 0, vulnerability_count: 0 };
|
|
71
|
+
breakdown[manifest.ecosystem].manifests++;
|
|
72
|
+
}
|
|
73
|
+
for (const pkg of packages) {
|
|
74
|
+
breakdown[pkg.ecosystem] ??= { manifests: 0, vulnerable_package_count: 0, vulnerability_count: 0 };
|
|
75
|
+
breakdown[pkg.ecosystem].vulnerable_package_count++;
|
|
76
|
+
breakdown[pkg.ecosystem].vulnerability_count += pkg.vulnerabilities.length;
|
|
77
|
+
}
|
|
78
|
+
return breakdown;
|
|
79
|
+
}
|
|
80
|
+
/** `>=X` / `~=X` の行は、osv-scannerが下限Xを使用中の版とみなしてスキャンしている */
|
|
81
|
+
function markLowerBounds(project, packages) {
|
|
82
|
+
return packages.map((pkg) => pkg.ecosystem === "PyPI" &&
|
|
83
|
+
project.lowerBounds.some((lb) => lb.name === normalizePypiName(pkg.name) && lb.version === pkg.version)
|
|
84
|
+
? { ...pkg, version_is_lower_bound: true }
|
|
85
|
+
: pkg);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* requirements.txtは検証済みの正規化行だけを専用の一時ディレクトリに書いてスキャンする
|
|
89
|
+
* (元ファイルの取り込み指定をosv-scannerにたどらせない)。成功・失敗とも削除する。
|
|
90
|
+
*/
|
|
91
|
+
async function scanWithRequirementsCopies(project, options) {
|
|
92
|
+
const copies = project.requirementsCopies.filter((copy) => copy.entries.length > 0);
|
|
93
|
+
if (project.targets.length === 0 && copies.length === 0)
|
|
94
|
+
return parseOsvScanOutput({ results: [] });
|
|
95
|
+
const dir = copies.length > 0 ? await mkdtemp(path.join(await realpath(os.tmpdir()), "osv-mcp-req-")) : null;
|
|
96
|
+
try {
|
|
97
|
+
const targets = [...project.targets];
|
|
98
|
+
for (const [index, copy] of copies.entries()) {
|
|
99
|
+
const copyPath = path.join(dir, `${index}.txt`);
|
|
100
|
+
await writeFile(copyPath, `${copy.entries.join("\n")}\n`, { mode: 0o600, flag: "wx" });
|
|
101
|
+
targets.push({ path: copyPath, format: "requirements.txt" });
|
|
102
|
+
}
|
|
103
|
+
return await runOsvScan(targets, options);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
if (dir !== null)
|
|
107
|
+
await rm(dir, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function handleScanProject(args, options = {}) {
|
|
111
|
+
try {
|
|
112
|
+
const project = await detectProject(args.project_path, { allowedRoot: options.allowedRoot });
|
|
113
|
+
const noRemoteResolution = isRemoteResolutionDisabled(options);
|
|
114
|
+
const report = await scanWithRequirementsCopies(project, { ...options, noRemoteResolution });
|
|
115
|
+
return jsonResult({
|
|
116
|
+
project_dir: project.projectDir,
|
|
117
|
+
dependency_resolution: dependencyResolution(noRemoteResolution, TRANSITIVE_OMITTED_WARNING),
|
|
118
|
+
coverage: buildCoverage(project),
|
|
119
|
+
ecosystem_breakdown: ecosystemBreakdown(project, report.packages),
|
|
120
|
+
vulnerable_package_count: report.vulnerable_package_count,
|
|
121
|
+
vulnerability_count: report.vulnerability_count,
|
|
122
|
+
severity_breakdown: report.severity_breakdown,
|
|
123
|
+
packages: markLowerBounds(project, report.packages),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
return errorResult(error);
|
|
128
|
+
}
|
|
129
|
+
}
|
package/dist/tools/suggestFix.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
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
|
+
import { dependencyResolution, skippedManifestsFields, } from "./scanJavaProject.js";
|
|
10
10
|
import { errorResult, jsonResult } from "./toolResult.js";
|
|
11
11
|
export async function handleSuggestFix(args, options = {}) {
|
|
12
12
|
try {
|
|
@@ -14,12 +14,13 @@ export async function handleSuggestFix(args, options = {}) {
|
|
|
14
14
|
allowedRoot: options.allowedRoot,
|
|
15
15
|
});
|
|
16
16
|
const noRemoteResolution = isRemoteResolutionDisabled(options);
|
|
17
|
-
const report = await runOsvScan(project.
|
|
17
|
+
const report = await runOsvScan(project.targets, { ...options, noRemoteResolution });
|
|
18
18
|
const suggestions = suggestUpgrades(report.packages);
|
|
19
19
|
const unfixedVulnerabilities = suggestions.reduce((sum, s) => sum + s.per_cve_detail.filter((d) => d.tier === "unfixed").length, 0);
|
|
20
20
|
return jsonResult({
|
|
21
21
|
project_dir: project.projectDir,
|
|
22
22
|
manifests: project.manifests,
|
|
23
|
+
...skippedManifestsFields(project.skipped),
|
|
23
24
|
dependency_resolution: dependencyResolution(noRemoteResolution),
|
|
24
25
|
vulnerable_package_count: suggestions.length,
|
|
25
26
|
unfixed_vulnerability_count: unfixedVulnerabilities,
|