osv-scanner-mcp 0.17.0 → 0.19.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 CHANGED
@@ -7,13 +7,13 @@
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, 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).
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, Packagist, and NuGet. Setup instructions are provided for Claude Code, Claude Desktop, Codex CLI, Antigravity, and VS Code (GitHub Copilot).
11
11
 
12
12
  ## Features
13
13
 
14
14
  - **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
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
+ - **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), PHP (Packagist), and .NET (NuGet). Known unfixed and informational advisories remain visible. The recommended version is also checked against the OSV database for additional fix-target vulnerabilities
17
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
18
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
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
@@ -22,12 +22,16 @@ An MCP server that wraps Google's [OSV-Scanner](https://github.com/google/osv-sc
22
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
23
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)
24
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
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
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, Composer ordering for Packagist, and NuGet ordering for NuGet
26
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
27
27
 
28
28
  ## Composer support
29
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.
30
+ Pass a project directory, `composer.lock`, or `composer.json` to `scan_project` or `suggest_fix`. Direct/transitive dependency analysis (source checkout) uses the companion `composer.json` in the lockfile's own directory. 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 companion's `require` and `require-dev` are direct dependencies. Packages reachable through the locked packages' `require` are transitive; a dependency's own `require-dev`, `suggest`, and `conflict` are not dependency edges. Direct dependencies include their project-relative `composer.json` in `declared_in`. `introduced_by` names each direct root that transitively requires a package (up to 10, with the remainder in `introduced_by_omitted`). A direct dependency also required by another root remains direct and includes that root in `introduced_by`; cycles do not introduce a package through itself. Transitive dependencies do not have `declared_in`, since the root does not declare them directly.
33
+
34
+ This is name-based graph traversal of the lockfile, not a Composer constraint solver or freshness check. Duplicate installed names, virtual requirements, and references affected by `provide`/`replace` are not guessed; unreachable packages remain `unknown`. Local path and skipped branch entries may connect roots to scanned packages, but their names/metadata are still excluded from scanner input. No path-repository URL or nested package's composer.json is followed. Missing, malformed, symlinked, oversized, or otherwise unreadable companion manifests leave relationships unknown, with a `composer_warnings` note; locked release packages continue to be scanned. The companion is captured safely once (4 MiB per file), shares the Composer 64 MiB input/JSON structure budgets, and traversal/indexing share a 2,000,000-operation budget across all Composer lockfiles. Relationship uncertainty does not itself make dependency scan coverage incomplete.
31
35
 
32
36
  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
37
 
@@ -308,9 +312,16 @@ Detects the lockfiles and manifests in a project and scans the Java / JavaScript
308
312
  - `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`
309
313
  - `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
314
  - .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
315
+ - Composer (source checkout): the companion root's `require` / `require-dev` are direct, and locked `require` reachability establishes transitive dependencies. `declared_in` names the companion for direct dependencies; `introduced_by` lists transitively requiring direct roots. See [Composer support](#composer-support) for ambiguity and work limits.
311
316
  - 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`
312
317
  - `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
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
318
+ - Upgrade recommendations (`suggest_fix`) are available for Maven, npm, PyPI, Go, Rust (crates.io), PHP (Packagist), and .NET (NuGet; source checkout).
319
+
320
+ **NuGet recommendations (source checkout).** `suggest_fix` compares NuGet release versions with one to four numeric components (missing components become zero), including legacy revisions such as `1.2.3.4`. Numeric components must fit a nonnegative 32-bit integer; unsupported formats withhold a recommendation. Prerelease identifiers compare numerically when they fit signed 32-bit integers, and otherwise by ASCII case-insensitive lexical order, matching NuGet VersionRelease; numeric prerelease identifiers with leading zeroes are rejected. Build metadata does not affect ordering. Package IDs match case-insensitively. Recommendations use OSV `ECOSYSTEM` affected ranges and are rechecked against OSV; unsupported or incomplete ranges cannot establish a verified fix. Stable candidates are preferred; prerelease-only fixes are labeled. The existing cautious 0.x upgrade tiers apply.
321
+
322
+ For direct dependencies, update the `PackageReference` (or `packages.config` in older projects); with central management, update `PackageVersion` in `Directory.Packages.props`. For transitive dependencies, update the parent dependency or explicitly reference a compatible fixed version if NuGet resolution allows it. Runtime packs (`Microsoft.NETCore.App.Runtime.*`, `Microsoft.AspNetCore.App.Runtime.*`, `Microsoft.WindowsDesktop.App.Runtime.*`) generally require an SDK/runtime or target-framework update rather than a new explicit package reference. Check target-framework/RID compatibility and all version constraints, regenerate restore/lockfiles, and rescan. These are suggestions, not edits; `dotnet restore` and MSBuild are not run.
323
+
324
+ `PackageReference Version="x"` or an inclusive minimum range is scanned at its lower bound, not at a confirmed restored version. Its recommendation retains `version_is_lower_bound: true`, and the note means raising that bound. Coverage still records missing restore/lockfiles and unresolved references. Neither a verified affected-range check nor `candidate_check: "clean"` proves framework compatibility, availability on a private feed, or absence of unknown vulnerabilities.
314
325
 
315
326
  ### `scan_java_project`
316
327
 
@@ -375,7 +386,7 @@ If an existing parent POM cannot be read (larger than 10 MiB, not a regular file
375
386
  ```
