osv-scanner-mcp 0.4.1 → 0.5.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 +22 -14
- package/dist/index.js +10 -8
- package/dist/osv/affectedVersions.js +36 -18
- package/dist/osv/scanReport.js +7 -3
- package/dist/osv/suggestFix.js +92 -102
- package/dist/osv/versionScheme.js +96 -0
- package/dist/tools/scanJavaArtifact.js +1 -1
- package/dist/tools/scanJavaProject.js +29 -10
- package/dist/tools/scanProject.js +9 -10
- package/dist/tools/suggestFix.js +12 -11
- package/dist/utils/safeRead.js +7 -1
- package/dist/utils/scanSnapshot.js +59 -14
- package/dist/utils/semverVersion.js +69 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
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`)。Java(Maven / Gradle)、JavaScript(npm / yarn / pnpm / bun)、Python(Poetry / uv / Pipenv / PDM / requirements.txt)、Go のlockfileに対応しています(修正版の推奨はJava
|
|
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・JavaScript・Go)。MCPクライアントは Claude Code / Claude Desktop / Codex CLI / Antigravity / VS Code(GitHub Copilot)での利用手順を用意しています。
|
|
11
11
|
|
|
12
12
|
## 特徴
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ Google製 [OSV-Scanner](https://github.com/google/osv-scanner) をラップす
|
|
|
16
16
|
- **JAR/WAR実体スキャン**: `scan_java_artifact` ツールで、lockfileが無い・shaded/fat JARしか手元にないプロジェクトでもアーカイブ内メタデータから既知の脆弱性を検出します(ベストエフォート同定であることを明示するcoverage情報付き)
|
|
17
17
|
- **SBOM入力スキャン**: `scan_sbom` ツールでCycloneDX/SPDXのJSON SBOMに記録された依存を検査します。SBOMの網羅性や実成果物との一致は未検証であることを明示します
|
|
18
18
|
- **深刻度順のレポート**: パッケージごとに脆弱性をCVSSスコア順に整理し、5段階の深刻度ラベル(critical / high / medium / low / unknown)とサマリ集計付きで返します
|
|
19
|
-
- **修正版の提示**: 各脆弱性の `fixed_versions`
|
|
19
|
+
- **修正版の提示**: 各脆弱性の `fixed_versions` を含めます。MavenはMavenバージョン優先順位規則(`2.17.1-RELEASE` のようなsemver非対応の表記にも対応)、npm・GoはSemantic Versioningの優先順位で正しくソートします
|
|
20
20
|
- **セキュリティ第一の設計**: シェル非経由の実行・引数ホワイトリスト・パス正規化と境界チェック・タイムアウト/出力サイズ上限を実装段階から組み込んでいます
|
|
21
21
|
|
|
22
22
|
## 動作要件
|
|
@@ -142,7 +142,7 @@ osv-scanner v2.4.0 で接続先を実機確認した結果です(2026-10-07)。
|
|
|
142
142
|
| 操作 | 接続先 | 送られる情報 |
|
|
143
143
|
|---|---|---|
|
|
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` 等に書かれた取得先へは接続しません |
|
|
145
|
+
| `requirements.txt` のスキャン(`scan_project` / `suggest_fix`) | `api.osv.dev`、**`api.deps.dev`** | `pom.xml` と同じく、推移的依存の解決のため記載された依存の名前とバージョンが deps.dev に送られます。`--index-url` 等に書かれた取得先へは接続しません |
|
|
146
146
|
| lockfileのスキャン(`gradle.lockfile`、`package-lock.json` 等のnpm系、`poetry.lock` 等のPython系、`go.mod`) | `api.osv.dev` | パッケージの名前とバージョン(lockfileに全依存が記載済みのため、解決のための外部接続はしません) |
|
|
147
147
|
| `scan_java_artifact` / `scan_sbom` | `api.osv.dev` | 同定できたパッケージの名前とバージョン |
|
|
148
148
|
| `explain_vulnerability` | `api.osv.dev` | 指定した脆弱性ID |
|
|
@@ -208,11 +208,11 @@ api.osv.dev と deps.dev はどちらも Google が運営するサービスで
|
|
|
208
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
209
|
- `unpinned_requirements`: requirements.txtのうち、版を固定していない行。`kind` は `unpinned`(版の指定なし)・`range`(`>`、`<`、`!=`、`==1.*`、範囲の組み合わせ等)・`lower_bound`(`>=`、`~=`)。`unpinned` と `range` の行はOSV-Scannerがスキャンせず、`lower_bound` の行は下限の版を使用中の版とみなしてスキャンします(該当パッケージには `version_is_lower_bound: true` が付き、実際の版とは異なる可能性があります)
|
|
210
210
|
- `unscannable_requirements`: スキャンされない行と理由。`-e`、`name @ URL`、パス指定、展開しなかった取り込み(プロジェクトディレクトリの外・存在しない・URL・上限超過)、制約ファイル(`-c`、適用しません)、解釈できないオプションや版の指定。解釈できない行は無視せず、ここに記録します
|
|
211
|
-
- `skipped_files`: スキャン対象から外したファイルと理由(requirements.txt自体が読めない・1MiBを超える場合、`pom.xml` の親POM
|
|
211
|
+
- `skipped_files`: スキャン対象から外したファイルと理由(requirements.txt自体が読めない・1MiBを超える場合、`pom.xml` の親POMが許可ルートの外を参照する場合)。親POMを読めず、親POMを含めずにスキャンした `pom.xml` もここに理由付きで示します([親POMの扱い](#親pomの扱い)を参照)
|
|
212
212
|
- 各一覧は200件までで、超えた分の件数を `omitted_items` に返します
|
|
213
213
|
- `ecosystem_breakdown` は、脆弱性0件のエコシステムも含めて「スキャンした」ことを示します
|
|
214
214
|
- `dependency_groups` はOSV-Scannerが付けた依存グループ(例: `dev`)の生の値です。lockfileの形式によって欠落・不正確なため(pnpmでは付かず、pdmでは `optional` になる等)、参考情報として扱ってください
|
|
215
|
-
- 修正版の推奨(`suggest_fix`)
|
|
215
|
+
- 修正版の推奨(`suggest_fix`)はJava・JavaScript・Goに対応しています(Pythonは未対応)
|
|
216
216
|
|
|
217
217
|
### `scan_java_project`
|
|
218
218
|
|
|
@@ -234,6 +234,8 @@ OSV-Scannerは `pom.xml` の `<parent>` が参照する親POM(`<relativePath>`
|
|
|
234
234
|
|
|
235
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
236
|
|
|
237
|
+
存在する親POMを読めない場合(10MiBを超える、名前付きパイプ等の通常のファイルでない、末尾がシンボリックリンク等)は、その `pom.xml` を親POMを含めずにスキャンし、親から継承する依存が欠ける可能性を応答の `incomplete_manifests`(`scan_project` では `coverage.skipped_files`、`coverage.complete` は `false`)に理由付きで示します。子の `pom.xml` に依存が無く `no_packages_found` になる場合も、エラーのメッセージに同じ理由を含めます。親POMが存在しない場合(ルートの `pom.xml` で既定の `../pom.xml` が無い等)は、元の配置でもOSV-Scannerは親を読まないため欠落として扱いません。
|
|
238
|
+
|
|
237
239
|
- 許可ルート内の親POMは従来どおり読みます(サブモジュールのスキャンは許可ルート内なら引き続き使えます)
|
|
238
240
|
- `<relativePath/>`(空)はローカルの親POMを参照しないため対象外です
|
|
239
241
|
- 親POMの指定は、OSV-Scanner(Go)のXMLの解釈に合わせて読みます。要素は名前空間の接頭辞に関係なく要素名で照合し(`<m:parent>` も親として扱う)、ルート要素の直下の `parent` だけを対象にし、文字参照を展開します
|
|
@@ -275,7 +277,7 @@ OSV-Scannerは `pom.xml` の `<parent>` が参照する親POM(`<relativePath>`
|
|
|
275
277
|
```
|
|
276
278
|
|
|
277
279
|
- `packages` は最も深刻な脆弱性を持つ順、各 `vulnerabilities` は深刻度順(unknownは末尾)
|
|
278
|
-
- `fixed_versions` はMaven
|
|
280
|
+
- `fixed_versions` はOSVに記載された修正版です。MavenはMaven優先順位、npm・GoはSemantic Versioningの優先順位で昇順(SemVerとして解釈できない表記は末尾)、PyPI等はOSVの記載順のまま(並び順は保証しません)。複数のリリース系統(例: 2.12系バックポートと2.15系)が混在することがあります。プレリリース版(`5.0.0-beta.3`)やGoの疑似バージョン(`0.0.0-20180925071336-cf3bd585ca2a`)が含まれることもあります。空配列は「OSVに修正版の記載がない」ことを意味します。v0.4.1以前はMaven以外のパッケージで常に空配列を返していました
|
|
279
281
|
- `severity_score` が取得できない脆弱性は `null` / `"unknown"` として扱います
|
|
280
282
|
|
|
281
283
|
### `scan_java_artifact`
|
|
@@ -331,23 +333,26 @@ OSV-Scanner 2.4.0の `java/archive` プラグインを使用し、ネストJAR
|
|
|
331
333
|
|
|
332
334
|
### `suggest_fix`
|
|
333
335
|
|
|
334
|
-
|
|
336
|
+
`scan_project` と同じ検出・スキャンを実行し、脆弱なパッケージごとに**推奨アップグレードバージョン**を提案します。推奨はJava(Maven / Gradle)・JavaScript(npm)・Goに対応し、Python(PyPI)は未対応です。単純な最大バージョンではなく、現在のバージョンに最も近いリリース系統の修正版を3段階フォールバックで選定します:
|
|
335
337
|
|
|
336
338
|
| Tier | 意味 |
|
|
337
339
|
|---|---|
|
|
338
|
-
| `same_minor` |
|
|
340
|
+
| `same_minor` | 現在と同じ系統内の修正版(最小の変更で済む) |
|
|
339
341
|
| `major_internal` | 同一メジャー内の修正版(マイナーバージョンアップが必要) |
|
|
340
342
|
| `cross_major` | メジャーアップグレードが必要(破壊的変更の可能性あり) |
|
|
341
343
|
|
|
342
|
-
|
|
344
|
+
npm・Goの「同じ系統」は、npmの `^`(キャレット)が互換とみなす範囲です。1.0.0以上は Maven と同じく major.minor 単位ですが、0.x では同じ `0.minor` 内だけを同じ系統とし、マイナー更新(`0.3` → `0.4`)は `cross_major`、0.0.x ではどの更新も `cross_major` として扱います(SemVerでは0.xの更新は互換を保証しないため)。
|
|
345
|
+
|
|
346
|
+
**入力**: `scan_project` と同じ(`project_path`)
|
|
343
347
|
|
|
344
348
|
**出力(成功時)**
|
|
345
349
|
|
|
346
350
|
```json
|
|
347
351
|
{
|
|
348
352
|
"project_dir": "/path/to/project",
|
|
349
|
-
"manifests": ["pom.xml"],
|
|
353
|
+
"manifests": ["pom.xml", "web/package-lock.json"],
|
|
350
354
|
"dependency_resolution": { "transitive_resolution": "enabled" },
|
|
355
|
+
"coverage": { "complete": true, "manifests": [ ... ], "lockfile_missing": [], "unpinned_requirements": [], "unscannable_requirements": [], "skipped_files": [] },
|
|
351
356
|
"vulnerable_package_count": 4,
|
|
352
357
|
"unfixed_vulnerability_count": 1,
|
|
353
358
|
"suggestions": [
|
|
@@ -368,10 +373,13 @@ OSV-Scanner 2.4.0の `java/archive` プラグインを使用し、ネストJAR
|
|
|
368
373
|
```
|
|
369
374
|
|
|
370
375
|
- `recommended_upgrade` は既知の修正版を候補に、修正対象の全CVEの影響範囲外と確認できたものを3段階Tier順・バージョン昇順で選びます。CVEごとの修正版の最大値を単純に採用せず、別系統で再び影響を受ける候補も除外します。全公開版の中での最小性や未検出の脆弱性がないことは保証しません。
|
|
371
|
-
-
|
|
376
|
+
- OSVの影響範囲(`introduced` / `fixed` / `last_affected` / 上限なし)を照合します。MavenはMavenの優先順位で `ECOSYSTEM` 範囲を、npm・GoはSemantic Versioningの優先順位で `SEMVER` / `ECOSYSTEM` 範囲を使います。`versions` に明示された影響も確認します。範囲欠落・不正・未対応形式(`GIT` 等)・`limit` による不完全な情報や、SemVerとして解釈できない版(一部のGHSAに残る `19.03.9` のような表記)を含む場合は安全と推定せず、候補を検証できなければ `recommended_upgrade: null`、`verification: "no_verified_candidate"` を返します。
|
|
377
|
+
- プレリリース版(`5.0.0-beta.3`、`15.6.0-canary.61`、Goの疑似バージョン)は、正式版の候補では全CVEを解消できない場合だけ推奨し、`recommended_is_prerelease: true` を付けます。同じTierのプレリリースより、上のTierの正式版を優先します。
|
|
372
378
|
- 推奨時は `verification: "verified"`、CVEごとの `recommended_status` は `affected` / `not_affected` / `unknown` です。推奨保留時は `not_evaluated` になります。`per_cve_detail.fixed_in` は各CVE単独の候補であり、最終推奨先の判定は `recommended_status` を参照してください。
|
|
373
|
-
- 現在より新しい修正版候補がないCVEは `tier: "unfixed"` として推奨の修正対象から除外します(情報欠落を含む場合があります)。除外したCVEも推奨先で判定し、その状態を表示します。全CVEがunfixedの場合も `recommended_upgrade` は `null`
|
|
374
|
-
-
|
|
379
|
+
- 現在より新しい修正版候補がないCVEは `tier: "unfixed"` として推奨の修正対象から除外します(情報欠落を含む場合があります)。除外したCVEも推奨先で判定し、その状態を表示します。全CVEがunfixedの場合も `recommended_upgrade` は `null` です。修正版の記載はあるがバージョンとして解釈できないCVE(SemVerでない `13.0` 等)は `tier: "unparseable_fix"` とし、修正版が無いとは扱わず修正対象に残すため、推奨は保留(`no_verified_candidate`)になります。
|
|
380
|
+
- npm・Goの提案には更新方法の `update_hint` を付けます。推移的依存の場合、npmでは要求している直接依存の更新か、ルートの `package.json` の `overrides`(ルートのプロジェクトでのみ有効)で版を指定します。Goでは `go get <module>@<version>` で更新できます。Goのv2以上のメジャーは別のモジュールパス(`/v2` 等)としてOSV上も別パッケージになるため、新しいメジャー系列の修正版は候補に含まれません。現在の版が疑似バージョン(タグのないコミット)の場合は `upgrade_note` に示します。
|
|
381
|
+
- Python(PyPI)など推奨に未対応のエコシステムは `verification: "unsupported_ecosystem"`、現在の版をバージョンとして解釈できない場合(npmのgit・ローカルパス依存等)は `verification: "unparseable_version"` を返し、どちらもCVEごとの `tier: "unsupported"` として `unfixed` には数えません(修正版の有無は判定していないため。修正版は `scan_project` の `fixed_versions` や `explain_vulnerability` で確認できます)。
|
|
382
|
+
- 応答の `coverage` は `scan_project` と同じです。lockfileの無いマニフェストや外したファイルがあれば `complete: false` になり、それらの依存は提案に含まれません。v0.4.2以前の `skipped_manifests` / `scope_warning` は `coverage.skipped_files` / `coverage.warning` に統合しました。
|
|
375
383
|
|
|
376
384
|
### `explain_vulnerability`
|
|
377
385
|
|
|
@@ -456,7 +464,7 @@ npm run build # dist/ へビルド
|
|
|
456
464
|
- [x] Gradle対応(lockfile方式)
|
|
457
465
|
- [x] `scan_java_artifact` ツール: JAR/WAR実体スキャン(lockfileが無い・shaded/fat JARのみのプロジェクト向け)
|
|
458
466
|
- [x] `scan_project` ツール: Java / JavaScript / Python / Go のlockfileをまとめてスキャン
|
|
459
|
-
- [
|
|
467
|
+
- [x] `suggest_fix` のJavaScript / Go対応(semver)
|
|
460
468
|
- [ ] `suggest_fix` のPython対応(PEP 440)、直接/推移的依存の区別
|
|
461
469
|
|
|
462
470
|
## ライセンス
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ if (startupWarning !== null) {
|
|
|
36
36
|
// NOTE: リリース時はpackage.jsonのversionと同じ値に更新すること
|
|
37
37
|
const server = new McpServer({
|
|
38
38
|
name: "osv-scanner-mcp",
|
|
39
|
-
version: "0.
|
|
39
|
+
version: "0.5.0",
|
|
40
40
|
});
|
|
41
41
|
server.registerTool("scan_project", {
|
|
42
42
|
title: "プロジェクトの依存の脆弱性スキャン(Java / JavaScript / Python / Go)",
|
|
@@ -45,7 +45,7 @@ server.registerTool("scan_project", {
|
|
|
45
45
|
"Python(poetry.lock / uv.lock / Pipfile.lock / pdm.lock / requirements.txt)、Go(go.mod)。" +
|
|
46
46
|
"パッケージマネージャーやビルドは実行しない。" +
|
|
47
47
|
"応答先頭のcoverageを必ず確認すること: lockfileが無いマニフェスト、バージョン未固定のrequirements行、スキャン対象から外したファイルを示す。" +
|
|
48
|
-
"coverage.complete=falseの場合は、検出0件でも安全とは判断しないこと。修正版の推奨(suggest_fix)
|
|
48
|
+
"coverage.complete=falseの場合は、検出0件でも安全とは判断しないこと。修正版の推奨(suggest_fix)はJava・JavaScript・Goに対応(Pythonは未対応)。",
|
|
49
49
|
inputSchema: {
|
|
50
50
|
project_path: z
|
|
51
51
|
.string()
|
|
@@ -68,18 +68,20 @@ server.registerTool("scan_java_project", {
|
|
|
68
68
|
}, async ({ project_path }) => handleScanJavaProject({ project_path }, { allowedRoot: allowedRootFromEnv() }));
|
|
69
69
|
server.registerTool("suggest_fix", {
|
|
70
70
|
title: "脆弱性を解消する推奨アップグレードの提案",
|
|
71
|
-
description: "
|
|
71
|
+
description: "scan_projectと同じ検出でプロジェクトをスキャンし、脆弱な依存パッケージごとに推奨アップグレードバージョンを提案する。" +
|
|
72
|
+
"推奨はJava(Maven / Gradle)・JavaScript(npm)・Goに対応し、Python(PyPI)はunsupported_ecosystemとして返す。" +
|
|
72
73
|
"現在のバージョンに最も近いリリース系統の修正版を優先する3段階フォールバック" +
|
|
73
|
-
"(same_minor:
|
|
74
|
-
"推奨バージョン・アップグレード距離(upgrade_tier)・CVEごとの修正版を返す。" +
|
|
75
|
-
"候補を全修正対象CVE
|
|
76
|
-
"現在より新しい修正版候補のないCVEはunfixedとして別表示し、推奨先での判定も返す。
|
|
74
|
+
"(same_minor: 同一系統内 → major_internal: 同一メジャー内 → cross_major: メジャーアップグレード)で選定し、" +
|
|
75
|
+
"推奨バージョン・アップグレード距離(upgrade_tier)・CVEごとの修正版を返す。npm・Goでは0.x系のマイナー更新もcross_major(破壊的変更の可能性)。" +
|
|
76
|
+
"候補を全修正対象CVEの影響範囲と照合し、情報不足の場合は推奨を保留する。プレリリース版は正式版で解消できない場合だけ推奨し、recommended_is_prereleaseを付ける。" +
|
|
77
|
+
"現在より新しい修正版候補のないCVEはunfixedとして別表示し、推奨先での判定も返す。" +
|
|
78
|
+
"応答のcoverageを必ず確認すること(complete=falseなら提案に含まれない依存がある)。" +
|
|
77
79
|
"dependency_resolution.warningがある場合は推移的依存の脆弱性が提案に含まれない。",
|
|
78
80
|
inputSchema: {
|
|
79
81
|
project_path: z
|
|
80
82
|
.string()
|
|
81
83
|
.min(1)
|
|
82
|
-
.describe("
|
|
84
|
+
.describe("スキャン対象のプロジェクトディレクトリ、または対応するlockfile・マニフェストの絶対パス"),
|
|
83
85
|
},
|
|
84
86
|
}, async ({ project_path }) => handleSuggestFix({ project_path }, { allowedRoot: allowedRootFromEnv() }));
|
|
85
87
|
server.registerTool("explain_vulnerability", {
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import { compareMavenVersions } from "../utils/mavenVersion.js";
|
|
2
1
|
import { asArray, asRecord, asString } from "../utils/unknownJson.js";
|
|
3
|
-
|
|
2
|
+
import { versionRangeTypes, versionSchemeFor } from "./versionScheme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Preserve uncertainty: unsupported or malformed ranges cannot prove a candidate safe.
|
|
5
|
+
* 比較・範囲の型はエコシステム別(versionScheme.ts)。比較器のないエコシステムは常に情報不足。
|
|
6
|
+
* 解釈できない版(SemVerでない値等)を含む範囲は判定に使わず、情報不足とする。
|
|
7
|
+
*/
|
|
4
8
|
export function extractAffectedVersions(details, ids, name, ecosystem) {
|
|
5
|
-
const
|
|
9
|
+
const scheme = versionSchemeFor(ecosystem);
|
|
10
|
+
const rangeTypes = versionRangeTypes(ecosystem);
|
|
11
|
+
const evidence = { complete: scheme !== null, intervals: [], versions: [] };
|
|
12
|
+
const valid = (value) => scheme?.isValid(value) ?? false;
|
|
13
|
+
const compare = (a, b) => scheme?.compare(a, b) ?? 0;
|
|
6
14
|
if (ids.length === 0 || ids.some(id => !details.some(d => d.id === id)))
|
|
7
15
|
evidence.complete = false;
|
|
8
16
|
for (const detail of details) {
|
|
@@ -20,7 +28,7 @@ export function extractAffectedVersions(details, ids, name, ecosystem) {
|
|
|
20
28
|
evidence.complete = false;
|
|
21
29
|
for (const version of asArray(affected?.versions)) {
|
|
22
30
|
const value = asString(version);
|
|
23
|
-
if (!value)
|
|
31
|
+
if (!value || !valid(value))
|
|
24
32
|
evidence.complete = false;
|
|
25
33
|
else
|
|
26
34
|
evidence.versions.push(value);
|
|
@@ -31,7 +39,8 @@ export function extractAffectedVersions(details, ids, name, ecosystem) {
|
|
|
31
39
|
evidence.complete = false;
|
|
32
40
|
for (const rawRange of ranges) {
|
|
33
41
|
const range = asRecord(rawRange);
|
|
34
|
-
|
|
42
|
+
const type = asString(range?.type);
|
|
43
|
+
if (range === null || type === null || !rangeTypes.has(type)) {
|
|
35
44
|
evidence.complete = false;
|
|
36
45
|
continue;
|
|
37
46
|
}
|
|
@@ -39,6 +48,8 @@ export function extractAffectedVersions(details, ids, name, ecosystem) {
|
|
|
39
48
|
let start = null;
|
|
40
49
|
let previousEnd = null;
|
|
41
50
|
let previousInclusive = false;
|
|
51
|
+
/** この区間の境界に解釈できない版がある(区間を判定に使わない) */
|
|
52
|
+
let startInvalid = false;
|
|
42
53
|
if (events.length === 0)
|
|
43
54
|
evidence.complete = false;
|
|
44
55
|
for (const rawEvent of events) {
|
|
@@ -50,12 +61,16 @@ export function extractAffectedVersions(details, ids, name, ecosystem) {
|
|
|
50
61
|
evidence.complete = false;
|
|
51
62
|
continue;
|
|
52
63
|
}
|
|
64
|
+
const invalid = !(kind === "introduced" && value === "0") && !valid(value);
|
|
65
|
+
if (invalid)
|
|
66
|
+
evidence.complete = false;
|
|
53
67
|
if (kind === "introduced") {
|
|
54
|
-
if (start !== null || (previousEnd !== null && (value === "0" ||
|
|
55
|
-
|
|
56
|
-
(previousInclusive &&
|
|
68
|
+
if (start !== null || (!invalid && previousEnd !== null && (value === "0" ||
|
|
69
|
+
compare(value, previousEnd) < 0 ||
|
|
70
|
+
(previousInclusive && compare(value, previousEnd) === 0))))
|
|
57
71
|
evidence.complete = false;
|
|
58
72
|
start = value;
|
|
73
|
+
startInvalid = invalid;
|
|
59
74
|
}
|
|
60
75
|
else {
|
|
61
76
|
if (start === null) {
|
|
@@ -63,19 +78,20 @@ export function extractAffectedVersions(details, ids, name, ecosystem) {
|
|
|
63
78
|
continue;
|
|
64
79
|
}
|
|
65
80
|
const inclusive = kind === "last_affected";
|
|
66
|
-
if (start !== "0" && (
|
|
67
|
-
(!inclusive &&
|
|
81
|
+
if (!invalid && !startInvalid && start !== "0" && (compare(start, value) > 0 ||
|
|
82
|
+
(!inclusive && compare(start, value) === 0)))
|
|
68
83
|
evidence.complete = false;
|
|
69
|
-
|
|
84
|
+
if (!invalid && !startInvalid)
|
|
85
|
+
evidence.intervals.push({ introduced: start, end: value, inclusive });
|
|
70
86
|
// limit bounds known affected versions, but is not evidence of a fix beyond it.
|
|
71
87
|
if (kind === "limit")
|
|
72
88
|
evidence.complete = false;
|
|
73
|
-
previousEnd = value;
|
|
89
|
+
previousEnd = invalid ? null : value;
|
|
74
90
|
previousInclusive = inclusive;
|
|
75
91
|
start = null;
|
|
76
92
|
}
|
|
77
93
|
}
|
|
78
|
-
if (start !== null)
|
|
94
|
+
if (start !== null && !startInvalid)
|
|
79
95
|
evidence.intervals.push({ introduced: start, end: null, inclusive: false });
|
|
80
96
|
}
|
|
81
97
|
}
|
|
@@ -86,12 +102,14 @@ export function extractAffectedVersions(details, ids, name, ecosystem) {
|
|
|
86
102
|
evidence.complete = false;
|
|
87
103
|
return evidence;
|
|
88
104
|
}
|
|
89
|
-
export function candidateStatus(evidence, version) {
|
|
90
|
-
|
|
105
|
+
export function candidateStatus(evidence, version, ecosystem) {
|
|
106
|
+
const scheme = versionSchemeFor(ecosystem);
|
|
107
|
+
if (!evidence || scheme === null || !scheme.isValid(version))
|
|
91
108
|
return "unknown";
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
const compare = scheme.compare;
|
|
110
|
+
if (evidence.versions.some(v => compare(v, version) === 0) || evidence.intervals.some(r => (r.introduced === "0" || compare(version, r.introduced) >= 0) &&
|
|
111
|
+
(r.end === null || compare(version, r.end) < 0 ||
|
|
112
|
+
(r.inclusive && compare(version, r.end) === 0))))
|
|
95
113
|
return "affected";
|
|
96
114
|
return evidence.complete ? "not_affected" : "unknown";
|
|
97
115
|
}
|
package/dist/osv/scanReport.js
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* OSV-Scannerの出力は外部由来データなので、形式が想定と異なっても例外を投げず
|
|
13
13
|
* 読み取れた範囲でレポートを構築する(欠損フィールドはスキップ)。
|
|
14
14
|
*/
|
|
15
|
-
import { compareMavenVersions } from "../utils/mavenVersion.js";
|
|
16
15
|
import { asArray, asRecord, asString, asStrings } from "../utils/unknownJson.js";
|
|
17
16
|
import { extractAffectedVersions } from "./affectedVersions.js";
|
|
17
|
+
import { sortVersions, versionRangeTypes } from "./versionScheme.js";
|
|
18
18
|
/** 外部由来のsummaryをLLMに渡す際の長さ上限(プロンプト肥大・DoS対策) */
|
|
19
19
|
const MAX_SUMMARY_LENGTH = 500;
|
|
20
20
|
const CVSS_CRITICAL = 9.0;
|
|
@@ -65,9 +65,12 @@ function extractSummary(vulnDetails) {
|
|
|
65
65
|
/**
|
|
66
66
|
* OSVエントリの`affected[].ranges[].events[].fixed`から修正版を収集する。
|
|
67
67
|
* 1つのOSVエントリが複数パッケージをカバーしうるため、対象パッケージ名で絞り込む。
|
|
68
|
+
* 範囲の型はエコシステム別(versionScheme.ts)。GIT型のコミットハッシュは集めない。
|
|
69
|
+
* v0.4.1以前はMavenだけを集めていたため、npm・Go・PyPI等で常に空になっていた。
|
|
68
70
|
*/
|
|
69
71
|
function extractFixedVersions(vulnDetails, packageName, ecosystem) {
|
|
70
72
|
const versions = new Set();
|
|
73
|
+
const rangeTypes = versionRangeTypes(ecosystem);
|
|
71
74
|
for (const detail of vulnDetails) {
|
|
72
75
|
for (const affectedRaw of asArray(detail.affected)) {
|
|
73
76
|
const affected = asRecord(affectedRaw);
|
|
@@ -77,7 +80,8 @@ function extractFixedVersions(vulnDetails, packageName, ecosystem) {
|
|
|
77
80
|
if (affectedName !== packageName || asString(asRecord(affected.package)?.ecosystem) !== ecosystem)
|
|
78
81
|
continue;
|
|
79
82
|
for (const rangeRaw of asArray(affected.ranges)) {
|
|
80
|
-
|
|
83
|
+
const type = asString(asRecord(rangeRaw)?.type);
|
|
84
|
+
if (type === null || !rangeTypes.has(type))
|
|
81
85
|
continue;
|
|
82
86
|
for (const eventRaw of asArray(asRecord(rangeRaw)?.events)) {
|
|
83
87
|
const fixed = asString(asRecord(eventRaw)?.fixed);
|
|
@@ -87,7 +91,7 @@ function extractFixedVersions(vulnDetails, packageName, ecosystem) {
|
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
|
-
return
|
|
94
|
+
return sortVersions(versions, ecosystem);
|
|
91
95
|
}
|
|
92
96
|
/**
|
|
93
97
|
* OSV-Scannerの`--format json`出力を`scan_java_project`のレポートに変換する。
|
package/dist/osv/suggestFix.js
CHANGED
|
@@ -6,32 +6,47 @@
|
|
|
6
6
|
* 最大バージョンではなく「現在のバージョンに最も近い系統の修正版」を優先して提案する。
|
|
7
7
|
*
|
|
8
8
|
* CVEごとの探索順:
|
|
9
|
-
* Tier 1 (same_minor):
|
|
9
|
+
* Tier 1 (same_minor): 現在と同じ系統内の修正版(最小の変更で済む)
|
|
10
10
|
* Tier 2 (major_internal): 同一メジャー内の最小の修正版(マイナーバージョンアップ)
|
|
11
11
|
* Tier 3 (cross_major): 全体最小の修正版(メジャーアップグレード、破壊的変更の可能性)
|
|
12
|
+
* 系統の判定はエコシステム別(versionScheme.ts)。npm・Goは0.xのマイナー更新もcross_major。
|
|
12
13
|
*
|
|
13
14
|
* パッケージ全体の候補を全修正対象CVEの影響範囲と照合して推奨する。
|
|
14
15
|
* 判定不能な候補は推奨しない。
|
|
15
16
|
* 現在より新しい修正版が存在しないCVEはunfixedとして明示し、推奨計算から除外する。
|
|
17
|
+
* プレリリース(canary版・Goの疑似バージョン等)は、正式版の候補で解消できない場合だけ推奨する。
|
|
16
18
|
*/
|
|
17
|
-
import { compareMavenVersions, mavenVersionSeries } from "../utils/mavenVersion.js";
|
|
18
19
|
import { candidateStatus } from "./affectedVersions.js";
|
|
19
|
-
|
|
20
|
-
const
|
|
20
|
+
import { versionSchemeFor } from "./versionScheme.js";
|
|
21
|
+
const TIER_ORDER = ["same_minor", "major_internal", "cross_major"];
|
|
22
|
+
const UPDATE_HINTS = {
|
|
23
|
+
npm: "直接依存ならpackage.jsonの指定を更新します。推移的依存の場合は、それを要求している直接依存の更新か、" +
|
|
24
|
+
"ルートのpackage.jsonのoverrides(ルートのプロジェクトでのみ有効)で版を指定します",
|
|
25
|
+
Go: "go get <module>@<version>で更新します(推移的依存もgo.modのrequireに追加されて更新されます)。" +
|
|
26
|
+
"Goではv2以上のメジャーは別のモジュールパス(/v2等)として別パッケージ扱いのため、新しいメジャー系列の修正版はここに含まれません",
|
|
27
|
+
};
|
|
28
|
+
/** Goの疑似バージョン(タグのないコミット): 末尾がタイムスタンプ14桁-コミットハッシュ12桁 */
|
|
29
|
+
const GO_PSEUDO_VERSION = /(?:^|[.-])\d{14}-[0-9a-f]{12}(?:\+|$)/;
|
|
30
|
+
function hintFields(ecosystem) {
|
|
31
|
+
const hint = UPDATE_HINTS[ecosystem];
|
|
32
|
+
return hint === undefined ? {} : { update_hint: hint };
|
|
33
|
+
}
|
|
21
34
|
/**
|
|
22
|
-
*
|
|
23
|
-
* 修正版の抽出がMaven
|
|
24
|
-
*
|
|
35
|
+
* 未対応エコシステム・解釈できない現在の版は推奨を出さず、CVEをunfixedにも数えない。
|
|
36
|
+
* 修正版の抽出がMaven専用だった頃、そのまま処理すると修正版のある脆弱性を
|
|
37
|
+
* 「修正版なし」と誤表示した(v0.3.3で確認した不具合)。
|
|
25
38
|
*/
|
|
26
|
-
function
|
|
39
|
+
function notEvaluatedSuggestion(pkg, verification) {
|
|
27
40
|
return {
|
|
28
41
|
package: pkg.name,
|
|
29
42
|
current_version: pkg.version,
|
|
30
43
|
ecosystem: pkg.ecosystem,
|
|
31
44
|
recommended_upgrade: null,
|
|
32
45
|
upgrade_tier: null,
|
|
33
|
-
upgrade_note:
|
|
34
|
-
|
|
46
|
+
upgrade_note: (verification === "unsupported_ecosystem"
|
|
47
|
+
? `${pkg.ecosystem}の修正版推奨には未対応です(修正版の有無は判定していません)。`
|
|
48
|
+
: `現在の版(${pkg.version})をバージョンとして解釈できないため、修正版の推奨を判定していません(git・ローカルパス等の依存の可能性があります)。`) +
|
|
49
|
+
"各脆弱性の修正版はfixed_versions(scan_project)またはexplain_vulnerabilityで確認してください",
|
|
35
50
|
per_cve_detail: pkg.vulnerabilities.map((vuln) => ({
|
|
36
51
|
id: vuln.id,
|
|
37
52
|
cve: vuln.cve,
|
|
@@ -39,124 +54,99 @@ function unsupportedEcosystemSuggestion(pkg) {
|
|
|
39
54
|
fixed_in: null,
|
|
40
55
|
tier: "unsupported",
|
|
41
56
|
})),
|
|
42
|
-
verification
|
|
57
|
+
verification,
|
|
43
58
|
};
|
|
44
59
|
}
|
|
45
|
-
function classifyTier(currentSeries, candidate) {
|
|
46
|
-
const candidateSeries = mavenVersionSeries(candidate);
|
|
47
|
-
if (currentSeries === null || candidateSeries === null)
|
|
48
|
-
return "cross_major";
|
|
49
|
-
if (currentSeries.major !== candidateSeries.major)
|
|
50
|
-
return "cross_major";
|
|
51
|
-
if (currentSeries.minor !== candidateSeries.minor)
|
|
52
|
-
return "major_internal";
|
|
53
|
-
return "same_minor";
|
|
54
|
-
}
|
|
55
60
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
61
|
+
* 現在より新しい候補を推奨の優先順に並べる: 正式版 → Tier順 → 版の昇順。
|
|
62
|
+
* (同じTierのプレリリースより、上のTierの正式版を優先する)
|
|
63
|
+
* 優先順位が等しい版(Goの`23.0.3`と`23.0.3+incompatible`)は、ビルドメタデータの有無が現在の版と同じものを先にする。
|
|
64
|
+
* `/vN`の無いモジュールパスでv2以上を使うには`+incompatible`付きの指定が必要なため。
|
|
58
65
|
*/
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
.
|
|
64
|
-
|
|
65
|
-
return null;
|
|
66
|
-
if (currentSeries !== null) {
|
|
67
|
-
for (const tier of ["same_minor", "major_internal"]) {
|
|
68
|
-
const found = candidates.find((v) => classifyTier(currentSeries, v) === tier);
|
|
69
|
-
if (found !== undefined)
|
|
70
|
-
return { version: found, tier };
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
const version = candidates[0];
|
|
74
|
-
return { version, tier: classifyTier(currentSeries, version) };
|
|
66
|
+
function rankCandidates(scheme, current, versions) {
|
|
67
|
+
const rank = (v) => (scheme.isPrerelease(v) ? TIER_ORDER.length : 0) + TIER_ORDER.indexOf(scheme.classify(current, v));
|
|
68
|
+
const buildMismatch = (v) => (v.includes("+") === current.includes("+") ? 0 : 1);
|
|
69
|
+
return [...new Set(versions)]
|
|
70
|
+
.filter((v) => scheme.isValid(v) && scheme.compare(v, current) > 0)
|
|
71
|
+
.sort((a, b) => rank(a) - rank(b) || scheme.compare(a, b) || buildMismatch(a) - buildMismatch(b));
|
|
75
72
|
}
|
|
76
|
-
function buildNote(
|
|
73
|
+
function buildNote(scheme, pkg, recommended, tier, fixableCount, unfixedCount) {
|
|
74
|
+
let note;
|
|
77
75
|
if (recommended === null) {
|
|
78
|
-
|
|
76
|
+
note = `全${unfixedCount}件のCVEに現在より新しい修正版候補がない(unfixed)。修正版情報の欠落を含む可能性があります`;
|
|
79
77
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
78
|
+
else {
|
|
79
|
+
const label = scheme.seriesLabel(pkg.version);
|
|
80
|
+
switch (tier) {
|
|
81
|
+
case "same_minor":
|
|
82
|
+
note = `現在の${label}系統内の候補${recommended}を推奨`;
|
|
83
|
+
break;
|
|
84
|
+
case "major_internal":
|
|
85
|
+
note = `同一メジャー(${label.split(".")[0]}.x)内の候補${recommended}を推奨`;
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
if (label === null)
|
|
89
|
+
note = `現在バージョンの系統を判定できないため、検証済み候補${recommended}を提示`;
|
|
90
|
+
else if (label.startsWith("0.") && scheme.seriesLabel(recommended)?.startsWith("0.")) {
|
|
91
|
+
note = `候補${recommended}への更新を推奨(0.x系のため、マイナー更新でも破壊的変更の可能性あり)`;
|
|
92
|
+
}
|
|
93
|
+
else
|
|
94
|
+
note = `候補${recommended}へのメジャーアップグレードを推奨(破壊的変更の可能性あり)`;
|
|
95
|
+
}
|
|
96
|
+
note += `。取得済みの影響範囲に基づき修正対象${fixableCount}件のCVEの範囲外と確認しました。全公開版の最小性や未検出の脆弱性がないことは保証しません`;
|
|
97
|
+
if (scheme.isPrerelease(recommended)) {
|
|
98
|
+
note += "。正式版の候補では全件を解消できないため、プレリリース版を推奨しています。正式版の公開を確認してください";
|
|
99
|
+
}
|
|
100
|
+
if (unfixedCount > 0)
|
|
101
|
+
note += `。残り${unfixedCount}件は現在より新しい修正版候補がなく、修正対象から除外しています。recommended_statusを確認してください`;
|
|
102
|
+
}
|
|
103
|
+
if (pkg.ecosystem === "Go" && GO_PSEUDO_VERSION.test(pkg.version)) {
|
|
104
|
+
note += "。現在の版は疑似バージョン(タグのないコミット)です";
|
|
94
105
|
}
|
|
95
|
-
note += `。取得済みの影響範囲に基づき修正対象${fixableCount}件のCVEの範囲外と確認しました。全公開版の最小性や未検出の脆弱性がないことは保証しません`;
|
|
96
|
-
if (unfixedCount > 0)
|
|
97
|
-
note += `。残り${unfixedCount}件は現在より新しい修正版候補がなく、修正対象から除外しています。recommended_statusを確認してください`;
|
|
98
106
|
return note;
|
|
99
107
|
}
|
|
100
108
|
/** 1パッケージ分のアップグレード提案を組み立てる。 */
|
|
101
109
|
export function suggestUpgradeForPackage(pkg) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (pick === null) {
|
|
111
|
-
unfixedCount++;
|
|
112
|
-
details.push({
|
|
113
|
-
id: vuln.id,
|
|
114
|
-
cve: vuln.cve,
|
|
115
|
-
severity: vuln.severity,
|
|
116
|
-
fixed_in: null,
|
|
117
|
-
tier: "unfixed",
|
|
118
|
-
});
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
details.push({
|
|
110
|
+
const scheme = versionSchemeFor(pkg.ecosystem);
|
|
111
|
+
if (scheme === null)
|
|
112
|
+
return notEvaluatedSuggestion(pkg, "unsupported_ecosystem");
|
|
113
|
+
if (!scheme.isValid(pkg.version))
|
|
114
|
+
return { ...notEvaluatedSuggestion(pkg, "unparseable_version"), ...hintFields(pkg.ecosystem) };
|
|
115
|
+
const details = pkg.vulnerabilities.map((vuln) => {
|
|
116
|
+
const fix = rankCandidates(scheme, pkg.version, vuln.fixed_versions)[0];
|
|
117
|
+
return {
|
|
122
118
|
id: vuln.id,
|
|
123
119
|
cve: vuln.cve,
|
|
124
120
|
severity: vuln.severity,
|
|
125
|
-
fixed_in:
|
|
126
|
-
tier:
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
fixed_in: fix ?? null,
|
|
122
|
+
tier: fix !== undefined ? scheme.classify(pkg.version, fix)
|
|
123
|
+
: vuln.fixed_versions.some((v) => !scheme.isValid(v)) ? "unparseable_fix" : "unfixed",
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const unfixedCount = details.filter((d) => d.tier === "unfixed").length;
|
|
127
|
+
const unparseableCount = details.filter((d) => d.tier === "unparseable_fix").length;
|
|
129
128
|
const fixableCount = details.length - unfixedCount;
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const tierOrder = ["same_minor", "major_internal", "cross_major"];
|
|
135
|
-
recommended = null;
|
|
136
|
-
for (const tier of tierOrder) {
|
|
137
|
-
const candidate = candidates.find(v => classifyTier(currentSeries, v) === tier &&
|
|
138
|
-
targets.every(target => candidateStatus(target.affected_versions, v) === "not_affected"));
|
|
139
|
-
if (candidate !== undefined) {
|
|
140
|
-
recommended = candidate;
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
129
|
+
// 解釈できない修正版のCVEも修正対象に残す。その影響範囲は情報不足のため、どの候補も検証できず推奨を保留する
|
|
130
|
+
const targets = pkg.vulnerabilities.filter((_, i) => details[i].tier !== "unfixed");
|
|
131
|
+
const candidates = rankCandidates(scheme, pkg.version, targets.flatMap((v) => v.fixed_versions));
|
|
132
|
+
const recommended = candidates.find((v) => targets.every((target) => candidateStatus(target.affected_versions, v, pkg.ecosystem) === "not_affected")) ?? null;
|
|
144
133
|
for (let i = 0; i < details.length; i++) {
|
|
145
134
|
details[i].recommended_status = recommended === null ? "not_evaluated" :
|
|
146
|
-
candidateStatus(pkg.vulnerabilities[i].affected_versions, recommended);
|
|
135
|
+
candidateStatus(pkg.vulnerabilities[i].affected_versions, recommended, pkg.ecosystem);
|
|
147
136
|
}
|
|
148
|
-
|
|
149
|
-
// (per-CVEのTierの寄せ集めではなく、実際に行うアップグレードの距離を表す)
|
|
150
|
-
const upgradeTier = recommended !== null ? classifyTier(currentSeries, recommended) : null;
|
|
137
|
+
const upgradeTier = recommended !== null ? scheme.classify(pkg.version, recommended) : null;
|
|
151
138
|
return {
|
|
152
139
|
package: pkg.name,
|
|
153
140
|
current_version: pkg.version,
|
|
154
141
|
ecosystem: pkg.ecosystem,
|
|
155
142
|
recommended_upgrade: recommended,
|
|
156
143
|
upgrade_tier: upgradeTier,
|
|
144
|
+
...(recommended !== null && scheme.isPrerelease(recommended) ? { recommended_is_prerelease: true } : {}),
|
|
157
145
|
upgrade_note: recommended === null && fixableCount > 0
|
|
158
|
-
? "既知の修正版候補から、全修正対象CVEの影響範囲外と確認できる版が見つかりません。情報不足・未対応の範囲形式を含む場合も推奨を保留します。"
|
|
159
|
-
|
|
146
|
+
? "既知の修正版候補から、全修正対象CVEの影響範囲外と確認できる版が見つかりません。情報不足・未対応の範囲形式を含む場合も推奨を保留します。" +
|
|
147
|
+
(unparseableCount > 0 ? `${unparseableCount}件のCVEは修正版の記載をバージョンとして解釈できないため(tier: unparseable_fix)、推奨を保留しています。` : "")
|
|
148
|
+
: buildNote(scheme, pkg, recommended, upgradeTier, fixableCount, unfixedCount),
|
|
149
|
+
...hintFields(pkg.ecosystem),
|
|
160
150
|
per_cve_detail: details,
|
|
161
151
|
verification: recommended === null ? "no_verified_candidate" : "verified",
|
|
162
152
|
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* エコシステムごとのバージョンの扱い(docs/DESIGN_TODO.md「suggest_fix npm/Go対応」)。
|
|
3
|
+
*
|
|
4
|
+
* - Maven: Mavenの優先順位(mavenVersion.ts)。範囲はECOSYSTEM型
|
|
5
|
+
* - npm・Go: SemVer 2.0.0の優先順位(semverVersion.ts)。範囲はSEMVER型とECOSYSTEM型
|
|
6
|
+
* - その他(PyPI等): 比較器なし。範囲はECOSYSTEM型
|
|
7
|
+
*
|
|
8
|
+
* GIT型の範囲(コミットハッシュ)はリリース版と無関係のため、どのエコシステムでも版として扱わない。
|
|
9
|
+
*/
|
|
10
|
+
import { compareMavenVersions, mavenVersionSeries } from "../utils/mavenVersion.js";
|
|
11
|
+
import { compareParsedSemver, parseSemver } from "../utils/semverVersion.js";
|
|
12
|
+
const SEMVER_ECOSYSTEMS = new Set(["npm", "Go"]);
|
|
13
|
+
const MAVEN_SCHEME = {
|
|
14
|
+
isValid: () => true,
|
|
15
|
+
compare: compareMavenVersions,
|
|
16
|
+
isPrerelease: () => false,
|
|
17
|
+
classify(current, candidate) {
|
|
18
|
+
const a = mavenVersionSeries(current);
|
|
19
|
+
const b = mavenVersionSeries(candidate);
|
|
20
|
+
if (a === null || b === null || a.major !== b.major)
|
|
21
|
+
return "cross_major";
|
|
22
|
+
return a.minor === b.minor ? "same_minor" : "major_internal";
|
|
23
|
+
},
|
|
24
|
+
seriesLabel(version) {
|
|
25
|
+
const series = mavenVersionSeries(version);
|
|
26
|
+
return series === null ? null : `${series.major}.${series.minor}`;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* npm・Go: npmの^(キャレット)が互換とみなす範囲を同じ系統とする。
|
|
31
|
+
* 1.0.0以上はmajor.minor、0.xは0.minor内だけが互換(マイナー更新は破壊的変更の可能性)、0.0.xはどの変更も非互換。
|
|
32
|
+
*/
|
|
33
|
+
const SEMVER_SCHEME = {
|
|
34
|
+
isValid: (version) => parseSemver(version) !== null,
|
|
35
|
+
compare(a, b) {
|
|
36
|
+
const x = parseSemver(a);
|
|
37
|
+
const y = parseSemver(b);
|
|
38
|
+
return x === null || y === null ? 0 : compareParsedSemver(x, y);
|
|
39
|
+
},
|
|
40
|
+
isPrerelease: (version) => (parseSemver(version)?.prerelease.length ?? 0) > 0,
|
|
41
|
+
classify(current, candidate) {
|
|
42
|
+
const a = parseSemver(current);
|
|
43
|
+
const b = parseSemver(candidate);
|
|
44
|
+
if (a === null || b === null || a.major !== b.major)
|
|
45
|
+
return "cross_major";
|
|
46
|
+
if (a.major === 0) {
|
|
47
|
+
if (a.minor !== b.minor)
|
|
48
|
+
return "cross_major";
|
|
49
|
+
if (a.minor === 0 && a.patch !== b.patch)
|
|
50
|
+
return "cross_major";
|
|
51
|
+
return "same_minor";
|
|
52
|
+
}
|
|
53
|
+
return a.minor === b.minor ? "same_minor" : "major_internal";
|
|
54
|
+
},
|
|
55
|
+
seriesLabel(version) {
|
|
56
|
+
const v = parseSemver(version);
|
|
57
|
+
if (v === null)
|
|
58
|
+
return null;
|
|
59
|
+
return v.major === 0 && v.minor === 0 ? `0.0.${v.patch}` : `${v.major}.${v.minor}`;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
/** 修正版の推奨に対応するエコシステムの操作。未対応ならnull */
|
|
63
|
+
export function versionSchemeFor(ecosystem) {
|
|
64
|
+
if (ecosystem === "Maven")
|
|
65
|
+
return MAVEN_SCHEME;
|
|
66
|
+
if (SEMVER_ECOSYSTEMS.has(ecosystem))
|
|
67
|
+
return SEMVER_SCHEME;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/** 修正版・影響範囲として読み取るOSVの範囲の型 */
|
|
71
|
+
export function versionRangeTypes(ecosystem) {
|
|
72
|
+
if (ecosystem === "Maven")
|
|
73
|
+
return new Set(["ECOSYSTEM"]);
|
|
74
|
+
if (SEMVER_ECOSYSTEMS.has(ecosystem))
|
|
75
|
+
return new Set(["SEMVER", "ECOSYSTEM"]);
|
|
76
|
+
return new Set(["ECOSYSTEM"]);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 重複を除いた版の一覧を、エコシステムの優先順位で昇順に並べる。
|
|
80
|
+
* - npm・Go: SemVerとして解釈できない版は末尾に記載順のまま置く
|
|
81
|
+
* - 比較器のないエコシステム: 記載順のまま返す
|
|
82
|
+
*/
|
|
83
|
+
export function sortVersions(versions, ecosystem) {
|
|
84
|
+
const unique = [...new Set(versions)];
|
|
85
|
+
if (ecosystem === "Maven")
|
|
86
|
+
return unique.sort(compareMavenVersions);
|
|
87
|
+
if (!SEMVER_ECOSYSTEMS.has(ecosystem))
|
|
88
|
+
return unique;
|
|
89
|
+
const parsed = unique.flatMap((raw) => {
|
|
90
|
+
const version = parseSemver(raw);
|
|
91
|
+
return version === null ? [] : [{ raw, version }];
|
|
92
|
+
});
|
|
93
|
+
const unparsed = unique.filter((raw) => parseSemver(raw) === null);
|
|
94
|
+
parsed.sort((a, b) => compareParsedSemver(a.version, b.version) || (a.raw < b.raw ? -1 : a.raw > b.raw ? 1 : 0));
|
|
95
|
+
return [...parsed.map((p) => p.raw), ...unparsed];
|
|
96
|
+
}
|
|
@@ -32,7 +32,7 @@ export async function handleScanJavaArtifact(args, options = {}) {
|
|
|
32
32
|
if (copy.ok)
|
|
33
33
|
originals.set(copy.path, artifact);
|
|
34
34
|
}
|
|
35
|
-
const raw = originals.size > 0 ? await runOsvArtifactScan([...originals.keys()], options) : { results: [] };
|
|
35
|
+
const raw = originals.size > 0 ? await snapshot.guard(() => runOsvArtifactScan([...originals.keys()], options)) : { results: [] };
|
|
36
36
|
return jsonResult(buildArtifactReport(restoreSourcePaths(raw, originals), artifactPaths));
|
|
37
37
|
}
|
|
38
38
|
finally {
|
|
@@ -21,20 +21,37 @@ export function dependencyResolution(noRemoteResolution, warning = TRANSITIVE_OM
|
|
|
21
21
|
? { transitive_resolution: "disabled", warning }
|
|
22
22
|
: { transitive_resolution: "enabled" };
|
|
23
23
|
}
|
|
24
|
-
const SKIPPED_MANIFESTS_WARNING = "
|
|
24
|
+
const SKIPPED_MANIFESTS_WARNING = "一部のマニフェストをスキャン対象から外したか、親POMを含めずにスキャンしました(skipped_manifests / incomplete_manifestsを参照)。" +
|
|
25
25
|
"それらの依存の脆弱性は結果に含まれないため、検出0件でも安全とは判断しないでください";
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
* (既存の出力を変えない)。件数より前に置き、検出0件を安全と誤読させない
|
|
27
|
+
* 外したマニフェスト(skipped)と、親POMを再現できずにスキャンしたマニフェスト(incomplete)。
|
|
28
|
+
* どちらも無ければ何も出力しない(既存の出力を変えない)。件数より前に置き、検出0件を安全と誤読させない
|
|
29
29
|
*/
|
|
30
|
-
export function skippedManifestsFields(skipped) {
|
|
31
|
-
if (skipped.length === 0)
|
|
30
|
+
export function skippedManifestsFields(skipped, incomplete = []) {
|
|
31
|
+
if (skipped.length === 0 && incomplete.length === 0)
|
|
32
32
|
return {};
|
|
33
|
+
const list = (items) => items.map((s) => ({ path: sanitizeExternalText(s.path), reason: sanitizeExternalText(s.reason) }));
|
|
33
34
|
return {
|
|
34
|
-
|
|
35
|
+
...(skipped.length > 0 ? { skipped_manifests: list(skipped) } : {}),
|
|
36
|
+
...(incomplete.length > 0 ? { incomplete_manifests: list(incomplete) } : {}),
|
|
35
37
|
scope_warning: SKIPPED_MANIFESTS_WARNING,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* スキャン対象を外した・親POMを含めずにスキャンしたマニフェストがある状態で「パッケージなし」になった場合、
|
|
42
|
+
* エラーにもその旨を含める(応答の一覧が出ないため、欠落を黙って「依存なし」と読ませない)。
|
|
43
|
+
*/
|
|
44
|
+
export async function withScopeNotes(notes, scan) {
|
|
45
|
+
try {
|
|
46
|
+
return await scan();
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (!(error instanceof ScanToolError) || error.kind !== "no_packages_found" || notes.length === 0)
|
|
50
|
+
throw error;
|
|
51
|
+
throw new ScanToolError(error.kind, `${error.message}。ただし次のマニフェストはスキャン対象から外したか、親POMを含めずにスキャンしたため、` +
|
|
52
|
+
`検出0件でも安全とは判断しないでください: ${notes.map((n) => `${n.path}: ${n.reason}`).join(" / ")}`, error.detail);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
38
55
|
/**
|
|
39
56
|
* 検出したマニフェストをスナップショットへコピーし、コピーをスキャンする(元のファイルは渡さない)。
|
|
40
57
|
* 応答に一時ディレクトリのパスを出さないよう、スキャナーのパスは元のファイルに戻す。
|
|
@@ -43,7 +60,7 @@ export function skippedManifestsFields(skipped) {
|
|
|
43
60
|
export async function scanJavaManifests(project, options) {
|
|
44
61
|
const snapshot = await ScanSnapshot.create();
|
|
45
62
|
try {
|
|
46
|
-
const { targets, skipped } = await snapshotManifests(snapshot, project.targets, {
|
|
63
|
+
const { targets, skipped, incomplete } = await snapshotManifests(snapshot, project.targets, {
|
|
47
64
|
projectDir: project.projectDir,
|
|
48
65
|
allowedRootReal: project.allowedRootReal,
|
|
49
66
|
});
|
|
@@ -56,10 +73,12 @@ export async function scanJavaManifests(project, options) {
|
|
|
56
73
|
throw new ScanToolError(skipped.every((s) => s.kind === "outside_allowed_root") ? "path_outside_allowed_root" : "no_manifest_found", `スキャンできるマニフェストがありません(${details})`);
|
|
57
74
|
}
|
|
58
75
|
const originals = new Map(targets.map((copy, i) => [copy.path, scanned[i].path]));
|
|
59
|
-
const
|
|
76
|
+
const incompleteRelative = incomplete.map((s) => ({ path: relative(s.path), reason: s.reason }));
|
|
77
|
+
const report = await withScopeNotes([...skippedRelative, ...incompleteRelative], () => snapshot.guard(() => runOsvScan(targets, options)));
|
|
60
78
|
return {
|
|
61
79
|
manifests: scanned.map((t) => relative(t.path)),
|
|
62
80
|
skipped: skippedRelative,
|
|
81
|
+
incomplete: incompleteRelative,
|
|
63
82
|
report: { ...report, source_files: report.source_files.map((file) => originals.get(file) ?? file) },
|
|
64
83
|
};
|
|
65
84
|
}
|
|
@@ -73,11 +92,11 @@ export async function handleScanJavaProject(args, options = {}) {
|
|
|
73
92
|
allowedRoot: options.allowedRoot,
|
|
74
93
|
});
|
|
75
94
|
const noRemoteResolution = isRemoteResolutionDisabled(options);
|
|
76
|
-
const { manifests, skipped, report } = await scanJavaManifests(project, { ...options, noRemoteResolution });
|
|
95
|
+
const { manifests, skipped, incomplete, report } = await scanJavaManifests(project, { ...options, noRemoteResolution });
|
|
77
96
|
return jsonResult({
|
|
78
97
|
project_dir: project.projectDir,
|
|
79
98
|
manifests,
|
|
80
|
-
...skippedManifestsFields(skipped),
|
|
99
|
+
...skippedManifestsFields(skipped, incomplete),
|
|
81
100
|
dependency_resolution: dependencyResolution(noRemoteResolution),
|
|
82
101
|
...report,
|
|
83
102
|
});
|
|
@@ -12,9 +12,9 @@ import { sanitizeExternalText } from "../utils/externalText.js";
|
|
|
12
12
|
import { detectProject } from "../utils/manifestDetector.js";
|
|
13
13
|
import { normalizePypiName } from "../utils/requirementsFile.js";
|
|
14
14
|
import { ScanSnapshot, snapshotManifests } from "../utils/scanSnapshot.js";
|
|
15
|
-
import { dependencyResolution } from "./scanJavaProject.js";
|
|
15
|
+
import { dependencyResolution, withScopeNotes } from "./scanJavaProject.js";
|
|
16
16
|
import { errorResult, jsonResult } from "./toolResult.js";
|
|
17
|
-
const TRANSITIVE_OMITTED_WARNING = "OSV_MCP_NO_REMOTE_RESOLUTIONの設定により、マニフェスト(pom.xml / requirements.txt)からの推移的依存の解決を省略しています。" +
|
|
17
|
+
export const TRANSITIVE_OMITTED_WARNING = "OSV_MCP_NO_REMOTE_RESOLUTIONの設定により、マニフェスト(pom.xml / requirements.txt)からの推移的依存の解決を省略しています。" +
|
|
18
18
|
"lockfile(package-lock.json / poetry.lock / go.mod / gradle.lockfile等)に記録された依存は対象ですが、" +
|
|
19
19
|
"pom.xml・requirements.txtに直接記載された依存の先にある推移的依存の脆弱性は含まれません。" +
|
|
20
20
|
"検出0件でも推移的依存の安全性は確認できていません";
|
|
@@ -26,7 +26,7 @@ const MAX_COVERAGE_ITEMS = 200;
|
|
|
26
26
|
function capped(items) {
|
|
27
27
|
return { items: items.slice(0, MAX_COVERAGE_ITEMS), omitted: Math.max(0, items.length - MAX_COVERAGE_ITEMS) };
|
|
28
28
|
}
|
|
29
|
-
function buildCoverage(project) {
|
|
29
|
+
export function buildCoverage(project) {
|
|
30
30
|
const rel = (file) => sanitizeExternalText(path.relative(project.projectDir, file));
|
|
31
31
|
const lockfileMissing = capped(project.lockfileMissing);
|
|
32
32
|
const unpinned = capped(project.requirementIssues);
|
|
@@ -84,22 +84,21 @@ function markLowerBounds(project, packages) {
|
|
|
84
84
|
? { ...pkg, version_is_lower_bound: true }
|
|
85
85
|
: pkg);
|
|
86
86
|
}
|
|
87
|
-
/**
|
|
88
|
-
* requirements.txtは検証済みの正規化行だけを専用の一時ディレクトリに書いてスキャンする
|
|
89
|
-
* (元ファイルの取り込み指定をosv-scannerにたどらせない)。成功・失敗とも削除する。
|
|
90
|
-
*/
|
|
91
87
|
/**
|
|
92
88
|
* 元のファイルはosv-scannerに渡さず、スナップショット(scanSnapshot.ts)のコピーをスキャンする。
|
|
93
89
|
* lockfile・pom.xml(親POMの連鎖を含む)は安全に読んだ内容のコピー、requirements.txtは
|
|
94
90
|
* 検証済みの正規化行だけを書いたコピー。コピーできず外したファイルはskippedFilesに記録する。
|
|
91
|
+
* スナップショットは成功・失敗とも削除する。suggest_fixも同じスキャンを使う。
|
|
95
92
|
*/
|
|
96
|
-
async function scanFromSnapshot(project, options) {
|
|
93
|
+
export async function scanFromSnapshot(project, options) {
|
|
97
94
|
const snapshot = await ScanSnapshot.create();
|
|
98
95
|
try {
|
|
99
|
-
const { targets, skipped } = await snapshotManifests(snapshot, project.targets, {
|
|
96
|
+
const { targets, skipped, incomplete } = await snapshotManifests(snapshot, project.targets, {
|
|
100
97
|
projectDir: project.projectDir,
|
|
101
98
|
allowedRootReal: project.allowedRootReal,
|
|
102
99
|
});
|
|
100
|
+
// 親POMを再現できずにスキャンしたpom.xmlも、欠落の可能性としてcoverageに出す(complete=falseになる)
|
|
101
|
+
project.skippedFiles.push(...incomplete.map((s) => ({ path: path.relative(project.projectDir, s.path), reason: s.reason })));
|
|
103
102
|
if (skipped.length > 0) {
|
|
104
103
|
const skippedPaths = new Set(skipped.map((s) => s.path));
|
|
105
104
|
project.manifests = project.manifests.filter((m) => !skippedPaths.has(path.join(project.projectDir, m.path)));
|
|
@@ -115,7 +114,7 @@ async function scanFromSnapshot(project, options) {
|
|
|
115
114
|
}
|
|
116
115
|
if (targets.length === 0)
|
|
117
116
|
return parseOsvScanOutput({ results: [] });
|
|
118
|
-
return await runOsvScan(targets, options);
|
|
117
|
+
return await withScopeNotes(project.skippedFiles, () => snapshot.guard(() => runOsvScan(targets, options)));
|
|
119
118
|
}
|
|
120
119
|
finally {
|
|
121
120
|
await snapshot.cleanup();
|
package/dist/tools/suggestFix.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `suggest_fix`ツールのハンドラ。
|
|
3
|
-
*
|
|
4
|
-
* 推奨アップグレードバージョン(3段階Tier)
|
|
3
|
+
* scan_projectと同じ検出・スキャンを実行し、脆弱なパッケージごとの
|
|
4
|
+
* 推奨アップグレードバージョン(3段階Tier)を返す。推奨はMaven・npm・Goに対応し、
|
|
5
|
+
* それ以外(PyPI等)はunsupported_ecosystemとして返す。
|
|
5
6
|
*/
|
|
6
7
|
import { isRemoteResolutionDisabled } from "../osv/runner.js";
|
|
7
8
|
import { suggestUpgrades } from "../osv/suggestFix.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
9
|
+
import { sanitizeExternalText } from "../utils/externalText.js";
|
|
10
|
+
import { detectProject } from "../utils/manifestDetector.js";
|
|
11
|
+
import { dependencyResolution, } from "./scanJavaProject.js";
|
|
12
|
+
import { buildCoverage, scanFromSnapshot, TRANSITIVE_OMITTED_WARNING } from "./scanProject.js";
|
|
10
13
|
import { errorResult, jsonResult } from "./toolResult.js";
|
|
11
14
|
export async function handleSuggestFix(args, options = {}) {
|
|
12
15
|
try {
|
|
13
|
-
const project = await
|
|
14
|
-
allowedRoot: options.allowedRoot,
|
|
15
|
-
});
|
|
16
|
+
const project = await detectProject(args.project_path, { allowedRoot: options.allowedRoot });
|
|
16
17
|
const noRemoteResolution = isRemoteResolutionDisabled(options);
|
|
17
|
-
const
|
|
18
|
+
const report = await scanFromSnapshot(project, { ...options, noRemoteResolution });
|
|
18
19
|
const suggestions = suggestUpgrades(report.packages);
|
|
19
20
|
const unfixedVulnerabilities = suggestions.reduce((sum, s) => sum + s.per_cve_detail.filter((d) => d.tier === "unfixed").length, 0);
|
|
20
21
|
return jsonResult({
|
|
21
22
|
project_dir: project.projectDir,
|
|
22
|
-
manifests,
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
manifests: project.manifests.map((m) => sanitizeExternalText(m.path)),
|
|
24
|
+
dependency_resolution: dependencyResolution(noRemoteResolution, TRANSITIVE_OMITTED_WARNING),
|
|
25
|
+
coverage: buildCoverage(project),
|
|
25
26
|
vulnerable_package_count: suggestions.length,
|
|
26
27
|
unfixed_vulnerability_count: unfixedVulnerabilities,
|
|
27
28
|
suggestions,
|
package/dist/utils/safeRead.js
CHANGED
|
@@ -80,7 +80,13 @@ export async function copyRegularFile(filePath, dest, options) {
|
|
|
80
80
|
result = "too_large";
|
|
81
81
|
break;
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
// writeは要求より少ないバイト数で戻りうるため、チャンクを書き切るまで繰り返す
|
|
84
|
+
for (let written = 0; written < bytesRead;) {
|
|
85
|
+
const { bytesWritten } = await target.write(chunk, written, bytesRead - written);
|
|
86
|
+
if (bytesWritten <= 0)
|
|
87
|
+
throw new Error("short write");
|
|
88
|
+
written += bytesWritten;
|
|
89
|
+
}
|
|
84
90
|
}
|
|
85
91
|
}
|
|
86
92
|
finally {
|
|
@@ -25,6 +25,8 @@ export class ScanSnapshot {
|
|
|
25
25
|
maxTotalBytes;
|
|
26
26
|
copied = new Map();
|
|
27
27
|
parentCache = new Map();
|
|
28
|
+
/** 再現した配置のルートごとのディレクトリ → 元のルート("/"等) */
|
|
29
|
+
mirroredRoots = new Map();
|
|
28
30
|
used = 0;
|
|
29
31
|
generated = 0;
|
|
30
32
|
constructor(dir, maxTotalBytes) {
|
|
@@ -42,7 +44,30 @@ export class ScanSnapshot {
|
|
|
42
44
|
/** 元の絶対パスに対応する、スナップショット内のパス */
|
|
43
45
|
mirror(absolute) {
|
|
44
46
|
const { root } = path.parse(absolute);
|
|
45
|
-
|
|
47
|
+
const mirroredRoot = path.join(this.treeRoot, root.replace(/[^A-Za-z0-9]/g, "_"));
|
|
48
|
+
this.mirroredRoots.set(mirroredRoot, root);
|
|
49
|
+
return path.join(mirroredRoot, path.relative(root, absolute));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* スキャナーのメッセージ(stderr等)に含まれるコピーのパスを元のパスに戻す。
|
|
53
|
+
* 生成したコピー(requirements.txt)など元のファイルが無いものは一時ディレクトリと分かる表記にする
|
|
54
|
+
*/
|
|
55
|
+
restorePaths(text) {
|
|
56
|
+
let restored = text;
|
|
57
|
+
for (const [mirroredRoot, root] of this.mirroredRoots)
|
|
58
|
+
restored = restored.split(mirroredRoot + path.sep).join(root);
|
|
59
|
+
return restored.split(this.dir).join("<一時コピー>");
|
|
60
|
+
}
|
|
61
|
+
/** スキャナーのエラーに一時ディレクトリのパスを出さない(応答のパスは常に元のファイルを指す) */
|
|
62
|
+
async guard(scan) {
|
|
63
|
+
try {
|
|
64
|
+
return await scan();
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (!(error instanceof ScanToolError))
|
|
68
|
+
throw error;
|
|
69
|
+
throw new ScanToolError(error.kind, this.restorePaths(error.message), error.detail === undefined ? undefined : this.restorePaths(error.detail));
|
|
70
|
+
}
|
|
46
71
|
}
|
|
47
72
|
/** 元のファイルを安全にコピーする。同じファイルは1回だけコピーする */
|
|
48
73
|
async copy(absolute, boundary, maxBytes = Infinity) {
|
|
@@ -89,11 +114,15 @@ const UNPARSEABLE = "親POMの指定を確実に解釈できないため(親要
|
|
|
89
114
|
/**
|
|
90
115
|
* pom.xmlと親POMの連鎖をスナップショットへコピーする。
|
|
91
116
|
* 許可ルート設定時、連鎖が許可ルートの外・スナップショットの外を参照するか、解釈できない場合は除外理由を返す。
|
|
117
|
+
* 存在する親POMを再現できなかった場合(サイズ超過・通常のファイルでない等)はスキャンを続けるが、
|
|
118
|
+
* 親から継承する依存が欠ける可能性があるためincompleteに理由を返す(黙って成功扱いにしない)。
|
|
92
119
|
*/
|
|
93
120
|
async function snapshotPom(snapshot, pomPath, context) {
|
|
94
|
-
const
|
|
95
|
-
if (!
|
|
96
|
-
return { ok: false, reason:
|
|
121
|
+
const copied = await snapshot.copy(pomPath, context.projectDir, MAX_POM_BYTES);
|
|
122
|
+
if (!copied.ok)
|
|
123
|
+
return { ok: false, reason: copied.message, kind: "unreadable" };
|
|
124
|
+
const first = { ok: true, path: copied.path };
|
|
125
|
+
const partial = (reason) => ({ ...first, incomplete: reason });
|
|
97
126
|
const restricted = context.allowedRootReal !== undefined;
|
|
98
127
|
let current = pomPath;
|
|
99
128
|
let currentCopy = first.path;
|
|
@@ -101,8 +130,11 @@ async function snapshotPom(snapshot, pomPath, context) {
|
|
|
101
130
|
const relativePath = await snapshot.parentOf(currentCopy);
|
|
102
131
|
if (relativePath === null || relativePath === "")
|
|
103
132
|
return first;
|
|
104
|
-
if (relativePath === undefined)
|
|
105
|
-
return restricted
|
|
133
|
+
if (relativePath === undefined) {
|
|
134
|
+
return restricted
|
|
135
|
+
? { ok: false, reason: UNPARSEABLE, kind: "unreadable" }
|
|
136
|
+
: partial("親POMの指定を確実に解釈できないため、親POMを含めずにスキャンしました。親から継承する依存が結果に含まれない可能性があります");
|
|
137
|
+
}
|
|
106
138
|
// Goのfilepath.Joinと同じ規則で結合する(絶対パスも連結し、..はルートで止まる)
|
|
107
139
|
const inCopy = path.join(path.dirname(currentCopy), relativePath);
|
|
108
140
|
if (!isInsideDir(snapshot.treeRoot, inCopy)) {
|
|
@@ -116,8 +148,12 @@ async function snapshotPom(snapshot, pomPath, context) {
|
|
|
116
148
|
if ((await stat(candidate)).isDirectory())
|
|
117
149
|
candidate = path.join(candidate, "pom.xml");
|
|
118
150
|
}
|
|
119
|
-
catch {
|
|
120
|
-
|
|
151
|
+
catch (error) {
|
|
152
|
+
// 参照先が無い: 元の配置でもosv-scannerは親を読まないため、欠落ではない
|
|
153
|
+
const code = error.code;
|
|
154
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
155
|
+
return first;
|
|
156
|
+
return partial(`親POM(relativePath: ${relativePath})を確認できないため、親POMを含めずにスキャンしました。親から継承する依存が結果に含まれない可能性があります`);
|
|
121
157
|
}
|
|
122
158
|
const parent = await snapshot.copy(candidate, context.allowedRootReal, MAX_POM_BYTES);
|
|
123
159
|
if (!parent.ok) {
|
|
@@ -131,27 +167,36 @@ async function snapshotPom(snapshot, pomPath, context) {
|
|
|
131
167
|
}
|
|
132
168
|
if (parent.failure === "changed")
|
|
133
169
|
return { ok: false, reason: `親POMの${parent.message}`, kind: "unreadable" };
|
|
134
|
-
|
|
170
|
+
// 読めない・通常のファイルでない・サイズ超過等: コピーしないため、osv-scannerも読まない
|
|
171
|
+
return partial(`親POM(relativePath: ${relativePath})を読めないため(${parent.message})、親POMを含めずにスキャンしました。` +
|
|
172
|
+
"親から継承する依存が結果に含まれない可能性があります(親のGAVが一致しない場合はもともと読まれません)");
|
|
135
173
|
}
|
|
136
174
|
current = candidate;
|
|
137
175
|
currentCopy = parent.path;
|
|
138
176
|
}
|
|
139
|
-
return restricted
|
|
177
|
+
return restricted
|
|
178
|
+
? { ok: false, reason: `親POMの連鎖が上限(${MAX_PARENT_DEPTH}段)を超えています`, kind: "unreadable" }
|
|
179
|
+
: partial(`親POMの連鎖が上限(${MAX_PARENT_DEPTH}段)を超えるため、それより上の親POMを含めずにスキャンしました`);
|
|
140
180
|
}
|
|
141
181
|
/**
|
|
142
182
|
* 検出済みのマニフェストをスナップショットへコピーし、osv-scannerに渡す対象(コピー)を返す。
|
|
143
|
-
* コピーできない・除外すべきものはskippedに理由付きで返す。
|
|
183
|
+
* コピーできない・除外すべきものはskippedに、親POMを再現できずスキャンしたものはincompleteに理由付きで返す。
|
|
144
184
|
*/
|
|
145
185
|
export async function snapshotManifests(snapshot, targets, context) {
|
|
146
186
|
const copies = [];
|
|
147
187
|
const skipped = [];
|
|
188
|
+
const incomplete = [];
|
|
148
189
|
for (const target of targets) {
|
|
149
190
|
if (target.format === "pom.xml") {
|
|
150
191
|
const result = await snapshotPom(snapshot, target.path, context);
|
|
151
|
-
if (result.ok)
|
|
192
|
+
if (result.ok) {
|
|
152
193
|
copies.push({ path: result.path, format: target.format });
|
|
153
|
-
|
|
194
|
+
if (result.incomplete !== undefined)
|
|
195
|
+
incomplete.push({ path: target.path, reason: result.incomplete });
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
154
198
|
skipped.push({ path: target.path, reason: result.reason, kind: result.kind });
|
|
199
|
+
}
|
|
155
200
|
continue;
|
|
156
201
|
}
|
|
157
202
|
const result = await snapshot.copy(target.path, context.projectDir);
|
|
@@ -160,5 +205,5 @@ export async function snapshotManifests(snapshot, targets, context) {
|
|
|
160
205
|
else
|
|
161
206
|
skipped.push({ path: target.path, reason: result.message, kind: "unreadable" });
|
|
162
207
|
}
|
|
163
|
-
return { targets: copies, skipped };
|
|
208
|
+
return { targets: copies, skipped, incomplete };
|
|
164
209
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Versioning 2.0.0のバージョン解釈と優先順位比較(semver.org 11節)。
|
|
3
|
+
*
|
|
4
|
+
* OSVのnpm・Goの範囲(SEMVER型、およびECOSYSTEM型)の順序はSemVerの優先順位に従う。
|
|
5
|
+
* 実データ(docs/DESIGN_TODO.md「suggest_fix npm/Go対応」)の要点:
|
|
6
|
+
* - Goの疑似バージョン(`0.0.0-20190101120000-abcdef123456`)はプレリリースとして正しく並ぶ
|
|
7
|
+
* - Goの`+incompatible`はビルドメタデータとして比較で無視する(`20.10.14+incompatible` = `20.10.14`)
|
|
8
|
+
* - `19.03.9`(数値部の先頭ゼロ)や`13.0`のような値はSemVerではないためnullを返す
|
|
9
|
+
*/
|
|
10
|
+
const NUMERIC = "0|[1-9]\\d*";
|
|
11
|
+
const PRE_ID = "(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)";
|
|
12
|
+
const BUILD_ID = "[0-9A-Za-z-]+";
|
|
13
|
+
const SEMVER_RE = new RegExp(`^v?(${NUMERIC})\\.(${NUMERIC})\\.(${NUMERIC})` +
|
|
14
|
+
`(?:-(${PRE_ID}(?:\\.${PRE_ID})*))?` +
|
|
15
|
+
`(?:\\+${BUILD_ID}(?:\\.${BUILD_ID})*)?$`);
|
|
16
|
+
/**
|
|
17
|
+
* SemVer 2.0.0として厳密に解釈する。解釈できなければnull。
|
|
18
|
+
* 先頭の`v`は防御的に1文字だけ受け付ける(OSVとosv-scannerはGoの版を`v`なしで返す)。
|
|
19
|
+
*/
|
|
20
|
+
export function parseSemver(version) {
|
|
21
|
+
const m = SEMVER_RE.exec(version);
|
|
22
|
+
if (!m)
|
|
23
|
+
return null;
|
|
24
|
+
const numbers = [m[1], m[2], m[3]].map(Number);
|
|
25
|
+
if (!numbers.every(Number.isSafeInteger))
|
|
26
|
+
return null;
|
|
27
|
+
return {
|
|
28
|
+
major: numbers[0],
|
|
29
|
+
minor: numbers[1],
|
|
30
|
+
patch: numbers[2],
|
|
31
|
+
prerelease: m[4] === undefined ? [] : m[4].split(".").map((id) => (/^\d+$/.test(id) ? BigInt(id) : id)),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function cmp(a, b) {
|
|
35
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
36
|
+
}
|
|
37
|
+
/** 解釈済みの2つの版をSemVerの優先順位で比較する(ビルドメタデータは無視)。 */
|
|
38
|
+
export function compareParsedSemver(a, b) {
|
|
39
|
+
const core = cmp(a.major, b.major) || cmp(a.minor, b.minor) || cmp(a.patch, b.patch);
|
|
40
|
+
if (core !== 0)
|
|
41
|
+
return core;
|
|
42
|
+
// プレリリースを持つ版は、同じ版の正式リリースより小さい
|
|
43
|
+
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
44
|
+
return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
|
|
45
|
+
}
|
|
46
|
+
const length = Math.min(a.prerelease.length, b.prerelease.length);
|
|
47
|
+
for (let i = 0; i < length; i++) {
|
|
48
|
+
const x = a.prerelease[i];
|
|
49
|
+
const y = b.prerelease[i];
|
|
50
|
+
if (typeof x === typeof y) {
|
|
51
|
+
const result = cmp(x, y);
|
|
52
|
+
if (result !== 0)
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// 数値の識別子は英数字の識別子より小さい
|
|
57
|
+
return typeof x === "bigint" ? -1 : 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return cmp(a.prerelease.length, b.prerelease.length);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 2つの版をSemVerの優先順位で比較する。どちらかを解釈できなければnull。
|
|
64
|
+
*/
|
|
65
|
+
export function compareSemver(a, b) {
|
|
66
|
+
const x = parseSemver(a);
|
|
67
|
+
const y = parseSemver(b);
|
|
68
|
+
return x === null || y === null ? null : compareParsedSemver(x, y);
|
|
69
|
+
}
|