osv-scanner-mcp 0.1.11 → 0.1.12
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 +6 -3
- package/dist/index.js +3 -2
- package/dist/osv/affectedVersions.js +97 -0
- package/dist/osv/scanReport.js +7 -3
- package/dist/osv/suggestFix.js +33 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -205,7 +205,8 @@ Java(Maven)プロジェクトをスキャンし、既知の脆弱性レポート
|
|
|
205
205
|
"ecosystem": "Maven",
|
|
206
206
|
"recommended_upgrade": "2.25.4",
|
|
207
207
|
"upgrade_tier": "major_internal",
|
|
208
|
-
"
|
|
208
|
+
"verification": "verified",
|
|
209
|
+
"upgrade_note": "取得済みの影響範囲に基づき修正対象CVEの範囲外と確認した候補です",
|
|
209
210
|
"per_cve_detail": [
|
|
210
211
|
{ "id": "GHSA-jfh8-c2jp-5v3q", "cve": "CVE-2021-44228", "severity": "critical", "fixed_in": "2.15.0", "tier": "major_internal" }
|
|
211
212
|
]
|
|
@@ -214,8 +215,10 @@ Java(Maven)プロジェクトをスキャンし、既知の脆弱性レポート
|
|
|
214
215
|
}
|
|
215
216
|
```
|
|
216
217
|
|
|
217
|
-
- `recommended_upgrade`
|
|
218
|
-
-
|
|
218
|
+
- `recommended_upgrade` は既知の修正版を候補に、修正対象の全CVEの影響範囲外と確認できたものを3段階Tier順・バージョン昇順で選びます。CVEごとの修正版の最大値を単純に採用せず、別系統で再び影響を受ける候補も除外します。全公開版の中での最小性や未検出の脆弱性がないことは保証しません。
|
|
219
|
+
- MavenのOSV `ECOSYSTEM` 範囲(`introduced` / `fixed` / `last_affected` / 上限なし)を照合します。`versions` に明示された影響も確認します。範囲欠落・不正・未対応形式・`limit` による不完全な情報では安全と推定せず、候補を検証できなければ `recommended_upgrade: null`、`verification: "no_verified_candidate"` を返します。
|
|
220
|
+
- 推奨時は `verification: "verified"`、CVEごとの `recommended_status` は `affected` / `not_affected` / `unknown` です。推奨保留時は `not_evaluated` になります。`per_cve_detail.fixed_in` は各CVE単独の候補であり、最終推奨先の判定は `recommended_status` を参照してください。
|
|
221
|
+
- 現在より新しい修正版候補がないCVEは `tier: "unfixed"` として推奨の修正対象から除外します(情報欠落を含む場合があります)。除外したCVEも推奨先で判定し、その状態を表示します。全CVEがunfixedの場合も `recommended_upgrade` は `null` です。
|
|
219
222
|
|
|
220
223
|
### `explain_vulnerability`
|
|
221
224
|
|
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@ if (startupWarning !== null) {
|
|
|
32
32
|
// NOTE: リリース時はpackage.jsonのversionと同じ値に更新すること
|
|
33
33
|
const server = new McpServer({
|
|
34
34
|
name: "osv-scanner-mcp",
|
|
35
|
-
version: "0.1.
|
|
35
|
+
version: "0.1.12",
|
|
36
36
|
});
|
|
37
37
|
server.registerTool("scan_java_project", {
|
|
38
38
|
title: "Javaプロジェクトの脆弱性スキャン",
|
|
@@ -52,7 +52,8 @@ server.registerTool("suggest_fix", {
|
|
|
52
52
|
"現在のバージョンに最も近いリリース系統の修正版を優先する3段階フォールバック" +
|
|
53
53
|
"(same_minor: 同一major.minor系統内 → major_internal: 同一メジャー内 → cross_major: メジャーアップグレード)で選定し、" +
|
|
54
54
|
"推奨バージョン・アップグレード距離(upgrade_tier)・CVEごとの修正版を返す。" +
|
|
55
|
-
"
|
|
55
|
+
"候補を全修正対象CVEの影響範囲と照合し、情報不足の場合は推奨を保留する。" +
|
|
56
|
+
"現在より新しい修正版候補のないCVEはunfixedとして別表示し、推奨先での判定も返す。Maven(pom.xml)とGradle(gradle.lockfile)に対応。",
|
|
56
57
|
inputSchema: {
|
|
57
58
|
project_path: z
|
|
58
59
|
.string()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { compareMavenVersions } from "../utils/mavenVersion.js";
|
|
2
|
+
import { asArray, asRecord, asString } from "../utils/unknownJson.js";
|
|
3
|
+
/** Preserve uncertainty: unsupported or malformed ranges cannot prove a candidate safe. */
|
|
4
|
+
export function extractAffectedVersions(details, ids, name, ecosystem) {
|
|
5
|
+
const evidence = { complete: ecosystem === "Maven", intervals: [], versions: [] };
|
|
6
|
+
if (ids.length === 0 || ids.some(id => !details.some(d => d.id === id)))
|
|
7
|
+
evidence.complete = false;
|
|
8
|
+
for (const detail of details) {
|
|
9
|
+
let matched = false;
|
|
10
|
+
for (const raw of asArray(detail.affected)) {
|
|
11
|
+
const affected = asRecord(raw);
|
|
12
|
+
const pkg = asRecord(affected?.package);
|
|
13
|
+
if (asString(pkg?.name) !== name || asString(pkg?.ecosystem) !== ecosystem) {
|
|
14
|
+
if (!asString(pkg?.name) || !asString(pkg?.ecosystem))
|
|
15
|
+
evidence.complete = false;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
matched = true;
|
|
19
|
+
if (affected?.versions !== undefined && !Array.isArray(affected.versions))
|
|
20
|
+
evidence.complete = false;
|
|
21
|
+
for (const version of asArray(affected?.versions)) {
|
|
22
|
+
const value = asString(version);
|
|
23
|
+
if (!value)
|
|
24
|
+
evidence.complete = false;
|
|
25
|
+
else
|
|
26
|
+
evidence.versions.push(value);
|
|
27
|
+
}
|
|
28
|
+
const ranges = asArray(affected?.ranges);
|
|
29
|
+
// A versions-only list does not establish that unlisted releases are unaffected.
|
|
30
|
+
if (ranges.length === 0)
|
|
31
|
+
evidence.complete = false;
|
|
32
|
+
for (const rawRange of ranges) {
|
|
33
|
+
const range = asRecord(rawRange);
|
|
34
|
+
if (range?.type !== "ECOSYSTEM") {
|
|
35
|
+
evidence.complete = false;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const events = asArray(range.events);
|
|
39
|
+
let start = null;
|
|
40
|
+
let previousEnd = null;
|
|
41
|
+
let previousInclusive = false;
|
|
42
|
+
if (events.length === 0)
|
|
43
|
+
evidence.complete = false;
|
|
44
|
+
for (const rawEvent of events) {
|
|
45
|
+
const event = asRecord(rawEvent);
|
|
46
|
+
const keys = event ? Object.keys(event) : [];
|
|
47
|
+
const kind = keys[0];
|
|
48
|
+
const value = kind ? asString(event?.[kind]) : null;
|
|
49
|
+
if (keys.length !== 1 || !value || !["introduced", "fixed", "last_affected", "limit"].includes(kind)) {
|
|
50
|
+
evidence.complete = false;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (kind === "introduced") {
|
|
54
|
+
if (start !== null || (previousEnd !== null && (value === "0" ||
|
|
55
|
+
compareMavenVersions(value, previousEnd) < 0 ||
|
|
56
|
+
(previousInclusive && compareMavenVersions(value, previousEnd) === 0))))
|
|
57
|
+
evidence.complete = false;
|
|
58
|
+
start = value;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if (start === null) {
|
|
62
|
+
evidence.complete = false;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const inclusive = kind === "last_affected";
|
|
66
|
+
if (start !== "0" && (compareMavenVersions(start, value) > 0 ||
|
|
67
|
+
(!inclusive && compareMavenVersions(start, value) === 0)))
|
|
68
|
+
evidence.complete = false;
|
|
69
|
+
evidence.intervals.push({ introduced: start, end: value, inclusive });
|
|
70
|
+
// limit bounds known affected versions, but is not evidence of a fix beyond it.
|
|
71
|
+
if (kind === "limit")
|
|
72
|
+
evidence.complete = false;
|
|
73
|
+
previousEnd = value;
|
|
74
|
+
previousInclusive = inclusive;
|
|
75
|
+
start = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (start !== null)
|
|
79
|
+
evidence.intervals.push({ introduced: start, end: null, inclusive: false });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!matched)
|
|
83
|
+
evidence.complete = false;
|
|
84
|
+
}
|
|
85
|
+
if (details.length === 0)
|
|
86
|
+
evidence.complete = false;
|
|
87
|
+
return evidence;
|
|
88
|
+
}
|
|
89
|
+
export function candidateStatus(evidence, version) {
|
|
90
|
+
if (!evidence)
|
|
91
|
+
return "unknown";
|
|
92
|
+
if (evidence.versions.some(v => compareMavenVersions(v, version) === 0) || evidence.intervals.some(r => (r.introduced === "0" || compareMavenVersions(version, r.introduced) >= 0) &&
|
|
93
|
+
(r.end === null || compareMavenVersions(version, r.end) < 0 ||
|
|
94
|
+
(r.inclusive && compareMavenVersions(version, r.end) === 0))))
|
|
95
|
+
return "affected";
|
|
96
|
+
return evidence.complete ? "not_affected" : "unknown";
|
|
97
|
+
}
|
package/dist/osv/scanReport.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { compareMavenVersions } from "../utils/mavenVersion.js";
|
|
16
16
|
import { asArray, asRecord, asString, asStrings } from "../utils/unknownJson.js";
|
|
17
|
+
import { extractAffectedVersions } from "./affectedVersions.js";
|
|
17
18
|
/** 外部由来のsummaryをLLMに渡す際の長さ上限(プロンプト肥大・DoS対策) */
|
|
18
19
|
const MAX_SUMMARY_LENGTH = 500;
|
|
19
20
|
const CVSS_CRITICAL = 9.0;
|
|
@@ -65,7 +66,7 @@ function extractSummary(vulnDetails) {
|
|
|
65
66
|
* OSVエントリの`affected[].ranges[].events[].fixed`から修正版を収集する。
|
|
66
67
|
* 1つのOSVエントリが複数パッケージをカバーしうるため、対象パッケージ名で絞り込む。
|
|
67
68
|
*/
|
|
68
|
-
function extractFixedVersions(vulnDetails, packageName) {
|
|
69
|
+
function extractFixedVersions(vulnDetails, packageName, ecosystem) {
|
|
69
70
|
const versions = new Set();
|
|
70
71
|
for (const detail of vulnDetails) {
|
|
71
72
|
for (const affectedRaw of asArray(detail.affected)) {
|
|
@@ -73,9 +74,11 @@ function extractFixedVersions(vulnDetails, packageName) {
|
|
|
73
74
|
if (!affected)
|
|
74
75
|
continue;
|
|
75
76
|
const affectedName = asString(asRecord(affected.package)?.name);
|
|
76
|
-
if (affectedName !==
|
|
77
|
+
if (affectedName !== packageName || asString(asRecord(affected.package)?.ecosystem) !== ecosystem)
|
|
77
78
|
continue;
|
|
78
79
|
for (const rangeRaw of asArray(affected.ranges)) {
|
|
80
|
+
if (asRecord(rangeRaw)?.type !== "ECOSYSTEM" || ecosystem !== "Maven")
|
|
81
|
+
continue;
|
|
79
82
|
for (const eventRaw of asArray(asRecord(rangeRaw)?.events)) {
|
|
80
83
|
const fixed = asString(asRecord(eventRaw)?.fixed);
|
|
81
84
|
if (fixed !== null && fixed !== "")
|
|
@@ -139,7 +142,8 @@ export function parseOsvScanOutput(raw) {
|
|
|
139
142
|
severity_score: score,
|
|
140
143
|
severity: severityFromScore(score),
|
|
141
144
|
summary: extractSummary(vulnDetails),
|
|
142
|
-
fixed_versions: extractFixedVersions(vulnDetails, name),
|
|
145
|
+
fixed_versions: extractFixedVersions(vulnDetails, name, ecosystem),
|
|
146
|
+
affected_versions: extractAffectedVersions(vulnDetails, ids, name, ecosystem),
|
|
143
147
|
});
|
|
144
148
|
}
|
|
145
149
|
}
|
package/dist/osv/suggestFix.js
CHANGED
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
* Tier 2 (major_internal): 同一メジャー内の最小の修正版(マイナーバージョンアップ)
|
|
11
11
|
* Tier 3 (cross_major): 全体最小の修正版(メジャーアップグレード、破壊的変更の可能性)
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* パッケージ全体の候補を全修正対象CVEの影響範囲と照合して推奨する。
|
|
14
|
+
* 判定不能な候補は推奨しない。
|
|
15
15
|
* 現在より新しい修正版が存在しないCVEはunfixedとして明示し、推奨計算から除外する。
|
|
16
16
|
*/
|
|
17
17
|
import { compareMavenVersions, mavenVersionSeries } from "../utils/mavenVersion.js";
|
|
18
|
+
import { candidateStatus } from "./affectedVersions.js";
|
|
18
19
|
function classifyTier(currentSeries, candidate) {
|
|
19
20
|
const candidateSeries = mavenVersionSeries(candidate);
|
|
20
21
|
if (currentSeries === null || candidateSeries === null)
|
|
@@ -48,26 +49,26 @@ function pickFixForCve(currentVersion, currentSeries, fixedVersions) {
|
|
|
48
49
|
}
|
|
49
50
|
function buildNote(currentSeries, recommended, tier, fixableCount, unfixedCount) {
|
|
50
51
|
if (recommended === null) {
|
|
51
|
-
return `全${unfixedCount}件のCVE
|
|
52
|
+
return `全${unfixedCount}件のCVEに現在より新しい修正版候補がない(unfixed)。修正版情報の欠落を含む可能性があります`;
|
|
52
53
|
}
|
|
53
54
|
const label = currentSeries !== null ? `${currentSeries.major}.${currentSeries.minor}` : null;
|
|
54
55
|
let note;
|
|
55
56
|
switch (tier) {
|
|
56
57
|
case "same_minor":
|
|
57
|
-
note = `現在の${label}
|
|
58
|
+
note = `現在の${label}系統内の候補${recommended}を推奨`;
|
|
58
59
|
break;
|
|
59
60
|
case "major_internal":
|
|
60
|
-
note =
|
|
61
|
+
note = `同一メジャー(${currentSeries.major}.x)内の候補${recommended}を推奨`;
|
|
61
62
|
break;
|
|
62
63
|
default:
|
|
63
64
|
note =
|
|
64
65
|
label !== null
|
|
65
|
-
?
|
|
66
|
-
:
|
|
67
|
-
}
|
|
68
|
-
if (unfixedCount > 0) {
|
|
69
|
-
note += `。残り${unfixedCount}件は修正版が存在せず、このアップグレードでは解消されない`;
|
|
66
|
+
? `候補${recommended}へのメジャーアップグレードを推奨(破壊的変更の可能性あり)`
|
|
67
|
+
: `現在バージョンの系統を判定できないため、検証済み候補${recommended}を提示`;
|
|
70
68
|
}
|
|
69
|
+
note += `。取得済みの影響範囲に基づき修正対象${fixableCount}件のCVEの範囲外と確認しました。全公開版の最小性や未検出の脆弱性がないことは保証しません`;
|
|
70
|
+
if (unfixedCount > 0)
|
|
71
|
+
note += `。残り${unfixedCount}件は現在より新しい修正版候補がなく、修正対象から除外しています。recommended_statusを確認してください`;
|
|
71
72
|
return note;
|
|
72
73
|
}
|
|
73
74
|
/** 1パッケージ分のアップグレード提案を組み立てる。 */
|
|
@@ -96,11 +97,26 @@ export function suggestUpgradeForPackage(pkg) {
|
|
|
96
97
|
fixed_in: pick.version,
|
|
97
98
|
tier: pick.tier,
|
|
98
99
|
});
|
|
99
|
-
if (recommended === null || compareMavenVersions(pick.version, recommended) > 0) {
|
|
100
|
-
recommended = pick.version;
|
|
101
|
-
}
|
|
102
100
|
}
|
|
103
101
|
const fixableCount = details.length - unfixedCount;
|
|
102
|
+
const targets = pkg.vulnerabilities.filter((_, i) => details[i].fixed_in !== null);
|
|
103
|
+
const candidates = [...new Set(targets.flatMap(v => v.fixed_versions))]
|
|
104
|
+
.filter(v => compareMavenVersions(v, pkg.version) > 0)
|
|
105
|
+
.sort(compareMavenVersions);
|
|
106
|
+
const tierOrder = ["same_minor", "major_internal", "cross_major"];
|
|
107
|
+
recommended = null;
|
|
108
|
+
for (const tier of tierOrder) {
|
|
109
|
+
const candidate = candidates.find(v => classifyTier(currentSeries, v) === tier &&
|
|
110
|
+
targets.every(target => candidateStatus(target.affected_versions, v) === "not_affected"));
|
|
111
|
+
if (candidate !== undefined) {
|
|
112
|
+
recommended = candidate;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (let i = 0; i < details.length; i++) {
|
|
117
|
+
details[i].recommended_status = recommended === null ? "not_evaluated" :
|
|
118
|
+
candidateStatus(pkg.vulnerabilities[i].affected_versions, recommended);
|
|
119
|
+
}
|
|
104
120
|
// 推奨バージョン自体のTierは「現在バージョンとの系統関係」で再分類する
|
|
105
121
|
// (per-CVEのTierの寄せ集めではなく、実際に行うアップグレードの距離を表す)
|
|
106
122
|
const upgradeTier = recommended !== null ? classifyTier(currentSeries, recommended) : null;
|
|
@@ -110,8 +126,11 @@ export function suggestUpgradeForPackage(pkg) {
|
|
|
110
126
|
ecosystem: pkg.ecosystem,
|
|
111
127
|
recommended_upgrade: recommended,
|
|
112
128
|
upgrade_tier: upgradeTier,
|
|
113
|
-
upgrade_note:
|
|
129
|
+
upgrade_note: recommended === null && fixableCount > 0
|
|
130
|
+
? "既知の修正版候補から、全修正対象CVEの影響範囲外と確認できる版が見つかりません。情報不足・未対応の範囲形式を含む場合も推奨を保留します。"
|
|
131
|
+
: buildNote(currentSeries, recommended, upgradeTier, fixableCount, unfixedCount),
|
|
114
132
|
per_cve_detail: details,
|
|
133
|
+
verification: recommended === null ? "no_verified_candidate" : "verified",
|
|
115
134
|
};
|
|
116
135
|
}
|
|
117
136
|
/** スキャンレポート全体からパッケージごとの提案一覧を作る(深刻度順を維持)。 */
|