osv-scanner-mcp 0.8.0 → 0.10.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 +41 -3
- package/dist/index.js +26 -2
- package/dist/osv/artifactReport.js +46 -6
- package/dist/osv/binaryDownloader.js +4 -2
- package/dist/osv/binaryManager.js +10 -3
- package/dist/osv/scanReport.js +14 -6
- package/dist/osv/suggestFix.js +8 -0
- package/dist/tools/scanProject.js +14 -5
- package/dist/tools/toolResult.js +4 -0
- package/dist/utils/artifactDetector.js +5 -1
- package/dist/utils/dependencyRelations.js +9 -0
- package/dist/utils/permissionCheck.js +38 -0
- package/dist/utils/projectWalk.js +21 -3
- package/dist/utils/safeRead.js +6 -5
- package/dist/utils/sbomInput.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -136,6 +136,41 @@ npm run build
|
|
|
136
136
|
>
|
|
137
137
|
> 依存の情報をどこに送るかは[通信先とプライバシー](#通信先とプライバシー)を確認してください。どの設定でも、脆弱性照会のためパッケージの名前とバージョンは `api.osv.dev` に送られます。
|
|
138
138
|
|
|
139
|
+
### 権限を絞って起動する
|
|
140
|
+
|
|
141
|
+
多層防御として、Nodeの権限モデル(`--permission`)でサーバーが読み書きできる範囲を絞れます(任意。既定の起動方法は変わりません)。`npx` ではNodeのフラグを渡せないため、`node` で直接起動します。
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"mcpServers": {
|
|
146
|
+
"osv-scanner": {
|
|
147
|
+
"command": "node",
|
|
148
|
+
"args": [
|
|
149
|
+
"--permission",
|
|
150
|
+
"--allow-fs-read=/path/to/OSV-Scanner-MCP",
|
|
151
|
+
"--allow-fs-read=/Users/you/projects",
|
|
152
|
+
"--allow-fs-read=/var/folders/xx/yyyy/T",
|
|
153
|
+
"--allow-fs-read=/private/var/folders/xx/yyyy/T",
|
|
154
|
+
"--allow-fs-write=/private/var/folders/xx/yyyy/T",
|
|
155
|
+
"--allow-fs-read=/opt/homebrew/bin/osv-scanner",
|
|
156
|
+
"--allow-child-process",
|
|
157
|
+
"/path/to/OSV-Scanner-MCP/dist/index.js"
|
|
158
|
+
],
|
|
159
|
+
"env": {
|
|
160
|
+
"OSV_MCP_ALLOWED_ROOT": "/Users/you/projects",
|
|
161
|
+
"OSV_SCANNER_PATH": "/opt/homebrew/bin/osv-scanner"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- 読み取り: サーバー本体(`dist` と `node_modules` を含むディレクトリ)、スキャン対象(`OSV_MCP_ALLOWED_ROOT`)、一時ディレクトリ、osv-scannerのバイナリ
|
|
169
|
+
- 一時ディレクトリ(`os.tmpdir()`、macOSでは `$TMPDIR`)は、**シンボリックリンクの解決前と解決後の両方のパス**に読み取りの許可が必要です(macOSの `/var/folders/...` は `/private/var/folders/...` へのリンク)。書き込みは解決後のパスに許可します
|
|
170
|
+
- 自動ダウンロードを使う場合は、キャッシュ(`$XDG_CACHE_HOME/osv-scanner-mcp`、既定は `~/.cache/osv-scanner-mcp`)の読み書きも許可します。使わない場合は `OSV_SCANNER_PATH` を指定します
|
|
171
|
+
- osv-scannerを起動するため `--allow-child-process` が必要です。**子プロセスのosv-scannerは権限モデルの制限を受けません**(Node自身もこのフラグは権限モデルを弱めると警告します)。osv-scannerには検証済みのコピーだけを渡しているため影響は限定的ですが、より強い隔離が必要ならコンテナ等のOSレベルの仕組みを併用してください
|
|
172
|
+
- 必要な許可が欠けている場合は起動時に標準エラー出力へ警告し、スキャン時は `permission_denied`(不足している許可と対象のパス)を返します
|
|
173
|
+
|
|
139
174
|
### 通信先とプライバシー
|
|
140
175
|
|
|
141
176
|
osv-scanner v2.4.0 で接続先を実機確認した結果です(2026-10-07)。
|
|
@@ -220,7 +255,8 @@ api.osv.dev と deps.dev はどちらも Google が運営するサービスで
|
|
|
220
255
|
- `package-lock.json`(v2以降): ルートとworkspaceのpackage.jsonの依存を、Nodeの解決規則(入れ子の `node_modules` から上位へ)で解決したものが直接依存、そこからたどれるものが推移的依存です。直接依存には宣言しているpackage.jsonを `declared_in` に、推移的依存にはそれを要求している直接依存の名前を `introduced_by`(最大10件、超えた分は `introduced_by_omitted`)に示します。直接依存でもあり他の依存からも要求される版は `direct` とし、`introduced_by` も付けます
|
|
221
256
|
- `go.mod`: `// indirect` の無い `require` が直接依存です。`replace` で置き換えているモジュールには `replaced_in_go_mod: true` を付けます(OSV-Scannerは置換先のパスと版で報告します)
|
|
222
257
|
- `requirements.txt`: ファイルに書かれた依存が直接依存、deps.devで解決された依存が推移的依存です
|
|
223
|
-
-
|
|
258
|
+
- `pom.xml`: OSV-Scannerは、`pom.xml`(と親POM)に宣言された依存と、deps.devで解決された推移的依存を別々の結果(`source.type` が `lockfile` / `unknown`)に分けて報告するため、それで判定します(親POM・プロファイル・プロパティ・依存管理の解釈はOSV-Scannerと同じになります)。`introduced_by` / `declared_in` は付きません。この判定はOSV-Scannerの文書化されていない出力の形に依存するため、想定外の形の場合は `unknown` にします
|
|
259
|
+
- 上記以外の形式(`gradle.lockfile`、`yarn.lock`、`pnpm-lock.yaml`、`bun.lock`、`poetry.lock`、`uv.lock`、`Pipfile.lock`、`pdm.lock`)と、lockfileVersion 1・どこからも要求されていないエントリは `unknown` です。複数のlockfileで判定が異なる場合は `mixed` です
|
|
224
260
|
- `dependency_groups` はOSV-Scannerが付けた依存グループ(例: `dev`)の生の値です。lockfileの形式によって欠落・不正確なため(pnpmでは付かず、pdmでは `optional` になる等)、参考情報として扱ってください
|
|
225
261
|
- 修正版の推奨(`suggest_fix`)はJava・JavaScript・Python・Goに対応しています
|
|
226
262
|
|
|
@@ -309,7 +345,8 @@ OSV-Scanner 2.4.0の `java/archive` プラグインを使用し、ネストJAR
|
|
|
309
345
|
**出力の読み方:**
|
|
310
346
|
|
|
311
347
|
- 先頭の `coverage` に `jars_found`、`jars_identified`、`unidentified_jars` を返します。件数はWARも含む、ファイルシステム上で列挙した外側のアーカイブ単位です。ネストJARの総数ではありません。
|
|
312
|
-
- `artifacts[].status` は `identified_with_vulnerabilities` / `identified_without_known_vulnerabilities` / `unidentified` の
|
|
348
|
+
- `artifacts[].status` は `identified_with_vulnerabilities` / `identified_without_known_vulnerabilities` / `inferred_only` / `unidentified` の4値です。「同定済み」は少なくとも1件のMaven座標を取得できた意味であり、全依存の同定ではありません。`inferred_only` は推測した座標(下記)だけで同定したアーカイブで、脆弱性が見つかった場合も含め `jars_identified` に数えず、`unidentified_jars` に理由付きで示します(検出件数は `identified_vulnerability_count` に示します。推測の誤ったgroupIdで他の脆弱性を取りこぼしている可能性があるため)。
|
|
349
|
+
- **推測した座標**: `pom.properties` を含まないJAR(Spring Frameworkの本体JARなど)について、OSV-Scannerはファイル名等からMaven座標を推測し、groupIdを誤ることがあります(例: `spring-beans:spring-beans`。正しくは `org.springframework:spring-beans`)。誤った座標はOSVで照合されず、**既知の脆弱性を取りこぼします**(実例: zipkin-server 2.23.2 のfat JARに含まれる spring-beans 5.3.2 のSpring4Shell(CVE-2022-22965)は検出されません)。groupIdに `.` を含まない座標を推測とみなし、`coverage.inferred_coordinates`(件数・一覧・警告)と、該当パッケージの `coordinates_inferred: true` で示します。`commons-io:commons-io` のような古い形式の正しい座標も含まれます(安全側)。`.` を含む誤った推測(`com.sun.jna:jna` 等)は区別できません。正確な結果には、ビルド元のlockfile・`pom.xml` を `scan_project` でスキャンしてください
|
|
313
350
|
- `coverage.completeness` は常に `incomplete`。`identified_vulnerability_count: 0` は安全性の保証ではありません。
|
|
314
351
|
- `packages` は同定できた脆弱なパッケージの詳細です。複数アーカイブに含まれる同一パッケージ・脆弱性は全体集計では重複排除します。
|
|
315
352
|
- JAR/WARが無い場合は `no_scannable_artifacts`、全件同定不能の場合は警告を含む成功レポートです。
|
|
@@ -387,7 +424,7 @@ npm・Go・PyPIの「同じ系統」は、npmの `^`(キャレット)が互換
|
|
|
387
424
|
- プレリリース版(`5.0.0-beta.3`、`15.6.0-canary.61`、Goの疑似バージョン、PyPIの `rc`・`dev` 版。post版は正式版扱い)は、正式版の候補では全CVEを解消できない場合だけ推奨し、`recommended_is_prerelease: true` を付けます。同じTierのプレリリースより、上のTierの正式版を優先します。
|
|
388
425
|
- 推奨時は `verification: "verified"`、CVEごとの `recommended_status` は `affected` / `not_affected` / `unknown` です。推奨保留時は `not_evaluated` になります。`per_cve_detail.fixed_in` は各CVE単独の候補であり、最終推奨先の判定は `recommended_status` を参照してください。
|
|
389
426
|
- 現在より新しい修正版候補がないCVEは `tier: "unfixed"` として推奨の修正対象から除外します(情報欠落を含む場合があります)。除外したCVEも推奨先で判定し、その状態を表示します。全CVEがunfixedの場合も `recommended_upgrade` は `null` です。修正版の記載はあるがバージョンとして解釈できないCVE(SemVerでない `13.0` 等)は `tier: "unparseable_fix"` とし、修正版が無いとは扱わず修正対象に残すため、推奨は保留(`no_verified_candidate`)になります。
|
|
390
|
-
- npm・Go・PyPIの提案には更新方法の `update_hint` を付けます。PyPIでは、requirements.txtやpyproject.toml・Pipfileの指定を更新してlockfileを再生成し、推移的依存はpipの制約ファイル(`-c`)やuv・Poetry
|
|
427
|
+
- npm・Go・PyPIの提案には更新方法の `update_hint` を付けます。PyPIでは、requirements.txtやpyproject.toml・Pipfileの指定を更新してlockfileを再生成し、推移的依存はpipの制約ファイル(`-c`)やuv・Poetryの上書き設定で版を指定します。Maven(`pom.xml` 由来)では、直接依存は `<dependency>` の版(親POM・プロパティ・BOMで管理していればそちら)、推移的依存は `<dependencyManagement>` での上書きを案内します。推移的依存の場合、npmでは要求している直接依存の更新か、ルートの `package.json` の `overrides`(ルートのプロジェクトでのみ有効)で版を指定します。Goでは `go get <module>@<version>` で更新できます。Goのv2以上のメジャーは別のモジュールパス(`/v2` 等)としてOSV上も別パッケージになるため、新しいメジャー系列の修正版は候補に含まれません。現在の版が疑似バージョン(タグのないコミット)の場合は `upgrade_note` に示します。
|
|
391
428
|
- **推奨先のOSV照会**: 推奨はスキャンで分かった脆弱性(現在の版に該当するもの)の範囲だけで検証しているため、推奨先に現在の版には該当しない新しい脆弱性がありえます(例: cryptography 3.2 の推奨候補 49.0.0 は、44.0.0 で混入し 50.0.0 で修正された2件に該当)。そこで推奨先を `api.osv.dev` に照会し、該当する脆弱性があれば、それも避けるよう修正版を候補に加えて選び直します(この例では 50.0.0 を推奨し、`upgrade_note` に理由を示します)。結果は `candidate_check` に示します:
|
|
392
429
|
- `clean`: 推奨先に該当する既知の脆弱性はありません
|
|
393
430
|
- `has_known_vulnerabilities`: 避けられる修正版の候補が見つからず、推奨先が既知の脆弱性に該当します(`recommended_known_vulnerabilities` にID)
|
|
@@ -433,6 +470,7 @@ npm・Go・PyPIの「同じ系統」は、npmの `^`(キャレット)が互換
|
|
|
433
470
|
|---|---|
|
|
434
471
|
| `binary_not_found` | OSV-Scannerが見つからない(インストール案内をmessageに含む) |
|
|
435
472
|
| `project_not_found` | 指定パスが存在しない・ディレクトリ/pom.xmlでない |
|
|
473
|
+
| `permission_denied` | Nodeの権限モデル(`--permission`)でファイルの読み書きが許可されていない(メッセージに不足している許可と対象のパスを示します。[権限を絞って起動する](#権限を絞って起動する)を参照) |
|
|
436
474
|
| `no_manifest_found` | 対応マニフェスト(pom.xml / gradle.lockfile)が見つからない |
|
|
437
475
|
| `scan_input_too_large` | スキャン対象ファイル(一時ディレクトリへのコピー)の合計サイズが上限(2GiB)を超えた。対象を絞って再実行する |
|
|
438
476
|
| `manifest_search_limit_exceeded` | マニフェスト探索が上限(20万エントリ・1,000マニフェスト)に達した。より狭いディレクトリかマニフェストを直接指定する |
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,10 @@ import { handleScanProject } from "./tools/scanProject.js";
|
|
|
21
21
|
import { handleScanJavaArtifact } from "./tools/scanJavaArtifact.js";
|
|
22
22
|
import { handleScanSbom } from "./tools/scanSbom.js";
|
|
23
23
|
import { handleSuggestFix } from "./tools/suggestFix.js";
|
|
24
|
+
import { realpathSync } from "node:fs";
|
|
25
|
+
import os from "node:os";
|
|
26
|
+
import { defaultCacheDir } from "./osv/binaryDownloader.js";
|
|
27
|
+
import { permissionModelWarnings } from "./utils/permissionCheck.js";
|
|
24
28
|
import { installShutdownHandlers, removeStaleTempDirs } from "./utils/processCleanup.js";
|
|
25
29
|
import { ALLOWED_ROOT_ENV, allowedRootFromEnv, allowedRootStartupError, allowedRootStartupWarning, } from "./utils/startupConfig.js";
|
|
26
30
|
export { ALLOWED_ROOT_ENV };
|
|
@@ -35,10 +39,29 @@ const startupWarning = allowedRootStartupWarning();
|
|
|
35
39
|
if (startupWarning !== null) {
|
|
36
40
|
console.error(`osv-scanner-mcp: [警告] ${startupWarning}`);
|
|
37
41
|
}
|
|
42
|
+
// Nodeの権限モデル(--permission)で起動された場合、必要な許可が欠けていればstderrで知らせる(起動は続ける)
|
|
43
|
+
{
|
|
44
|
+
const tmpDir = os.tmpdir();
|
|
45
|
+
let tmpDirReal = tmpDir;
|
|
46
|
+
try {
|
|
47
|
+
tmpDirReal = realpathSync(tmpDir);
|
|
48
|
+
}
|
|
49
|
+
catch { /* 解決前のパスの読み取りが拒否されている場合も、警告で示す */ }
|
|
50
|
+
const scannerPath = process.env.OSV_SCANNER_PATH?.trim();
|
|
51
|
+
for (const warning of permissionModelWarnings({
|
|
52
|
+
allowedRoot: allowedRootFromEnv(),
|
|
53
|
+
tmpDir,
|
|
54
|
+
tmpDirReal,
|
|
55
|
+
cacheDir: scannerPath ? undefined : defaultCacheDir(),
|
|
56
|
+
scannerPath: scannerPath || undefined,
|
|
57
|
+
})) {
|
|
58
|
+
console.error(`osv-scanner-mcp: [警告] ${warning}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
38
61
|
// NOTE: リリース時はpackage.jsonのversionと同じ値に更新すること
|
|
39
62
|
const server = new McpServer({
|
|
40
63
|
name: "osv-scanner-mcp",
|
|
41
|
-
version: "0.
|
|
64
|
+
version: "0.10.0",
|
|
42
65
|
});
|
|
43
66
|
server.registerTool("scan_project", {
|
|
44
67
|
title: "プロジェクトの依存の脆弱性スキャン(Java / JavaScript / Python / Go)",
|
|
@@ -47,7 +70,7 @@ server.registerTool("scan_project", {
|
|
|
47
70
|
"Python(poetry.lock / uv.lock / Pipfile.lock / pdm.lock / requirements.txt)、Go(go.mod)。" +
|
|
48
71
|
"パッケージマネージャーやビルドは実行しない。" +
|
|
49
72
|
"応答先頭のcoverageを必ず確認すること: lockfileが無いマニフェスト、バージョン未固定のrequirements行、スキャン対象から外したファイルを示す。" +
|
|
50
|
-
"各パッケージのdependency_relationは直接依存(direct)か推移的依存(transitive)か(package-lock.json・go.mod・requirements.txtで判定。それ以外はunknown)。" +
|
|
73
|
+
"各パッケージのdependency_relationは直接依存(direct)か推移的依存(transitive)か(package-lock.json・go.mod・requirements.txt・pom.xmlで判定。それ以外はunknown)。" +
|
|
51
74
|
"coverage.complete=falseの場合は、検出0件でも安全とは判断しないこと。修正版の推奨(suggest_fix)はJava・JavaScript・Python・Goに対応。",
|
|
52
75
|
inputSchema: {
|
|
53
76
|
project_path: z
|
|
@@ -104,6 +127,7 @@ server.registerTool("scan_java_artifact", {
|
|
|
104
127
|
title: "JAR/WAR実体の脆弱性スキャン",
|
|
105
128
|
description: "JAR/WARファイルまたはディレクトリ内の実体をスキャンする。ビルドやJavaコードの実行は行わない。" +
|
|
106
129
|
"メタデータによるベストエフォート同定のため、coverageと同定不能ファイルを必ず確認すること。" +
|
|
130
|
+
"pom.propertiesのないJARは座標を推測し、groupIdの誤りで既知の脆弱性を取りこぼしうる(coverage.inferred_coordinates、status: inferred_only)。" +
|
|
107
131
|
"completenessは常にincomplete。検出0件でも安全性や全依存の同定を保証しない。",
|
|
108
132
|
inputSchema: {
|
|
109
133
|
artifact_path: z.string().min(1).describe("JAR/WARファイル、または探索するディレクトリの絶対パス"),
|
|
@@ -4,6 +4,23 @@ import { asArray, asRecord } from "../utils/unknownJson.js";
|
|
|
4
4
|
import { parseOsvScanOutput } from "./scanReport.js";
|
|
5
5
|
const WARNING = "Best-effort metadata identification only. Missing or removed metadata can hide dependencies, even in identified archives. Zero identified vulnerabilities does not establish safety.";
|
|
6
6
|
const HINT = "Metadata may be missing (shaded/minimized archive). Scan the build project's lockfile instead.";
|
|
7
|
+
const INFERRED_WARNING = "Some Maven coordinates were inferred from file names because the JAR has no pom.properties (inferred_coordinates). " +
|
|
8
|
+
"Inferred groupIds are often wrong (e.g. spring-beans:spring-beans instead of org.springframework:spring-beans), " +
|
|
9
|
+
"so known vulnerabilities of these packages may be missed. Scan the build project's lockfile or pom.xml (scan_project) for accurate results.";
|
|
10
|
+
const INFERRED_HINT = "Identified only by inferred coordinates (no pom.properties). Known vulnerabilities may be missed.";
|
|
11
|
+
/** coverage.inferred_coordinatesの上限(巨大なWARで応答を膨らませない) */
|
|
12
|
+
const MAX_INFERRED_ITEMS = 200;
|
|
13
|
+
/**
|
|
14
|
+
* osv-scannerがpom.propertiesのないJARについて、ファイル名等から推測した座標か(B3の実物の検証で判明)。
|
|
15
|
+
* 推測の座標はgroupIdを誤りやすく(`spring-beans:spring-beans`、`jar:grpc-netty-shaded`、`all:opentelemetry-api`)、
|
|
16
|
+
* OSVで照会しても0件になり既知の脆弱性を取りこぼす。osv-scannerの出力には座標の出所がないため、
|
|
17
|
+
* groupIdに`.`がないものを推測とみなす。`commons-io:commons-io`のような古い形式の正しい座標も含む(安全側)。
|
|
18
|
+
* `.`を含む誤った推測(`com.sun.jna:jna`)は区別できない
|
|
19
|
+
*/
|
|
20
|
+
export function isInferredCoordinate(name) {
|
|
21
|
+
const groupId = name.split(":")[0] ?? "";
|
|
22
|
+
return !groupId.includes(".");
|
|
23
|
+
}
|
|
7
24
|
function isIdentifiedPackage(raw) {
|
|
8
25
|
const info = asRecord(asRecord(raw)?.package);
|
|
9
26
|
if (info?.ecosystem !== "Maven" || typeof info.name !== "string" || typeof info.version !== "string")
|
|
@@ -31,30 +48,53 @@ export function buildArtifactReport(raw, artifactPaths) {
|
|
|
31
48
|
}
|
|
32
49
|
const normalized = [...byPath].map(([file, packages]) => ({ source: { path: file }, packages }));
|
|
33
50
|
const report = parseOsvScanOutput({ results: normalized });
|
|
51
|
+
const coordinateOf = (raw) => {
|
|
52
|
+
const info = asRecord(asRecord(raw)?.package);
|
|
53
|
+
return { name: String(info.name), version: String(info.version) };
|
|
54
|
+
};
|
|
34
55
|
const artifacts = artifactPaths.map((file) => {
|
|
35
56
|
const packages = byPath.get(file);
|
|
36
|
-
const identified = packages !== undefined;
|
|
37
57
|
const perFile = parseOsvScanOutput({ results: [{ packages: packages ?? [] }] });
|
|
58
|
+
// 推測の座標だけで同定したアーカイブは、脆弱性の有無に関係なく「同定済み」にしない(同定の確度と検出件数を分ける)。
|
|
59
|
+
// 脆弱性が見つかっても、推測の誤ったgroupIdで他の脆弱性を取りこぼしている可能性がある
|
|
60
|
+
const inferredOnly = packages !== undefined && packages.every((raw) => isInferredCoordinate(coordinateOf(raw).name));
|
|
38
61
|
return {
|
|
39
62
|
path: sanitizeExternalText(file),
|
|
40
|
-
status:
|
|
63
|
+
status: packages === undefined ? "unidentified" : inferredOnly ? "inferred_only" : perFile.vulnerability_count > 0
|
|
41
64
|
? "identified_with_vulnerabilities" : "identified_without_known_vulnerabilities",
|
|
42
65
|
identified_vulnerability_count: perFile.vulnerability_count,
|
|
43
66
|
};
|
|
44
67
|
});
|
|
68
|
+
// 推測の座標の一覧(既知の脆弱性のないものも含む。名前と版で重複を除く)
|
|
69
|
+
const inferred = [...new Map([...byPath.values()].flat().map(coordinateOf)
|
|
70
|
+
.filter((c) => isInferredCoordinate(c.name)).map((c) => [`${c.name}@${c.version}`, c])).values()]
|
|
71
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
45
72
|
return {
|
|
46
73
|
coverage: {
|
|
47
74
|
jars_found: artifactPaths.length,
|
|
48
|
-
jars_identified:
|
|
49
|
-
unidentified_jars: artifacts.filter((item) => item.status === "unidentified")
|
|
50
|
-
.map(({ path }) => ({ path, hint: HINT })),
|
|
75
|
+
jars_identified: artifacts.filter((item) => item.status !== "unidentified" && item.status !== "inferred_only").length,
|
|
76
|
+
unidentified_jars: artifacts.filter((item) => item.status === "unidentified" || item.status === "inferred_only")
|
|
77
|
+
.map(({ path, status }) => ({ path, hint: status === "inferred_only" ? INFERRED_HINT : HINT })),
|
|
51
78
|
completeness: "incomplete",
|
|
52
79
|
warning: WARNING,
|
|
80
|
+
...(inferred.length > 0 ? {
|
|
81
|
+
inferred_coordinates: {
|
|
82
|
+
count: inferred.length,
|
|
83
|
+
items: inferred.slice(0, MAX_INFERRED_ITEMS).map((c) => ({ name: sanitizeExternalText(c.name), version: sanitizeExternalText(c.version) })),
|
|
84
|
+
...(inferred.length > MAX_INFERRED_ITEMS ? { omitted: inferred.length - MAX_INFERRED_ITEMS } : {}),
|
|
85
|
+
warning: INFERRED_WARNING,
|
|
86
|
+
},
|
|
87
|
+
} : {}),
|
|
53
88
|
},
|
|
54
89
|
artifacts,
|
|
55
90
|
identified_vulnerability_count: report.vulnerability_count,
|
|
56
91
|
identified_vulnerable_package_count: report.vulnerable_package_count,
|
|
57
92
|
severity_breakdown: report.severity_breakdown,
|
|
58
|
-
packages: report.packages
|
|
93
|
+
packages: report.packages.map((pkg) => {
|
|
94
|
+
if (!isInferredCoordinate(pkg.name))
|
|
95
|
+
return pkg;
|
|
96
|
+
const { vulnerabilities, ...rest } = pkg;
|
|
97
|
+
return { ...rest, coordinates_inferred: true, vulnerabilities };
|
|
98
|
+
}),
|
|
59
99
|
};
|
|
60
100
|
}
|
|
@@ -19,7 +19,9 @@ import { readResponseBytes } from "../utils/readResponseBytes.js";
|
|
|
19
19
|
/**
|
|
20
20
|
* ピン留めするOSV-Scannerのバージョン。更新時は下のチェックサムも必ず更新すること。
|
|
21
21
|
* 範囲外の読み込みを防ぐ検証はこの版の挙動に合わせているため、更新時は
|
|
22
|
-
* docs/DESIGN_TODO.md「B5」の監査と、requirements.txt・親POM
|
|
22
|
+
* docs/DESIGN_TODO.md「B5」の監査と、requirements.txt・親POMの解釈の実機確認をやり直すこと。
|
|
23
|
+
* pom.xmlの直接/推移的依存の判定(dependencyRelations.tsのpomRelations)も、この版が同じpom.xmlを
|
|
24
|
+
* source.type "lockfile"(宣言された依存)と"unknown"(deps.devで解決された推移的依存)に分けて報告する挙動に依存するため再確認すること
|
|
23
25
|
*/
|
|
24
26
|
export const PINNED_OSV_SCANNER_VERSION = "2.4.0";
|
|
25
27
|
/**
|
|
@@ -45,7 +47,7 @@ export function assetNameForPlatform(platform = process.platform, arch = process
|
|
|
45
47
|
return null;
|
|
46
48
|
return `osv-scanner_${osName}_${cpuName}${osName === "windows" ? ".exe" : ""}`;
|
|
47
49
|
}
|
|
48
|
-
function defaultCacheDir() {
|
|
50
|
+
export function defaultCacheDir() {
|
|
49
51
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
50
52
|
const base = xdg !== undefined && xdg.trim() !== "" ? xdg : path.join(os.homedir(), ".cache");
|
|
51
53
|
return path.join(base, "osv-scanner-mcp");
|
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
* 探索順: 環境変数 OSV_SCANNER_PATH → PATH上の osv-scanner
|
|
5
5
|
*/
|
|
6
6
|
import { access, constants, stat } from "node:fs/promises";
|
|
7
|
+
import { isAccessDenied, permissionDeniedError } from "../utils/projectWalk.js";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { ScanToolError } from "../errors.js";
|
|
9
10
|
export const OSV_SCANNER_PATH_ENV = "OSV_SCANNER_PATH";
|
|
10
11
|
const BINARY_NAME = process.platform === "win32" ? "osv-scanner.exe" : "osv-scanner";
|
|
11
|
-
|
|
12
|
+
/**
|
|
13
|
+
* 実行可能な通常のファイルか。rethrowDeniedのとき、Nodeの権限モデル(--permission)による拒否は
|
|
14
|
+
* 「見つからない」にせず投げる(明示指定のパスで原因を誤らせない。PATHの探索では読めない場所を飛ばす)
|
|
15
|
+
*/
|
|
16
|
+
async function isExecutableFile(filePath, rethrowDenied = false) {
|
|
12
17
|
try {
|
|
13
18
|
const stats = await stat(filePath);
|
|
14
19
|
if (!stats.isFile())
|
|
@@ -16,7 +21,9 @@ async function isExecutableFile(filePath) {
|
|
|
16
21
|
await access(filePath, constants.X_OK);
|
|
17
22
|
return true;
|
|
18
23
|
}
|
|
19
|
-
catch {
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (rethrowDenied && isAccessDenied(error))
|
|
26
|
+
throw permissionDeniedError(error);
|
|
20
27
|
return false;
|
|
21
28
|
}
|
|
22
29
|
}
|
|
@@ -25,7 +32,7 @@ export async function findOsvScannerBinary(env = process.env) {
|
|
|
25
32
|
const explicit = env[OSV_SCANNER_PATH_ENV];
|
|
26
33
|
if (explicit !== undefined && explicit.trim() !== "") {
|
|
27
34
|
// 明示指定が無効な場合はPATHにフォールバックせず失敗させる(意図しないバイナリの実行を防ぐ)
|
|
28
|
-
return (await isExecutableFile(explicit)) ? explicit : null;
|
|
35
|
+
return (await isExecutableFile(explicit, true)) ? explicit : null;
|
|
29
36
|
}
|
|
30
37
|
for (const dir of (env.PATH ?? "").split(path.delimiter)) {
|
|
31
38
|
if (dir === "")
|
package/dist/osv/scanReport.js
CHANGED
|
@@ -94,7 +94,7 @@ export function extractFixedVersions(vulnDetails, packageName, ecosystem) {
|
|
|
94
94
|
return sortVersions(versions, ecosystem);
|
|
95
95
|
}
|
|
96
96
|
/**
|
|
97
|
-
* パッケージごとのスキャン元ファイル(osv-scannerの`results[].source.path`)。
|
|
97
|
+
* パッケージごとのスキャン元ファイル(osv-scannerの`results[].source.path`と`type`)。
|
|
98
98
|
* 応答のJSONには出さない(既存の応答を変えない)ため、パッケージのオブジェクトをキーに別に持つ。
|
|
99
99
|
* 直接/推移的依存の判定(dependencyRelations.ts)が、どのlockfileの依存かを知るために使う
|
|
100
100
|
*/
|
|
@@ -116,6 +116,7 @@ export function parseOsvScanOutput(raw) {
|
|
|
116
116
|
if (!result)
|
|
117
117
|
continue;
|
|
118
118
|
const sourcePath = asString(asRecord(result.source)?.path);
|
|
119
|
+
const sourceType = asString(asRecord(result.source)?.type);
|
|
119
120
|
if (sourcePath !== null && !sourceFiles.includes(sourcePath)) {
|
|
120
121
|
sourceFiles.push(sourcePath);
|
|
121
122
|
}
|
|
@@ -133,11 +134,14 @@ export function parseOsvScanOutput(raw) {
|
|
|
133
134
|
const key = `${ecosystem}:${name}@${version}`;
|
|
134
135
|
let entry = packageMap.get(key);
|
|
135
136
|
if (!entry) {
|
|
136
|
-
entry = { name, version, ecosystem, groups: new Set(), sources: new Set(), vulns: new Map() };
|
|
137
|
+
entry = { name, version, ecosystem, groups: new Set(), sources: new Set(), sourceList: [], vulns: new Map() };
|
|
137
138
|
packageMap.set(key, entry);
|
|
138
139
|
}
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
const sourceKey = JSON.stringify([sourcePath, sourceType]);
|
|
141
|
+
if (sourcePath !== null && !entry.sources.has(sourceKey)) {
|
|
142
|
+
entry.sources.add(sourceKey);
|
|
143
|
+
entry.sourceList.push({ path: sourcePath, type: sourceType });
|
|
144
|
+
}
|
|
141
145
|
for (const group of asStrings(pkgObj.dependency_groups))
|
|
142
146
|
entry.groups.add(group);
|
|
143
147
|
const details = asArray(pkgObj.vulnerabilities);
|
|
@@ -161,7 +165,11 @@ export function parseOsvScanOutput(raw) {
|
|
|
161
165
|
severity: severityFromScore(score),
|
|
162
166
|
summary: extractSummary(vulnDetails),
|
|
163
167
|
fixed_versions: extractFixedVersions(vulnDetails, name, ecosystem),
|
|
164
|
-
|
|
168
|
+
});
|
|
169
|
+
// 内部でだけ使うため列挙不可にする(JSON.stringifyに含まれない)
|
|
170
|
+
Object.defineProperty(entry.vulns.get(primaryId), "affected_versions", {
|
|
171
|
+
value: extractAffectedVersions(vulnDetails, ids, name, ecosystem),
|
|
172
|
+
enumerable: false,
|
|
165
173
|
});
|
|
166
174
|
}
|
|
167
175
|
}
|
|
@@ -196,7 +204,7 @@ function buildReport(sourceFiles, packageMap) {
|
|
|
196
204
|
...(entry.groups.size > 0 ? { dependency_groups: [...entry.groups].sort() } : {}),
|
|
197
205
|
vulnerabilities,
|
|
198
206
|
};
|
|
199
|
-
packageSourceMap.set(pkg,
|
|
207
|
+
packageSourceMap.set(pkg, entry.sourceList);
|
|
200
208
|
return pkg;
|
|
201
209
|
})
|
|
202
210
|
.sort((a, b) => maxScore(b.vulnerabilities) - maxScore(a.vulnerabilities) || cmpId(a.name, b.name));
|
package/dist/osv/suggestFix.js
CHANGED
|
@@ -59,6 +59,14 @@ function updateHint(pkg) {
|
|
|
59
59
|
}
|
|
60
60
|
return UPDATE_HINTS.Go;
|
|
61
61
|
}
|
|
62
|
+
case "Maven":
|
|
63
|
+
if (relation === "direct") {
|
|
64
|
+
return "直接依存です。pom.xmlの<dependency>の版を更新します。版を親POMの<dependencyManagement>・プロパティ・BOMで管理している場合は、そちらを更新します";
|
|
65
|
+
}
|
|
66
|
+
if (relation === "transitive") {
|
|
67
|
+
return "推移的依存です。pom.xmlの<dependencyManagement>で推奨版を指定して上書きする(Mavenの依存の調停で優先されます)か、それを要求している直接依存を更新します";
|
|
68
|
+
}
|
|
69
|
+
return undefined; // gradle.lockfile等: 判定できないため具体的な案内はしない(従来どおり)
|
|
62
70
|
case "PyPI":
|
|
63
71
|
if (relation === "direct") {
|
|
64
72
|
return "直接依存です。requirements.txtの版の指定、またはpyproject.toml・Pipfileの指定を更新し、lockfileを再生成します";
|
|
@@ -8,7 +8,7 @@ import path from "node:path";
|
|
|
8
8
|
import { ScanToolError } from "../errors.js";
|
|
9
9
|
import { isRemoteResolutionDisabled, runOsvScan } from "../osv/runner.js";
|
|
10
10
|
import { packageSources, parseOsvScanOutput } from "../osv/scanReport.js";
|
|
11
|
-
import { combineRelations, goModRelations, npmLockRelations, requirementsRelations, } from "../utils/dependencyRelations.js";
|
|
11
|
+
import { combineRelations, goModRelations, npmLockRelations, pomRelations, requirementsRelations, } from "../utils/dependencyRelations.js";
|
|
12
12
|
import { sanitizeExternalText } from "../utils/externalText.js";
|
|
13
13
|
import { readRegularFile } from "../utils/safeRead.js";
|
|
14
14
|
import { detectProject } from "../utils/manifestDetector.js";
|
|
@@ -115,6 +115,9 @@ export async function buildRelationLookups(copies, maxTotalBytes = MAX_RELATION_
|
|
|
115
115
|
lookup = format === "go.mod" ? goModRelations(text) : npmLockRelations(JSON.parse(text));
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
else if (format === "pom.xml") {
|
|
119
|
+
lookup = pomRelations(); // ファイルは読まず、osv-scannerのsource.typeで判定する
|
|
120
|
+
}
|
|
118
121
|
else if (format === "requirements.txt" && entries !== undefined) {
|
|
119
122
|
lookup = requirementsRelations(entries);
|
|
120
123
|
}
|
|
@@ -129,15 +132,21 @@ export async function buildRelationLookups(copies, maxTotalBytes = MAX_RELATION_
|
|
|
129
132
|
/** パッケージに直接/推移的依存の別を付ける。項目は脆弱性の一覧より前に置く */
|
|
130
133
|
function annotateRelations(packages, lookups) {
|
|
131
134
|
return packages.map((pkg) => {
|
|
132
|
-
|
|
135
|
+
// 同じファイルが複数の結果に分かれる(pom.xml・requirements.txtの宣言と推移的依存)ため、ファイルごとに1つにまとめる。
|
|
136
|
+
// 同じファイルで直接依存と判定された結果があれば直接依存とする
|
|
137
|
+
const byFile = new Map();
|
|
133
138
|
const declaredIn = new Set();
|
|
139
|
+
const rank = { direct: 2, transitive: 1, unknown: 0 };
|
|
134
140
|
for (const source of packageSources(pkg)) {
|
|
135
|
-
const entry = lookups.get(source);
|
|
136
|
-
const info = entry?.lookup ? entry.lookup(pkg.name, pkg.version) : { relation: "unknown" };
|
|
137
|
-
|
|
141
|
+
const entry = lookups.get(source.path);
|
|
142
|
+
const info = entry?.lookup ? entry.lookup(pkg.name, pkg.version, source.type) : { relation: "unknown" };
|
|
143
|
+
const previous = byFile.get(source.path);
|
|
144
|
+
if (previous === undefined || rank[info.relation] > rank[previous.relation])
|
|
145
|
+
byFile.set(source.path, info);
|
|
138
146
|
for (const manifest of info.declaredIn ?? [])
|
|
139
147
|
declaredIn.add(path.join(entry.lockDir, manifest));
|
|
140
148
|
}
|
|
149
|
+
const infos = [...byFile.values()];
|
|
141
150
|
const { relation, introducedBy, replaced } = combineRelations(infos);
|
|
142
151
|
const { vulnerabilities, ...rest } = pkg;
|
|
143
152
|
return {
|
package/dist/tools/toolResult.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { ScanToolError } from "../errors.js";
|
|
10
10
|
import { sanitizeExternalText } from "../utils/externalText.js";
|
|
11
|
+
import { isAccessDenied, permissionDeniedError } from "../utils/projectWalk.js";
|
|
11
12
|
export function jsonResult(payload, isError = false) {
|
|
12
13
|
return {
|
|
13
14
|
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
@@ -28,6 +29,9 @@ export function errorResult(error) {
|
|
|
28
29
|
},
|
|
29
30
|
}, true);
|
|
30
31
|
}
|
|
32
|
+
// Nodeの権限モデルの拒否は、許可の付け方が分かるエラーにする(内部の例外の詳細は返さない)
|
|
33
|
+
if (isAccessDenied(error))
|
|
34
|
+
return errorResult(permissionDeniedError(error));
|
|
31
35
|
// 想定外の例外はスタックトレース等の内部情報をクライアントへ返さない
|
|
32
36
|
return jsonResult({ error: { kind: "internal_error", message: "予期しないエラーが発生しました" } }, true);
|
|
33
37
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { opendir, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ScanToolError } from "../errors.js";
|
|
4
|
+
import { isAccessDenied, permissionDeniedError } from "./projectWalk.js";
|
|
4
5
|
// Build output directories must be included for artifact scans.
|
|
5
6
|
const SKIPPED_DIRS = new Set([".git", "node_modules", ".idea", ".vscode"]);
|
|
6
7
|
const isArchive = (file) => /\.(jar|war)$/i.test(file);
|
|
@@ -12,7 +13,10 @@ export async function detectJavaArtifacts(inputPath, options = {}) {
|
|
|
12
13
|
try {
|
|
13
14
|
targetPath = await realpath(inputPath);
|
|
14
15
|
}
|
|
15
|
-
catch {
|
|
16
|
+
catch (error) {
|
|
17
|
+
// Nodeの権限モデルの拒否を「存在しない」と誤って伝えない
|
|
18
|
+
if (isAccessDenied(error))
|
|
19
|
+
throw permissionDeniedError(error);
|
|
16
20
|
throw new ScanToolError("project_not_found", `Path does not exist: ${inputPath}`);
|
|
17
21
|
}
|
|
18
22
|
if (options.allowedRoot !== undefined) {
|
|
@@ -236,6 +236,15 @@ export function goModRelations(text) {
|
|
|
236
236
|
return replaced.has(`${name}@${version}`) || replacedLocal.has(name) ? { relation, replaced: true } : { relation };
|
|
237
237
|
};
|
|
238
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* pom.xmlの依存の関係を、osv-scannerの`source.type`で判定する(pom.xmlは自前で解析しない)。
|
|
241
|
+
* osv-scanner 2.4.0は同じpom.xmlを、宣言された依存(親POM・プロファイル・プロパティ・依存管理をosv-scannerが解釈したもの)を
|
|
242
|
+
* `type: "lockfile"`、deps.devで解決された推移的依存を`type: "unknown"`の結果に分けて報告する(実データで確認)。
|
|
243
|
+
* 文書化された仕様ではないため、想定外のtypeはunknownにする(osv-scannerのピン留め更新時に再確認する)。
|
|
244
|
+
*/
|
|
245
|
+
export function pomRelations() {
|
|
246
|
+
return (_name, _version, sourceType) => sourceType === "lockfile" ? { relation: "direct" } : sourceType === "unknown" ? { relation: "transitive" } : UNKNOWN;
|
|
247
|
+
}
|
|
239
248
|
/** PEP 503の正規化 */
|
|
240
249
|
function normalizePypi(name) {
|
|
241
250
|
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nodeの権限モデル(--permission)で起動された場合の、必要な許可の確認(docs/DESIGN_TODO.md「B3」)。
|
|
3
|
+
*
|
|
4
|
+
* 権限モデルは任意の多層防御として使える(既定の起動方法は変えない)。必要な許可が欠けると
|
|
5
|
+
* スキャンの途中でERR_ACCESS_DENIEDになるため、起動時にstderrで知らせる(起動は拒否しない)。
|
|
6
|
+
* 子プロセス(osv-scanner)は権限モデルの制限を受けない点に注意(READMEに明記)。
|
|
7
|
+
*/
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
/** 権限モデルが有効なときに、欠けている許可を警告文の一覧で返す。無効なら空 */
|
|
10
|
+
export function permissionModelWarnings(input, permission = process.permission) {
|
|
11
|
+
if (permission === undefined)
|
|
12
|
+
return [];
|
|
13
|
+
const inside = (dir) => path.join(dir, "x"); // ディレクトリ配下の読み書きの許可を確かめる
|
|
14
|
+
const warnings = [];
|
|
15
|
+
if (!permission.has("child")) {
|
|
16
|
+
warnings.push("子プロセスの起動が許可されていません。osv-scannerを実行できません(--allow-child-process)");
|
|
17
|
+
}
|
|
18
|
+
const tmpDirs = [...new Set([input.tmpDir, input.tmpDirReal])];
|
|
19
|
+
const unreadableTmp = tmpDirs.filter((dir) => !permission.has("fs.read", inside(dir)));
|
|
20
|
+
if (unreadableTmp.length > 0) {
|
|
21
|
+
warnings.push(`一時ディレクトリの読み取りが許可されていません: ${unreadableTmp.join(", ")}` +
|
|
22
|
+
"(シンボリックリンクの解決前と解決後の両方のパスに--allow-fs-readが必要です)");
|
|
23
|
+
}
|
|
24
|
+
if (!permission.has("fs.write", inside(input.tmpDirReal))) {
|
|
25
|
+
warnings.push(`一時ディレクトリへの書き込みが許可されていません: ${input.tmpDirReal}(--allow-fs-write)`);
|
|
26
|
+
}
|
|
27
|
+
if (input.allowedRoot !== undefined && !permission.has("fs.read", inside(input.allowedRoot))) {
|
|
28
|
+
warnings.push(`OSV_MCP_ALLOWED_ROOTの読み取りが許可されていません: ${input.allowedRoot}(--allow-fs-read)`);
|
|
29
|
+
}
|
|
30
|
+
if (input.scannerPath !== undefined && !permission.has("fs.read", input.scannerPath)) {
|
|
31
|
+
warnings.push(`OSV_SCANNER_PATHの読み取りが許可されていません: ${input.scannerPath}(--allow-fs-read)`);
|
|
32
|
+
}
|
|
33
|
+
if (input.cacheDir !== undefined && !permission.has("fs.write", inside(input.cacheDir))) {
|
|
34
|
+
warnings.push(`osv-scannerのキャッシュへの書き込みが許可されていません: ${input.cacheDir}` +
|
|
35
|
+
"(自動ダウンロードを使う場合は--allow-fs-writeと--allow-fs-read、使わない場合はOSV_SCANNER_PATHを指定)");
|
|
36
|
+
}
|
|
37
|
+
return warnings;
|
|
38
|
+
}
|
|
@@ -10,11 +10,26 @@ import { ScanToolError } from "../errors.js";
|
|
|
10
10
|
const DEFAULT_MAX_DEPTH = 64;
|
|
11
11
|
const DEFAULT_MAX_ENTRIES = 200_000;
|
|
12
12
|
const DEFAULT_MAX_MANIFESTS = 1_000;
|
|
13
|
+
/** Nodeの権限モデル(--permission)による拒否か */
|
|
14
|
+
export function isAccessDenied(error) {
|
|
15
|
+
return error instanceof Error && error.code === "ERR_ACCESS_DENIED";
|
|
16
|
+
}
|
|
17
|
+
/** 権限モデルの拒否を、許可の付け方が分かるエラーにする */
|
|
18
|
+
export function permissionDeniedError(error) {
|
|
19
|
+
const resource = typeof error.resource === "string" ? `: ${error.resource}` : "";
|
|
20
|
+
const kind = error.permission === "FileSystemWrite" ? "書き込み(--allow-fs-write)" : error.permission === "ChildProcess"
|
|
21
|
+
? "子プロセスの起動(--allow-child-process)" : "読み取り(--allow-fs-read)";
|
|
22
|
+
return new ScanToolError("permission_denied", `Nodeの権限モデル(--permission)で${kind}が許可されていません${resource}。` +
|
|
23
|
+
"スキャン対象・一時ディレクトリ(シンボリックリンクの解決前と解決後の両方のパス)・osv-scannerのキャッシュへの許可が必要です(READMEの「権限を絞って起動する」を参照)");
|
|
24
|
+
}
|
|
13
25
|
export async function resolveExistingPath(inputPath) {
|
|
14
26
|
try {
|
|
15
27
|
return await realpath(path.resolve(inputPath));
|
|
16
28
|
}
|
|
17
|
-
catch {
|
|
29
|
+
catch (error) {
|
|
30
|
+
// Nodeの権限モデルの拒否を「存在しない」と誤って伝えない
|
|
31
|
+
if (isAccessDenied(error))
|
|
32
|
+
throw permissionDeniedError(error);
|
|
18
33
|
throw new ScanToolError("project_not_found", `指定されたパスが存在しません: ${inputPath}`);
|
|
19
34
|
}
|
|
20
35
|
}
|
|
@@ -54,8 +69,11 @@ export async function walkProjectFiles(rootDir, options, skippedDirs, onFile) {
|
|
|
54
69
|
try {
|
|
55
70
|
entries = await readdir(dir, { withFileTypes: true });
|
|
56
71
|
}
|
|
57
|
-
catch {
|
|
58
|
-
|
|
72
|
+
catch (error) {
|
|
73
|
+
// Nodeの権限モデルの拒否は、スキャン範囲を黙って欠落させずにエラーにする
|
|
74
|
+
if (isAccessDenied(error))
|
|
75
|
+
throw permissionDeniedError(error);
|
|
76
|
+
continue; // 読めないディレクトリはスキップ(OSの権限不足等)
|
|
59
77
|
}
|
|
60
78
|
for (const entry of entries) {
|
|
61
79
|
if (++visited > limits.maxEntries)
|
package/dist/utils/safeRead.js
CHANGED
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { constants } from "node:fs";
|
|
16
16
|
import { open, realpath, rm, stat } from "node:fs/promises";
|
|
17
|
-
import { isInsideDir } from "./projectWalk.js";
|
|
17
|
+
import { isAccessDenied, isInsideDir } from "./projectWalk.js";
|
|
18
18
|
const OPEN_FLAGS = constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0);
|
|
19
19
|
const MESSAGES = {
|
|
20
20
|
not_found: "ファイルを開けません",
|
|
21
|
+
denied: "Nodeの権限モデル(--permission)で読み取りが許可されていません",
|
|
21
22
|
not_regular: "通常のファイルではありません(名前付きパイプ・デバイス等)",
|
|
22
23
|
too_large: "サイズが上限を超えています",
|
|
23
24
|
outside: "スキャン範囲の外のファイルです",
|
|
@@ -56,8 +57,8 @@ export async function copyRegularFile(filePath, dest, options) {
|
|
|
56
57
|
try {
|
|
57
58
|
source = await open(filePath, OPEN_FLAGS);
|
|
58
59
|
}
|
|
59
|
-
catch {
|
|
60
|
-
return failure("not_found");
|
|
60
|
+
catch (error) {
|
|
61
|
+
return failure(isAccessDenied(error) ? "denied" : "not_found");
|
|
61
62
|
}
|
|
62
63
|
let total = 0;
|
|
63
64
|
let result = null;
|
|
@@ -114,8 +115,8 @@ export async function readRegularFile(filePath, options) {
|
|
|
114
115
|
try {
|
|
115
116
|
handle = await open(filePath, OPEN_FLAGS);
|
|
116
117
|
}
|
|
117
|
-
catch {
|
|
118
|
-
return failure("not_found");
|
|
118
|
+
catch (error) {
|
|
119
|
+
return failure(isAccessDenied(error) ? "denied" : "not_found");
|
|
119
120
|
}
|
|
120
121
|
let info;
|
|
121
122
|
const chunks = [];
|
package/dist/utils/sbomInput.js
CHANGED
|
@@ -3,6 +3,7 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { open, realpath } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { ScanToolError } from "../errors.js";
|
|
6
|
+
import { isAccessDenied, permissionDeniedError } from "./projectWalk.js";
|
|
6
7
|
import { verifyOpenedFile } from "./safeRead.js";
|
|
7
8
|
import { asRecord } from "./unknownJson.js";
|
|
8
9
|
const DEFAULT_MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
@@ -16,7 +17,10 @@ export async function loadSbom(inputPath, options = {}) {
|
|
|
16
17
|
try {
|
|
17
18
|
sourcePath = await realpath(inputPath);
|
|
18
19
|
}
|
|
19
|
-
catch {
|
|
20
|
+
catch (error) {
|
|
21
|
+
// Nodeの権限モデルの拒否を「存在しない」と誤って伝えない
|
|
22
|
+
if (isAccessDenied(error))
|
|
23
|
+
throw permissionDeniedError(error);
|
|
20
24
|
throw new ScanToolError("sbom_not_found", "The specified SBOM file does not exist");
|
|
21
25
|
}
|
|
22
26
|
if (options.allowedRoot !== undefined) {
|
|
@@ -69,6 +73,8 @@ export async function loadSbom(inputPath, options = {}) {
|
|
|
69
73
|
catch (error) {
|
|
70
74
|
if (error instanceof ScanToolError)
|
|
71
75
|
throw error;
|
|
76
|
+
if (isAccessDenied(error))
|
|
77
|
+
throw permissionDeniedError(error);
|
|
72
78
|
throw new ScanToolError("sbom_not_found", "The specified SBOM file could not be read");
|
|
73
79
|
}
|
|
74
80
|
let raw;
|