osv-scanner-mcp 0.1.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.
@@ -0,0 +1,122 @@
1
+ /**
2
+ * スキャン対象パスの検証とJavaプロジェクト(Maven / Gradle)の検出。
3
+ *
4
+ * 対応マニフェスト:
5
+ * - Maven: pom.xml
6
+ * - Gradle: gradle.lockfile / buildscript-gradle.lockfile(lockfile方式)
7
+ * ビルド実行方式(gradle dependencies)はbuild.gradle自体が任意コードとして
8
+ * 実行されるため採用しない(docs/DESIGN_TODO.md参照)。lockfileが無い場合は
9
+ * 生成手順を案内する専用エラー(gradle_lockfile_missing)を返す
10
+ *
11
+ * `project_path`はLLM・ユーザー由来の信頼できない入力として扱う:
12
+ * - `realpath`で正規化し、シンボリックリンクを解決した実体パスで判定する
13
+ * - `allowedRoot`指定時は、解決後のパスがその配下にあることを検証する(パストラバーサル対策)
14
+ * - マニフェスト探索は深さ・件数に上限を設け、シンボリックリンクのディレクトリは辿らない
15
+ */
16
+ import { readdir, realpath, stat } from "node:fs/promises";
17
+ import path from "node:path";
18
+ import { ScanToolError } from "../errors.js";
19
+ const DEFAULT_MAX_DEPTH = 3;
20
+ /** 探索を打ち切るマニフェスト件数の上限(巨大モノレポでの暴走防止) */
21
+ const MAX_MANIFESTS = 100;
22
+ /** ビルド成果物・VCS等、マニフェスト探索でスキップするディレクトリ */
23
+ const SKIPPED_DIRS = new Set([".git", "node_modules", "target", "build", ".idea", ".vscode"]);
24
+ /** OSV-Scannerがスキャンできるマニフェスト(実機確認済み) */
25
+ const MANIFEST_FILENAMES = new Set(["pom.xml", "gradle.lockfile", "buildscript-gradle.lockfile"]);
26
+ /** Gradleプロジェクトの存在を示すが、それ自体はスキャンできないビルドファイル */
27
+ const GRADLE_BUILD_FILENAMES = new Set([
28
+ "build.gradle",
29
+ "build.gradle.kts",
30
+ "settings.gradle",
31
+ "settings.gradle.kts",
32
+ ]);
33
+ const GRADLE_LOCKFILE_GUIDANCE = "Gradleプロジェクトを検出しましたが、gradle.lockfileがありません。" +
34
+ "本ツールはlockfile方式のみ対応しています(ビルド実行方式はbuild.gradleの任意コード実行を伴うため非対応)。" +
35
+ "`./gradlew dependencies --write-locks` でlockfileを生成してから再実行してください" +
36
+ "(依存ロックが未設定の場合は build.gradle に dependencyLocking { lockAllConfigurations() } の追加が必要です)";
37
+ async function resolveExistingPath(inputPath) {
38
+ try {
39
+ return await realpath(path.resolve(inputPath));
40
+ }
41
+ catch {
42
+ throw new ScanToolError("project_not_found", `指定されたパスが存在しません: ${inputPath}`);
43
+ }
44
+ }
45
+ function assertInsideAllowedRoot(resolvedDir, allowedRootReal) {
46
+ const relative = path.relative(allowedRootReal, resolvedDir);
47
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
48
+ throw new ScanToolError("path_outside_allowed_root", `指定されたパスは許可されたディレクトリ(${allowedRootReal})の外にあります`);
49
+ }
50
+ }
51
+ /** 深さ・件数上限付きでマニフェストを探索する。シンボリックリンクは辿らない。 */
52
+ async function findManifests(rootDir, maxDepth) {
53
+ const manifests = [];
54
+ let gradleBuildFileFound = false;
55
+ let currentLevel = [rootDir];
56
+ for (let depth = 1; depth <= maxDepth && currentLevel.length > 0; depth++) {
57
+ const nextLevel = [];
58
+ for (const dir of currentLevel) {
59
+ let entries;
60
+ try {
61
+ entries = await readdir(dir, { withFileTypes: true });
62
+ }
63
+ catch {
64
+ continue; // 読めないディレクトリはスキップ(権限不足等)
65
+ }
66
+ for (const entry of entries) {
67
+ if (entry.isFile() && MANIFEST_FILENAMES.has(entry.name)) {
68
+ manifests.push(path.relative(rootDir, path.join(dir, entry.name)));
69
+ if (manifests.length >= MAX_MANIFESTS)
70
+ return { manifests, gradleBuildFileFound };
71
+ }
72
+ else if (entry.isFile() && GRADLE_BUILD_FILENAMES.has(entry.name)) {
73
+ gradleBuildFileFound = true;
74
+ }
75
+ else if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) {
76
+ // isDirectory()はシンボリックリンクに対してfalseを返すため、リンクは自然に除外される
77
+ nextLevel.push(path.join(dir, entry.name));
78
+ }
79
+ }
80
+ }
81
+ currentLevel = nextLevel;
82
+ }
83
+ return { manifests, gradleBuildFileFound };
84
+ }
85
+ /**
86
+ * 入力パスを検証し、スキャン対象のJavaプロジェクトとして解決する。
87
+ * ディレクトリ、またはマニフェスト(pom.xml / gradle.lockfile)・
88
+ * Gradleビルドファイルのパスを受け付ける。
89
+ */
90
+ export async function detectJavaProject(inputPath, options = {}) {
91
+ if (typeof inputPath !== "string" || inputPath.trim() === "") {
92
+ throw new ScanToolError("project_not_found", "スキャン対象のパスが指定されていません");
93
+ }
94
+ const resolved = await resolveExistingPath(inputPath);
95
+ const stats = await stat(resolved);
96
+ let projectDir;
97
+ if (stats.isDirectory()) {
98
+ projectDir = resolved;
99
+ }
100
+ else if (stats.isFile() &&
101
+ (MANIFEST_FILENAMES.has(path.basename(resolved)) ||
102
+ GRADLE_BUILD_FILENAMES.has(path.basename(resolved)))) {
103
+ // build.gradle等の直接指定も受け付け、ディレクトリとして解決する
104
+ // (lockfileが無ければ後段でgradle_lockfile_missingの案内になる)
105
+ projectDir = path.dirname(resolved);
106
+ }
107
+ else {
108
+ throw new ScanToolError("project_not_found", `指定されたパスはディレクトリでも対応マニフェスト(pom.xml / gradle.lockfile)でもありません: ${inputPath}`);
109
+ }
110
+ if (options.allowedRoot !== undefined) {
111
+ assertInsideAllowedRoot(projectDir, await resolveExistingPath(options.allowedRoot));
112
+ }
113
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
114
+ const { manifests, gradleBuildFileFound } = await findManifests(projectDir, maxDepth);
115
+ if (manifests.length === 0) {
116
+ if (gradleBuildFileFound) {
117
+ throw new ScanToolError("gradle_lockfile_missing", GRADLE_LOCKFILE_GUIDANCE);
118
+ }
119
+ throw new ScanToolError("no_manifest_found", `対応マニフェスト(pom.xml / gradle.lockfile)が見つかりません(深さ${maxDepth}まで探索): ${projectDir}`);
120
+ }
121
+ return { projectDir, manifests };
122
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 外部由来JSONを型安全に読むための防御的アクセサ。
3
+ * 形式が想定と異なっても例外を投げず、読み取れない値はnull/空として扱う。
4
+ */
5
+ export function asRecord(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value)
7
+ ? value
8
+ : null;
9
+ }
10
+ export function asArray(value) {
11
+ return Array.isArray(value) ? value : [];
12
+ }
13
+ export function asString(value) {
14
+ return typeof value === "string" ? value : null;
15
+ }
16
+ export function asStrings(value) {
17
+ return asArray(value).filter((v) => typeof v === "string");
18
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "osv-scanner-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server that wraps Google's OSV-Scanner to scan Java projects for known vulnerabilities",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "author": "tedorigawa001",
8
+ "keywords": [
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "osv",
12
+ "osv-scanner",
13
+ "vulnerability",
14
+ "security",
15
+ "maven",
16
+ "java",
17
+ "sca"
18
+ ],
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/tedorigawa001/OSV-Scanner-MCP.git"
25
+ },
26
+ "homepage": "https://github.com/tedorigawa001/OSV-Scanner-MCP#readme",
27
+ "bugs": {
28
+ "url": "https://github.com/tedorigawa001/OSV-Scanner-MCP/issues"
29
+ },
30
+ "engines": {
31
+ "node": ">=20.19"
32
+ },
33
+ "bin": {
34
+ "osv-scanner-mcp": "dist/index.js"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.build.json",
38
+ "typecheck": "tsc --noEmit",
39
+ "test": "vitest run",
40
+ "prepack": "npm run build",
41
+ "prepublishOnly": "npm run typecheck && npm test"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^26.1.0",
45
+ "@vitest/coverage-v8": "^4.1.9",
46
+ "typescript": "^6.0.3",
47
+ "vitest": "^4.1.9"
48
+ },
49
+ "dependencies": {
50
+ "@modelcontextprotocol/sdk": "^1.29.0",
51
+ "zod": "^4.4.3"
52
+ }
53
+ }