osv-scanner-mcp 0.12.1 → 0.14.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 +23 -15
- package/dist/index.js +8 -6
- package/dist/osv/binaryDownloader.js +12 -10
- package/dist/osv/candidateCheck.js +6 -3
- package/dist/osv/runner.js +6 -2
- package/dist/osv/suggestFix.js +106 -9
- package/dist/osv/versionScheme.js +4 -1
- package/dist/tools/scanProject.js +63 -6
- package/dist/tools/suggestFix.js +1 -1
- package/dist/utils/cargoLock.js +209 -54
- package/dist/utils/dependencyRelations.js +121 -7
- package/dist/utils/manifestDetector.js +50 -13
- package/dist/utils/manifestFormats.js +1 -1
- package/dist/utils/npmLockCopy.js +183 -0
- package/dist/utils/pomParent.js +10 -7
- package/dist/utils/requirementsFile.js +24 -5
- package/dist/utils/scanSnapshot.js +57 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,19 +7,19 @@
|
|
|
7
7
|
|
|
8
8
|
An MCP server that wraps Google's [OSV-Scanner](https://github.com/google/osv-scanner). Ask an MCP client such as Claude to "check this project for vulnerabilities", and it returns the known vulnerabilities (CVE / GHSA) in your dependencies as a report sorted by severity, together with recommended upgrades.
|
|
9
9
|
|
|
10
|
-
> **Status**: [Published on npm](https://www.npmjs.com/package/osv-scanner-mcp) (`npx -y osv-scanner-mcp`). Supports lockfiles and manifests for Java (Maven / Gradle), JavaScript (npm / yarn / pnpm / bun), Python (Poetry / uv / Pipenv / PDM / requirements.txt), Go, and Rust (`Cargo.lock`), with upgrade recommendations for
|
|
10
|
+
> **Status**: [Published on npm](https://www.npmjs.com/package/osv-scanner-mcp) (`npx -y osv-scanner-mcp`). Supports lockfiles and manifests for Java (Maven / Gradle), JavaScript (npm / yarn / pnpm / bun), Python (Poetry / uv / Pipenv / PDM / requirements.txt), Go, and Rust (`Cargo.lock`), with upgrade recommendations for Maven, npm, PyPI, Go, and crates.io. Setup instructions are provided for Claude Code, Claude Desktop, Codex CLI, Antigravity, and VS Code (GitHub Copilot).
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
14
|
- **One-shot multi-language scan**: Pass a project path to `scan_project`, and it detects and scans the Java, JavaScript, Python, Go, and Rust lockfiles in the project together. Dependencies that could not be scanned (for example, a manifest without a lockfile or an unpinned requirement) are reported up front in `coverage`
|
|
15
|
-
- **Upgrade recommendations**: `suggest_fix` recommends, for each vulnerable package, the upgrade closest to the current release line that
|
|
15
|
+
- **Upgrade recommendations**: `suggest_fix` recommends, for each vulnerable package, the upgrade closest to the current release line that addresses its fix targets, for Maven, npm, PyPI, Go, and Rust (crates.io). Known unfixed and informational advisories remain visible. The recommended version is also checked against the OSV database for additional fix-target vulnerabilities
|
|
16
16
|
- **Rust without false positives**: For `Cargo.lock`, only crates from crates.io are scanned. Your own workspace crates, git dependencies, and crates from private registries are not looked up as crates.io crates (OSV-Scanner alone would match them by name, and send their names to the OSV API). RustSec notices about unmaintained or unsound crates are labeled `informational` and counted apart from vulnerabilities
|
|
17
17
|
- **Direct and transitive dependencies**: Packages are marked as direct or transitive dependencies (from `package-lock.json`, `go.mod`, `requirements.txt`, and `pom.xml`), and update hints are tailored accordingly, for example by naming the direct dependency that pulls in a transitive one
|
|
18
18
|
- **Java project scan**: `scan_java_project` scans only the Java (Maven / Gradle) manifests and returns a formatted report
|
|
19
19
|
- **JAR/WAR scan**: `scan_java_artifact` detects known vulnerabilities from the metadata inside JAR/WAR archives, for projects without a lockfile or with only shaded/fat JARs (best-effort identification, stated explicitly in `coverage`)
|
|
20
20
|
- **SBOM scan**: `scan_sbom` checks the dependencies recorded in a CycloneDX/SPDX JSON SBOM. It states explicitly that the SBOM's completeness and its match with the actual artifacts are not verified
|
|
21
21
|
- **Severity-sorted reports**: Vulnerabilities are grouped by package and sorted by CVSS score, with five severity labels (critical / high / medium / low / unknown) and summary counts
|
|
22
|
-
- **Fixed versions**: Each vulnerability includes `fixed_versions`, sorted correctly by Maven version precedence (including non-semver forms such as `2.17.1-RELEASE`), Semantic Versioning precedence for npm and
|
|
22
|
+
- **Fixed versions**: Each vulnerability includes `fixed_versions`, sorted correctly by Maven version precedence (including non-semver forms such as `2.17.1-RELEASE`), Semantic Versioning precedence for npm, Go, and Rust, and PEP 440 for PyPI
|
|
23
23
|
- **Security first**: No shell execution, a fixed argument allowlist, path normalization and boundary checks, scanning private copies instead of the original files, and timeouts and output size limits are built in
|
|
24
24
|
|
|
25
25
|
## Requirements
|
|
@@ -29,6 +29,7 @@ An MCP server that wraps Google's [OSV-Scanner](https://github.com/google/osv-sc
|
|
|
29
29
|
- A manually installed binary (on `PATH` or set with `OSV_SCANNER_PATH`) is used first
|
|
30
30
|
- Set `OSV_MCP_AUTO_DOWNLOAD=0` to disable the automatic download
|
|
31
31
|
- Set `OSV_MCP_PREFER_DOWNLOAD=1` to always use the verified downloaded binary instead of one on `PATH` (recommended for production)
|
|
32
|
+
- The pinned version is osv-scanner 2.7.0. Its binary is cached in `~/.cache/osv-scanner-mcp/v2.7.0/`; directories of earlier versions (such as `v2.4.0/`) are no longer used and can be deleted
|
|
32
33
|
- Scans, `suggest_fix`, and `explain_vulnerability` access the network. Vulnerabilities are looked up in the OSV database (`api.osv.dev`), and **scanning `pom.xml` or `requirements.txt` also connects to deps.dev (`api.deps.dev`) to resolve transitive dependencies**. See [Network destinations and privacy](#network-destinations-and-privacy) for details and how to turn this off
|
|
33
34
|
|
|
34
35
|
## Setup
|
|
@@ -176,13 +177,13 @@ As an additional layer of defense, you can limit what the server can read and wr
|
|
|
176
177
|
|
|
177
178
|
### Network destinations and privacy
|
|
178
179
|
|
|
179
|
-
Destinations confirmed with osv-scanner v2.4.0 (2026-10-07):
|
|
180
|
+
Destinations confirmed with osv-scanner v2.4.0 (2026-10-07) and v2.7.0, the pinned version (2026-10-15):
|
|
180
181
|
|
|
181
182
|
| Operation | Destinations | Information sent |
|
|
182
183
|
|---|---|---|
|
|
183
184
|
| Scanning `pom.xml` (`scan_project` / `scan_java_project` / `suggest_fix`) | `api.osv.dev`, **`api.deps.dev`** | Package names and versions. To resolve transitive dependencies, the names and versions of the dependencies declared in `pom.xml` (including internal packages) are sent to deps.dev |
|
|
184
185
|
| Scanning `requirements.txt` (`scan_project` / `suggest_fix`) | `api.osv.dev`, **`api.deps.dev`** | As with `pom.xml`, the names and versions of the listed dependencies are sent to deps.dev to resolve transitive dependencies. Package indexes given with `--index-url` and similar options are not contacted |
|
|
185
|
-
| Scanning lockfiles (`gradle.lockfile`, npm lockfiles such as `package-lock.json`, Python lockfiles such as `poetry.lock`, `go.mod`, `Cargo.lock`) | `api.osv.dev` | Package names and versions (lockfiles already list every dependency, so nothing is resolved remotely). For `Cargo.lock`, only crates from crates.io are sent: the names of your own crates, git dependencies, and private-registry crates are not sent (confirmed 2026-10-11) |
|
|
186
|
+
| Scanning lockfiles (`gradle.lockfile`, npm lockfiles such as `package-lock.json`, Python lockfiles such as `poetry.lock`, `go.mod`, `Cargo.lock`) | `api.osv.dev` | Package names and versions (lockfiles already list every dependency, so nothing is resolved remotely). For `Cargo.lock`, only crates from crates.io are sent: the names of your own crates, git dependencies, and private-registry crates are not sent (confirmed 2026-10-11). **Git dependencies in npm lockfiles** (`package-lock.json`, `yarn.lock`, and so on) are looked up by their repository URL (ecosystem `GIT`) and commit, so the host name and path of a private git repository (for example `https://git.example.com/team/repo`) are sent (since osv-scanner 2.6.0) |
|
|
186
187
|
| `scan_java_artifact` / `scan_sbom` | `api.osv.dev` | Names and versions of the identified packages |
|
|
187
188
|
| `suggest_fix` checking recommended versions | `api.osv.dev` | The names of packages with a recommendation (already sent during the scan) and the candidate versions (published fixed versions). Disable with `OSV_MCP_NO_CANDIDATE_CHECK=1` |
|
|
188
189
|
| `explain_vulnerability` | `api.osv.dev` | The vulnerability ID |
|
|
@@ -219,7 +220,11 @@ Detects the lockfiles and manifests in a project and scans the Java / JavaScript
|
|
|
219
220
|
|
|
220
221
|
**`requirements.txt` files are not passed to OSV-Scanner as they are.** The server parses them, rewrites only the dependency lines it can interpret into simple forms such as `name==version`, and scans a private temporary copy (deleted after the scan). OSV-Scanner interprets include directives on its own (it even treats `- r ../x.txt`, with a space, as an include and reads files outside the scan scope), so the copy never contains include directives or options. Includes (`-r` / `--requirement`) are expanded by the server, and only when the included file is inside the project directory.
|
|
221
222
|
|
|
222
|
-
**`Cargo.lock` files are not passed to OSV-Scanner as they are either.** OSV-Scanner (v2.4.0) ignores where each package comes from and looks every package up as a crates.io crate, so a workspace crate, a git fork, or a private-registry crate with the same name and version as a vulnerable crates.io crate is reported as vulnerable, and its name is sent to the OSV API. The server parses `Cargo.lock` and scans a private copy that lists only the packages whose source is crates.io (`registry+https://github.com/rust-lang/crates.io-index` or `sparse+https://index.crates.io/`), like `cargo audit`. Packages replaced with `[replace]` are not scanned, because they are not used (the replacement is classified by its own source). The parser accepts only what Cargo writes; if a `Cargo.lock` contains anything else, the whole file is excluded and listed in `skipped_files` instead of being scanned partially. Cargo is never run.
|
|
223
|
+
**`Cargo.lock` files are not passed to OSV-Scanner as they are either.** OSV-Scanner (v2.4.0 and v2.7.0) ignores where each package comes from and looks every package up as a crates.io crate, so a workspace crate, a git fork, or a private-registry crate with the same name and version as a vulnerable crates.io crate is reported as vulnerable, and its name is sent to the OSV API. The server parses `Cargo.lock` and scans a private copy that lists only the packages whose source is crates.io (`registry+https://github.com/rust-lang/crates.io-index` or `sparse+https://index.crates.io/`), like `cargo audit`. Packages replaced with `[replace]` are not scanned, because they are not used (the replacement is classified by its own source). The parser accepts only what Cargo writes; if a `Cargo.lock` contains anything else, the whole file is excluded and listed in `skipped_files` instead of being scanned partially. Cargo is never run.
|
|
224
|
+
|
|
225
|
+
**`package-lock.json` copies get a `resolved` URL where it is missing.** osv-scanner 2.7.0 treats entries without `resolved` as local packages and does not look them up, which would silently skip bundled dependencies (`inBundle`) and registry packages in lockfiles generated with npm's `omit-lockfile-registry-resolved=true`. The private copy that is scanned gets a public-registry URL for those entries (entries under `node_modules/` that are not links and have a plain version, and aliases such as `npm:lodash@4.17.20` in lockfile version 1), so they are looked up as before; the original file is not changed.
|
|
226
|
+
|
|
227
|
+
**npm JSON limits:** Normalization of `package-lock.json` and `npm-shrinkwrap.json` is limited to 16 MiB per file and 64 MiB per scan, for both input and output. Before parsing, JSON nesting is limited to 128 levels and structural markers (containers, commas, and colons outside strings) to 1,000,000 per file and 2,000,000 per analysis phase. Changed copies use compact JSON, with their size checked before serialization. A lockfile that exceeds these limits is scanned as it is, without filling in `resolved` (OSV-Scanner parses large lockfiles itself; the limits only bound this server's own parsing and rewriting), and it is listed in `coverage.skipped_files` with the reason, so `complete` is `false`: entries without `resolved` in it may not have been looked up. Workspace membership and dependency-relation lookups use the same structural checks and remain unconfirmed or unknown when limits are reached.
|
|
223
228
|
|
|
224
229
|
**Reading the output**
|
|
225
230
|
|
|
@@ -262,14 +267,15 @@ Detects the lockfiles and manifests in a project and scans the Java / JavaScript
|
|
|
262
267
|
- Each list holds up to 200 entries; the number of omitted entries is returned in `omitted_items`
|
|
263
268
|
- `ecosystem_breakdown` includes ecosystems with zero findings, to show that they were scanned
|
|
264
269
|
- **Informational advisories (Rust)**: RustSec also publishes notices that are not vulnerabilities: `unmaintained` (the crate is no longer maintained), `unsound` (a soundness bug), and `notice`. Most have no fixed version. When every advisory in a group is such a notice (every related ID, including aliases, must have its details available and be a notice of a known kind; otherwise it is treated as a vulnerability), the vulnerability has `informational` set to its kind, is listed after the vulnerabilities of the package, and is counted in `informational_count` (and in `ecosystem_breakdown`) instead of `vulnerability_count`, `severity_breakdown`, and `vulnerable_package_count`. If another database (such as GitHub's) lists the same issue as a vulnerability, it stays a vulnerability. `informational_count` appears only when there is at least one
|
|
265
|
-
- `dependency_relation` shows whether a package is a direct (`direct`) or transitive (`transitive`) dependency. OSV-Scanner does not report this, so the server determines it
|
|
270
|
+
- `dependency_relation` shows whether a package is a direct (`direct`) or transitive (`transitive`) dependency. OSV-Scanner does not report this, so the server determines it from validated manifest data and scanned copies:
|
|
266
271
|
- `package-lock.json` (v2 or later): dependencies of the root and workspace `package.json` files, resolved with Node's lookup rules (nested `node_modules` first, then parent directories), are direct; everything reachable from them is transitive. Direct dependencies list the `package.json` files that declare them in `declared_in`; transitive dependencies list the direct dependencies that require them in `introduced_by` (up to 10, with the rest counted in `introduced_by_omitted`). A version that is both a direct dependency and required by another dependency is `direct` and also has `introduced_by`
|
|
267
272
|
- `go.mod`: `require` lines without `// indirect` are direct. Modules affected by a `replace` directive are marked `replaced_in_go_mod: true` (OSV-Scanner reports the replacement module and version)
|
|
268
273
|
- `requirements.txt`: dependencies written in the file are direct; dependencies resolved through deps.dev are transitive
|
|
269
274
|
- `pom.xml`: OSV-Scanner reports the dependencies declared in `pom.xml` (and its parent POMs) and the transitive dependencies resolved through deps.dev as separate results (`source.type` `lockfile` / `unknown`), and this split is used (so parent POMs, profiles, properties, and dependency management are interpreted exactly as OSV-Scanner does). `introduced_by` / `declared_in` are not available. This relies on undocumented OSV-Scanner output, so anything unexpected is reported as `unknown`
|
|
270
|
-
-
|
|
275
|
+
- `Cargo.lock`: crates.io dependencies immediately required by a local/workspace/path crate are direct; crates.io dependencies reachable through them (including git and alternate-registry origins) are transitive. `introduced_by` names the first direct dependency on each path. References are resolved by exact name, version, and source where provided; ambiguous references are not guessed. Replacements are followed. `declared_in` lists project-relative Cargo.toml paths only when every declaring crate has a unique matching manifest. Otherwise `declaring_crates` names the unmapped declaring crates (up to 10, with the rest in `declaring_crates_omitted`). Dev/build/target-specific distinctions are unavailable. Traversal over 2,000,000 edges returns `unknown`
|
|
276
|
+
- Other formats (`gradle.lockfile`, `yarn.lock`, `pnpm-lock.yaml`, `bun.lock`, `poetry.lock`, `uv.lock`, `Pipfile.lock`, and `pdm.lock`), npm lockfile version 1, and entries not reachable from the project are `unknown`. If lockfiles disagree, the value is `mixed`
|
|
271
277
|
- `dependency_groups` is the raw dependency group reported by OSV-Scanner (for example `dev`). It is missing or inaccurate for some lockfile formats (absent for pnpm, `optional` for pdm, and so on), so treat it as informational
|
|
272
|
-
- Upgrade recommendations (`suggest_fix`) are available for
|
|
278
|
+
- Upgrade recommendations (`suggest_fix`) are available for Maven, npm, PyPI, Go, and Rust (crates.io)
|
|
273
279
|
|
|
274
280
|
### `scan_java_project`
|
|
275
281
|
|
|
@@ -334,7 +340,7 @@ If an existing parent POM cannot be read (larger than 10 MiB, not a regular file
|
|
|
334
340
|
```
|
|
335
341
|
|
|
336
342
|
- `packages` is sorted by the most severe vulnerability, and each `vulnerabilities` list by severity (unknown last)
|
|
337
|
-
- `fixed_versions` lists the fixed versions recorded in OSV, in ascending order: by Maven precedence for Maven, Semantic Versioning precedence for npm and
|
|
343
|
+
- `fixed_versions` lists the fixed versions recorded in OSV, in ascending order: by Maven precedence for Maven, Semantic Versioning precedence for npm, Go, and Rust (values that are not valid SemVer come last), and PEP 440 for PyPI (in the order listed by OSV up to v0.5.0); other ecosystems keep the order listed by OSV (the order is not guaranteed). Several release lines may be mixed (for example a 2.12 backport and the 2.15 line). Pre-releases (`5.0.0-beta.3`) and Go pseudo-versions (`0.0.0-20180925071336-cf3bd585ca2a`) may be included. An empty array means that OSV lists no fixed version. Up to v0.4.1, this was always empty for packages outside Maven
|
|
338
344
|
- Vulnerabilities without a severity score are reported as `null` / `"unknown"`
|
|
339
345
|
|
|
340
346
|
### `scan_java_artifact`
|
|
@@ -350,7 +356,7 @@ A directory search includes `target` and `build`, and excludes `.git`, `node_mod
|
|
|
350
356
|
The search is limited to a depth of 8, 100 archives, and 10,000 entries. If the search cannot complete within the limits, `artifact_search_limit_exceeded` is returned instead of silently truncating the results; narrow the target and try again.
|
|
351
357
|
`OSV_MCP_ALLOWED_ROOT` applies as well.
|
|
352
358
|
|
|
353
|
-
OSV-Scanner
|
|
359
|
+
OSV-Scanner's `java/archive` plugin is used, and nested JARs are also analyzed by the scanner. No Java code is executed and no build is run.
|
|
354
360
|
Identification relies on metadata inside the archives, so dependencies whose metadata was removed, and dependencies inside shaded/minimized JARs, may be missed.
|
|
355
361
|
|
|
356
362
|
**Reading the output:**
|
|
@@ -391,7 +397,7 @@ Invalid JSON returns `invalid_sbom`, an unsupported format `unsupported_sbom_for
|
|
|
391
397
|
|
|
392
398
|
### `suggest_fix`
|
|
393
399
|
|
|
394
|
-
Runs the same detection and scan as `scan_project` and recommends an **upgrade version** for each vulnerable package. Recommendations are available for Java (Maven / Gradle), JavaScript (npm), Python (PyPI), and
|
|
400
|
+
Runs the same detection and scan as `scan_project` and recommends an **upgrade version** for each vulnerable package. Recommendations are available for Java (Maven / Gradle), JavaScript (npm), Python (PyPI), Go, and Rust (crates.io). Instead of simply taking the highest fixed version, it picks the fixed version closest to the current release line, falling back through three tiers:
|
|
395
401
|
|
|
396
402
|
| Tier | Meaning |
|
|
397
403
|
|---|---|
|
|
@@ -399,7 +405,7 @@ Runs the same detection and scan as `scan_project` and recommends an **upgrade v
|
|
|
399
405
|
| `major_internal` | A fixed version within the same major version (a minor upgrade) |
|
|
400
406
|
| `cross_major` | A major upgrade is needed (may include breaking changes) |
|
|
401
407
|
|
|
402
|
-
For npm, Go, and
|
|
408
|
+
For npm, Go, PyPI, and Rust, the "same release line" is the range a caret (`^`) treats as compatible (PyPI has no common compatibility rule, but some 0.x packages make breaking changes in minor releases, so the same rule is used; a change of epoch is also `cross_major`). From 1.0.0 the line is major.minor, as for Maven. For 0.x, only the same `0.minor` is the same line, so a minor update (`0.3` → `0.4`) is `cross_major`, and for 0.0.x every update is `cross_major` (SemVer does not guarantee compatibility for 0.x updates).
|
|
403
409
|
|
|
404
410
|
**Input**: the same as `scan_project` (`project_path`)
|
|
405
411
|
|
|
@@ -438,14 +444,16 @@ For npm, Go, and PyPI, the "same release line" is the range npm's caret (`^`) tr
|
|
|
438
444
|
- Pre-releases (`5.0.0-beta.3`, `15.6.0-canary.61`, Go pseudo-versions, PyPI `rc` and `dev` versions; post-releases count as stable) are recommended only when no stable candidate fixes every vulnerability, and are marked `recommended_is_prerelease: true`. A stable version in a higher tier is preferred over a pre-release in a lower tier.
|
|
439
445
|
- When a version is recommended, `verification` is `verified`, and each CVE's `recommended_status` is `affected` / `not_affected` / `unknown` (`not_evaluated` when the recommendation is withheld). `per_cve_detail.fixed_in` is the candidate for that CVE alone; see `recommended_status` for the final recommendation.
|
|
440
446
|
- CVEs with no fixed version newer than the current one get `tier: "unfixed"` and are left out of the recommendation (the data may be incomplete). They are still evaluated against the recommended version, and the result is shown. If every CVE is unfixed, `recommended_upgrade` is `null` as well. A CVE whose fixed version is listed but cannot be parsed as a version (such as `13.0`, which is not SemVer) gets `tier: "unparseable_fix"`; it is not treated as unfixed but stays in the set to be fixed, so the recommendation is withheld (`no_verified_candidate`).
|
|
447
|
+
- Rust uses SemVer (`SEMVER` ranges) and Cargo caret compatibility: 0.x minor updates and 0.0.x patch updates are `cross_major`. `unmaintained` and `notice` advisories, and `unsound` advisories without a newer fixed version, remain in `per_cve_detail` with `tier: "informational"`, their kind, and `recommended_status`, but are not fix targets or counted as unfixed. An `unsound` advisory with a newer fixed version is a version-specific defect, so it is a fix target like a vulnerability (its tier is computed normally and it keeps `informational: "unsound"`). Packages with no fix target left return `verification: "informational_only"` without an upgrade. Unmaintained crates may need replacement; use `explain_vulnerability` for notices.
|
|
441
448
|
- Suggestions include an `update_hint`, tailored to whether the package is a direct or transitive dependency:
|
|
442
449
|
- Maven (from `pom.xml`): for a direct dependency, update the `<dependency>` version (or the parent POM, property, or BOM that manages it); for a transitive dependency, override the version in `<dependencyManagement>` or update the direct dependency that requires it
|
|
443
450
|
- npm: for a direct dependency, update the version in the `package.json` listed in `declared_in`; for a transitive dependency, update the direct dependency named in `introduced_by`, or set the version with `overrides` in the root `package.json` (effective only in the root project)
|
|
444
451
|
- Go: `go get <module>@<version>` (also for `// indirect` modules). For a module affected by `replace`, update the version in the `replace` directive instead of `require`. Major versions 2 and later use a different module path (`/v2` and so on) and are separate packages in OSV, so fixes in a newer major line are not included as candidates. If the current version is a pseudo-version (an untagged commit), `upgrade_note` says so
|
|
445
452
|
- PyPI: update the version in `requirements.txt`, `pyproject.toml`, or `Pipfile` and regenerate the lockfile; for a transitive dependency, use a pip constraints file (`-c`) or the override settings of uv or Poetry
|
|
453
|
+
- Rust: where version requirements permit it, `cargo update -p <crate>@<current> --precise <recommended>` updates the lockfile. Otherwise change the direct requirement in the declaring Cargo.toml or update the parent dependency in `introduced_by`. Cross-major upgrades may break compatibility, including 0.x updates. Pre-releases may require explicit `=<version>` requirements and must be allowed by transitive parents. Build metadata is omitted from `--precise`. `[patch.crates-io]` changes a source, not the allowed version range; backports must satisfy parent requirements. Check yanked versions, MSRV, and compiler compatibility. Cargo is never executed by the server
|
|
446
454
|
- When the relation is `unknown` or `mixed`, both cases are described (no hint is given for Maven packages from `gradle.lockfile`)
|
|
447
455
|
- **Checking recommended versions against OSV**: Recommendations are verified only against the vulnerabilities found in the scan (those affecting the current version), so a recommended version could be affected by newer vulnerabilities (for example, cryptography 3.2's candidate 49.0.0 is affected by two vulnerabilities introduced in 44.0.0 and fixed in 50.0.0). The server therefore queries `api.osv.dev` about the recommended version, and if it is affected, adds those vulnerabilities to the set to be fixed and chooses again with their fixed versions as candidates (50.0.0 in this example, with the reason in `upgrade_note`). The result is reported in `candidate_check`:
|
|
448
|
-
- `clean`: OSV reports no additional
|
|
456
|
+
- `clean`: OSV reports no additional fix-target vulnerabilities after excluding known `unfixed` advisories, known informational advisories that are not fix targets, and new Rust `unmaintained` / `notice` records. These advisories may still apply; check `per_cve_detail[].recommended_status`. Newly discovered `unsound` records are added as fix targets
|
|
449
457
|
- `has_known_vulnerabilities`: no candidate avoids them, so the recommended version has known vulnerabilities (IDs in `recommended_known_vulnerabilities`)
|
|
450
458
|
- `conflict`: OSV reports that a candidate is affected by a vulnerability the scan had evaluated as not affecting it (the range data disagree), and no other candidate is available, so the recommendation is withheld (`recommended_upgrade: null`, `verification: "no_verified_candidate"`)
|
|
451
459
|
- `failed`: the query failed or returned a malformed response (the recommendation, verified against the scanned vulnerabilities, is still returned)
|
|
@@ -557,7 +565,7 @@ Design notes and open items are in [docs/DESIGN_TODO.md](docs/DESIGN_TODO.md) (i
|
|
|
557
565
|
- [x] Running under Node's permission model
|
|
558
566
|
- [x] English tool descriptions and messages
|
|
559
567
|
- [x] `scan_project` for Rust (`Cargo.lock`, crates.io crates only) and RustSec informational advisories
|
|
560
|
-
- [
|
|
568
|
+
- [x] `suggest_fix` and direct/transitive dependencies for Rust
|
|
561
569
|
- [ ] Direct and transitive dependencies for `gradle.lockfile` and the YAML/TOML lockfiles
|
|
562
570
|
|
|
563
571
|
## License
|
package/dist/index.js
CHANGED
|
@@ -61,7 +61,7 @@ if (startupWarning !== null) {
|
|
|
61
61
|
// NOTE: リリース時はpackage.jsonのversionと同じ値に更新すること
|
|
62
62
|
const server = new McpServer({
|
|
63
63
|
name: "osv-scanner-mcp",
|
|
64
|
-
version: "0.
|
|
64
|
+
version: "0.14.0",
|
|
65
65
|
});
|
|
66
66
|
server.registerTool("scan_project", {
|
|
67
67
|
title: "Scan project dependencies for vulnerabilities (Java / JavaScript / Python / Go / Rust)",
|
|
@@ -70,10 +70,11 @@ server.registerTool("scan_project", {
|
|
|
70
70
|
"Python (poetry.lock / uv.lock / Pipfile.lock / pdm.lock / requirements.txt), Go (go.mod), Rust (Cargo.lock). " +
|
|
71
71
|
"Package managers and builds are never run. " +
|
|
72
72
|
"Always check coverage at the top of the response: it lists manifests without a lockfile, requirements lines without a pinned version, and excluded files. " +
|
|
73
|
+
"Git dependencies in npm lockfiles are looked up by repository URL, so private git host names and paths are sent to api.osv.dev. " +
|
|
73
74
|
"For Cargo.lock, only crates from crates.io are scanned: the project's own crates are counted in local_crates, and git or private-registry crates are listed in unscannable_crates. " +
|
|
74
75
|
"Vulnerabilities marked informational (RustSec notices such as unmaintained or unsound crates) are not counted in vulnerability_count; they are counted in informational_count. " +
|
|
75
76
|
"Each package's dependency_relation tells whether it is a direct or transitive dependency (determined for package-lock.json, go.mod, requirements.txt, and pom.xml; unknown otherwise). " +
|
|
76
|
-
"If coverage.complete is false, do not conclude that the project is safe even with zero findings. Upgrade recommendations (suggest_fix) are available for Java, JavaScript, Python, and
|
|
77
|
+
"If coverage.complete is false, do not conclude that the project is safe even with zero findings. Upgrade recommendations (suggest_fix) are available for Java, JavaScript, Python, Go, and Rust. Cargo.lock dependency graphs provide direct/transitive relations, introduced_by, and uniquely mapped declaring Cargo.toml paths in declared_in.",
|
|
77
78
|
inputSchema: {
|
|
78
79
|
project_path: z
|
|
79
80
|
.string()
|
|
@@ -97,17 +98,18 @@ server.registerTool("scan_java_project", {
|
|
|
97
98
|
server.registerTool("suggest_fix", {
|
|
98
99
|
title: "Recommend upgrades that fix vulnerabilities",
|
|
99
100
|
description: "Scans the project with the same detection as scan_project and recommends an upgrade version for each vulnerable package. " +
|
|
100
|
-
"Recommendations are available for Java (Maven / Gradle), JavaScript (npm), Python (PyPI), and
|
|
101
|
+
"Recommendations are available for Java (Maven / Gradle), JavaScript (npm), Python (PyPI), Go, and Rust (crates.io). " +
|
|
101
102
|
"The fixed version closest to the current release line is chosen with a three-tier fallback " +
|
|
102
103
|
"(same_minor: same release line -> major_internal: same major version -> cross_major: major upgrade), " +
|
|
103
|
-
"and the response gives the recommended version, the upgrade distance (upgrade_tier), and the fixed version per CVE. For npm, Go, and
|
|
104
|
+
"and the response gives the recommended version, the upgrade distance (upgrade_tier), and the fixed version per CVE. For npm, Go, PyPI, and Rust, a minor update within 0.x is also cross_major (may include breaking changes). " +
|
|
104
105
|
"Dependencies scanned at a requirements.txt lower bound (>=) are marked version_is_lower_bound, and the recommendation then means raising the lower bound. " +
|
|
105
|
-
"update_hint explains how to upgrade, depending on whether the package is a direct or transitive dependency (dependency_relation; for npm also introduced_by and declared_in). " +
|
|
106
|
+
"update_hint explains how to upgrade, depending on whether the package is a direct or transitive dependency (dependency_relation; for npm and Cargo.lock also introduced_by and declared_in). Cargo updates are conditional on parent version requirements; cross-major updates may require Cargo.toml changes. " +
|
|
106
107
|
"The recommended version is checked against api.osv.dev, and versions with known vulnerabilities that do not affect the current version are avoided (result in candidate_check). " +
|
|
107
|
-
"clean
|
|
108
|
+
"clean excludes known unfixed advisories, known informational advisories that are not fix targets, and new Rust unmaintained/notice records; these may still apply. New unsound records are fix targets. " +
|
|
108
109
|
"With has_known_vulnerabilities, failed, or skipped, the recommended version has not been confirmed safe; conflict means the data disagreed and the recommendation is withheld. " +
|
|
109
110
|
"Candidates are checked against the affected ranges of every vulnerability to fix, and the recommendation is withheld when the data are insufficient. Pre-releases are recommended only when no stable version fixes everything, and are marked recommended_is_prerelease. " +
|
|
110
111
|
"CVEs with no fixed version newer than the current one are reported separately as unfixed, together with their status at the recommended version. " +
|
|
112
|
+
"Rust unmaintained/notice advisories and unsound advisories without a newer fixed version have tier informational and are not fix targets; unsound advisories with a fixed version are fixed like vulnerabilities. Packages with no fix target left have verification informational_only and no recommendation. " +
|
|
111
113
|
"Always check coverage in the response (if complete is false, some dependencies are not included in the suggestions). " +
|
|
112
114
|
"If dependency_resolution.warning is present, vulnerabilities in transitive dependencies are not included.",
|
|
113
115
|
inputSchema: {
|
|
@@ -21,20 +21,22 @@ import { readResponseBytes } from "../utils/readResponseBytes.js";
|
|
|
21
21
|
* 範囲外の読み込みを防ぐ検証はこの版の挙動に合わせているため、更新時は
|
|
22
22
|
* docs/DESIGN_TODO.md「B5」の監査と、requirements.txt・親POMの解釈の実機確認をやり直すこと。
|
|
23
23
|
* pom.xmlの直接/推移的依存の判定(dependencyRelations.tsのpomRelations)も、この版が同じpom.xmlを
|
|
24
|
-
* source.type "lockfile"(宣言された依存)と"unknown"(deps.devで解決された推移的依存)
|
|
24
|
+
* source.type "lockfile"(宣言された依存)と"unknown"(deps.devで解決された推移的依存)に分けて報告する挙動に依存するため再確認すること。
|
|
25
|
+
* 2.7.0への更新時の確認結果はdocs/DESIGN_TODO.md「osv-scannerのピン留め更新(2.4.0 → 2.7.0)」(2026-10-15)
|
|
25
26
|
*/
|
|
26
|
-
export const PINNED_OSV_SCANNER_VERSION = "2.
|
|
27
|
+
export const PINNED_OSV_SCANNER_VERSION = "2.7.0";
|
|
27
28
|
/**
|
|
28
|
-
* v2.
|
|
29
|
-
*
|
|
29
|
+
* v2.7.0公式リリースのSHA256(2026-10-15取得)。osv-scanner_SHA256SUMSとGitHub APIのリリース資産のdigestの
|
|
30
|
+
* 両方で一致を確認し、darwin_arm64はダウンロードした実体のハッシュとも照合した。
|
|
31
|
+
* https://github.com/google/osv-scanner/releases/tag/v2.7.0
|
|
30
32
|
*/
|
|
31
33
|
const ASSET_CHECKSUMS = {
|
|
32
|
-
"osv-scanner_darwin_amd64": "
|
|
33
|
-
"osv-scanner_darwin_arm64": "
|
|
34
|
-
"osv-scanner_linux_amd64": "
|
|
35
|
-
"osv-scanner_linux_arm64": "
|
|
36
|
-
"osv-scanner_windows_amd64.exe": "
|
|
37
|
-
"osv-scanner_windows_arm64.exe": "
|
|
34
|
+
"osv-scanner_darwin_amd64": "e82c5df7ab0a42dff29a1cd0660711447b56b4c0483ff66ad8634df10fd81a45",
|
|
35
|
+
"osv-scanner_darwin_arm64": "d4926ef8da134332543be276dff157e4c170f99ad91fc48e89448a8816bdf70b",
|
|
36
|
+
"osv-scanner_linux_amd64": "160c6253e2922e9e0a6a9771c5c4c40a4b3c6090b34a3cdd5e80c96cc2e4218b",
|
|
37
|
+
"osv-scanner_linux_arm64": "4061ef4c021ed3c47f203b818f337e351336cc5cd67c0274fc36bd1dfc728dd7",
|
|
38
|
+
"osv-scanner_windows_amd64.exe": "9964ed64ec9a387391eb4cc006ad5c531154821aef29b793e02c448de4686438",
|
|
39
|
+
"osv-scanner_windows_arm64.exe": "3e630381c1f1fd3c9c4d447e6ff2ef6c737083371722773db8337eead0f97962",
|
|
38
40
|
};
|
|
39
41
|
const DOWNLOAD_TIMEOUT_MS = 300_000;
|
|
40
42
|
/** バイナリは実測50〜55MB。余裕を持たせつつ暴走は防ぐ */
|
|
@@ -15,7 +15,7 @@ import { asString, asStrings } from "../utils/unknownJson.js";
|
|
|
15
15
|
import { sanitizeExternalText } from "../utils/externalText.js";
|
|
16
16
|
import { extractAffectedVersions } from "./affectedVersions.js";
|
|
17
17
|
import { queryOsvPackageVersion } from "./osvApi.js";
|
|
18
|
-
import { extractFixedVersions } from "./scanReport.js";
|
|
18
|
+
import { extractFixedVersions, recordInformationalKind } from "./scanReport.js";
|
|
19
19
|
import { suggestUpgradeForPackage } from "./suggestFix.js";
|
|
20
20
|
export const NO_CANDIDATE_CHECK_ENV = "OSV_MCP_NO_CANDIDATE_CHECK";
|
|
21
21
|
/** 環境変数で無効化されているか(1/true/yes) */
|
|
@@ -43,7 +43,7 @@ class QueryBudget {
|
|
|
43
43
|
}
|
|
44
44
|
async function checkPackage(pkg, initial, budget, options) {
|
|
45
45
|
const known = new Set(pkg.vulnerabilities.flatMap((v) => [v.id, ...v.aliases]));
|
|
46
|
-
const unfixed = new Set(initial.per_cve_detail.filter((v) => v.tier === "unfixed").map((v) => v.id));
|
|
46
|
+
const unfixed = new Set(initial.per_cve_detail.filter((v) => v.tier === "unfixed" || v.tier === "informational").map((v) => v.id));
|
|
47
47
|
const ignoredKnown = new Set(pkg.vulnerabilities.filter((v) => unfixed.has(v.id)).flatMap((v) => [v.id, ...v.aliases]));
|
|
48
48
|
// 別名が修正対象とも重なる場合は、その対象の判定を優先する。
|
|
49
49
|
for (const vuln of pkg.vulnerabilities) {
|
|
@@ -74,7 +74,10 @@ async function checkPackage(pkg, initial, budget, options) {
|
|
|
74
74
|
// 新しく照会で見つかった対象はknownへ追加されるが、ignoredKnownには入れない。
|
|
75
75
|
records = records.filter((record) => {
|
|
76
76
|
const matched = recordIds(record).filter((id) => known.has(id));
|
|
77
|
-
|
|
77
|
+
if (matched.length > 0)
|
|
78
|
+
return matched.some((id) => !ignoredKnown.has(id));
|
|
79
|
+
const kind = pkg.ecosystem === "crates.io" ? recordInformationalKind(record, pkg.name, pkg.ecosystem) : null;
|
|
80
|
+
return kind !== "unmaintained" && kind !== "notice";
|
|
78
81
|
});
|
|
79
82
|
if (records.length === 0)
|
|
80
83
|
return { suggestion, status: "clean", rejected };
|
package/dist/osv/runner.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - タイムアウトと出力サイズ上限を設ける(ハング・巨大出力によるDoS対策)
|
|
13
13
|
* - サーバーの終了時に実行中のプロセスを残さない(processCleanup.tsに登録し、終了時にSIGKILL)
|
|
14
14
|
*
|
|
15
|
-
* 終了コード(2.4.0で実機確認):
|
|
15
|
+
* 終了コード(2.4.0・2.7.0で実機確認):
|
|
16
16
|
* 0 = スキャン成功・脆弱性なし / 1 = スキャン成功・脆弱性あり / 128 = 対象パッケージなし
|
|
17
17
|
*/
|
|
18
18
|
import { spawn } from "node:child_process";
|
|
@@ -56,7 +56,7 @@ export function isRemoteResolutionDisabled(options = {}) {
|
|
|
56
56
|
/**
|
|
57
57
|
* OSV-Scannerに渡す固定引数。ここに無いオプションは一切使わない(ホワイトリスト)
|
|
58
58
|
*
|
|
59
|
-
* pom.xml/requirements.txtの推移的依存はapi.deps.devで解決される(2.4.0で実機確認)。
|
|
59
|
+
* pom.xml/requirements.txtの推移的依存はapi.deps.devで解決される(2.4.0・2.7.0で実機確認)。
|
|
60
60
|
* `--data-source native` はスキャン対象pom.xmlの<repositories>に書かれた任意のURLへ
|
|
61
61
|
* 接続するため使わない。既定値の変更に備えてdeps.devを明示する。
|
|
62
62
|
*/
|
|
@@ -71,6 +71,10 @@ export function buildProjectTargetArgs(targets) {
|
|
|
71
71
|
return ["--lockfile", `${target.format}:${target.path}`];
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* `--experimental-`のフラグは2.7.0で非推奨(stderrに警告。新名は`--x-`)だが、新名は2.4.0では未定義でexit 127になる。
|
|
76
|
+
* OSV_SCANNER_PATH・PATH上の古い版でも動くよう、両方の版で使える旧名を使う(2026-10-15 実機確認)
|
|
77
|
+
*/
|
|
74
78
|
const FIXED_ARTIFACT_ARGS = [
|
|
75
79
|
"scan", "source", "--format", "json", "--all-packages", "--no-ignore",
|
|
76
80
|
"--experimental-no-default-plugins", "--experimental-plugins", "java/archive",
|
package/dist/osv/suggestFix.js
CHANGED
|
@@ -79,8 +79,54 @@ function updateHint(pkg) {
|
|
|
79
79
|
return UPDATE_HINTS[pkg.ecosystem];
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
/** OSV由来の版・crate名を案内のコマンドに埋め込めるか(外部データのため文字の範囲を限定する) */
|
|
83
|
+
const SAFE_VERSION = /^[0-9A-Za-z.+-]{1,64}$/;
|
|
84
|
+
const SAFE_CRATE_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
85
|
+
/**
|
|
86
|
+
* crates.ioの更新方法(docs/DESIGN_TODO.md「suggest_fixのRust対応」の表)。直接/推移的依存とTierで分ける。
|
|
87
|
+
* 推奨がない場合はコマンドを示さず、方向だけを示す
|
|
88
|
+
*/
|
|
89
|
+
function cargoUpdateHint(pkg, recommended, tier) {
|
|
90
|
+
const manifest = pkg.declared_in?.join(", ") ?? (pkg.declaring_crates?.length
|
|
91
|
+
? `the Cargo.toml of ${pkg.declaring_crates.join(", ")}` : "the declaring Cargo.toml");
|
|
92
|
+
const parent = pkg.introduced_by?.join(", ") ?? "the direct dependency that requires this crate";
|
|
93
|
+
if (recommended === null) {
|
|
94
|
+
const direct = `No verified upgrade is recommended (see upgrade_note and per_cve_detail). Any fix requires a version allowed by the requirement in ${manifest}.`;
|
|
95
|
+
const transitive = `No verified upgrade is recommended (see upgrade_note and per_cve_detail). Any fix must be allowed by the requirements of ${parent}.`;
|
|
96
|
+
return pkg.dependency_relation === "direct" ? `Direct dependency. ${direct}`
|
|
97
|
+
: pkg.dependency_relation === "transitive" ? `Transitive dependency. ${transitive}`
|
|
98
|
+
: `For a direct dependency: ${direct} For a transitive dependency: ${transitive}`;
|
|
99
|
+
}
|
|
100
|
+
const embeddable = SAFE_VERSION.test(recommended) && SAFE_VERSION.test(pkg.version) && SAFE_CRATE_NAME.test(pkg.name);
|
|
101
|
+
// OSVの修正版はビルドメタデータを含むことがある。cargoは`--precise`・`=`の要求でメタデータなしの版を受け付ける(cargo 1.98で確認)
|
|
102
|
+
const precise = recommended.split("+")[0];
|
|
103
|
+
const target = embeddable ? recommended : "the recommended version";
|
|
104
|
+
const command = embeddable
|
|
105
|
+
? `cargo update -p ${pkg.name}@${pkg.version} --precise ${precise}`
|
|
106
|
+
: "cargo update with an appropriate -p package specification and --precise version";
|
|
107
|
+
const direct = tier === "cross_major"
|
|
108
|
+
? `Change the requirement in ${manifest} to allow ${target}; this may include breaking changes (in 0.x, even a minor update).`
|
|
109
|
+
: `If the requirement in ${manifest} allows ${target} (the default caret requirement does), run ${command}; only Cargo.lock changes. ` +
|
|
110
|
+
`If the requirement pins the version (= or ~), change it in ${manifest}.`;
|
|
111
|
+
const transitive = tier === "cross_major"
|
|
112
|
+
? `Update ${parent} to a version whose requirements permit ${target}. ` +
|
|
113
|
+
"[patch.crates-io] does not override version requirements: use it only for a fix (such as a backport) from another source that still satisfies the requirements of the crates that depend on it."
|
|
114
|
+
: `If the requirements of ${parent} allow ${target}, run ${command}; only Cargo.lock changes. If cargo reports that the requirements cannot be satisfied, update ${parent}.`;
|
|
115
|
+
let hint = pkg.dependency_relation === "direct" ? `Direct dependency. ${direct}`
|
|
116
|
+
: pkg.dependency_relation === "transitive" ? `Transitive dependency. ${transitive}`
|
|
117
|
+
: `For a direct dependency: ${direct} For a transitive dependency: ${transitive}`;
|
|
118
|
+
if (pkg.dependency_relation === "direct" && pkg.introduced_by?.length)
|
|
119
|
+
hint += ` It is also required by ${parent}, whose requirements must allow ${target} as well.`;
|
|
120
|
+
if (versionSchemeFor("crates.io").isPrerelease(recommended)) {
|
|
121
|
+
hint += ` ${target} is a pre-release: Cargo requirements match a pre-release only when they name one, so cargo update --precise alone may fail. ` +
|
|
122
|
+
(embeddable ? `For a direct dependency, set the requirement to "=${precise}"` : "For a direct dependency, set the requirement to the exact pre-release version") +
|
|
123
|
+
"; for a transitive dependency, the crate that requires it must allow it.";
|
|
124
|
+
}
|
|
125
|
+
hint += " Before updating, check that the version is not yanked and that it supports your Rust version (MSRV).";
|
|
126
|
+
return hint;
|
|
127
|
+
}
|
|
128
|
+
function hintFields(pkg, recommended = null, tier = null) {
|
|
129
|
+
const hint = pkg.ecosystem === "crates.io" ? cargoUpdateHint(pkg, recommended, tier) : updateHint(pkg);
|
|
84
130
|
return hint === undefined ? {} : { update_hint: hint };
|
|
85
131
|
}
|
|
86
132
|
/** スキャン結果に付いた直接/推移的依存の項目を提案にも写す(無ければ何も出さない) */
|
|
@@ -90,6 +136,8 @@ function relationFields(pkg) {
|
|
|
90
136
|
...(pkg.introduced_by !== undefined ? { introduced_by: pkg.introduced_by } : {}),
|
|
91
137
|
...(pkg.introduced_by_omitted !== undefined ? { introduced_by_omitted: pkg.introduced_by_omitted } : {}),
|
|
92
138
|
...(pkg.declared_in !== undefined ? { declared_in: pkg.declared_in } : {}),
|
|
139
|
+
...(pkg.declaring_crates !== undefined ? { declaring_crates: pkg.declaring_crates } : {}),
|
|
140
|
+
...(pkg.declaring_crates_omitted !== undefined ? { declaring_crates_omitted: pkg.declaring_crates_omitted } : {}),
|
|
93
141
|
...(pkg.replaced_in_go_mod ? { replaced_in_go_mod: true } : {}),
|
|
94
142
|
};
|
|
95
143
|
}
|
|
@@ -115,7 +163,8 @@ function notEvaluatedSuggestion(pkg, verification) {
|
|
|
115
163
|
cve: vuln.cve,
|
|
116
164
|
severity: vuln.severity,
|
|
117
165
|
fixed_in: null,
|
|
118
|
-
tier: "unsupported",
|
|
166
|
+
tier: vuln.informational !== undefined ? "informational" : "unsupported",
|
|
167
|
+
...(vuln.informational !== undefined ? { informational: vuln.informational, recommended_status: "not_evaluated" } : {}),
|
|
119
168
|
})),
|
|
120
169
|
verification,
|
|
121
170
|
};
|
|
@@ -172,6 +221,36 @@ function buildNote(scheme, pkg, recommended, tier, fixableCount, unfixedCount) {
|
|
|
172
221
|
}
|
|
173
222
|
return note;
|
|
174
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* 1パッケージ分のアップグレード提案を組み立てる(同期・純粋)。
|
|
226
|
+
* contextを渡すと、照会で見つかった脆弱性も避けて候補を選び直す(contextなしの結果が従来の推奨)。
|
|
227
|
+
*/
|
|
228
|
+
/**
|
|
229
|
+
* 修正対象にしない情報提供のアドバイザリ。unmaintained・noticeは版を上げても解消しない(版と無関係)。
|
|
230
|
+
* unsoundは版ごとの欠陥のため、現在より新しい修正版があれば通常の脆弱性と同じく修正対象にする
|
|
231
|
+
* (v0.13.0の実装中に判明: 修正版のあるunsoundを外すと、lru 0.6.0に0.7.1(RUSTSEC-2026-0253の0.18.2未満)を推奨した)
|
|
232
|
+
*/
|
|
233
|
+
function isExcludedInformational(vuln, scheme, current) {
|
|
234
|
+
if (vuln.informational === undefined)
|
|
235
|
+
return false;
|
|
236
|
+
if (vuln.informational !== "unsound")
|
|
237
|
+
return true;
|
|
238
|
+
return rankCandidates(scheme, current, vuln.fixed_versions).length === 0 && vuln.fixed_versions.every((v) => scheme.isValid(v));
|
|
239
|
+
}
|
|
240
|
+
function informationalNote(pkg, excluded) {
|
|
241
|
+
const notes = [];
|
|
242
|
+
const kinds = (pick) => new Set(pkg.vulnerabilities.filter((v, i) => v.informational !== undefined && pick(excluded[i])).map((v) => v.informational));
|
|
243
|
+
const out = kinds((e) => e);
|
|
244
|
+
if (out.has("unmaintained"))
|
|
245
|
+
notes.push("Unmaintained advisories are not resolved by upgrading; consider an alternative crate.");
|
|
246
|
+
if (out.has("unsound"))
|
|
247
|
+
notes.push("Unsound advisories without a fixed version remain; check recommended_status.");
|
|
248
|
+
if (out.has("notice"))
|
|
249
|
+
notes.push("For notices, use explain_vulnerability for details.");
|
|
250
|
+
if (kinds((e) => !e).has("unsound"))
|
|
251
|
+
notes.push("Unsound advisories with a fixed version are included in the fix.");
|
|
252
|
+
return notes.join(" ");
|
|
253
|
+
}
|
|
175
254
|
/**
|
|
176
255
|
* 1パッケージ分のアップグレード提案を組み立てる(同期・純粋)。
|
|
177
256
|
* contextを渡すと、照会で見つかった脆弱性も避けて候補を選び直す(contextなしの結果が従来の推奨)。
|
|
@@ -182,7 +261,23 @@ export function suggestUpgradeForPackage(pkg, context = {}) {
|
|
|
182
261
|
return notEvaluatedSuggestion(pkg, "unsupported_ecosystem");
|
|
183
262
|
if (!scheme.isValid(pkg.version))
|
|
184
263
|
return { ...notEvaluatedSuggestion(pkg, "unparseable_version"), ...hintFields(pkg) };
|
|
185
|
-
const
|
|
264
|
+
const excluded = pkg.vulnerabilities.map((vuln) => isExcludedInformational(vuln, scheme, pkg.version));
|
|
265
|
+
const informationalNotes = informationalNote(pkg, excluded);
|
|
266
|
+
if (pkg.vulnerabilities.length > 0 && excluded.every((e) => e)) {
|
|
267
|
+
return {
|
|
268
|
+
package: pkg.name, current_version: pkg.version, ecosystem: pkg.ecosystem, ...relationFields(pkg),
|
|
269
|
+
recommended_upgrade: null, upgrade_tier: null, upgrade_note: informationalNotes,
|
|
270
|
+
per_cve_detail: pkg.vulnerabilities.map((v) => ({ id: v.id, cve: v.cve, severity: v.severity,
|
|
271
|
+
fixed_in: null, tier: "informational", informational: v.informational, recommended_status: "not_evaluated" })),
|
|
272
|
+
verification: "informational_only",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const details = pkg.vulnerabilities.map((vuln, i) => {
|
|
276
|
+
if (excluded[i])
|
|
277
|
+
return {
|
|
278
|
+
id: vuln.id, cve: vuln.cve, severity: vuln.severity, fixed_in: null,
|
|
279
|
+
tier: "informational", informational: vuln.informational,
|
|
280
|
+
};
|
|
186
281
|
const fix = rankCandidates(scheme, pkg.version, vuln.fixed_versions)[0];
|
|
187
282
|
return {
|
|
188
283
|
id: vuln.id,
|
|
@@ -191,14 +286,16 @@ export function suggestUpgradeForPackage(pkg, context = {}) {
|
|
|
191
286
|
fixed_in: fix ?? null,
|
|
192
287
|
tier: fix !== undefined ? scheme.classify(pkg.version, fix)
|
|
193
288
|
: vuln.fixed_versions.some((v) => !scheme.isValid(v)) ? "unparseable_fix" : "unfixed",
|
|
289
|
+
// 修正対象にした情報提供(修正版のあるunsound)も種類を示す
|
|
290
|
+
...(vuln.informational !== undefined ? { informational: vuln.informational } : {}),
|
|
194
291
|
};
|
|
195
292
|
});
|
|
196
293
|
const unfixedCount = details.filter((d) => d.tier === "unfixed").length;
|
|
197
294
|
const unparseableCount = details.filter((d) => d.tier === "unparseable_fix").length;
|
|
198
|
-
const fixableCount = details.
|
|
295
|
+
const fixableCount = details.filter((d) => d.tier !== "unfixed" && d.tier !== "informational").length;
|
|
199
296
|
// 解釈できない修正版のCVEも修正対象に残す。その影響範囲は情報不足のため、どの候補も検証できず推奨を保留する
|
|
200
297
|
const targets = [
|
|
201
|
-
...pkg.vulnerabilities.filter((_, i) => details[i].tier !== "unfixed"),
|
|
298
|
+
...pkg.vulnerabilities.filter((_, i) => details[i].tier !== "unfixed" && details[i].tier !== "informational"),
|
|
202
299
|
...(context.extraTargets ?? []),
|
|
203
300
|
];
|
|
204
301
|
const candidates = rankCandidates(scheme, pkg.version, targets.flatMap((v) => v.fixed_versions));
|
|
@@ -217,11 +314,11 @@ export function suggestUpgradeForPackage(pkg, context = {}) {
|
|
|
217
314
|
recommended_upgrade: recommended,
|
|
218
315
|
upgrade_tier: upgradeTier,
|
|
219
316
|
...(recommended !== null && scheme.isPrerelease(recommended) ? { recommended_is_prerelease: true } : {}),
|
|
220
|
-
upgrade_note: recommended === null && fixableCount > 0
|
|
317
|
+
upgrade_note: (recommended === null && fixableCount > 0
|
|
221
318
|
? "No known fixed version could be confirmed to be outside the affected ranges of every CVE to fix. The recommendation is also withheld when the data are insufficient or use unsupported range types." +
|
|
222
319
|
(unparseableCount > 0 ? ` ${unparseableCount} CVEs list fixed versions that cannot be parsed as versions (tier: unparseable_fix), so the recommendation is withheld.` : "")
|
|
223
|
-
: buildNote(scheme, pkg, recommended, upgradeTier, fixableCount, unfixedCount),
|
|
224
|
-
...hintFields(pkg),
|
|
320
|
+
: buildNote(scheme, pkg, recommended, upgradeTier, fixableCount, unfixedCount)) + (informationalNotes ? ` ${informationalNotes}` : ""),
|
|
321
|
+
...hintFields(pkg, recommended, upgradeTier),
|
|
225
322
|
...(pkg.version_is_lower_bound ? { version_is_lower_bound: true } : {}),
|
|
226
323
|
per_cve_detail: details,
|
|
227
324
|
verification: recommended === null ? "no_verified_candidate" : "verified",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - Maven: Mavenの優先順位(mavenVersion.ts)。範囲はECOSYSTEM型
|
|
5
5
|
* - npm・Go: SemVer 2.0.0の優先順位(semverVersion.ts)。範囲はSEMVER型とECOSYSTEM型
|
|
6
|
+
* - crates.io: SemVerの優先順位とCargoキャレット互換。範囲はSEMVER型
|
|
6
7
|
* - PyPI: PEP 440の優先順位(pep440Version.ts)。範囲はECOSYSTEM型。名前はPEP 503で正規化して照合する
|
|
7
8
|
* - その他: 比較器なし。範囲はECOSYSTEM型
|
|
8
9
|
*
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
import { compareMavenVersions, mavenVersionSeries } from "../utils/mavenVersion.js";
|
|
12
13
|
import { compareParsedPep440, parsePep440 } from "../utils/pep440Version.js";
|
|
13
14
|
import { compareParsedSemver, parseSemver } from "../utils/semverVersion.js";
|
|
14
|
-
const SEMVER_ECOSYSTEMS = new Set(["npm", "Go"]);
|
|
15
|
+
const SEMVER_ECOSYSTEMS = new Set(["npm", "Go", "crates.io"]);
|
|
15
16
|
const MAVEN_SCHEME = {
|
|
16
17
|
isValid: () => true,
|
|
17
18
|
compare: compareMavenVersions,
|
|
@@ -126,6 +127,8 @@ export function samePackageName(a, b, ecosystem) {
|
|
|
126
127
|
export function versionRangeTypes(ecosystem) {
|
|
127
128
|
if (ecosystem === "Maven")
|
|
128
129
|
return new Set(["ECOSYSTEM"]);
|
|
130
|
+
if (ecosystem === "crates.io")
|
|
131
|
+
return new Set(["SEMVER"]);
|
|
129
132
|
if (SEMVER_ECOSYSTEMS.has(ecosystem))
|
|
130
133
|
return new Set(["SEMVER", "ECOSYSTEM"]);
|
|
131
134
|
return new Set(["ECOSYSTEM"]);
|