osv-scanner-mcp 0.15.0 → 0.16.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 +26 -8
- package/dist/history/historyRecord.js +4 -1
- package/dist/index.js +8 -4
- package/dist/osv/binaryManager.js +2 -2
- package/dist/tools/scanProject.js +55 -9
- package/dist/utils/manifestDetector.js +49 -6
- package/dist/utils/manifestFormats.js +2 -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 +9 -2
package/README.md
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
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), 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
|
-
- **One-shot multi-language scan**: Pass a project path to `scan_project`, and it detects and scans the Java, JavaScript, Python, Go, and
|
|
14
|
+
- **One-shot multi-language scan**: Pass a project path to `scan_project`, and it detects and scans the Java, JavaScript, Python, Go, Rust, and .NET 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
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
|
+
- **.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
|
|
18
|
+
- **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
19
|
- **Java project scan**: `scan_java_project` scans only the Java (Maven / Gradle) manifests and returns a formatted report
|
|
19
20
|
- **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
21
|
- **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
|
|
@@ -162,6 +163,7 @@ As an additional layer of defense, you can limit what the server can read and wr
|
|
|
162
163
|
"--allow-fs-write=/private/var/folders/xx/yyyy/T",
|
|
163
164
|
"--allow-fs-read=/opt/homebrew/bin/osv-scanner",
|
|
164
165
|
"--allow-child-process",
|
|
166
|
+
"--allow-addons",
|
|
165
167
|
"/path/to/OSV-Scanner-MCP/dist/index.js"
|
|
166
168
|
],
|
|
167
169
|
"env": {
|
|
@@ -178,6 +180,7 @@ As an additional layer of defense, you can limit what the server can read and wr
|
|
|
178
180
|
- 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
181
|
- 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
182
|
- `--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
|
|
183
|
+
- 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
184
|
- 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
185
|
|
|
183
186
|
### Network destinations and privacy
|
|
@@ -188,7 +191,7 @@ Destinations confirmed with osv-scanner v2.4.0 (2026-10-07) and v2.7.0, the pinn
|
|
|
188
191
|
|---|---|---|
|
|
189
192
|
| 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
193
|
| 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) |
|
|
194
|
+
| 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
195
|
| `scan_java_artifact` / `scan_sbom` | `api.osv.dev` | Names and versions of the identified packages |
|
|
193
196
|
| `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
197
|
| `explain_vulnerability` | `api.osv.dev` | The vulnerability ID |
|
|
@@ -203,7 +206,7 @@ Both api.osv.dev and deps.dev are operated by Google. **In every configuration,
|
|
|
203
206
|
|
|
204
207
|
### `scan_project`
|
|
205
208
|
|
|
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.
|
|
209
|
+
Detects the lockfiles and manifests in a project and scans the Java / JavaScript / Python / Go / Rust / .NET dependencies together. Package managers and builds are never run.
|
|
207
210
|
|
|
208
211
|
**Input**
|
|
209
212
|
|
|
@@ -220,13 +223,24 @@ Detects the lockfiles and manifests in a project and scans the Java / JavaScript
|
|
|
220
223
|
| Python (PyPI) | `poetry.lock`, `uv.lock`, `Pipfile.lock`, `pdm.lock`, `requirements.txt` (and variants such as `requirements-dev.txt`) |
|
|
221
224
|
| Go | `go.mod` |
|
|
222
225
|
| Rust (crates.io) | `Cargo.lock` (lockfile versions 1 to 4) |
|
|
226
|
+
| .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) |
|
|
223
227
|
|
|
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`.
|
|
228
|
+
`.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
229
|
|
|
226
230
|
**`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
231
|
|
|
228
232
|
**`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
233
|
|
|
234
|
+
**.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.
|
|
235
|
+
|
|
236
|
+
- **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
|
|
237
|
+
- **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
|
|
238
|
+
- **`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
|
|
239
|
+
- **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
|
|
240
|
+
- **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)
|
|
241
|
+
- **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`
|
|
242
|
+
- 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
|
|
243
|
+
|
|
230
244
|
**`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
245
|
|
|
232
246
|
**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 +282,7 @@ Detects the lockfiles and manifests in a project and scans the Java / JavaScript
|
|
|
268
282
|
- `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
283
|
- `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
284
|
- `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
|
|
285
|
+
- `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
286
|
- `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
287
|
- Each list holds up to 200 entries; the number of omitted entries is returned in `omitted_items`
|
|
273
288
|
- `ecosystem_breakdown` includes ecosystems with zero findings, to show that they were scanned
|
|
@@ -278,9 +293,10 @@ Detects the lockfiles and manifests in a project and scans the Java / JavaScript
|
|
|
278
293
|
- `requirements.txt`: dependencies written in the file are direct; dependencies resolved through deps.dev are transitive
|
|
279
294
|
- `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
295
|
- `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`
|
|
296
|
+
- .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
297
|
- 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
298
|
- `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, and Rust (crates.io)
|
|
299
|
+
- Upgrade recommendations (`suggest_fix`) are available for Maven, npm, PyPI, Go, and Rust (crates.io). NuGet packages are returned with `verification: "unsupported_ecosystem"` for now
|
|
284
300
|
|
|
285
301
|
### `scan_java_project`
|
|
286
302
|
|
|
@@ -575,7 +591,7 @@ The following measures keep the vulnerability scanner itself from becoming an at
|
|
|
575
591
|
- **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
592
|
- **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
593
|
- **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))
|
|
594
|
+
- **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
595
|
- **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
596
|
- **Fail-closed mode**: `OSV_MCP_REQUIRE_ALLOWED_ROOT=1` refuses to start the server when no allowed root is set
|
|
581
597
|
- **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 +629,8 @@ Design notes and open items are in [docs/DESIGN_TODO.md](docs/DESIGN_TODO.md) (i
|
|
|
613
629
|
- [x] `scan_project` for Rust (`Cargo.lock`, crates.io crates only) and RustSec informational advisories
|
|
614
630
|
- [x] `suggest_fix` and direct/transitive dependencies for Rust
|
|
615
631
|
- [x] Scan history and changes between runs (`scan_history`, opt-in)
|
|
632
|
+
- [x] `scan_project` for .NET (NuGet), with per-project matching and lower bounds for project files
|
|
633
|
+
- [ ] `suggest_fix` for .NET (NuGet)
|
|
616
634
|
- [ ] Direct and transitive dependencies for `gradle.lockfile` and the YAML/TOML lockfiles
|
|
617
635
|
|
|
618
636
|
## 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.16.0";
|
|
70
70
|
const server = new McpServer({
|
|
71
71
|
name: "osv-scanner-mcp",
|
|
72
72
|
version: SERVER_VERSION,
|
|
@@ -75,17 +75,21 @@ 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)",
|
|
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), " +
|
|
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
|
-
"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.",
|
|
92
|
+
"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 (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
93
|
inputSchema: {
|
|
90
94
|
project_path: z
|
|
91
95
|
.string()
|
|
@@ -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";
|
|
@@ -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,6 +87,25 @@ 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) })),
|
|
84
110
|
...(omitted > 0 ? { omitted_items: omitted } : {}),
|
|
85
111
|
};
|
|
@@ -101,12 +127,24 @@ function ecosystemBreakdown(project, packages) {
|
|
|
101
127
|
}
|
|
102
128
|
return breakdown;
|
|
103
129
|
}
|
|
104
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* `>=X` / `~=X` の行(requirements.txt)と、`.csproj`の`Version="X"`(X以上)は、下限Xを使用中の版とみなしてスキャンしている。
|
|
132
|
+
* .NETは同じ名前・版を確定版として照会したファイルもあれば、下限とは示さない
|
|
133
|
+
*/
|
|
105
134
|
export function markLowerBounds(project, packages) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
135
|
+
const nugetExact = new Set();
|
|
136
|
+
const nugetLower = new Set();
|
|
137
|
+
for (const copy of project.nugetCopies) {
|
|
138
|
+
for (const pkg of copy.packages)
|
|
139
|
+
(pkg.lowerBound ? nugetLower : nugetExact).add(JSON.stringify([nugetKey(pkg.name), pkg.version]));
|
|
140
|
+
}
|
|
141
|
+
return packages.map((pkg) => {
|
|
142
|
+
const nugetId = JSON.stringify([nugetKey(pkg.name), pkg.version]);
|
|
143
|
+
const lower = pkg.ecosystem === "PyPI"
|
|
144
|
+
? project.lowerBounds.some((lb) => lb.name === normalizePypiName(pkg.name) && lb.version === pkg.version)
|
|
145
|
+
: pkg.ecosystem === "NuGet" && nugetLower.has(nugetId) && !nugetExact.has(nugetId);
|
|
146
|
+
return lower ? { ...pkg, version_is_lower_bound: true } : pkg;
|
|
147
|
+
});
|
|
110
148
|
}
|
|
111
149
|
/**
|
|
112
150
|
* 元のファイルはosv-scannerに渡さず、スナップショット(scanSnapshot.ts)のコピーをスキャンする。
|
|
@@ -125,7 +163,7 @@ export async function buildRelationLookups(copies, maxTotalBytes = MAX_RELATION_
|
|
|
125
163
|
const lookups = new Map();
|
|
126
164
|
const npmWorkBudget = createNpmLockWorkBudget();
|
|
127
165
|
let remaining = maxTotalBytes;
|
|
128
|
-
for (const { copy, format, originalRelative, entries, cargoPackages, declaringManifests } of copies) {
|
|
166
|
+
for (const { copy, format, originalRelative, entries, cargoPackages, declaringManifests, nugetPackages } of copies) {
|
|
129
167
|
const lockDir = originalRelative === null ? "" : path.dirname(originalRelative);
|
|
130
168
|
let lookup = null;
|
|
131
169
|
try {
|
|
@@ -147,6 +185,9 @@ export async function buildRelationLookups(copies, maxTotalBytes = MAX_RELATION_
|
|
|
147
185
|
else if (format === "Cargo.lock" && cargoPackages !== undefined) {
|
|
148
186
|
lookup = cargoLockRelations(cargoPackages, declaringManifests);
|
|
149
187
|
}
|
|
188
|
+
else if (format === "packages.lock.json" && nugetPackages !== undefined) {
|
|
189
|
+
lookup = nugetRelations(nugetPackages);
|
|
190
|
+
}
|
|
150
191
|
}
|
|
151
192
|
catch {
|
|
152
193
|
lookup = null; // 解析できないファイルはunknown(スキャン自体は続ける)
|
|
@@ -272,6 +313,11 @@ export async function scanFromSnapshot(project, options) {
|
|
|
272
313
|
relationSources.push({ copy: generated, format: "Cargo.lock", originalRelative: copy.path,
|
|
273
314
|
cargoPackages: copy.allPackages, declaringManifests: copy.declaringManifests });
|
|
274
315
|
}
|
|
316
|
+
for (const copy of project.nugetCopies) {
|
|
317
|
+
const generated = await snapshot.writeGenerated(nugetLockCopyContent(copy.packages));
|
|
318
|
+
targets.push({ path: generated, format: "packages.lock.json" });
|
|
319
|
+
relationSources.push({ copy: generated, format: "packages.lock.json", originalRelative: copy.path, nugetPackages: copy.packages });
|
|
320
|
+
}
|
|
275
321
|
if (targets.length === 0)
|
|
276
322
|
return parseOsvScanOutput({ results: [] });
|
|
277
323
|
const report = await withScopeNotes(project.skippedFiles, () => snapshot.guard(() => runOsvScan(targets, options)));
|
|
@@ -1,21 +1,24 @@
|
|
|
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
|
* - 検出したファイルだけをosv-scannerに渡す(ディレクトリは渡さない)。解析形式も明示する
|
|
5
5
|
* - パッケージマネージャー・ビルドは実行しない。lockfileが無いマニフェストはcoverageに記録する
|
|
6
6
|
* - requirements.txtは取り込み先を検証し、プロジェクト外を読む場合はスキャン対象から外す
|
|
7
7
|
* - Cargo.lockはここで解析し、crates.io由来のパッケージだけをスキャンする(cargoLock.ts)
|
|
8
|
+
* - .NETはプロジェクトのファイルごとに情報源を対応付け、照会してよいパッケージだけをスキャンする(nugetProject.ts)
|
|
8
9
|
*
|
|
9
10
|
* 検出・除外の根拠はdocs/DESIGN_TODO.md「対象エコシステム拡大 詳細設計メモ」を参照。
|
|
10
11
|
*/
|
|
11
12
|
import { stat } from "node:fs/promises";
|
|
12
13
|
import path from "node:path";
|
|
13
14
|
import { ScanToolError } from "../errors.js";
|
|
15
|
+
import { isManifestFormat } from "./manifestFormats.js";
|
|
14
16
|
import { assertInsideAllowedRoot, resolveExistingPath, walkProjectFiles, } from "./projectWalk.js";
|
|
15
17
|
import { capitalizeReason, readRegularFile } from "./safeRead.js";
|
|
16
18
|
import { createNpmLockWorkBudget, MAX_NPM_LOCK_BYTES, MAX_NPM_LOCK_TOTAL_BYTES, parseNpmLock } from "./npmLockCopy.js";
|
|
17
19
|
import { analyzeRequirementsFile, createRequirementsReadContext, } from "./requirementsFile.js";
|
|
18
20
|
import { cargoTomlPackageName, cargoWorkspace, workspaceIncludes, createGlobWorkBudget, classifyCargoPackages, createCargoLockParseBudget, parseCargoLock, UNSCANNABLE_CRATE_REASONS, } from "./cargoLock.js";
|
|
21
|
+
import { analyzeNuget, collectNugetFile, emptyNugetFiles, NUGET_DEPS_JSON, NUGET_LOCKFILE, NUGET_PACKAGES_CONFIG, NUGET_PROJECT_ASSETS, NUGET_PROJECT_FILE, } from "./nugetProject.js";
|
|
19
22
|
/** ファイル名→(エコシステム, 解析形式)。npm-shrinkwrap.jsonはpackage-lock.json形式で解析する */
|
|
20
23
|
const LOCKFILES = new Map([
|
|
21
24
|
["pom.xml", { ecosystem: "Maven", format: "pom.xml" }],
|
|
@@ -63,6 +66,16 @@ const SKIPPED_DIRS = new Set([
|
|
|
63
66
|
".git", "node_modules", "target", "build", ".idea", ".vscode",
|
|
64
67
|
".venv", "venv", "site-packages", "__pycache__", ".tox", "vendor", ".cargo",
|
|
65
68
|
]);
|
|
69
|
+
/** .NETのプロジェクトのディレクトリのビルド出力(同じプロジェクトの重複。obj/project.assets.jsonは直接読む) */
|
|
70
|
+
const DOTNET_OUTPUT_DIRS = new Set(["bin", "obj"]);
|
|
71
|
+
const NONE = new Set();
|
|
72
|
+
function dotnetOutputDirs(entryNames) {
|
|
73
|
+
return entryNames.some((name) => NUGET_PROJECT_FILE.test(name)) ? DOTNET_OUTPUT_DIRS : NONE;
|
|
74
|
+
}
|
|
75
|
+
/** 直接指定できる.NETのファイル(プロジェクトのファイルは、そのディレクトリを走査する) */
|
|
76
|
+
function isDirectNugetFile(name) {
|
|
77
|
+
return NUGET_LOCKFILE.test(name) || NUGET_PACKAGES_CONFIG.test(name) || NUGET_DEPS_JSON.test(name) || name === NUGET_PROJECT_ASSETS;
|
|
78
|
+
}
|
|
66
79
|
function classifyFile(name) {
|
|
67
80
|
const lockfile = LOCKFILES.get(name);
|
|
68
81
|
if (lockfile)
|
|
@@ -261,7 +274,7 @@ async function findLockfileMissing(projectDir, markers, manifests, cargoCache) {
|
|
|
261
274
|
}
|
|
262
275
|
return missing;
|
|
263
276
|
}
|
|
264
|
-
async function finalize(projectDir, manifests, markers, allowedRootReal) {
|
|
277
|
+
async function finalize(projectDir, manifests, markers, allowedRootReal, nugetFiles = emptyNugetFiles()) {
|
|
265
278
|
const cargoCache = new CargoLockCache();
|
|
266
279
|
const result = {
|
|
267
280
|
projectDir,
|
|
@@ -277,6 +290,12 @@ async function finalize(projectDir, manifests, markers, allowedRootReal) {
|
|
|
277
290
|
requirementReferences: [],
|
|
278
291
|
lowerBounds: [],
|
|
279
292
|
skippedFiles: [],
|
|
293
|
+
nugetCopies: [],
|
|
294
|
+
localProjectCount: 0,
|
|
295
|
+
unscannableNugetPackages: [],
|
|
296
|
+
unresolvedPackageReferences: [],
|
|
297
|
+
nugetWarnings: [],
|
|
298
|
+
hasNuget: Object.values(nugetFiles).some((list) => list.length > 0),
|
|
280
299
|
};
|
|
281
300
|
const readContext = createRequirementsReadContext();
|
|
282
301
|
// A null value permanently marks a crate name with multiple declaration paths.
|
|
@@ -322,11 +341,22 @@ async function finalize(projectDir, manifests, markers, allowedRootReal) {
|
|
|
322
341
|
result.localCrateCount += lock.localCrates.length;
|
|
323
342
|
result.unscannableCrates.push(...lock.unscannable.map((c) => ({ file: manifest.path, name: c.name, version: c.version, reason: UNSCANNABLE_CRATE_REASONS[c.kind] })));
|
|
324
343
|
}
|
|
325
|
-
else {
|
|
344
|
+
else if (isManifestFormat(manifest.format)) {
|
|
326
345
|
result.targets.push({ path: absolute, format: manifest.format });
|
|
327
346
|
}
|
|
328
347
|
result.manifests.push(manifest);
|
|
329
348
|
}
|
|
349
|
+
if (result.hasNuget) {
|
|
350
|
+
const nuget = await analyzeNuget(projectDir, nugetFiles);
|
|
351
|
+
result.manifests.push(...nuget.manifests.map((m) => ({ path: m.path, ecosystem: "NuGet", format: m.format })));
|
|
352
|
+
result.nugetCopies = nuget.copies;
|
|
353
|
+
result.localProjectCount = nuget.localProjects;
|
|
354
|
+
result.unscannableNugetPackages = nuget.unscannable;
|
|
355
|
+
result.unresolvedPackageReferences = nuget.unresolved;
|
|
356
|
+
result.nugetWarnings = nuget.warnings;
|
|
357
|
+
result.lockfileMissing.push(...nuget.lockfileMissing.map((m) => ({ ...m, ecosystem: "NuGet" })));
|
|
358
|
+
result.skippedFiles.push(...nuget.skippedFiles);
|
|
359
|
+
}
|
|
330
360
|
if (result.manifests.length === 0) {
|
|
331
361
|
const details = [
|
|
332
362
|
...result.lockfileMissing.map((m) => `${m.path}: ${m.hint}`),
|
|
@@ -349,11 +379,12 @@ export async function detectProject(inputPath, options = {}) {
|
|
|
349
379
|
const stats = await stat(resolved);
|
|
350
380
|
const name = path.basename(resolved);
|
|
351
381
|
const direct = stats.isFile() ? classifyFile(name) : null;
|
|
382
|
+
const directNuget = stats.isFile() && isDirectNugetFile(name);
|
|
352
383
|
let projectDir;
|
|
353
384
|
if (stats.isDirectory()) {
|
|
354
385
|
projectDir = resolved;
|
|
355
386
|
}
|
|
356
|
-
else if (stats.isFile() && (direct !== null || LOCK_MARKERS.has(name))) {
|
|
387
|
+
else if (stats.isFile() && (direct !== null || directNuget || LOCK_MARKERS.has(name) || NUGET_PROJECT_FILE.test(name))) {
|
|
357
388
|
projectDir = path.dirname(resolved);
|
|
358
389
|
}
|
|
359
390
|
else {
|
|
@@ -365,18 +396,30 @@ export async function detectProject(inputPath, options = {}) {
|
|
|
365
396
|
if (direct !== null) {
|
|
366
397
|
return finalize(projectDir, [{ path: name, ...direct }], [], allowedRootReal);
|
|
367
398
|
}
|
|
399
|
+
if (directNuget) {
|
|
400
|
+
const nugetFiles = emptyNugetFiles();
|
|
401
|
+
if (name === NUGET_PROJECT_ASSETS)
|
|
402
|
+
nugetFiles.projectAssets.push(name);
|
|
403
|
+
else
|
|
404
|
+
collectNugetFile(nugetFiles, name, name);
|
|
405
|
+
return finalize(projectDir, [], [], allowedRootReal, nugetFiles);
|
|
406
|
+
}
|
|
368
407
|
const manifests = [];
|
|
369
408
|
const markers = [];
|
|
409
|
+
const nugetFiles = emptyNugetFiles();
|
|
370
410
|
await walkProjectFiles(projectDir, options, SKIPPED_DIRS, (relativePath, fileName) => {
|
|
371
411
|
const found = classifyFile(fileName);
|
|
372
412
|
if (found) {
|
|
373
413
|
manifests.push({ path: relativePath, ...found });
|
|
374
414
|
return true;
|
|
375
415
|
}
|
|
416
|
+
const nuget = collectNugetFile(nugetFiles, relativePath, fileName);
|
|
417
|
+
if (nuget !== null)
|
|
418
|
+
return nuget;
|
|
376
419
|
const group = LOCK_MARKERS.get(fileName);
|
|
377
420
|
if (group)
|
|
378
421
|
markers.push({ path: relativePath, group });
|
|
379
422
|
return false;
|
|
380
|
-
});
|
|
381
|
-
return finalize(projectDir, manifests, markers, allowedRootReal);
|
|
423
|
+
}, dotnetOutputDirs);
|
|
424
|
+
return finalize(projectDir, manifests, markers, allowedRootReal, nugetFiles);
|
|
382
425
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* osv-scannerに`--lockfile <形式>:<絶対パス>`で個別に渡せる解析形式(2.4.0・2.7.0で実機確認)。
|
|
3
3
|
* ここに無い形式はosv-scannerに渡さない。npm-shrinkwrap.jsonは"package-lock.json"形式で解析する。
|
|
4
4
|
* Cargo.lockは元のファイルを渡さず、crates.io由来のパッケージだけを書いたコピーを渡す(cargoLock.ts)。
|
|
5
|
+
* packages.lock.jsonは.NETの各形式から本サーバーが書いたコピーだけを渡す(nugetFiles.ts。元のファイルは渡さない)。
|
|
5
6
|
*/
|
|
6
7
|
export const MANIFEST_FORMATS = [
|
|
7
8
|
"pom.xml",
|
|
@@ -18,6 +19,7 @@ export const MANIFEST_FORMATS = [
|
|
|
18
19
|
"requirements.txt",
|
|
19
20
|
"go.mod",
|
|
20
21
|
"Cargo.lock",
|
|
22
|
+
"packages.lock.json",
|
|
21
23
|
];
|
|
22
24
|
const FORMAT_SET = new Set(MANIFEST_FORMATS);
|
|
23
25
|
export function isManifestFormat(value) {
|