376
387
 
377
388
  - `packages` is sorted by the most severe vulnerability, and each `vulnerabilities` list by severity (unknown 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
389
+ - `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), Composer ordering for Packagist (since v0.17.0), and NuGet ordering for NuGet (since v0.18.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
379
390
  - Vulnerabilities without a severity score are reported as `null` / `"unknown"`
380
391
 
381
392
  ### `scan_java_artifact`
@@ -432,7 +443,7 @@ Invalid JSON returns `invalid_sbom`, an unsupported format `unsupported_sbom_for
432
443
 
433
444
  ### `suggest_fix`
434
445
 
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:
446
+ 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), PHP (Packagist / Composer), and .NET (NuGet; source checkout). Instead of simply taking the highest fixed version, it picks the fixed version closest to the current release line, falling back through three tiers:
436
447
 
437
448
  | Tier | Meaning |
438
449
  |---|---|
@@ -483,6 +494,7 @@ For npm, Go, PyPI, and Rust, the "same release line" is the range a caret (`^`)
483
494
  - Suggestions include an `update_hint`, tailored to whether the package is a direct or transitive dependency:
484
495
  - Maven (from `pom.xml`): for a direct dependency, update the `<dependency>` version (or the parent POM, property, or BOM that manages it); for a transitive dependency, override the version in `<dependencyManagement>` or update the direct dependency that requires it
485
496
  - npm: for a direct dependency, update the version in the `package.json` listed in `declared_in`; for a transitive dependency, update the direct dependency named in `introduced_by`, or set the version with `overrides` in the root `package.json` (effective only in the root project)
497
+ - Composer: for direct dependencies, update the constraint in `declared_in`; for transitive dependencies, update the direct roots in `introduced_by` or their constraints, then regenerate the lockfile with dependency updates allowed, scripts and plugins disabled. PHP and dependency compatibility still require verification.
486
498
  - Go: `go get <module>@<version>` (also for `// indirect` modules). For a module affected by `replace`, update the version in the `replace` directive instead of `require`. Major versions 2 and later use a different module path (`/v2` and so on) and are separate packages in OSV, so fixes in a newer major line are not included as candidates. If the current version is a pseudo-version (an untagged commit), `upgrade_note` says so
487
499
  - PyPI: update the version in `requirements.txt`, `pyproject.toml`, or `Pipfile` and regenerate the lockfile; for a transitive dependency, use a pip constraints file (`-c`) or the override settings of uv or Poetry
488
500
  - Rust: where version requirements permit it, `cargo update -p <crate>@<current> --precise <recommended>` updates the lockfile. Otherwise change the direct requirement in the declaring Cargo.toml or update the parent dependency in `introduced_by`. Cross-major upgrades may break compatibility, including 0.x updates. Pre-releases may require explicit `=<version>` requirements and must be allowed by transitive parents. Build metadata is omitted from `--precise`. `[patch.crates-io]` changes a source, not the allowed version range; backports must satisfy parent requirements. Check yanked versions, MSRV, and compiler compatibility. Cargo is never executed by the server
