osv-scanner-mcp 0.3.4 → 0.4.1
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 +91 -11
- package/dist/index.js +22 -6
- package/dist/osv/binaryDownloader.js +5 -1
- package/dist/osv/runner.js +17 -18
- package/dist/osv/scanReport.js +10 -2
- package/dist/tools/scanJavaArtifact.js +34 -3
- package/dist/tools/scanJavaProject.js +56 -6
- package/dist/tools/scanProject.js +143 -0
- package/dist/tools/suggestFix.js +5 -4
- package/dist/utils/manifestDetector.js +252 -0
- package/dist/utils/manifestFormats.js +23 -0
- package/dist/utils/pomParent.js +150 -0
- package/dist/utils/projectDetector.js +33 -81
- package/dist/utils/projectWalk.js +77 -0
- package/dist/utils/requirementsFile.js +223 -0
- package/dist/utils/safeRead.js +144 -0
- package/dist/utils/sbomInput.js +10 -1
- package/dist/utils/scanSnapshot.js +164 -0
- package/dist/utils/startupConfig.js +11 -4
- package/package.json +1 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* スキャン対象のスナップショット: osv-scannerには元のファイルを渡さず、安全に1回だけ読んだ内容を
|
|
3
|
+
* 専用の一時ディレクトリ(所有者のみアクセス可、終了時に削除)へコピーしてスキャンする。
|
|
4
|
+
*
|
|
5
|
+
* - 検査(境界・親POMの検証)はコピーの内容に対して行うため、検査とosv-scannerの読み込みの間に
|
|
6
|
+
* 元のファイルを差し替えても結果に影響しない
|
|
7
|
+
* - 親POMは元の配置をスナップショット内に再現してコピーする。osv-scannerが相対パスで親をたどっても、
|
|
8
|
+
* 見つかるのは検証してコピーしたファイルだけになる(解析が多少ずれても範囲外は読まれず、
|
|
9
|
+
* 最悪でも親が見つからないだけ)
|
|
10
|
+
* - `..`を重ねた参照でスナップショットの外(本物のファイルシステム)に出る親POMは除外する
|
|
11
|
+
* - コピーの合計サイズに上限を設ける
|
|
12
|
+
*/
|
|
13
|
+
import { mkdir, mkdtemp, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { ScanToolError } from "../errors.js";
|
|
17
|
+
import { decodePomBytes, parentRelativePath } from "./pomParent.js";
|
|
18
|
+
import { isInsideDir } from "./projectWalk.js";
|
|
19
|
+
import { copyRegularFile } from "./safeRead.js";
|
|
20
|
+
const DEFAULT_MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024;
|
|
21
|
+
const MAX_POM_BYTES = 10 * 1024 * 1024;
|
|
22
|
+
const MAX_PARENT_DEPTH = 10;
|
|
23
|
+
export class ScanSnapshot {
|
|
24
|
+
dir;
|
|
25
|
+
maxTotalBytes;
|
|
26
|
+
copied = new Map();
|
|
27
|
+
parentCache = new Map();
|
|
28
|
+
used = 0;
|
|
29
|
+
generated = 0;
|
|
30
|
+
constructor(dir, maxTotalBytes) {
|
|
31
|
+
this.dir = dir;
|
|
32
|
+
this.maxTotalBytes = maxTotalBytes;
|
|
33
|
+
}
|
|
34
|
+
static async create(maxTotalBytes = DEFAULT_MAX_TOTAL_BYTES) {
|
|
35
|
+
const dir = await mkdtemp(path.join(await realpath(os.tmpdir()), "osv-mcp-snap-"));
|
|
36
|
+
return new ScanSnapshot(dir, maxTotalBytes);
|
|
37
|
+
}
|
|
38
|
+
/** 再現した配置のルート(実際のファイルシステムの"/"に相当) */
|
|
39
|
+
get treeRoot() {
|
|
40
|
+
return path.join(this.dir, "tree");
|
|
41
|
+
}
|
|
42
|
+
/** 元の絶対パスに対応する、スナップショット内のパス */
|
|
43
|
+
mirror(absolute) {
|
|
44
|
+
const { root } = path.parse(absolute);
|
|
45
|
+
return path.join(this.treeRoot, root.replace(/[^A-Za-z0-9]/g, "_"), path.relative(root, absolute));
|
|
46
|
+
}
|
|
47
|
+
/** 元のファイルを安全にコピーする。同じファイルは1回だけコピーする */
|
|
48
|
+
async copy(absolute, boundary, maxBytes = Infinity) {
|
|
49
|
+
const existing = this.copied.get(absolute);
|
|
50
|
+
if (existing !== undefined)
|
|
51
|
+
return { ok: true, path: existing };
|
|
52
|
+
const remaining = this.maxTotalBytes - this.used;
|
|
53
|
+
const dest = this.mirror(absolute);
|
|
54
|
+
await mkdir(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
55
|
+
const result = await copyRegularFile(absolute, dest, { maxBytes: Math.min(maxBytes, remaining), boundary });
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
if (result.failure === "too_large" && remaining < maxBytes) {
|
|
58
|
+
throw new ScanToolError("scan_input_too_large", `スキャン対象ファイルの合計サイズが上限(${this.maxTotalBytes}バイト)を超えるため、スキャンを中止しました。対象を絞って再実行してください`);
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
this.used += result.bytes;
|
|
63
|
+
this.copied.set(absolute, dest);
|
|
64
|
+
return { ok: true, path: dest };
|
|
65
|
+
}
|
|
66
|
+
/** 本サーバーが生成した内容(requirements.txtの正規化コピー等)を書く */
|
|
67
|
+
async writeGenerated(content) {
|
|
68
|
+
const dir = path.join(this.dir, "generated");
|
|
69
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
70
|
+
const file = path.join(dir, `${this.generated++}.txt`);
|
|
71
|
+
await writeFile(file, content, { mode: 0o600, flag: "wx" });
|
|
72
|
+
return file;
|
|
73
|
+
}
|
|
74
|
+
/** コピー済みのpom.xmlの親参照(同じファイルは1回だけ解析する) */
|
|
75
|
+
async parentOf(copyPath) {
|
|
76
|
+
if (this.parentCache.has(copyPath))
|
|
77
|
+
return this.parentCache.get(copyPath);
|
|
78
|
+
const xml = decodePomBytes(await readFile(copyPath));
|
|
79
|
+
const relativePath = xml === null ? undefined : parentRelativePath(xml);
|
|
80
|
+
this.parentCache.set(copyPath, relativePath);
|
|
81
|
+
return relativePath;
|
|
82
|
+
}
|
|
83
|
+
async cleanup() {
|
|
84
|
+
await rm(this.dir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const UNPARSEABLE = "親POMの指定を確実に解釈できないため(親要素の重複、CDATA・DOCTYPE、プロパティ参照、UTF-8以外の文字コード等)、" +
|
|
88
|
+
"許可ルート内か確認できずスキャン対象から外しました";
|
|
89
|
+
/**
|
|
90
|
+
* pom.xmlと親POMの連鎖をスナップショットへコピーする。
|
|
91
|
+
* 許可ルート設定時、連鎖が許可ルートの外・スナップショットの外を参照するか、解釈できない場合は除外理由を返す。
|
|
92
|
+
*/
|
|
93
|
+
async function snapshotPom(snapshot, pomPath, context) {
|
|
94
|
+
const first = await snapshot.copy(pomPath, context.projectDir, MAX_POM_BYTES);
|
|
95
|
+
if (!first.ok)
|
|
96
|
+
return { ok: false, reason: first.message, kind: "unreadable" };
|
|
97
|
+
const restricted = context.allowedRootReal !== undefined;
|
|
98
|
+
let current = pomPath;
|
|
99
|
+
let currentCopy = first.path;
|
|
100
|
+
for (let depth = 0; depth < MAX_PARENT_DEPTH; depth++) {
|
|
101
|
+
const relativePath = await snapshot.parentOf(currentCopy);
|
|
102
|
+
if (relativePath === null || relativePath === "")
|
|
103
|
+
return first;
|
|
104
|
+
if (relativePath === undefined)
|
|
105
|
+
return restricted ? { ok: false, reason: UNPARSEABLE, kind: "unreadable" } : first;
|
|
106
|
+
// Goのfilepath.Joinと同じ規則で結合する(絶対パスも連結し、..はルートで止まる)
|
|
107
|
+
const inCopy = path.join(path.dirname(currentCopy), relativePath);
|
|
108
|
+
if (!isInsideDir(snapshot.treeRoot, inCopy)) {
|
|
109
|
+
// ..を重ねるとスナップショットの外(本物のファイルシステム)に届くため、コピーではなく元のファイルが読まれる
|
|
110
|
+
return restricted
|
|
111
|
+
? { ok: false, reason: `親POM(relativePath: ${relativePath})の参照がスキャン範囲の外に出るため、スキャン対象から外しました`, kind: "outside_allowed_root" }
|
|
112
|
+
: first;
|
|
113
|
+
}
|
|
114
|
+
let candidate = path.join(path.dirname(current), relativePath);
|
|
115
|
+
try {
|
|
116
|
+
if ((await stat(candidate)).isDirectory())
|
|
117
|
+
candidate = path.join(candidate, "pom.xml");
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return first; // 参照先が無い: スナップショット内にも無いため、osv-scannerは親を読まない
|
|
121
|
+
}
|
|
122
|
+
const parent = await snapshot.copy(candidate, context.allowedRootReal, MAX_POM_BYTES);
|
|
123
|
+
if (!parent.ok) {
|
|
124
|
+
if (parent.failure === "outside") {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
reason: `親POM(relativePath: ${relativePath})が許可ルート(OSV_MCP_ALLOWED_ROOT)の外を参照しているため、スキャン対象から外しました` +
|
|
128
|
+
"(親のGAVが一致しなければosv-scannerは読みませんが、境界の外のため確認せず除外します)",
|
|
129
|
+
kind: "outside_allowed_root",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (parent.failure === "changed")
|
|
133
|
+
return { ok: false, reason: `親POMの${parent.message}`, kind: "unreadable" };
|
|
134
|
+
return first; // 読めない・通常のファイルでない等: コピーしないため、osv-scannerも読まない
|
|
135
|
+
}
|
|
136
|
+
current = candidate;
|
|
137
|
+
currentCopy = parent.path;
|
|
138
|
+
}
|
|
139
|
+
return restricted ? { ok: false, reason: `親POMの連鎖が上限(${MAX_PARENT_DEPTH}段)を超えています`, kind: "unreadable" } : first;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 検出済みのマニフェストをスナップショットへコピーし、osv-scannerに渡す対象(コピー)を返す。
|
|
143
|
+
* コピーできない・除外すべきものはskippedに理由付きで返す。
|
|
144
|
+
*/
|
|
145
|
+
export async function snapshotManifests(snapshot, targets, context) {
|
|
146
|
+
const copies = [];
|
|
147
|
+
const skipped = [];
|
|
148
|
+
for (const target of targets) {
|
|
149
|
+
if (target.format === "pom.xml") {
|
|
150
|
+
const result = await snapshotPom(snapshot, target.path, context);
|
|
151
|
+
if (result.ok)
|
|
152
|
+
copies.push({ path: result.path, format: target.format });
|
|
153
|
+
else
|
|
154
|
+
skipped.push({ path: target.path, reason: result.reason, kind: result.kind });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const result = await snapshot.copy(target.path, context.projectDir);
|
|
158
|
+
if (result.ok)
|
|
159
|
+
copies.push({ path: result.path, format: target.format });
|
|
160
|
+
else
|
|
161
|
+
skipped.push({ path: target.path, reason: result.message, kind: "unreadable" });
|
|
162
|
+
}
|
|
163
|
+
return { targets: copies, skipped };
|
|
164
|
+
}
|
|
@@ -14,14 +14,22 @@ function isTruthy(value) {
|
|
|
14
14
|
const normalized = value.trim().toLowerCase();
|
|
15
15
|
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* 許可ルートの設定値。空文字・空白のみは未設定(undefined)として扱う。
|
|
19
|
+
* 起動時の検証とツールへの受け渡しで判定がずれないよう、必ずこの関数で読む
|
|
20
|
+
* (空文字をそのまま渡すと`path.resolve("")`=起動ディレクトリが許可ルートになっていた)。
|
|
21
|
+
*/
|
|
22
|
+
export function allowedRootFromEnv(env = process.env) {
|
|
23
|
+
const allowedRoot = env[ALLOWED_ROOT_ENV];
|
|
24
|
+
return allowedRoot !== undefined && allowedRoot.trim() !== "" ? allowedRoot : undefined;
|
|
25
|
+
}
|
|
17
26
|
/**
|
|
18
27
|
* 起動を拒否すべき設定不備があればエラーメッセージを返す。問題なければnull。
|
|
19
28
|
*/
|
|
20
29
|
export function allowedRootStartupError(env = process.env) {
|
|
21
30
|
if (!isTruthy(env[REQUIRE_ALLOWED_ROOT_ENV]))
|
|
22
31
|
return null;
|
|
23
|
-
|
|
24
|
-
if (allowedRoot !== undefined && allowedRoot.trim() !== "")
|
|
32
|
+
if (allowedRootFromEnv(env) !== undefined)
|
|
25
33
|
return null;
|
|
26
34
|
return (`${REQUIRE_ALLOWED_ROOT_ENV}が有効ですが、${ALLOWED_ROOT_ENV}が未設定のため起動を中止します。` +
|
|
27
35
|
`スキャンを許可するルートディレクトリを${ALLOWED_ROOT_ENV}に設定してください`);
|
|
@@ -31,8 +39,7 @@ export function allowedRootStartupError(env = process.env) {
|
|
|
31
39
|
* (許可ルート未設定=任意の絶対パスをスキャン可能な状態の可視化)
|
|
32
40
|
*/
|
|
33
41
|
export function allowedRootStartupWarning(env = process.env) {
|
|
34
|
-
|
|
35
|
-
if (allowedRoot !== undefined && allowedRoot.trim() !== "")
|
|
42
|
+
if (allowedRootFromEnv(env) !== undefined)
|
|
36
43
|
return null;
|
|
37
44
|
return (`${ALLOWED_ROOT_ENV}が未設定のため、任意の絶対パスをスキャンできる状態です。` +
|
|
38
45
|
`プロジェクト置き場のルートを${ALLOWED_ROOT_ENV}に設定することを推奨します` +
|