osv-scanner-mcp 0.15.0 → 0.17.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 +45 -12
- package/dist/history/historyRecord.js +4 -1
- package/dist/index.js +11 -5
- package/dist/osv/binaryManager.js +2 -2
- package/dist/osv/suggestFix.js +1 -0
- package/dist/osv/versionScheme.js +19 -0
- package/dist/tools/scanProject.js +63 -9
- package/dist/utils/composerLock.js +40 -0
- package/dist/utils/composerVersion.js +36 -0
- package/dist/utils/manifestDetector.js +80 -8
- package/dist/utils/manifestFormats.js +3 -0
- package/dist/utils/nugetFiles.js +548 -0
- package/dist/utils/nugetProject.js +328 -0
- package/dist/utils/openedFilePath.js +65 -0
- package/dist/utils/permissionCheck.js +7 -1
- package/dist/utils/pomParent.js +20 -11
- package/dist/utils/projectWalk.js +5 -2
- package/dist/utils/safeRead.js +18 -8
- package/dist/utils/sbomInput.js +8 -7
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -7,22 +7,36 @@
|
|
|
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,
|
|
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, Rust (`Cargo.lock`), and .NET (NuGet: `packages.lock.json`, `obj/project.assets.json`, `packages.config`, `*.deps.json`, and project files), and PHP (Composer: `composer.lock`), with upgrade recommendations for Maven, npm, PyPI, Go, crates.io, and Packagist. 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
|
-
- **
|
|
15
|
-
- **
|
|
14
|
+
- **PHP / Composer (source checkout)**: `scan_project` scans `composer.lock`, including both `packages` and `packages-dev`, as Packagist packages. `suggest_fix` recommends release versions and checks candidates against OSV. Composer and PHP code are never executed. See [Composer support](#composer-support).
|
|
15
|
+
- **One-shot multi-language scan**: Pass a project path to `scan_project`, and it detects and scans the Java, JavaScript, Python, Go, Rust, .NET, and PHP 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`
|
|
16
|
+
- **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, Rust (crates.io), and PHP (Packagist). Known unfixed and informational advisories remain visible. The recommended version is also checked against the OSV database for additional fix-target vulnerabilities
|
|
16
17
|
- **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
|
-
-
|
|
18
|
+
- **.NET without guessing versions**: Each project file is matched to its own lockfile or `obj/project.assets.json`. Without one, only its direct `PackageReference` items are scanned, and a `Version="x"` (which means x or higher) is reported as a lower bound. References whose version cannot be known without running MSBuild (properties, conditions, floating versions, `Update` / `Remove`) are listed instead of being looked up: OSV treats an unparsable version as older than every release and would report every vulnerability for it
|
|
19
|
+
- **Direct and transitive dependencies**: Packages are marked as direct or transitive dependencies (from `package-lock.json`, `go.mod`, `requirements.txt`, `pom.xml`, `Cargo.lock`, and the .NET files), and update hints are tailored accordingly, for example by naming the direct dependency that pulls in a transitive one
|
|
18
20
|
- **Java project scan**: `scan_java_project` scans only the Java (Maven / Gradle) manifests and returns a formatted report
|
|
19
21
|
- **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
22
|
- **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
23
|
- **Scan history (opt-in)**: With `OSV_MCP_HISTORY=1`, each scan is recorded locally, and `scan_history` lists past runs and shows what changed since the previous run (new and resolved vulnerabilities, version changes, coverage changes)
|
|
22
24
|
- **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
|
|
23
|
-
- **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,
|
|
25
|
+
- **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, PEP 440 for PyPI, and Composer ordering for Packagist
|
|
24
26
|
- **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
|
|
25
27
|
|
|
28
|
+
## Composer support
|
|
29
|
+
|
|
30
|
+
Pass a project directory, `composer.lock`, or `composer.json` to `scan_project` or `suggest_fix`. The lockfile records both direct and transitive dependencies; their relationship is currently reported as `unknown`. A `composer.json` without a lockfile in the same directory is reported as missing/unconfirmed in `coverage.lockfile_missing` (a project with no scannable files returns `no_manifest_found`). An ancestor's lockfile does not establish coverage for an independent child project. `vendor` directories are not traversed.
|
|
31
|
+
|
|
32
|
+
The server reads each lockfile safely and scans a private sanitized copy containing only package names and release versions. Repository URLs, dist/source commit references, scripts, and credentials are not passed to OSV-Scanner. Both production and development packages are included. JSON is limited to 16 MiB per lockfile and 64 MiB per scan, with structural work limits. Invalid JSON is excluded; invalid entries and unsupported versions are omitted and reported in `skipped_files`, making `coverage.complete` false.
|
|
33
|
+
|
|
34
|
+
Composer version comparison supports classical numeric releases with one to four components, an optional `v` prefix, and alpha/beta/RC/stable/patch suffixes (including their short forms). Branch/dev versions, aliases, and other formats such as date-based tags are not evaluated. An advisory with unsupported version boundaries cannot establish a verified fix. Upgrade tiers follow the existing cautious 0.x rules; they do not guarantee compatibility with your constraints or PHP runtime. Update the applicable constraints in `composer.json` and regenerate the lockfile in a trusted environment; check PHP and extension requirements first. The server does not run Composer or change project files.
|
|
35
|
+
|
|
36
|
+
Packages with `dist.type: "path"` are local code: they are excluded before building scanner input, so neither their names nor versions are sent to OSV. They are your own code, so they are counted in `coverage.local_composer_packages` (like `local_crates` and `local_projects`) and do not make `coverage.complete` false; their own dependencies are listed in `composer.lock` and scanned. This applies to both production and development dependencies, even when a local package uses the same name and release version as a public package.
|
|
37
|
+
|
|
38
|
+
**Privacy:** other private Composer package names and versions are also sent to `api.osv.dev`. A lockfile alone cannot establish whether a non-path package comes from public Packagist; same-name private packages and VCS forks can produce false positives. `coverage.composer_warnings` warns when scanned entries lack the standard `notification-url` (`https://packagist.org/downloads/`); this is a heuristic, not proof of origin. `coverage.complete` describes inspected dependency entries, not the lockfile's freshness or its match with `composer.json`, and does not verify the PHP runtime or extensions.
|
|
39
|
+
|
|
26
40
|
## Requirements
|
|
27
41
|
|
|
28
42
|
- Node.js >= 20.19 (development: Node.js 26.12.1, as specified in `.node-version`)
|
|
@@ -162,6 +176,7 @@ As an additional layer of defense, you can limit what the server can read and wr
|
|
|
162
176
|
"--allow-fs-write=/private/var/folders/xx/yyyy/T",
|
|
163
177
|
"--allow-fs-read=/opt/homebrew/bin/osv-scanner",
|
|
164
178
|
"--allow-child-process",
|
|
179
|
+
"--allow-addons",
|
|
165
180
|
"/path/to/OSV-Scanner-MCP/dist/index.js"
|
|
166
181
|
],
|
|
167
182
|
"env": {
|
|
@@ -178,6 +193,7 @@ As an additional layer of defense, you can limit what the server can read and wr
|
|
|
178
193
|
- If you use the automatic download, also allow reading and writing the cache (`$XDG_CACHE_HOME/osv-scanner-mcp`, by default `~/.cache/osv-scanner-mcp`). Otherwise, set `OSV_SCANNER_PATH`
|
|
179
194
|
- If you enable the scan history (`OSV_MCP_HISTORY=1`), also allow reading and writing the history directory (`OSV_MCP_HISTORY_DIR`, or the default under `~/.local/state`). The server warns at startup if it is not allowed
|
|
180
195
|
- `--allow-child-process` is required to run osv-scanner. **osv-scanner runs as a child process and is not restricted by the permission model** (Node itself warns that this flag weakens the permission model). The impact is limited because osv-scanner only receives verified private copies, but use an OS-level mechanism such as a container if you need stronger isolation
|
|
196
|
+
- On macOS and Windows, `--allow-addons` is also required for the trusted Koffi native binding used to verify the path of an open file handle. Native code is outside Node's permission sandbox. On Linux, use `--allow-fs-read=/proc/self/fd` instead; `/proc` must be mounted. If the OS cannot verify the opened object, boundary-protected reads fail closed. Keep OS-level isolation when scanning in a stronger sandbox.
|
|
181
197
|
- If a required permission is missing, the server prints a warning to stderr at startup, and scans return `permission_denied` (naming the missing permission and the path)
|
|
182
198
|
|
|
183
199
|
### Network destinations and privacy
|
|
@@ -188,7 +204,7 @@ Destinations confirmed with osv-scanner v2.4.0 (2026-10-07) and v2.7.0, the pinn
|
|
|
188
204
|
|---|---|---|
|
|
189
205
|
| 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 |
|
|
190
206
|
| 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 |
|
|
191
|
-
| 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) |
|
|
207
|
+
| 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). **For .NET, packages from private NuGet feeds are sent as well**: lockfiles do not record which feed a package came from, so they cannot be told apart (a `nuget.config` in the scanned directory that lists other sources adds a warning to `coverage.nuget_warnings`). The names of your own projects (`type: project` and so on) are not sent. **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) |
|
|
192
208
|
| `scan_java_artifact` / `scan_sbom` | `api.osv.dev` | Names and versions of the identified packages |
|
|
193
209
|
| `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` |
|
|
194
210
|
| `explain_vulnerability` | `api.osv.dev` | The vulnerability ID |
|
|
@@ -203,7 +219,7 @@ Both api.osv.dev and deps.dev are operated by Google. **In every configuration,
|
|
|
203
219
|
|
|
204
220
|
### `scan_project`
|
|
205
221
|
|
|
206
|
-
Detects the lockfiles and manifests in a project and scans the Java / JavaScript / Python / Go / Rust dependencies together. Package managers and builds are never run.
|
|
222
|
+
Detects the lockfiles and manifests in a project and scans the Java / JavaScript / Python / Go / Rust / .NET / PHP dependencies together. Package managers and builds are never run.
|
|
207
223
|
|
|
208
224
|
**Input**
|
|
209
225
|
|
|
@@ -220,13 +236,25 @@ Detects the lockfiles and manifests in a project and scans the Java / JavaScript
|
|
|
220
236
|
| Python (PyPI) | `poetry.lock`, `uv.lock`, `Pipfile.lock`, `pdm.lock`, `requirements.txt` (and variants such as `requirements-dev.txt`) |
|
|
221
237
|
| Go | `go.mod` |
|
|
222
238
|
| Rust (crates.io) | `Cargo.lock` (lockfile versions 1 to 4) |
|
|
239
|
+
| .NET (NuGet) | `packages.lock.json` / `packages.<project>.lock.json`, `obj/project.assets.json`, `packages.config` / `packages.<project>.config`, `*.deps.json`, and `*.csproj` / `*.fsproj` / `*.vbproj` (direct references only, when no lockfile matches) |
|
|
240
|
+
| PHP (Packagist) | `composer.lock` (`packages` and `packages-dev`; see [Composer support](#composer-support)) |
|
|
223
241
|
|
|
224
|
-
`.git`, `node_modules`, `target`, `build`, `.idea`, `.vscode`, `.venv`, `venv`, `site-packages`, `__pycache__`, `.tox`, `vendor`, `.cargo`, and symbolic links are not searched. Only the detected files are passed to OSV-Scanner, each with its format given explicitly (`coverage.manifests` in the response is exactly the scan scope). The search limits are the same as for `scan_java_project`.
|
|
242
|
+
`.git`, `node_modules`, `target`, `build`, `.idea`, `.vscode`, `.venv`, `venv`, `site-packages`, `__pycache__`, `.tox`, `vendor`, `.cargo`, `bin` and `obj` in a directory that contains a .NET project file (build output of that project), and symbolic links are not searched. Only the detected files are passed to OSV-Scanner, each with its format given explicitly (`coverage.manifests` in the response is exactly the scan scope). The search limits are the same as for `scan_java_project`.
|
|
225
243
|
|
|
226
244
|
**`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.
|
|
227
245
|
|
|
228
246
|
**`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.
|
|
229
247
|
|
|
248
|
+
**.NET files are not passed to OSV-Scanner as they are either.** OSV-Scanner (v2.7.0) looks the project's own projects in `*.deps.json` up as NuGet packages, and passes `.csproj` versions such as `$(Version)` or `[1.0,2.0)` to OSV as they are; OSV treats a version it cannot parse as older than every release, so every known vulnerability of the package is reported (confirmed 2026-10-23). The server parses every .NET file and scans a private copy in `packages.lock.json` format that lists only the packages it can look up. Neither `dotnet restore` nor MSBuild is run, and project files are never evaluated.
|
|
249
|
+
|
|
250
|
+
- **Matching per project file.** For each `*.csproj` / `*.fsproj` / `*.vbproj`, the first source confirmed to belong to it is used: `packages.<project>.lock.json`; `packages.lock.json` only when the directory has a single project file; neither, when the project sets `NuGetLockFilePath`; `packages.<project>.config` or `packages.config` under the same rule; then `obj/project.assets.json`, only when its `project.restore.projectPath` names this project file (projects in one directory share `obj`). A lockfile that cannot be matched to a project is still scanned, but no project counts as covered by it
|
|
251
|
+
- **Projects without a matching source** (including projects with no `PackageReference` of their own, since `Directory.Build.props` or the SDK can add references) are scanned from their direct `PackageReference` items only, and listed in `lockfile_missing` with `status: "direct_only"` (or `"unconfirmed"` when a lockfile exists but cannot be matched). Transitive dependencies are not scanned, so `complete` is `false`. Enable `RestorePackagesWithLockFile` to scan them
|
|
252
|
+
- **`PackageReference` versions.** `[x]` is looked up as x. `x`, `[x,)`, and `[x,y)` mean x or higher (NuGet prefers the lowest available version, which may be above x), so they are looked up at x and the packages are marked `version_is_lower_bound: true`. A lower bound that is excluded or missing, `$(...)`, `*`, and a missing version (central package management) are not looked up
|
|
253
|
+
- **References that might not apply are not looked up**: a `Condition` on the reference, its `ItemGroup`, or its `Version`; references inside `Choose` or `Target`; names that also have `Update` or `Remove` in the file, or in a `Directory.Build.props` / `Directory.Build.targets` in the scanned directory; names included more than once; and `VersionOverride`. They are listed in `unresolved_package_references` with the reason
|
|
254
|
+
- **Runtime packs** of self-contained apps in `*.deps.json` are looked up under their package names (the SDK writes them as `runtimepack.<name>`, which OSV does not match)
|
|
255
|
+
- **Not looked up from restore results**: entries of an unknown type, and versions that are not plain NuGet versions (listed in `unscannable_nuget_packages`). Your own projects are counted in `local_projects`
|
|
256
|
+
- XML (project files, `packages.config`, `nuget.config`) is read by a small parser that rejects `DOCTYPE`, entity declarations, and CDATA. JSON files use the same structural checks as the npm lockfiles (16 MiB per file). Up to 200,000 packages and 1,000 .NET files are looked up per scan; files beyond the limit are listed in `skipped_files`. A character reference that is not a valid character (such as `�`) makes the file uninterpretable instead of failing the scan
|
|
257
|
+
|
|
230
258
|
**`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.
|
|
231
259
|
|
|
232
260
|
**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.
|
|
@@ -268,6 +296,7 @@ Detects the lockfiles and manifests in a project and scans the Java / JavaScript
|
|
|
268
296
|
- `unscannable_requirements`: lines that are not scanned, with the reason: `-e`, `name @ URL`, paths, includes that were not expanded (outside the project directory, missing, URLs, or over the limits), constraint files (`-c`, not applied), and options or version specifiers that cannot be interpreted. Lines that cannot be interpreted are recorded here instead of being ignored
|
|
269
297
|
- `local_crates` (only when a `Cargo.lock` was scanned): the number of crates without a source in `Cargo.lock` (workspace members and path dependencies). They are your own code and are not scanned; they do not make `complete` false. A third-party crate vendored as a path dependency is not scanned either (as with `cargo audit`)
|
|
270
298
|
- `unscannable_crates` (only when a `Cargo.lock` was scanned): git dependencies (their versions do not correspond to crates.io releases) and crates from registries other than crates.io (a crate with the same name on crates.io may be a different crate). They are not scanned, so `complete` is false. Source URLs are not included in the response
|
|
299
|
+
- `local_projects`, `unscannable_nuget_packages`, `unresolved_package_references`, `nuget_warnings` (only when a .NET file was found): see the .NET notes above. `nuget_warnings` notes lower bounds, a possibly stale `project.assets.json` (more than a minute older than its project file; smaller differences are ignored because a clone writes files in any order), references that cannot be checked from project files alone (SDK-implicit references and `Directory.Build.*` outside the scanned directory), and package sources other than nuget.org. Entries in `unscannable_nuget_packages` and `unresolved_package_references` make `complete` false
|
|
271
300
|
- `skipped_files`: files excluded from the scan, with the reason (a `Cargo.lock` that cannot be interpreted with certainty, a `requirements.txt` that cannot be read or is larger than 1 MiB, or a `pom.xml` whose parent POM chain references a file outside the allowed root). A `pom.xml` scanned without its parent POM, because the parent could not be read, is also listed here with the reason (see [Parent POMs](#parent-poms))
|
|
272
301
|
- Each list holds up to 200 entries; the number of omitted entries is returned in `omitted_items`
|
|
273
302
|
- `ecosystem_breakdown` includes ecosystems with zero findings, to show that they were scanned
|
|
@@ -278,9 +307,10 @@ Detects the lockfiles and manifests in a project and scans the Java / JavaScript
|
|
|
278
307
|
- `requirements.txt`: dependencies written in the file are direct; dependencies resolved through deps.dev are transitive
|
|
279
308
|
- `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`
|
|
280
309
|
- `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`
|
|
310
|
+
- .NET: `Direct` / `Transitive` in `packages.lock.json`; the dependencies of `project.frameworks` in `project.assets.json`; the dependencies of the project library in `*.deps.json`; and every reference read from a project file are direct. `packages.config` does not say, so it is `unknown`. `introduced_by` and `declared_in` are not available yet
|
|
281
311
|
- 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`
|
|
282
312
|
- `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
|
|
283
|
-
- Upgrade recommendations (`suggest_fix`) are available for Maven, npm, PyPI, Go,
|
|
313
|
+
- Upgrade recommendations (`suggest_fix`) are available for Maven, npm, PyPI, Go, Rust (crates.io), and PHP (Packagist). NuGet packages are returned with `verification: "unsupported_ecosystem"` for now
|
|
284
314
|
|
|
285
315
|
### `scan_java_project`
|
|
286
316
|
|
|
@@ -345,7 +375,7 @@ If an existing parent POM cannot be read (larger than 10 MiB, not a regular file
|
|
|
345
375
|
```
|
|
346
376
|
|
|
347
377
|
- `packages` is sorted by the most severe vulnerability, and each `vulnerabilities` list by severity (unknown last)
|
|
348
|
-
- `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),
|
|
378
|
+
- `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), PEP 440 for PyPI (in the order listed by OSV up to v0.5.0), and Composer ordering for Packagist (since v0.17.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
|
|
349
379
|
- Vulnerabilities without a severity score are reported as `null` / `"unknown"`
|
|
350
380
|
|
|
351
381
|
### `scan_java_artifact`
|
|
@@ -402,7 +432,7 @@ Invalid JSON returns `invalid_sbom`, an unsupported format `unsupported_sbom_for
|
|
|
402
432
|
|
|
403
433
|
### `suggest_fix`
|
|
404
434
|
|
|
405
|
-
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,
|
|
435
|
+
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, Rust (crates.io), and PHP (Packagist / Composer). Instead of simply taking the highest fixed version, it picks the fixed version closest to the current release line, falling back through three tiers:
|
|
406
436
|
|
|
407
437
|
| Tier | Meaning |
|
|
408
438
|
|---|---|
|
|
@@ -575,7 +605,7 @@ The following measures keep the vulnerability scanner itself from becoming an at
|
|
|
575
605
|
- **Supply chain**: The automatic download is limited to the official GitHub Releases and a pinned version, and is verified against **a SHA256 checksum embedded in the package** (the SHA256SUMS file from the release is not trusted, so tampering on the release side is detected). The binary is not made executable until it passes verification, and cached binaries are verified again on every use. `OSV_MCP_PREFER_DOWNLOAD=1` avoids unverified binaries on `PATH`
|
|
576
606
|
- **Command injection**: Commands are run with `spawn` and an argument array, never through a shell. OSV-Scanner receives only a fixed list of arguments, plus verified absolute paths
|
|
577
607
|
- **Snapshots (no gap between checking and reading)**: OSV-Scanner never receives the original files. Lockfiles, `pom.xml` (including the parent POM chain), and JAR/WARs are read safely once, copied into a private temporary directory (accessible only by the owner, deleted afterwards), and the copies are both checked and scanned. Replacing the original files or directories after the check has no effect on the results. Parent POMs are copied into the temporary directory at their original relative positions, so OSV-Scanner can follow relative paths but only finds the verified copies (references that climb out of the temporary directory with repeated `..` are excluded). Reads refuse a symbolic link at the final path component and anything that is not a regular file (so named pipes cannot stall the server), and afterwards the path is resolved again to confirm that it is still inside the boundary and is the same file that was opened. The copies are limited to 2 GiB in total (`scan_input_too_large`). When the server is terminated (SIGTERM/SIGINT/SIGHUP, or stdin closing), the temporary directories are deleted and running OSV-Scanner processes are stopped. Directories left behind by an uncatchable termination such as SIGKILL are deleted at a later startup (only directories whose names exactly match this server's prefixes, owned by the current user, not symbolic links, and unmodified for 24 hours or more)
|
|
578
|
-
- **Path traversal**: Input paths are resolved with `realpath` (following symbolic links) before the boundary check. Manifest searches do not follow symbolic links. OSV-Scanner never receives a directory, only the detected manifests, each with its format given explicitly (given a directory, OSV-Scanner also reads `requirements.txt` files there and follows their `-r ../x.txt` includes outside the scan scope). `requirements.txt` files are not passed as they are: only the dependency lines the server could interpret are normalized and written to a private copy without any include directives, so even if OSV-Scanner's interpretation of includes differs from the server's, files outside the scope are never read. `Cargo.lock` files are likewise replaced by a copy the server writes, listing only crates.io packages A `pom.xml` whose parent POM chain references a file outside `OSV_MCP_ALLOWED_ROOT` is excluded ([Parent POMs](#parent-poms))
|
|
608
|
+
- **Path traversal**: Input paths are resolved with `realpath` (following symbolic links) before the boundary check. Manifest searches do not follow symbolic links. OSV-Scanner never receives a directory, only the detected manifests, each with its format given explicitly (given a directory, OSV-Scanner also reads `requirements.txt` files there and follows their `-r ../x.txt` includes outside the scan scope). `requirements.txt` files are not passed as they are: only the dependency lines the server could interpret are normalized and written to a private copy without any include directives, so even if OSV-Scanner's interpretation of includes differs from the server's, files outside the scope are never read. `Cargo.lock` files are likewise replaced by a copy the server writes, listing only crates.io packages, and every .NET file by a `packages.lock.json` copy listing only packages with a known version. A `pom.xml` whose parent POM chain references a file outside `OSV_MCP_ALLOWED_ROOT` is excluded ([Parent POMs](#parent-poms))
|
|
579
609
|
- **Denial of service**: Timeouts and limits on stdout and on the stderr excerpt. Scanner output is parsed defensively and malformed output does not throw. The number of concurrent scans is limited (2 by default) so that parallel requests cannot start unlimited processes. The server's own analysis reads each file once and reuses the result (lockfiles for npm workspace membership, `Cargo.lock` files, shared `requirements.txt` includes, parent POMs), with a limit on the total amount read. Responses do not include the internal affected-range data, which keeps large scans to a fraction of their former size
|
|
580
610
|
- **Fail-closed mode**: `OSV_MCP_REQUIRE_ALLOWED_ROOT=1` refuses to start the server when no allowed root is set
|
|
581
611
|
- **Restricted permissions (optional)**: The server works under Node's permission model, reports denied access as `permission_denied`, and warns about missing permissions at startup ([Running with restricted permissions](#running-with-restricted-permissions))
|
|
@@ -613,6 +643,9 @@ Design notes and open items are in [docs/DESIGN_TODO.md](docs/DESIGN_TODO.md) (i
|
|
|
613
643
|
- [x] `scan_project` for Rust (`Cargo.lock`, crates.io crates only) and RustSec informational advisories
|
|
614
644
|
- [x] `suggest_fix` and direct/transitive dependencies for Rust
|
|
615
645
|
- [x] Scan history and changes between runs (`scan_history`, opt-in)
|
|
646
|
+
- [x] `scan_project` for .NET (NuGet), with per-project matching and lower bounds for project files
|
|
647
|
+
- [x] `scan_project` and `suggest_fix` for PHP (Composer `composer.lock`)
|
|
648
|
+
- [ ] `suggest_fix` for .NET (NuGet)
|
|
616
649
|
- [ ] Direct and transitive dependencies for `gradle.lockfile` and the YAML/TOML lockfiles
|
|
617
650
|
|
|
618
651
|
## License
|
|
@@ -103,7 +103,10 @@ function severityOf(severity) {
|
|
|
103
103
|
return null;
|
|
104
104
|
return Object.fromEntries(Object.entries(severity).filter(([, v]) => count(v) !== null).slice(0, MAX_SEVERITY_KEYS).map(([k, v]) => [k.slice(0, 32), count(v)]));
|
|
105
105
|
}
|
|
106
|
-
const GAP_FIELDS = [
|
|
106
|
+
const GAP_FIELDS = [
|
|
107
|
+
"lockfile_missing", "unpinned_requirements", "unscannable_requirements", "unscannable_crates", "unscannable_nuget_packages",
|
|
108
|
+
"unresolved_package_references", "skipped_files", "unidentified_jars", "unidentified_packages",
|
|
109
|
+
];
|
|
107
110
|
function gapsOf(payload) {
|
|
108
111
|
const coverage = asRecord(payload.coverage);
|
|
109
112
|
const gaps = {};
|
package/dist/index.js
CHANGED
|
@@ -66,7 +66,7 @@ if (startupWarning !== null) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
// NOTE: リリース時はpackage.jsonのversionと同じ値に更新すること
|
|
69
|
-
const SERVER_VERSION = "0.
|
|
69
|
+
const SERVER_VERSION = "0.17.0";
|
|
70
70
|
const server = new McpServer({
|
|
71
71
|
name: "osv-scanner-mcp",
|
|
72
72
|
version: SERVER_VERSION,
|
|
@@ -75,17 +75,22 @@ const historyConfig = historyConfigFromEnv();
|
|
|
75
75
|
/** スキャンのツールを実行履歴に記録する(OSV_MCP_HISTORYが無効なら何もしない) */
|
|
76
76
|
const recorded = (tool, input, run) => withHistory(tool, input, run, { config: historyConfig, serverVersion: SERVER_VERSION, allowedRootSet: allowedRootFromEnv() !== undefined });
|
|
77
77
|
server.registerTool("scan_project", {
|
|
78
|
-
title: "Scan project dependencies for vulnerabilities (Java / JavaScript / Python / Go / Rust)",
|
|
78
|
+
title: "Scan project dependencies for vulnerabilities (Java / JavaScript / Python / Go / Rust / .NET / PHP)",
|
|
79
79
|
description: "Detects the lockfiles and manifests in a project and scans the dependencies for known vulnerabilities (CVE/GHSA). " +
|
|
80
80
|
"Supported: Java (pom.xml / gradle.lockfile), JavaScript (package-lock.json / npm-shrinkwrap.json / yarn.lock / pnpm-lock.yaml / bun.lock), " +
|
|
81
|
-
"Python (poetry.lock / uv.lock / Pipfile.lock / pdm.lock / requirements.txt), Go (go.mod), Rust (Cargo.lock). " +
|
|
81
|
+
"Python (poetry.lock / uv.lock / Pipfile.lock / pdm.lock / requirements.txt), Go (go.mod), Rust (Cargo.lock), PHP (composer.lock, including packages-dev), " +
|
|
82
|
+
".NET (packages.lock.json / obj/project.assets.json / packages.config / *.deps.json, and the direct PackageReference items of .csproj / .fsproj / .vbproj files without them). " +
|
|
82
83
|
"Package managers and builds are never run. " +
|
|
83
84
|
"Always check coverage at the top of the response: it lists manifests without a lockfile, requirements lines without a pinned version, and excluded files. " +
|
|
84
85
|
"Git dependencies in npm lockfiles are looked up by repository URL, so private git host names and paths are sent to api.osv.dev. " +
|
|
85
86
|
"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. " +
|
|
87
|
+
"For .NET, each project file is matched to its own lockfile or obj/project.assets.json; projects without one are scanned from their direct PackageReference items only (lockfile_missing status direct_only or unconfirmed), " +
|
|
88
|
+
"a PackageReference Version=\"x\" is looked up at its lower bound (version_is_lower_bound), and references with properties, ranges without a lower bound, floating versions, conditions, or Update/Remove are not looked up (unresolved_package_references). " +
|
|
89
|
+
"The project's own projects are counted in local_projects. Names of packages from private NuGet feeds are also sent to api.osv.dev; see nuget_warnings. " +
|
|
86
90
|
"Vulnerabilities marked informational (RustSec notices such as unmaintained or unsound crates) are not counted in vulnerability_count; they are counted in informational_count. " +
|
|
87
91
|
"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). " +
|
|
88
|
-
"
|
|
92
|
+
"Composer files are scanned from sanitized copies containing package names and release versions only; local dist.type=path packages are not looked up and are counted in local_composer_packages; branch/dev versions and invalid entries are excluded and reported in skipped_files. Composer and PHP code are never run. Other private Composer package names are sent to api.osv.dev; composer_warnings flags missing standard Packagist notification URLs, which are only a heuristic for origin. " +
|
|
93
|
+
"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, Rust, and PHP (not yet for .NET). Cargo.lock dependency graphs provide direct/transitive relations, introduced_by, and uniquely mapped declaring Cargo.toml paths in declared_in.",
|
|
89
94
|
inputSchema: {
|
|
90
95
|
project_path: z
|
|
91
96
|
.string()
|
|
@@ -109,7 +114,8 @@ server.registerTool("scan_java_project", {
|
|
|
109
114
|
server.registerTool("suggest_fix", {
|
|
110
115
|
title: "Recommend upgrades that fix vulnerabilities",
|
|
111
116
|
description: "Scans the project with the same detection as scan_project and recommends an upgrade version for each vulnerable package. " +
|
|
112
|
-
"Recommendations are available for Java (Maven / Gradle), JavaScript (npm), Python (PyPI), Go,
|
|
117
|
+
"Recommendations are available for Java (Maven / Gradle), JavaScript (npm), Python (PyPI), Go, Rust (crates.io), and PHP (Packagist / Composer). " +
|
|
118
|
+
"Composer recommendations support classical numeric release versions with up to four components and alpha/beta/RC/patch suffixes; dev branches, aliases, and unsupported version formats are not evaluated. Check PHP/extension constraints and application compatibility before upgrading. " +
|
|
113
119
|
"The fixed version closest to the current release line is chosen with a three-tier fallback " +
|
|
114
120
|
"(same_minor: same release line -> major_internal: same major version -> cross_major: major upgrade), " +
|
|
115
121
|
"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). " +
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OSV-Scannerバイナリの解決(docs/DESIGN_TODO.md 方式C)。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 探索順: 環境変数 OSV_SCANNER_PATH → PATH上の osv-scanner → ピン留めした版の自動ダウンロード
|
|
4
|
+
* (チェックサム検証付き。binaryDownloader.ts)。詳細はresolveOsvScannerBinaryを参照
|
|
5
5
|
*/
|
|
6
6
|
import { access, constants, stat } from "node:fs/promises";
|
|
7
7
|
import { isAccessDenied, permissionDeniedError } from "../utils/projectWalk.js";
|
package/dist/osv/suggestFix.js
CHANGED
|
@@ -20,6 +20,7 @@ import { candidateStatus } from "./affectedVersions.js";
|
|
|
20
20
|
import { versionSchemeFor } from "./versionScheme.js";
|
|
21
21
|
const TIER_ORDER = ["same_minor", "major_internal", "cross_major"];
|
|
22
22
|
const UPDATE_HINTS = {
|
|
23
|
+
Packagist: "Update the applicable constraints in composer.json, then regenerate composer.lock in a trusted environment with composer update vendor/package --with-all-dependencies --no-scripts --no-plugins. For a transitive dependency, update the package that requires it or adjust its constraints. Check PHP and extension requirements and application compatibility before updating; Composer is not executed by this server",
|
|
23
24
|
npm: "For a direct dependency, update the version in package.json. For a transitive dependency, update the direct dependency that requires it, " +
|
|
24
25
|
"or set the version with overrides in the root package.json (effective only in the root project)",
|
|
25
26
|
Go: "Update with go get <module>@<version> (a transitive dependency is added to the require block of go.mod and updated as well). " +
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { compareMavenVersions, mavenVersionSeries } from "../utils/mavenVersion.js";
|
|
13
13
|
import { compareParsedPep440, parsePep440 } from "../utils/pep440Version.js";
|
|
14
14
|
import { compareParsedSemver, parseSemver } from "../utils/semverVersion.js";
|
|
15
|
+
import { compareComposerVersions, parseComposerVersion } from "../utils/composerVersion.js";
|
|
15
16
|
const SEMVER_ECOSYSTEMS = new Set(["npm", "Go", "crates.io"]);
|
|
16
17
|
const MAVEN_SCHEME = {
|
|
17
18
|
isValid: () => true,
|
|
@@ -103,8 +104,26 @@ const PEP440_SCHEME = {
|
|
|
103
104
|
return major === 0n && minor === 0n ? `${epoch}0.0.${patch}` : `${epoch}${major}.${minor}`;
|
|
104
105
|
},
|
|
105
106
|
};
|
|
107
|
+
/** Composer: classical release ordering, with conservative 0.x upgrade tiers. */
|
|
108
|
+
const COMPOSER_SCHEME = {
|
|
109
|
+
isValid: (v) => parseComposerVersion(v) !== null,
|
|
110
|
+
compare: compareComposerVersions,
|
|
111
|
+
isPrerelease: (v) => (parseComposerVersion(v)?.stability ?? 3) < 3,
|
|
112
|
+
classify(current, candidate) {
|
|
113
|
+
const a = parseComposerVersion(current), b = parseComposerVersion(candidate);
|
|
114
|
+
if (!a || !b)
|
|
115
|
+
return "cross_major";
|
|
116
|
+
return classifyCaret({ major: a.parts[0], minor: a.parts[1], patch: a.parts[2] }, { major: b.parts[0], minor: b.parts[1], patch: b.parts[2] });
|
|
117
|
+
},
|
|
118
|
+
seriesLabel(v) {
|
|
119
|
+
const parsed = parseComposerVersion(v);
|
|
120
|
+
return parsed ? `${parsed.parts[0]}.${parsed.parts[1]}` : null;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
106
123
|
/** 修正版の推奨に対応するエコシステムの操作。未対応ならnull */
|
|
107
124
|
export function versionSchemeFor(ecosystem) {
|
|
125
|
+
if (ecosystem === "Packagist")
|
|
126
|
+
return COMPOSER_SCHEME;
|
|
108
127
|
if (ecosystem === "Maven")
|
|
109
128
|
return MAVEN_SCHEME;
|
|
110
129
|
if (SEMVER_ECOSYSTEMS.has(ecosystem))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `scan_project`ツールのハンドラ: Java / JavaScript / Python / Go / Rustのlockfileをまとめてスキャンする。
|
|
2
|
+
* `scan_project`ツールのハンドラ: Java / JavaScript / Python / Go / Rust / .NETのlockfileをまとめてスキャンする。
|
|
3
3
|
*
|
|
4
4
|
* 誤要約対策(JAR実体スキャンと同じ原則): スキャンできなかった依存を示すcoverageを件数より前に置き、
|
|
5
5
|
* complete=falseの場合は「検出0件でも安全とは言えない」旨の警告を付ける。
|
|
@@ -11,6 +11,7 @@ import { isRemoteResolutionDisabled, runOsvScan } from "../osv/runner.js";
|
|
|
11
11
|
import { hasVulnerability, packageSources, parseOsvScanOutput } from "../osv/scanReport.js";
|
|
12
12
|
import { combineRelations, cargoLockRelations, goModRelations, npmLockRelations, pomRelations, requirementsRelations, } from "../utils/dependencyRelations.js";
|
|
13
13
|
import { cargoLockCopyContent } from "../utils/cargoLock.js";
|
|
14
|
+
import { nugetKey, nugetLockCopyContent, nugetRelations } from "../utils/nugetFiles.js";
|
|
14
15
|
import { sanitizeExternalText } from "../utils/externalText.js";
|
|
15
16
|
import { readRegularFile } from "../utils/safeRead.js";
|
|
16
17
|
import { detectProject } from "../utils/manifestDetector.js";
|
|
@@ -24,7 +25,8 @@ export const TRANSITIVE_OMITTED_WARNING = "Because OSV_MCP_NO_REMOTE_RESOLUTION
|
|
|
24
25
|
"but vulnerabilities in the transitive dependencies of the dependencies listed directly in pom.xml or requirements.txt are not included. " +
|
|
25
26
|
"Zero findings does not confirm that the transitive dependencies are safe";
|
|
26
27
|
const INCOMPLETE_WARNING = "Some dependencies were not scanned, or were scanned at an assumed version " +
|
|
27
|
-
"(see lockfile_missing / unpinned_requirements / unscannable_requirements / unscannable_crates /
|
|
28
|
+
"(see lockfile_missing / unpinned_requirements / unscannable_requirements / unscannable_crates / " +
|
|
29
|
+
"unscannable_nuget_packages / unresolved_package_references / skipped_files). " +
|
|
28
30
|
"Zero findings does not confirm that those dependencies are safe";
|
|
29
31
|
/** coverageの各一覧の上限(巨大なrequirements.txtで応答を膨らませない) */
|
|
30
32
|
const MAX_COVERAGE_ITEMS = 200;
|
|
@@ -38,12 +40,17 @@ export function buildCoverage(project) {
|
|
|
38
40
|
const unscannable = capped(project.requirementReferences);
|
|
39
41
|
const skipped = capped(project.skippedFiles);
|
|
40
42
|
const crates = capped(project.unscannableCrates);
|
|
43
|
+
const nugetUnscannable = capped(project.unscannableNugetPackages);
|
|
44
|
+
const nugetUnresolved = capped(project.unresolvedPackageReferences);
|
|
41
45
|
const complete = project.lockfileMissing.length === 0 &&
|
|
42
46
|
project.requirementIssues.length === 0 &&
|
|
43
47
|
project.requirementReferences.length === 0 &&
|
|
44
48
|
project.unscannableCrates.length === 0 &&
|
|
49
|
+
project.unscannableNugetPackages.length === 0 &&
|
|
50
|
+
project.unresolvedPackageReferences.length === 0 &&
|
|
45
51
|
project.skippedFiles.length === 0;
|
|
46
|
-
const omitted = lockfileMissing.omitted + unpinned.omitted + unscannable.omitted + crates.omitted + skipped.omitted
|
|
52
|
+
const omitted = lockfileMissing.omitted + unpinned.omitted + unscannable.omitted + crates.omitted + skipped.omitted +
|
|
53
|
+
nugetUnscannable.omitted + nugetUnresolved.omitted;
|
|
47
54
|
const hasCargo = project.manifests.some((m) => m.format === "Cargo.lock");
|
|
48
55
|
return {
|
|
49
56
|
complete,
|
|
@@ -80,7 +87,29 @@ export function buildCoverage(project) {
|
|
|
80
87
|
})),
|
|
81
88
|
}
|
|
82
89
|
: {}),
|
|
90
|
+
// .NET: 従来のエコシステムの応答を変えないよう、.NETのファイルを検出した場合だけ付ける
|
|
91
|
+
...(project.hasNuget
|
|
92
|
+
? {
|
|
93
|
+
local_projects: project.localProjectCount,
|
|
94
|
+
unscannable_nuget_packages: nugetUnscannable.items.map((p) => ({
|
|
95
|
+
file: sanitizeExternalText(p.file),
|
|
96
|
+
name: sanitizeExternalText(p.name),
|
|
97
|
+
version: sanitizeExternalText(p.version),
|
|
98
|
+
reason: p.reason,
|
|
99
|
+
})),
|
|
100
|
+
unresolved_package_references: nugetUnresolved.items.map((r) => ({
|
|
101
|
+
file: sanitizeExternalText(r.file),
|
|
102
|
+
name: sanitizeExternalText(r.name),
|
|
103
|
+
specifier: sanitizeExternalText(r.specifier),
|
|
104
|
+
reason: r.reason,
|
|
105
|
+
})),
|
|
106
|
+
...(project.nugetWarnings.length > 0 ? { nuget_warnings: project.nugetWarnings.map(sanitizeExternalText) } : {}),
|
|
107
|
+
}
|
|
108
|
+
: {}),
|
|
83
109
|
skipped_files: skipped.items.map((s) => ({ path: sanitizeExternalText(s.path), reason: sanitizeExternalText(s.reason) })),
|
|
110
|
+
// Composer: 自分のコード(dist.type=path)は数だけ示し、completeには影響させない(local_crates・local_projectsと同じ)
|
|
111
|
+
...(project.manifests.some((m) => m.format === "composer.lock") ? { local_composer_packages: project.composerLocalPackages ?? 0 } : {}),
|
|
112
|
+
...((project.composerWarnings?.length ?? 0) > 0 ? { composer_warnings: project.composerWarnings.slice(0, MAX_COVERAGE_ITEMS).map(sanitizeExternalText) } : {}),
|
|
84
113
|
...(omitted > 0 ? { omitted_items: omitted } : {}),
|
|
85
114
|
};
|
|
86
115
|
}
|
|
@@ -101,12 +130,24 @@ function ecosystemBreakdown(project, packages) {
|
|
|
101
130
|
}
|
|
102
131
|
return breakdown;
|
|
103
132
|
}
|
|
104
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* `>=X` / `~=X` の行(requirements.txt)と、`.csproj`の`Version="X"`(X以上)は、下限Xを使用中の版とみなしてスキャンしている。
|
|
135
|
+
* .NETは同じ名前・版を確定版として照会したファイルもあれば、下限とは示さない
|
|
136
|
+
*/
|
|
105
137
|
export function markLowerBounds(project, packages) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
138
|
+
const nugetExact = new Set();
|
|
139
|
+
const nugetLower = new Set();
|
|
140
|
+
for (const copy of project.nugetCopies) {
|
|
141
|
+
for (const pkg of copy.packages)
|
|
142
|
+
(pkg.lowerBound ? nugetLower : nugetExact).add(JSON.stringify([nugetKey(pkg.name), pkg.version]));
|
|
143
|
+
}
|
|
144
|
+
return packages.map((pkg) => {
|
|
145
|
+
const nugetId = JSON.stringify([nugetKey(pkg.name), pkg.version]);
|
|
146
|
+
const lower = pkg.ecosystem === "PyPI"
|
|
147
|
+
? project.lowerBounds.some((lb) => lb.name === normalizePypiName(pkg.name) && lb.version === pkg.version)
|
|
148
|
+
: pkg.ecosystem === "NuGet" && nugetLower.has(nugetId) && !nugetExact.has(nugetId);
|
|
149
|
+
return lower ? { ...pkg, version_is_lower_bound: true } : pkg;
|
|
150
|
+
});
|
|
110
151
|
}
|
|
111
152
|
/**
|
|
112
153
|
* 元のファイルはosv-scannerに渡さず、スナップショット(scanSnapshot.ts)のコピーをスキャンする。
|
|
@@ -125,7 +166,7 @@ export async function buildRelationLookups(copies, maxTotalBytes = MAX_RELATION_
|
|
|
125
166
|
const lookups = new Map();
|
|
126
167
|
const npmWorkBudget = createNpmLockWorkBudget();
|
|
127
168
|
let remaining = maxTotalBytes;
|
|
128
|
-
for (const { copy, format, originalRelative, entries, cargoPackages, declaringManifests } of copies) {
|
|
169
|
+
for (const { copy, format, originalRelative, entries, cargoPackages, declaringManifests, nugetPackages } of copies) {
|
|
129
170
|
const lockDir = originalRelative === null ? "" : path.dirname(originalRelative);
|
|
130
171
|
let lookup = null;
|
|
131
172
|
try {
|
|
@@ -147,6 +188,9 @@ export async function buildRelationLookups(copies, maxTotalBytes = MAX_RELATION_
|
|
|
147
188
|
else if (format === "Cargo.lock" && cargoPackages !== undefined) {
|
|
148
189
|
lookup = cargoLockRelations(cargoPackages, declaringManifests);
|
|
149
190
|
}
|
|
191
|
+
else if (format === "packages.lock.json" && nugetPackages !== undefined) {
|
|
192
|
+
lookup = nugetRelations(nugetPackages);
|
|
193
|
+
}
|
|
150
194
|
}
|
|
151
195
|
catch {
|
|
152
196
|
lookup = null; // 解析できないファイルはunknown(スキャン自体は続ける)
|
|
@@ -272,6 +316,16 @@ export async function scanFromSnapshot(project, options) {
|
|
|
272
316
|
relationSources.push({ copy: generated, format: "Cargo.lock", originalRelative: copy.path,
|
|
273
317
|
cargoPackages: copy.allPackages, declaringManifests: copy.declaringManifests });
|
|
274
318
|
}
|
|
319
|
+
for (const copy of project.nugetCopies) {
|
|
320
|
+
const generated = await snapshot.writeGenerated(nugetLockCopyContent(copy.packages));
|
|
321
|
+
targets.push({ path: generated, format: "packages.lock.json" });
|
|
322
|
+
relationSources.push({ copy: generated, format: "packages.lock.json", originalRelative: copy.path, nugetPackages: copy.packages });
|
|
323
|
+
}
|
|
324
|
+
for (const copy of project.composerCopies ?? []) {
|
|
325
|
+
const generated = await snapshot.writeGenerated(copy.content);
|
|
326
|
+
targets.push({ path: generated, format: "composer.lock" });
|
|
327
|
+
relationSources.push({ copy: generated, format: "composer.lock", originalRelative: copy.path });
|
|
328
|
+
}
|
|
275
329
|
if (targets.length === 0)
|
|
276
330
|
return parseOsvScanOutput({ results: [] });
|
|
277
331
|
const report = await withScopeNotes(project.skippedFiles, () => snapshot.guard(() => runOsvScan(targets, options)));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { parseNpmLock } from "./npmLockCopy.js";
|
|
2
|
+
import { parseComposerVersion } from "./composerVersion.js";
|
|
3
|
+
const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
/** Only names and release versions reach OSV; repository URLs, scripts, and commit IDs are never forwarded. */
|
|
5
|
+
export function composerLockCopy(text, budget) {
|
|
6
|
+
const data = parseNpmLock(text, budget);
|
|
7
|
+
if (!record(data) || !Array.isArray(data.packages) || (data["packages-dev"] !== undefined && !Array.isArray(data["packages-dev"]))) {
|
|
8
|
+
throw new Error("composer.lock must contain a packages array and an optional packages-dev array");
|
|
9
|
+
}
|
|
10
|
+
const issues = [];
|
|
11
|
+
const warnings = [];
|
|
12
|
+
let uncertainOrigin = 0;
|
|
13
|
+
// dist.type=pathは自分のコード(monorepoのパッケージ)。Rustのlocal_crates・.NETのlocal_projectsと同じく数だけ返し、範囲の欠けにしない
|
|
14
|
+
let localPackages = 0;
|
|
15
|
+
const output = {};
|
|
16
|
+
for (const group of ["packages", "packages-dev"]) {
|
|
17
|
+
const packages = [];
|
|
18
|
+
let skipped = 0;
|
|
19
|
+
for (const raw of (data[group] ?? [])) {
|
|
20
|
+
if (record(raw) && record(raw.dist) && raw.dist.type === "path") {
|
|
21
|
+
localPackages++;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (!record(raw) || typeof raw.name !== "string" || !/^[a-z0-9][a-z0-9_.-]*\/[a-z0-9][a-z0-9_.-]*$/.test(raw.name) || raw.name.length > 200 ||
|
|
25
|
+
typeof raw.version !== "string" || parseComposerVersion(raw.version) === null) {
|
|
26
|
+
skipped++;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
packages.push({ name: raw.name, version: raw.version });
|
|
30
|
+
if (raw["notification-url"] !== "https://packagist.org/downloads/")
|
|
31
|
+
uncertainOrigin++;
|
|
32
|
+
}
|
|
33
|
+
output[group] = packages;
|
|
34
|
+
if (skipped > 0)
|
|
35
|
+
issues.push(`${skipped} entries in ${group} were not scanned: invalid package names or non-release versions (branches, aliases, and dev versions are not evaluated)`);
|
|
36
|
+
}
|
|
37
|
+
if (uncertainOrigin > 0)
|
|
38
|
+
warnings.push(`${uncertainOrigin} Composer package entries lack the standard Packagist notification URL. Their names and versions are sent to api.osv.dev as Packagist packages; private or forked packages may produce false positives. The notification URL is only a heuristic and does not prove origin`);
|
|
39
|
+
return { content: JSON.stringify(output), issues, warnings, packageCount: output.packages.length + output["packages-dev"].length, localPackages };
|
|
40
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const STABILITY = { alpha: 0, a: 0, beta: 1, b: 1, rc: 2, stable: 3, patch: 4, pl: 4, p: 4 };
|
|
2
|
+
export function parseComposerVersion(version) {
|
|
3
|
+
if (version.length > 256)
|
|
4
|
+
return null;
|
|
5
|
+
const match = /^v?(\d{1,5}(?:\.\d+){0,3})(?:[._-]?(alpha|a|beta|b|RC|stable|patch|pl|p)((?:[.-]?\d+(?:[.-]\d+)*)?))?(?:\+[0-9A-Za-z.-]+)?$/i.exec(version);
|
|
6
|
+
if (!match)
|
|
7
|
+
return null;
|
|
8
|
+
const parts = match[1].split(".").map(BigInt);
|
|
9
|
+
while (parts.length < 4)
|
|
10
|
+
parts.push(0n);
|
|
11
|
+
return {
|
|
12
|
+
parts,
|
|
13
|
+
stability: match[2] === undefined ? 3 : STABILITY[match[2].toLowerCase()],
|
|
14
|
+
suffix: match[2]?.toLowerCase() === "stable" ? [] : (match[3] ?? "").replace(/^[.-]/, "").split(/[.-]/).filter(Boolean).map(BigInt),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function compareComposerVersions(a, b) {
|
|
18
|
+
const x = parseComposerVersion(a), y = parseComposerVersion(b);
|
|
19
|
+
if (x === null || y === null)
|
|
20
|
+
return 0;
|
|
21
|
+
for (let i = 0; i < 4; i++) {
|
|
22
|
+
if (x.parts[i] !== y.parts[i])
|
|
23
|
+
return x.parts[i] < y.parts[i] ? -1 : 1;
|
|
24
|
+
}
|
|
25
|
+
if (x.stability !== y.stability)
|
|
26
|
+
return x.stability < y.stability ? -1 : 1;
|
|
27
|
+
for (let i = 0; i < Math.max(x.suffix.length, y.suffix.length); i++) {
|
|
28
|
+
// PHP version_compare treats even an extra zero component as greater.
|
|
29
|
+
if (i >= x.suffix.length || i >= y.suffix.length)
|
|
30
|
+
return i >= x.suffix.length ? -1 : 1;
|
|
31
|
+
const left = x.suffix[i], right = y.suffix[i];
|
|
32
|
+
if (left !== right)
|
|
33
|
+
return left < right ? -1 : 1;
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|