@@ -645,7 +657,7 @@ Design notes and open items are in [docs/DESIGN_TODO.md](docs/DESIGN_TODO.md) (i
645
657
  - [x] Scan history and changes between runs (`scan_history`, opt-in)
646
658
  - [x] `scan_project` for .NET (NuGet), with per-project matching and lower bounds for project files
647
659
  - [x] `scan_project` and `suggest_fix` for PHP (Composer `composer.lock`)
648
- - [ ] `suggest_fix` for .NET (NuGet)
660
+ - [x] `suggest_fix` for .NET (NuGet; source checkout)
649
661
  - [ ] Direct and transitive dependencies for `gradle.lockfile` and the YAML/TOML lockfiles
650
662
 
651
663
  ## License
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.17.0";
69
+ const SERVER_VERSION = "0.19.0";
70
70
  const server = new McpServer({
71
71
  name: "osv-scanner-mcp",
72
72
  version: SERVER_VERSION,
@@ -88,9 +88,9 @@ server.registerTool("scan_project", {
88
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
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. " +
90
90
  "Vulnerabilities marked informational (RustSec notices such as unmaintained or unsound crates) are not counted in vulnerability_count; they are counted in informational_count. " +
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). " +
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.",
91
+ "Each package's dependency_relation tells whether it is a direct or transitive dependency (determined for package-lock.json, go.mod, requirements.txt, pom.xml, Cargo.lock, .NET files and Composer; unknown where the format or captured metadata cannot establish it). " +
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. A safely captured companion composer.json defines direct require/require-dev roots; locked require edges establish introduced_by, and direct declarations use declared_in. Missing/uninterpretable companions, ambiguous providers/replacements or names, and analysis limits leave relationships unknown. 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, PHP, and .NET (NuGet). Cargo.lock dependency graphs provide direct/transitive relations, introduced_by, and uniquely mapped declaring Cargo.toml paths in declared_in.",
94
94
  inputSchema: {
95
95
  project_path: z
96
96
  .string()
@@ -114,13 +114,14 @@ server.registerTool("scan_java_project", {
114
114
  server.registerTool("suggest_fix", {
115
115
  title: "Recommend upgrades that fix vulnerabilities",
116
116
  description: "Scans the project with the same detection as scan_project and recommends an upgrade version for each vulnerable package. " +
117
- "Recommendations are available for Java (Maven / Gradle), JavaScript (npm), Python (PyPI), Go, Rust (crates.io), and PHP (Packagist / Composer). " +
117
+ "Recommendations are available for Java (Maven / Gradle), JavaScript (npm), Python (PyPI), Go, Rust (crates.io), PHP (Packagist / Composer), and .NET (NuGet). " +
118
+ "NuGet versions support up to four numeric components, case-insensitive prerelease labels, and ignored build metadata. Updates must satisfy target-framework and dependency constraints; runtime packs generally require SDK/runtime or target-framework updates. No dotnet restore or MSBuild is run. " +
118
119
  "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. " +
119
120
  "The fixed version closest to the current release line is chosen with a three-tier fallback " +
120
121
  "(same_minor: same release line -> major_internal: same major version -> cross_major: major upgrade), " +
121
122
  "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). " +
122
- "Dependencies scanned at a requirements.txt lower bound (>=) are marked version_is_lower_bound, and the recommendation then means raising the lower bound. " +
123
- "update_hint explains how to upgrade, depending on whether the package is a direct or transitive dependency (dependency_relation; for npm and Cargo.lock also introduced_by and declared_in). Cargo updates are conditional on parent version requirements; cross-major updates may require Cargo.toml changes. " +
123
+ "Dependencies scanned at a requirements.txt lower bound (>=) or a NuGet PackageReference minimum/range are marked version_is_lower_bound, and the recommendation then means raising the lower bound rather than confirming the installed version. " +
124
+ "update_hint explains how to upgrade, depending on whether the package is a direct or transitive dependency (dependency_relation; for npm, Cargo.lock and Composer also introduced_by and declared_in). Composer hints name the declaring manifest for direct dependencies and the introducing roots for transitive dependencies; regenerate the lockfile in a trusted environment and verify constraints. Cargo updates are conditional on parent version requirements; cross-major updates may require Cargo.toml changes. " +
124
125
  "The recommended version is checked against api.osv.dev, and versions with known vulnerabilities that do not affect the current version are avoided (result in candidate_check). " +
125
126
  "clean excludes known unfixed advisories, known informational advisories that are not fix targets, and new Rust unmaintained/notice records; these may still apply. New unsound records are fix targets. " +
126
127
  "With has_known_vulnerabilities, failed, or skipped, the recommended version has not been confirmed safe; conflict means the data disagreed and the recommendation is withheld. " +
@@ -38,6 +38,26 @@ function updateHint(pkg) {
38
38
  const relation = pkg.dependency_relation;
39
39
  const by = pkg.introduced_by?.join(", ");
40
40
  switch (pkg.ecosystem) {
41
+ case "Packagist": {
42
+ if (relation === "direct")
43
+ return `Direct dependency. Update its constraint in ${pkg.declared_in?.join(", ") ?? "composer.json"}` +
44
+ (by ? `. It is also required transitively by ${by}, whose constraints may need updating` : "") +
45
+ ". Regenerate composer.lock in a trusted environment with Composer scripts and plugins disabled; check PHP, extension and dependency constraints. Composer is not executed by this server";
46
+ if (relation === "transitive")
47
+ return `Transitive dependency. Update ${by ? `the direct dependencies that require it (${by})` : "the direct dependency that requires it"} or adjust their constraints, then regenerate composer.lock with --with-all-dependencies --no-scripts --no-plugins in a trusted environment. Check PHP and extension requirements. Composer is not executed by this server`;
48
+ return UPDATE_HINTS.Packagist;
49
+ }
50
+ case "NuGet": {
51
+ if (/^Microsoft\.(?:NETCore|AspNetCore|WindowsDesktop)\.App\.Runtime\./i.test(pkg.name)) {
52
+ return "Runtime pack. Update the .NET SDK/runtime or target framework that supplies this pack and regenerate restore/build outputs; do not add an explicit PackageReference merely to change the runtime pack. Check framework/RID compatibility and rescan. This server does not run dotnet restore or MSBuild";
53
+ }
54
+ const shared = "If centrally managed, change PackageVersion in Directory.Packages.props. Regenerate restore/lockfiles and rescan in a trusted environment; verify target-framework compatibility and dependency constraints. This server does not run dotnet restore or MSBuild";
55
+ if (relation === "direct")
56
+ return "Direct dependency. Update its PackageReference in the project file (or packages.config for older projects). " + shared;
57
+ if (relation === "transitive")
58
+ return "Transitive dependency. Update the direct dependency that requires it, or explicitly reference a compatible fixed version when NuGet dependency resolution permits it. " + shared;
59
+ return "Update the dependency version in the project file or packages.config; for a transitive dependency, update its parent or explicitly reference a compatible fixed version. " + shared;
60
+ }
41
61
  case "npm":
42
62
  if (relation === "direct") {
43
63
  return `Direct dependency. Update the version in ${pkg.declared_in?.join(", ") ?? "package.json"}` +
@@ -133,6 +153,7 @@ function hintFields(pkg, recommended = null, tier = null) {
133
153
  /** スキャン結果に付いた直接/推移的依存の項目を提案にも写す(無ければ何も出さない) */
134
154
  function relationFields(pkg) {
135
155
  return {
156
+ ...(pkg.version_is_lower_bound ? { version_is_lower_bound: true } : {}),
136
157
  ...(pkg.dependency_relation !== undefined ? { dependency_relation: pkg.dependency_relation } : {}),
137
158
  ...(pkg.introduced_by !== undefined ? { introduced_by: pkg.introduced_by } : {}),
138
159
  ...(pkg.introduced_by_omitted !== undefined ? { introduced_by_omitted: pkg.introduced_by_omitted } : {}),
@@ -213,15 +234,19 @@ function buildNote(scheme, pkg, recommended, tier, fixableCount, unfixedCount) {
213
234
  if (unfixedCount > 0)
214
235
  note += `. The remaining ${unfixedCount} CVEs have no fixed version newer than the current one and are excluded from the fix. Check recommended_status`;
215
236
  }
216
- if (pkg.version_is_lower_bound) {
217
- note += `. The current version (${pkg.version}) is the lower bound in requirements.txt (>= or ~=) and may differ from the installed version. ` +
218
- "The recommendation means raising the lower bound to at least that version";
219
- }
237
+ note += lowerBoundNote(pkg);
220
238
  if (pkg.ecosystem === "Go" && GO_PSEUDO_VERSION.test(pkg.version)) {
221
239
  note += ". The current version is a pseudo-version (an untagged commit)";
222
240
  }
223
241
  return note;
224
242
  }
243
+ function lowerBoundNote(pkg) {
244
+ if (!pkg.version_is_lower_bound)
245
+ return "";
246
+ const source = pkg.ecosystem === "NuGet" ? "PackageReference (NuGet minimum/range)" : "requirements.txt (>= or ~=)";
247
+ return `. The current version (${pkg.version}) is the lower bound in ${source} and may differ from the installed version. ` +
248
+ "Any recommendation means raising the lower bound to at least that version";
249
+ }
225
250
  /**
226
251
  * 1パッケージ分のアップグレード提案を組み立てる(同期・純粋)。
227
252
  * contextを渡すと、照会で見つかった脆弱性も避けて候補を選び直す(contextなしの結果が従来の推奨)。
@@ -318,9 +343,9 @@ export function suggestUpgradeForPackage(pkg, context = {}) {
318
343
  upgrade_note: (recommended === null && fixableCount > 0
319
344
  ? "No known fixed version could be confirmed to be outside the affected ranges of every CVE to fix. The recommendation is also withheld when the data are insufficient or use unsupported range types." +
320
345
  (unparseableCount > 0 ? ` ${unparseableCount} CVEs list fixed versions that cannot be parsed as versions (tier: unparseable_fix), so the recommendation is withheld.` : "")
321
- : buildNote(scheme, pkg, recommended, upgradeTier, fixableCount, unfixedCount)) + (informationalNotes ? ` ${informationalNotes}` : ""),
346
+ : buildNote(scheme, pkg, recommended, upgradeTier, fixableCount, unfixedCount)) +
347
+ (recommended === null && fixableCount > 0 ? lowerBoundNote(pkg) : "") + (informationalNotes ? ` ${informationalNotes}` : ""),
322
348
  ...hintFields(pkg, recommended, upgradeTier),
323
- ...(pkg.version_is_lower_bound ? { version_is_lower_bound: true } : {}),
324
349
  per_cve_detail: details,
325
350
  verification: recommended === null ? "no_verified_candidate" : "verified",
326
351
  };
@@ -13,6 +13,7 @@ import { compareMavenVersions, mavenVersionSeries } from "../utils/mavenVersion.
13
13
  import { compareParsedPep440, parsePep440 } from "../utils/pep440Version.js";
14
14
  import { compareParsedSemver, parseSemver } from "../utils/semverVersion.js";
15
15
  import { compareComposerVersions, parseComposerVersion } from "../utils/composerVersion.js";
16
+ import { compareNugetVersions, parseNugetVersion } from "../utils/nugetVersion.js";
16
17
  const SEMVER_ECOSYSTEMS = new Set(["npm", "Go", "crates.io"]);
17
18
  const MAVEN_SCHEME = {
18
19
  isValid: () => true,
@@ -120,8 +121,27 @@ const COMPOSER_SCHEME = {
120
121
  return parsed ? `${parsed.parts[0]}.${parsed.parts[1]}` : null;
121
122
  },
122
123
  };
124
+ /** NuGet VersionRelease ordering; prerelease names and package IDs ignore ASCII case. */
125
+ const NUGET_SCHEME = {
126
+ isValid: (v) => parseNugetVersion(v) !== null,
127
+ compare: compareNugetVersions,
128
+ isPrerelease: (v) => (parseNugetVersion(v)?.prerelease.length ?? 0) > 0,
129
+ classify(current, candidate) {
130
+ const a = parseNugetVersion(current), b = parseNugetVersion(candidate);
131
+ if (!a || !b)
132
+ return "cross_major";
133
+ 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] });
134
+ },
135
+ seriesLabel(v) {
136
+ const parsed = parseNugetVersion(v);
137
+ return parsed ? parsed.parts[0] === 0 && parsed.parts[1] === 0
138
+ ? `0.0.${parsed.parts[2]}` : `${parsed.parts[0]}.${parsed.parts[1]}` : null;
139
+ },
140
+ };
123
141
  /** 修正版の推奨に対応するエコシステムの操作。未対応ならnull */
124
142
  export function versionSchemeFor(ecosystem) {
143
+ if (ecosystem === "NuGet")
144
+ return NUGET_SCHEME;
125
145
  if (ecosystem === "Packagist")
126
146
  return COMPOSER_SCHEME;
127
147
  if (ecosystem === "Maven")
@@ -137,6 +157,8 @@ export function versionSchemeFor(ecosystem) {
137
157
  * PyPIはPEP 503の正規化(小文字化、`-` `_` `.`の連続を`-`)で比較する(実データでは完全一致したが防御的に)
138
158
  */
139
159
  export function samePackageName(a, b, ecosystem) {
160
+ if (ecosystem === "NuGet")
161
+ return a.toLowerCase() === b.toLowerCase();
140
162
  if (ecosystem !== "PyPI")
141
163
  return a === b;
142
164
  const normalize = (name) => name.toLowerCase().replace(/[-_.]+/g, "-");
@@ -166,7 +166,7 @@ export async function buildRelationLookups(copies, maxTotalBytes = MAX_RELATION_
166
166
  const lookups = new Map();
167
167
  const npmWorkBudget = createNpmLockWorkBudget();
168
168
  let remaining = maxTotalBytes;
169
- for (const { copy, format, originalRelative, entries, cargoPackages, declaringManifests, nugetPackages } of copies) {
169
+ for (const { copy, format, originalRelative, entries, cargoPackages, declaringManifests, nugetPackages, composerLookup } of copies) {
170
170
  const lockDir = originalRelative === null ? "" : path.dirname(originalRelative);
171
171
  let lookup = null;
172
172
  try {
@@ -191,6 +191,9 @@ export async function buildRelationLookups(copies, maxTotalBytes = MAX_RELATION_
191
191
  else if (format === "packages.lock.json" && nugetPackages !== undefined) {
192
192
  lookup = nugetRelations(nugetPackages);
193
193
  }
194
+ else if (format === "composer.lock") {
195
+ lookup = composerLookup ?? null;
196
+ }
194
197
  }
195
198
  catch {
196
199
  lookup = null; // 解析できないファイルはunknown(スキャン自体は続ける)
@@ -324,7 +327,7 @@ export async function scanFromSnapshot(project, options) {
324
327
  for (const copy of project.composerCopies ?? []) {
325
328
  const generated = await snapshot.writeGenerated(copy.content);
326
329
  targets.push({ path: generated, format: "composer.lock" });
327
- relationSources.push({ copy: generated, format: "composer.lock", originalRelative: copy.path });
330
+ relationSources.push({ copy: generated, format: "composer.lock", originalRelative: copy.path, composerLookup: copy.relations });
328
331
  }
329
332
  if (targets.length === 0)
330
333
  return parseOsvScanOutput({ results: [] });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `suggest_fix`ツールのハンドラ。
3
3
  * scan_projectと同じ検出・スキャンを実行し、脆弱なパッケージごとの
4
- * 推奨アップグレードバージョン(3段階Tier)を返す。推奨はMaven・npm・Go・PyPI・crates.ioに対応し、
4
+ * 推奨アップグレードバージョン(3段階Tier)を返す。推奨はMaven・npm・Go・PyPI・crates.io・Packagist・NuGetに対応し、
5
5
  * それ以外はunsupported_ecosystemとして返す。
6
6
  */
7
7
  import { isRemoteResolutionDisabled } from "../osv/runner.js";
@@ -1,5 +1,6 @@
1
1
  import { parseNpmLock } from "./npmLockCopy.js";
2
2
  import { parseComposerVersion } from "./composerVersion.js";
3
+ import { composerLinkNames, composerPackageName } from "./composerRelations.js";
3
4
  const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4
5
  /** Only names and release versions reach OSV; repository URLs, scripts, and commit IDs are never forwarded. */
5
6
  export function composerLockCopy(text, budget) {
@@ -9,6 +10,7 @@ export function composerLockCopy(text, budget) {
9
10
  }
10
11
  const issues = [];
11
12
  const warnings = [];
13
+ const relationPackages = [];
12
14
  let uncertainOrigin = 0;
13
15
  // dist.type=pathは自分のコード(monorepoのパッケージ)。Rustのlocal_crates・.NETのlocal_projectsと同じく数だけ返し、範囲の欠けにしない
14
16
  let localPackages = 0;
@@ -17,11 +19,16 @@ export function composerLockCopy(text, budget) {
17
19
  const packages = [];
18
20
  let skipped = 0;
19
21
  for (const raw of (data[group] ?? [])) {
22
+ if (record(raw) && composerPackageName(raw.name)) {
23
+ const provides = composerLinkNames(raw.provide), replaces = composerLinkNames(raw.replace);
24
+ relationPackages.push({ name: raw.name, version: typeof raw.version === "string" ? raw.version : "",
25
+ requires: composerLinkNames(raw.require), aliases: provides === null || replaces === null ? null : [...provides, ...replaces] });
26
+ }
20
27
  if (record(raw) && record(raw.dist) && raw.dist.type === "path") {
21
28
  localPackages++;
22
29
  continue;
23
30
  }
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 ||
31
+ if (!record(raw) || !composerPackageName(raw.name) ||
25
32
  typeof raw.version !== "string" || parseComposerVersion(raw.version) === null) {
26
33
  skipped++;
27
34
  continue;
@@ -36,5 +43,5 @@ export function composerLockCopy(text, budget) {
36
43
  }
37
44
  if (uncertainOrigin > 0)
38
45
  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 };
46
+ return { content: JSON.stringify(output), issues, warnings, packageCount: output.packages.length + output["packages-dev"].length, localPackages, relationPackages };
40
47
  }
@@ -0,0 +1,96 @@
1
+ import { MAX_DEPENDENCY_EDGES } from "./dependencyRelations.js";
2
+ import { parseNpmLock } from "./npmLockCopy.js";
3
+ export const createComposerRelationBudget = () => ({ remaining: MAX_DEPENDENCY_EDGES });
4
+ const record = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5
+ export const composerPackageName = (v) => typeof v === "string" && v.length <= 200 && /^[a-z0-9][a-z0-9_.-]*\/[a-z0-9][a-z0-9_.-]*$/.test(v);
6
+ /** Platform requirements such as php, ext-json and lib-* are not package edges. */
7
+ export function composerLinkNames(value) {
8
+ if (value === undefined)
9
+ return [];
10
+ if (!record(value))
11
+ return null;
12
+ const names = [];
13
+ for (const [name, constraint] of Object.entries(value)) {
14
+ if (typeof constraint !== "string")
15
+ return null;
16
+ const looksLikePackage = name.includes("/");
17
+ if (composerPackageName(name))
18
+ names.push(name);
19
+ else if (looksLikePackage)
20
+ return null;
21
+ }
22
+ return names;
23
+ }
24
+ /** Analyze only the captured companion manifest, never another root or a path repository's URL. */
25
+ export function composerRootRequirements(text, budget) {
26
+ const root = parseNpmLock(text, budget);
27
+ if (!record(root))
28
+ return null;
29
+ const production = composerLinkNames(root.require), dev = composerLinkNames(root["require-dev"]);
30
+ return production === null || dev === null ? null : [...new Set([...production, ...dev])];
31
+ }
32
+ /** Name-based reachability of the locked solution, not a Composer constraint solver.
33
+ * require-dev is root-only; suggest/conflict/replace/provide are not dependency edges.
34
+ * Provider/replacement ambiguity and duplicate installed names are never guessed.
35
+ */
36
+ export function composerRelations(packages, roots, budget = createComposerRelationBudget()) {
37
+ const charge = () => --budget.remaining >= 0;
38
+ const byName = new Map();
39
+ const aliased = new Set();
40
+ for (const pkg of packages) {
41
+ if (!charge() || pkg.aliases === null)
42
+ return null;
43
+ byName.set(pkg.name, byName.has(pkg.name) ? null : pkg);
44
+ for (const alias of pkg.aliases) {
45
+ if (!charge())
46
+ return null;
47
+ if (alias !== pkg.name)
48
+ aliased.add(alias);
49
+ }
50
+ }
51
+ const resolve = (name) => aliased.has(name) ? null : byName.get(name) ?? null;
52
+ const direct = new Set();
53
+ for (const name of roots) {
54
+ if (!charge())
55
+ return null;
56
+ if (resolve(name))
57
+ direct.add(name);
58
+ }
59
+ const introduced = new Map();
60
+ for (const origin of direct) {
61
+ const visited = new Set([origin]);
62
+ const queue = [origin];
63
+ while (queue.length > 0) {
64
+ if (!charge())
65
+ return null;
66
+ const current = resolve(queue.pop());
67
+ for (const name of current?.requires ?? []) {
68
+ if (!charge())
69
+ return null;
70
+ const target = resolve(name);
71
+ if (!target || visited.has(name))
72
+ continue;
73
+ visited.add(name);
74
+ queue.push(name);
75
+ let sources = introduced.get(name);
76
+ if (!sources)
77
+ introduced.set(name, sources = new Set());
78
+ sources.add(origin);
79
+ }
80
+ }
81
+ }
82
+ const results = new Map();
83
+ for (const [name, pkg] of byName) {
84
+ if (!charge())
85
+ return null;
86
+ if (!pkg || aliased.has(name))
87
+ continue;
88
+ const sources = introduced.get(name);
89
+ results.set(JSON.stringify([name, pkg.version]), {
90
+ relation: direct.has(name) ? "direct" : sources ? "transitive" : "unknown",
91
+ ...(direct.has(name) ? { declaredIn: new Set(["composer.json"]) } : {}),
92
+ ...(sources ? { introducedBy: sources } : {}),
93
+ });
94
+ }
95
+ return (name, version) => results.get(JSON.stringify([name, version])) ?? { relation: "unknown" };
96
+ }
@@ -20,6 +20,7 @@ import { analyzeRequirementsFile, createRequirementsReadContext, } from "./requi
20
20
  import { cargoTomlPackageName, cargoWorkspace, workspaceIncludes, createGlobWorkBudget, classifyCargoPackages, createCargoLockParseBudget, parseCargoLock, UNSCANNABLE_CRATE_REASONS, } from "./cargoLock.js";
21
21
  import { analyzeNuget, collectNugetFile, emptyNugetFiles, NUGET_DEPS_JSON, NUGET_LOCKFILE, NUGET_PACKAGES_CONFIG, NUGET_PROJECT_ASSETS, NUGET_PROJECT_FILE, } from "./nugetProject.js";
22
22
  import { composerLockCopy } from "./composerLock.js";
23
+ import { composerRelations, composerRootRequirements, createComposerRelationBudget } from "./composerRelations.js";
23
24
  /** ファイル名→(エコシステム, 解析形式)。npm-shrinkwrap.jsonはpackage-lock.json形式で解析する */
24
25
  const LOCKFILES = new Map([
25
26
  ["pom.xml", { ecosystem: "Maven", format: "pom.xml" }],
@@ -306,6 +307,7 @@ async function finalize(projectDir, manifests, markers, allowedRootReal, nugetFi
306
307
  };
307
308
  const readContext = createRequirementsReadContext();
308
309
  const composerBudget = createNpmLockWorkBudget();
310
+ const composerRelationBudget = createComposerRelationBudget();
309
311
  let composerBytes = MAX_NPM_LOCK_TOTAL_BYTES;
310
312
  // A null value permanently marks a crate name with multiple declaration paths.
311
313
  const cargoManifests = new Map();
@@ -330,8 +332,24 @@ async function finalize(projectDir, manifests, markers, allowedRootReal, nugetFi
330
332
  composerBytes -= read.bytes.length;
331
333
  try {
332
334
  const parsed = composerLockCopy(read.bytes.toString("utf8"), composerBudget);
333
- if (parsed.packageCount > 0)
334
- result.composerCopies.push({ path: manifest.path, content: parsed.content });
335
+ if (parsed.packageCount > 0) {
336
+ let relations = null;
337
+ const companion = await readRegularFile(path.join(path.dirname(absolute), "composer.json"), {
338
+ maxBytes: Math.min(4 * 1024 * 1024, composerBytes), boundary: projectDir,
339
+ });
340
+ if (companion.ok) {
341
+ composerBytes -= companion.bytes.length;
342
+ try {
343
+ const roots = composerRootRequirements(companion.bytes.toString("utf8"), composerBudget);
344
+ if (roots !== null)
345
+ relations = composerRelations(parsed.relationPackages, roots, composerRelationBudget);
346
+ }
347
+ catch { /* A companion failure affects relationships, not the scanned lockfile. */ }
348
+ }
349
+ if (relations === null)
350
+ result.composerWarnings.push(`${manifest.path}: Dependency relationships are unknown because its companion composer.json is unavailable/uninterpretable or the dependency analysis limit was reached; locked release packages are still scanned`);
351
+ result.composerCopies.push({ path: manifest.path, content: parsed.content, relations });
352
+ }
335
353
  result.skippedFiles.push(...parsed.issues.map((reason) => ({ path: manifest.path, reason })));
336
354
  result.composerWarnings.push(...parsed.warnings.map((warning) => `${manifest.path}: ${warning}`));
337
355
  result.composerLocalPackages += parsed.localPackages;
@@ -0,0 +1,41 @@
1
+ const INT_MAX = 2_147_483_647;
2
+ export function parseNugetVersion(version) {
3
+ if (version.length > 256)
4
+ return null;
5
+ const match = /^(\d{1,10}(?:\.\d{1,10}){0,3})(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version);
6
+ if (!match || match[0] !== version)
7
+ return null;
8
+ const parts = match[1].split(".").map(Number);
9
+ if (parts.some(n => n > INT_MAX))
10
+ return null;
11
+ while (parts.length < 4)
12
+ parts.push(0);
13
+ const prerelease = match[2]?.toLowerCase().split(".") ?? [];
14
+ // NuGet rejects purely numeric release labels with leading zeroes.
15
+ if (prerelease.some(label => /^0\d+$/.test(label)))
16
+ return null;
17
+ return { parts, prerelease };
18
+ }
19
+ export function compareNugetVersions(a, b) {
20
+ const x = parseNugetVersion(a), y = parseNugetVersion(b);
21
+ if (!x || !y)
22
+ return 0;
23
+ for (let i = 0; i < 4; i++)
24
+ if (x.parts[i] !== y.parts[i])
25
+ return x.parts[i] < y.parts[i] ? -1 : 1;
26
+ if (x.prerelease.length === 0 || y.prerelease.length === 0) {
27
+ return x.prerelease.length === y.prerelease.length ? 0 : x.prerelease.length === 0 ? 1 : -1;
28
+ }
29
+ for (let i = 0; i < Math.min(x.prerelease.length, y.prerelease.length); i++) {
30
+ const left = x.prerelease[i], right = y.prerelease[i];
31
+ const numeric = (label) => /^-?\d+$/.test(label) && Number(label) >= -2_147_483_648 && Number(label) <= INT_MAX;
32
+ const leftNumeric = numeric(left);
33
+ const rightNumeric = numeric(right);
34
+ if (leftNumeric !== rightNumeric)
35
+ return leftNumeric ? -1 : 1;
36
+ const l = leftNumeric ? Number(left) : left, r = rightNumeric ? Number(right) : right;
37
+ if (l !== r)
38
+ return l < r ? -1 : 1;
39
+ }
40
+ return Math.sign(x.prerelease.length - y.prerelease.length);
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osv-scanner-mcp",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "MCP server that wraps Google's OSV-Scanner to scan Java, JavaScript, Python, Go, Rust, .NET, and PHP dependencies for known vulnerabilities and recommend fixes",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",