osv-scanner-mcp 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -5
- package/dist/index.js +5 -5
- package/dist/osv/suggestFix.js +12 -3
- package/dist/tools/scanProject.js +26 -7
- package/dist/utils/composerLock.js +9 -2
- package/dist/utils/composerRelations.js +96 -0
- package/dist/utils/manifestDetector.js +72 -14
- package/dist/utils/nugetDependencyGraph.js +164 -0
- package/dist/utils/nugetFiles.js +4 -2
- package/dist/utils/nugetProject.js +59 -3
- package/dist/utils/pnpmLockCache.js +33 -0
- package/dist/utils/pnpmRelations.js +259 -0
- package/dist/utils/yarnLock.js +368 -0
- package/dist/utils/yarnLockCache.js +67 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -27,7 +27,11 @@ An MCP server that wraps Google's [OSV-Scanner](https://github.com/google/osv-sc
|
|
|
27
27
|
|
|
28
28
|
## Composer support
|
|
29
29
|
|
|
30
|
-
Pass a project directory, `composer.lock`, or `composer.json` to `scan_project` or `suggest_fix`.
|
|
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
|
|
|
@@ -291,7 +295,7 @@ Detects the lockfiles and manifests in a project and scans the Java / JavaScript
|
|
|
291
295
|
```
|
|
292
296
|
|
|
293
297
|
- **Always check `coverage`.** If `complete` is `false`, some dependencies were not scanned, so zero findings does not mean the project is safe
|
|
294
|
-
- `lockfile_missing`: manifests without a lockfile (`package.json`, `pyproject.toml`, `Pipfile`, `setup.py`, `build.gradle`, `Cargo.toml`, and so on). Not recorded if a lockfile of the same ecosystem is in the same directory. If there is only a lockfile in a parent directory, the manifest is not recorded only when `package-lock.json` (v2 or later) is confirmed to contain that directory (npm workspaces), or when `Cargo.lock` contains a local crate with the `[package] name` of the `Cargo.toml` and the directory is a member of the workspace according to `members` / `exclude` of the `Cargo.toml` next to `Cargo.lock` (Cargo workspaces; `members` globs with `*` and `?` are supported). A crate with the same name that cannot be confirmed as a member, such as a path dependency outside the workspace, is recorded as `unconfirmed`. Library crates often do not commit `Cargo.lock`, so their `Cargo.toml` is recorded here; generate one with `cargo generate-lockfile`. Otherwise it is recorded with `status: "missing"`, or `status: "unconfirmed"` for formats that cannot be checked (
|
|
298
|
+
- `lockfile_missing`: manifests without a lockfile (`package.json`, `pyproject.toml`, `Pipfile`, `setup.py`, `build.gradle`, `Cargo.toml`, and so on). Not recorded if a lockfile of the same ecosystem is in the same directory. If there is only a lockfile in a parent directory, the manifest is not recorded only when `package-lock.json` (v2 or later) is confirmed to contain that directory (npm workspaces), when `pnpm-lock.yaml` (formats 5.3/5.4/6.0/9.0) contains its exact normalized importer path (pnpm workspaces; source checkout), when Yarn workspace membership and captured declarations can be confirmed (see below; source checkout), or when `Cargo.lock` contains a local crate with the `[package] name` of the `Cargo.toml` and the directory is a member of the workspace according to `members` / `exclude` of the `Cargo.toml` next to `Cargo.lock` (Cargo workspaces; `members` globs with `*` and `?` are supported). A crate with the same name that cannot be confirmed as a member, such as a path dependency outside the workspace, is recorded as `unconfirmed`. Library crates often do not commit `Cargo.lock`, so their `Cargo.toml` is recorded here; generate one with `cargo generate-lockfile`. Otherwise it is recorded with `status: "missing"`, or `status: "unconfirmed"` for formats that cannot be checked (unsupported Yarn layouts, Python lockfiles, and so on). Generate the lockfile as described in `hint` (in a trusted environment) and scan again
|
|
295
299
|
- `unpinned_requirements`: lines in `requirements.txt` that do not pin a version. `kind` is `unpinned` (no version), `range` (`>`, `<`, `!=`, `==1.*`, combined ranges, and so on), or `lower_bound` (`>=`, `~=`). OSV-Scanner does not scan `unpinned` and `range` lines, and scans `lower_bound` lines at the lower bound (those packages are marked `version_is_lower_bound: true` and may differ from the installed version)
|
|
296
300
|
- `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
|
|
297
301
|
- `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`)
|
|
@@ -307,8 +311,21 @@ Detects the lockfiles and manifests in a project and scans the Java / JavaScript
|
|
|
307
311
|
- `requirements.txt`: dependencies written in the file are direct; dependencies resolved through deps.dev are transitive
|
|
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
|
-
- .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`.
|
|
311
|
-
-
|
|
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`. Origin/declaration enrichment is available in the source checkout as described below.
|
|
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.
|
|
316
|
+
- `pnpm-lock.yaml` (source checkout): lockfile formats 5.3, 5.4, 6.0 and 9.0 are supported. Importer `dependencies`, `devDependencies` and `optionalDependencies` are direct, with the lockfile's declaring importer `package.json` paths in `declared_in`. Locked dependency and optional-dependency edges establish `introduced_by` and transitive reachability. Workspace importers, npm aliases, scoped packages, multiple origins and peer-context-specific snapshots are supported. A package that is both direct and transitively required remains direct and also lists its origins. Local/Git references, unresolved identities and unreachable packages do not establish relationships; links and source files are never followed. Declaration paths reflect the lockfile, not independently verified current manifests. Workspace coverage (source checkout) uses the exact normalized importer path relative to an ancestor pnpm-lock.yaml: included children are covered, absent importers are `missing`, and unsupported/malformed/ambiguous or analysis-limited importer metadata is `unconfirmed`. Empty importer maps and Unicode/space paths are supported. Importer presence does not prove freshness or agreement with current package.json dependencies. Only lockfiles found inside the selected scan directory are considered. Coverage and vulnerability scanning use the same captured lockfile bytes (16 MiB per file, 64 MiB total); unreadable or oversized inputs are skipped with a reason. Importer parsing/indexing shares a 2,000,000-operation budget per detection, separate from relationship analysis. The lockfile is read from YAML's syntax tree in linear time (only strings, maps, and sequences; a 28,000-package lockfile takes about a second). Unsupported/malformed YAML, duplicate keys, anchors, aliases, tags, multi-document files, conservative nesting/line limits, inputs above 16 MiB, or more than 2,000,000 relationship operations per scan (shared by all pnpm lockfiles) leave relationships unknown; scanning continues. The existing shared relationship input byte limit and 10-origin output limit apply.
|
|
317
|
+
- `yarn.lock` (source checkout): Classic v1 and Berry metadata versions 4/5/6/8 support direct/transitive relations, `introduced_by` and `declared_in`. Classic uses captured root/workspace `package.json` declarations; Berry uses exact locked descriptors and `workspace:` entries. Dependencies and optional dependencies are followed; peers are not independently guessed or installed. Multiple selectors, scoped/npm aliases, cycles, multiple roots and direct/transitive overlap are supported. Local/Git/patch/portal/virtual identities that cannot establish a registry release remain unknown and are never followed to source files.
|
|
318
|
+
- Other formats (`gradle.lockfile`, `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`
|
|
319
|
+
|
|
320
|
+
**Yarn workspace coverage (source checkout):** Classic does not contain workspace importer records, so a captured private root's `workspaces` array (or `workspaces.packages`) must include the child and its captured dependency descriptors must resolve in the lockfile or to a uniquely named configured local workspace satisfying the requested SemVer range. Berry matches exact normalized `workspace:` resolution paths and checks captured dependency declarations against the locked workspace. Classic children need a nonempty name and usable version; manifests without them are not workspace members. Wildcards do not consume dot-prefixed path segments unless the pattern explicitly starts that segment with a dot. `*`, `?`, whole-segment `**`, exclusion patterns, one trailing `/` (as Yarn strips it), and Unicode/space paths are supported; other empty segments are unconfirmed; brace/extglob/character-class patterns remain unconfirmed. Independent children are missing; unreadable declarations, unresolved/ambiguous entries and analysis limits are unconfirmed. Coverage confirmation does not prove lockfile freshness or platform compatibility.
|
|
321
|
+
|
|
322
|
+
Yarn lockfiles and manifests are captured once within the selected scan boundary; coverage, relation metadata and scanning use those same inputs. Yarn is never run during a scan, and links, configuration plugins and source paths are not evaluated. Lockfiles are limited to 16 MiB each / 64 MiB total; captured package manifests to 4 MiB each / 64 MiB total with bounded JSON parsing. YAML nesting/line limits apply. Parsing and workspace coverage share one 2,000,000-operation budget per scan, and relationship analysis has a separate 2,000,000-operation budget, so exceeding the relationship limit leaves relations unknown without making confirmed coverage unconfirmed. Unsupported metadata leaves relations unknown and membership unconfirmed; unreadable/oversized lockfiles are skipped with a reason. A selected lockfile alone does not cause child manifests or files outside the selected scan directory to be searched. Berry declaration paths may come from locked workspace entries when companions were not captured.
|
|
323
|
+
|
|
324
|
+
**NuGet origins and declarations (source checkout):** `introduced_by` follows dependency edges from direct NuGet packages in each locked TFM or assets/deps target separately, then merges origins only for the same package/version in the report. Case-insensitive IDs and equivalent normalized versions match. `packages.lock.json` and assets targets contain a resolved solution, so edges resolve a unique installed name within that target; dependency constraints are not solved again. `deps.json` edges additionally require an exact normalized version match. Multiple origins and cycles are supported; a root does not introduce itself. Duplicate/ambiguous names and unmatched target frameworks are not guessed. Local project nodes can be traversed but are not guessed as direct package origins and remain excluded from OSV input. `introduced_by` retains the existing 10-name limit and omitted count.
|
|
325
|
+
|
|
326
|
+
`declared_in` is added to direct packages only when the captured source is uniquely associated with a `.csproj`/`.fsproj`/`.vbproj` and that captured project contains an unambiguous, unconditional `PackageReference` for the ID. A missing/property/range/floating version can still identify the reference when the restore result confirms its direct package; version resolution continues to come from restore metadata. Conditional, removed, updated, or duplicated references are not guessed. The field names the reference's project file, not the storage location of its centrally managed version. Direct-only project scans also expose this path and preserve lower-bound/coverage warnings. A standalone selected lockfile/assets file without captured project evidence, an ambiguous shared lockfile, `packages.config`, and a deployed `deps.json` do not imply a project declaration path. Paths embedded in restore/build outputs are not followed to read source files.
|
|
327
|
+
|
|
328
|
+
Graphs and declarations use already captured, size-limited inputs, without running restore or MSBuild or forwarding graph metadata to OSV. A shared 2,000,000-operation relationship budget bounds indexing/traversal/declaration work across the scan. Exhaustion omits uncertain metadata and adds a NuGet warning; existing vulnerability scanning and direct/transitive classification continue. Neither dependency reachability nor declaration matching proves freshness, constraint satisfaction, or TFM/RID compatibility. Update hints in both tools name a confirmed declaring project or the direct package origins where available.
|
|
312
329
|
- `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
330
|
- Upgrade recommendations (`suggest_fix`) are available for Maven, npm, PyPI, Go, Rust (crates.io), PHP (Packagist), and .NET (NuGet; source checkout).
|
|
314
331
|
|
|
@@ -488,7 +505,8 @@ For npm, Go, PyPI, and Rust, the "same release line" is the range a caret (`^`)
|
|
|
488
505
|
- Rust uses SemVer (`SEMVER` ranges) and Cargo caret compatibility: 0.x minor updates and 0.0.x patch updates are `cross_major`. `unmaintained` and `notice` advisories, and `unsound` advisories without a newer fixed version, remain in `per_cve_detail` with `tier: "informational"`, their kind, and `recommended_status`, but are not fix targets or counted as unfixed. An `unsound` advisory with a newer fixed version is a version-specific defect, so it is a fix target like a vulnerability (its tier is computed normally and it keeps `informational: "unsound"`). Packages with no fix target left return `verification: "informational_only"` without an upgrade. Unmaintained crates may need replacement; use `explain_vulnerability` for notices.
|
|
489
506
|
- Suggestions include an `update_hint`, tailored to whether the package is a direct or transitive dependency:
|
|
490
507
|
- 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
|
|
491
|
-
- 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
|
|
508
|
+
- npm (including pnpm): 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 use your package manager's root override configuration. Regenerate the lockfile and rescan.
|
|
509
|
+
- 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.
|
|
492
510
|
- 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
|
|
493
511
|
- 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
|
|
494
512
|
- 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
|
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.20.0";
|
|
70
70
|
const server = new McpServer({
|
|
71
71
|
name: "osv-scanner-mcp",
|
|
72
72
|
version: SERVER_VERSION,
|
|
@@ -86,10 +86,10 @@ server.registerTool("scan_project", {
|
|
|
86
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
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
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. " +
|
|
89
|
+
"The project's own projects are counted in local_projects. NuGet introduced_by follows target-scoped restore/build dependency graphs; declared_in names a uniquely associated, captured project with an unambiguous unconditional PackageReference. Ambiguous declarations, standalone build files, or analysis limits omit uncertain metadata. Graph paths are not followed and restore/MSBuild are not run. 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,
|
|
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. " +
|
|
91
|
+
"Each package's dependency_relation tells whether it is a direct or transitive dependency (determined for package-lock.json, pnpm-lock.yaml formats 5.3/5.4/6.0/9.0, Yarn Classic/Berry, go.mod, requirements.txt, pom.xml, Cargo.lock, .NET files and Composer; unknown where the format or captured metadata cannot establish it). pnpm uses locked importer declarations for declared_in and peer-context-specific dependency reachability for introduced_by; local links are not followed and unsupported/limited analysis remains unknown. Yarn derives origins/declarations from captured Classic manifests or Berry workspace entries; workspace coverage uses confirmed workspace membership and locked dependency descriptors, with ambiguous/unsupported cases unconfirmed. Yarn and project code are not executed. pnpm workspace coverage matches child project paths against captured ancestor lockfile importers; absent children are missing and unverifiable membership remains unconfirmed. " +
|
|
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
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
|
|
@@ -121,7 +121,7 @@ server.registerTool("suggest_fix", {
|
|
|
121
121
|
"(same_minor: same release line -> major_internal: same major version -> cross_major: major upgrade), " +
|
|
122
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). " +
|
|
123
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
|
|
124
|
+
"update_hint explains how to upgrade, depending on whether the package is a direct or transitive dependency (dependency_relation; for npm, Cargo.lock, Composer and NuGet also introduced_by and declared_in where evidence permits). NuGet hints name a confirmed declaring project or introducing direct packages; central management and SDK/runtime constraints still require verification. 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. " +
|
|
125
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). " +
|
|
126
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. " +
|
|
127
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. " +
|
package/dist/osv/suggestFix.js
CHANGED
|
@@ -38,15 +38,24 @@ 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
|
+
}
|
|
41
50
|
case "NuGet": {
|
|
42
51
|
if (/^Microsoft\.(?:NETCore|AspNetCore|WindowsDesktop)\.App\.Runtime\./i.test(pkg.name)) {
|
|
43
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";
|
|
44
53
|
}
|
|
45
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";
|
|
46
55
|
if (relation === "direct")
|
|
47
|
-
return
|
|
56
|
+
return `Direct dependency. Update its PackageReference in ${pkg.declared_in?.join(", ") ?? "the project file"} (or packages.config for older projects). ` + shared;
|
|
48
57
|
if (relation === "transitive")
|
|
49
|
-
return
|
|
58
|
+
return `Transitive dependency. Update ${by ? `the direct dependencies that require it (${by})` : "the direct dependency that requires it"}, or explicitly reference a compatible fixed version when NuGet dependency resolution permits it. ` + shared;
|
|
50
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;
|
|
51
60
|
}
|
|
52
61
|
case "npm":
|
|
@@ -56,7 +65,7 @@ function updateHint(pkg) {
|
|
|
56
65
|
}
|
|
57
66
|
if (relation === "transitive") {
|
|
58
67
|
return `Transitive dependency. Update ${by ? `the direct dependency that requires it (${by})` : "the direct dependency that requires it"} to a version that requires the recommended version or later. ` +
|
|
59
|
-
"If updating the direct dependency does not fix it,
|
|
68
|
+
"If updating the direct dependency does not fix it, use the package manager's root override configuration to select a compatible fixed version, regenerate the lockfile, and rescan";
|
|
60
69
|
}
|
|
61
70
|
return UPDATE_HINTS.npm;
|
|
62
71
|
case "Go": {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* complete=falseの場合は「検出0件でも安全とは言えない」旨の警告を付ける。
|
|
6
6
|
*/
|
|
7
7
|
import { writeFile } from "node:fs/promises";
|
|
8
|
+
import { createPnpmWorkBudget, pnpmLockRelations } from "../utils/pnpmRelations.js";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import { ScanToolError } from "../errors.js";
|
|
10
11
|
import { isRemoteResolutionDisabled, runOsvScan } from "../osv/runner.js";
|
|
@@ -165,20 +166,25 @@ const MAX_INTRODUCED_BY = 10;
|
|
|
165
166
|
export async function buildRelationLookups(copies, maxTotalBytes = MAX_RELATION_LOCKFILE_BYTES) {
|
|
166
167
|
const lookups = new Map();
|
|
167
168
|
const npmWorkBudget = createNpmLockWorkBudget();
|
|
169
|
+
// pnpmの関係の解析の作業量はスキャン全体で共有する(Composer・NuGetと同じ)
|
|
170
|
+
const pnpmWorkBudget = createPnpmWorkBudget();
|
|
168
171
|
let remaining = maxTotalBytes;
|
|
169
|
-
for (const { copy, format, originalRelative, entries, cargoPackages, declaringManifests, nugetPackages } of copies) {
|
|
172
|
+
for (const { copy, format, originalRelative, entries, cargoPackages, declaringManifests, nugetPackages, nugetLookup, composerLookup, yarnLookup } of copies) {
|
|
170
173
|
const lockDir = originalRelative === null ? "" : path.dirname(originalRelative);
|
|
171
174
|
let lookup = null;
|
|
172
175
|
try {
|
|
173
|
-
if (format === "package-lock.json" || format === "go.mod") {
|
|
176
|
+
if (format === "package-lock.json" || format === "pnpm-lock.yaml" || format === "go.mod") {
|
|
174
177
|
// 残りの予算を上限に、読み込みの途中で打ち切る(予算を超えるファイルは全体を読まずにunknownにする)
|
|
175
|
-
const read = await readRegularFile(copy, { maxBytes: format
|
|
178
|
+
const read = await readRegularFile(copy, { maxBytes: format !== "go.mod" ? Math.min(MAX_NPM_LOCK_BYTES, remaining) : remaining });
|
|
176
179
|
if (read.ok) {
|
|
177
180
|
remaining -= read.bytes.length;
|
|
178
181
|
const text = read.bytes.toString("utf8");
|
|
179
|
-
lookup = format === "go.mod" ? goModRelations(text) : npmLockRelations(parseNpmLock(text, npmWorkBudget));
|
|
182
|
+
lookup = format === "go.mod" ? goModRelations(text) : format === "pnpm-lock.yaml" ? pnpmLockRelations(text, pnpmWorkBudget) : npmLockRelations(parseNpmLock(text, npmWorkBudget));
|
|
180
183
|
}
|
|
181
184
|
}
|
|
185
|
+
else if (format === "yarn.lock") {
|
|
186
|
+
lookup = yarnLookup ?? null;
|
|
187
|
+
}
|
|
182
188
|
else if (format === "pom.xml") {
|
|
183
189
|
lookup = pomRelations(); // ファイルは読まず、osv-scannerのsource.typeで判定する
|
|
184
190
|
}
|
|
@@ -189,7 +195,10 @@ export async function buildRelationLookups(copies, maxTotalBytes = MAX_RELATION_
|
|
|
189
195
|
lookup = cargoLockRelations(cargoPackages, declaringManifests);
|
|
190
196
|
}
|
|
191
197
|
else if (format === "packages.lock.json" && nugetPackages !== undefined) {
|
|
192
|
-
lookup = nugetRelations(nugetPackages);
|
|
198
|
+
lookup = nugetLookup ?? nugetRelations(nugetPackages);
|
|
199
|
+
}
|
|
200
|
+
else if (format === "composer.lock") {
|
|
201
|
+
lookup = composerLookup ?? null;
|
|
193
202
|
}
|
|
194
203
|
}
|
|
195
204
|
catch {
|
|
@@ -301,6 +310,16 @@ export async function scanFromSnapshot(project, options) {
|
|
|
301
310
|
throw new ScanToolError(skipped.every((s) => s.kind === "outside_allowed_root") ? "path_outside_allowed_root" : "no_manifest_found", `No lockfile or manifest can be scanned (${project.skippedFiles.map((s) => `${s.path}: ${s.reason}`).join(" / ")})`);
|
|
302
311
|
}
|
|
303
312
|
}
|
|
313
|
+
for (const copy of project.yarnCopies ?? []) {
|
|
314
|
+
const generated = await snapshot.writeGenerated(copy.content);
|
|
315
|
+
targets.push({ path: generated, format: "yarn.lock" });
|
|
316
|
+
relationSources.push({ copy: generated, format: "yarn.lock", originalRelative: copy.path, yarnLookup: copy.relations });
|
|
317
|
+
}
|
|
318
|
+
for (const copy of project.pnpmCopies ?? []) {
|
|
319
|
+
const generated = await snapshot.writeGenerated(copy.content);
|
|
320
|
+
targets.push({ path: generated, format: "pnpm-lock.yaml" });
|
|
321
|
+
relationSources.push({ copy: generated, format: "pnpm-lock.yaml", originalRelative: copy.path });
|
|
322
|
+
}
|
|
304
323
|
for (const copy of project.requirementsCopies) {
|
|
305
324
|
if (copy.entries.length === 0)
|
|
306
325
|
continue;
|
|
@@ -319,12 +338,12 @@ export async function scanFromSnapshot(project, options) {
|
|
|
319
338
|
for (const copy of project.nugetCopies) {
|
|
320
339
|
const generated = await snapshot.writeGenerated(nugetLockCopyContent(copy.packages));
|
|
321
340
|
targets.push({ path: generated, format: "packages.lock.json" });
|
|
322
|
-
relationSources.push({ copy: generated, format: "packages.lock.json", originalRelative: copy.path, nugetPackages: copy.packages });
|
|
341
|
+
relationSources.push({ copy: generated, format: "packages.lock.json", originalRelative: copy.path, nugetPackages: copy.packages, nugetLookup: copy.relations });
|
|
323
342
|
}
|
|
324
343
|
for (const copy of project.composerCopies ?? []) {
|
|
325
344
|
const generated = await snapshot.writeGenerated(copy.content);
|
|
326
345
|
targets.push({ path: generated, format: "composer.lock" });
|
|
327
|
-
relationSources.push({ copy: generated, format: "composer.lock", originalRelative: copy.path });
|
|
346
|
+
relationSources.push({ copy: generated, format: "composer.lock", originalRelative: copy.path, composerLookup: copy.relations });
|
|
328
347
|
}
|
|
329
348
|
if (targets.length === 0)
|
|
330
349
|
return parseOsvScanOutput({ results: [] });
|
|
@@ -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) ||
|
|
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
|
+
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* 検出・除外の根拠はdocs/DESIGN_TODO.md「対象エコシステム拡大 詳細設計メモ」を参照。
|
|
11
11
|
*/
|
|
12
12
|
import { stat } from "node:fs/promises";
|
|
13
|
+
import { PnpmLockCache } from "./pnpmLockCache.js";
|
|
14
|
+
import { YarnLockCache } from "./yarnLockCache.js";
|
|
13
15
|
import path from "node:path";
|
|
14
16
|
import { ScanToolError } from "../errors.js";
|
|
15
17
|
import { isManifestFormat } from "./manifestFormats.js";
|
|
@@ -20,6 +22,7 @@ import { analyzeRequirementsFile, createRequirementsReadContext, } from "./requi
|
|
|
20
22
|
import { cargoTomlPackageName, cargoWorkspace, workspaceIncludes, createGlobWorkBudget, classifyCargoPackages, createCargoLockParseBudget, parseCargoLock, UNSCANNABLE_CRATE_REASONS, } from "./cargoLock.js";
|
|
21
23
|
import { analyzeNuget, collectNugetFile, emptyNugetFiles, NUGET_DEPS_JSON, NUGET_LOCKFILE, NUGET_PACKAGES_CONFIG, NUGET_PROJECT_ASSETS, NUGET_PROJECT_FILE, } from "./nugetProject.js";
|
|
22
24
|
import { composerLockCopy } from "./composerLock.js";
|
|
25
|
+
import { composerRelations, composerRootRequirements, createComposerRelationBudget } from "./composerRelations.js";
|
|
23
26
|
/** ファイル名→(エコシステム, 解析形式)。npm-shrinkwrap.jsonはpackage-lock.json形式で解析する */
|
|
24
27
|
const LOCKFILES = new Map([
|
|
25
28
|
["pom.xml", { ecosystem: "Maven", format: "pom.xml" }],
|
|
@@ -233,14 +236,27 @@ async function npmLockfileRecords(cache, projectDir, lockfile, memberDir) {
|
|
|
233
236
|
const key = path.relative(path.dirname(lockAbs), path.join(projectDir, memberDir)).split(path.sep).join("/");
|
|
234
237
|
return keys.has(key);
|
|
235
238
|
}
|
|
236
|
-
/**
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
239
|
+
/** pnpmのimporterの完全一致で収録を確認する。親や同名のpackageからは推測しない。 */
|
|
240
|
+
async function pnpmLockfileRecords(cache, projectDir, lockfile, memberDir) {
|
|
241
|
+
const lockAbs = path.join(projectDir, lockfile.path);
|
|
242
|
+
const captured = await cache.get(lockAbs, projectDir);
|
|
243
|
+
if ("reason" in captured || captured.importers === null)
|
|
244
|
+
return null;
|
|
245
|
+
const key = path.relative(path.dirname(lockAbs), path.join(projectDir, memberDir)).split(path.sep).join("/");
|
|
246
|
+
return captured.importers.has(key);
|
|
247
|
+
}
|
|
248
|
+
async function yarnLockfileRecords(cache, projectDir, lockfile, member, packageFiles) {
|
|
249
|
+
const absolute = path.join(projectDir, lockfile.path);
|
|
250
|
+
const captured = await cache.get(absolute, projectDir, packageFiles);
|
|
251
|
+
if ("reason" in captured)
|
|
252
|
+
return null;
|
|
253
|
+
const key = path.relative(path.dirname(absolute), path.join(projectDir, member)).split(path.sep).join("/");
|
|
254
|
+
return captured.members.get(key) ?? null;
|
|
255
|
+
}
|
|
256
|
+
/** 同じディレクトリのlockfile、または収録を確認できた上位lockfileだけで充足とする。 */
|
|
257
|
+
async function findLockfileMissing(projectDir, markers, manifests, cargoCache, pnpmCache, yarnCache) {
|
|
243
258
|
const cache = new LockfileKeyCache();
|
|
259
|
+
const packageFiles = markers.filter(m => m.group === "npm").map(m => m.path);
|
|
244
260
|
const missing = [];
|
|
245
261
|
for (const marker of markers) {
|
|
246
262
|
const markerDir = path.dirname(marker.path);
|
|
@@ -257,9 +273,13 @@ async function findLockfileMissing(projectDir, markers, manifests, cargoCache) {
|
|
|
257
273
|
for (const candidate of candidates) {
|
|
258
274
|
const result = candidate.format === "package-lock.json"
|
|
259
275
|
? await npmLockfileRecords(cache, projectDir, candidate, markerDir)
|
|
260
|
-
: candidate.format === "
|
|
261
|
-
? await
|
|
262
|
-
:
|
|
276
|
+
: candidate.format === "pnpm-lock.yaml"
|
|
277
|
+
? await pnpmLockfileRecords(pnpmCache, projectDir, candidate, markerDir)
|
|
278
|
+
: candidate.format === "yarn.lock"
|
|
279
|
+
? await yarnLockfileRecords(yarnCache, projectDir, candidate, marker.path, packageFiles)
|
|
280
|
+
: candidate.format === "Cargo.lock"
|
|
281
|
+
? await cargoLockfileRecords(cargoCache, projectDir, candidate, marker.path)
|
|
282
|
+
: null;
|
|
263
283
|
if (result === true)
|
|
264
284
|
recorded = true;
|
|
265
285
|
if (result !== false)
|
|
@@ -280,6 +300,9 @@ async function findLockfileMissing(projectDir, markers, manifests, cargoCache) {
|
|
|
280
300
|
}
|
|
281
301
|
async function finalize(projectDir, manifests, markers, allowedRootReal, nugetFiles = emptyNugetFiles()) {
|
|
282
302
|
const cargoCache = new CargoLockCache();
|
|
303
|
+
const pnpmCache = new PnpmLockCache();
|
|
304
|
+
const yarnCache = new YarnLockCache();
|
|
305
|
+
const packageFiles = markers.filter(m => m.group === "npm").map(m => m.path);
|
|
283
306
|
const result = {
|
|
284
307
|
projectDir,
|
|
285
308
|
allowedRootReal,
|
|
@@ -288,11 +311,13 @@ async function finalize(projectDir, manifests, markers, allowedRootReal, nugetFi
|
|
|
288
311
|
requirementsCopies: [],
|
|
289
312
|
cargoCopies: [],
|
|
290
313
|
composerCopies: [],
|
|
314
|
+
pnpmCopies: [],
|
|
315
|
+
yarnCopies: [],
|
|
291
316
|
composerWarnings: [],
|
|
292
317
|
composerLocalPackages: 0,
|
|
293
318
|
localCrateCount: 0,
|
|
294
319
|
unscannableCrates: [],
|
|
295
|
-
lockfileMissing: await findLockfileMissing(projectDir, markers, manifests, cargoCache),
|
|
320
|
+
lockfileMissing: await findLockfileMissing(projectDir, markers, manifests, cargoCache, pnpmCache, yarnCache),
|
|
296
321
|
requirementIssues: [],
|
|
297
322
|
requirementReferences: [],
|
|
298
323
|
lowerBounds: [],
|
|
@@ -306,6 +331,7 @@ async function finalize(projectDir, manifests, markers, allowedRootReal, nugetFi
|
|
|
306
331
|
};
|
|
307
332
|
const readContext = createRequirementsReadContext();
|
|
308
333
|
const composerBudget = createNpmLockWorkBudget();
|
|
334
|
+
const composerRelationBudget = createComposerRelationBudget();
|
|
309
335
|
let composerBytes = MAX_NPM_LOCK_TOTAL_BYTES;
|
|
310
336
|
// A null value permanently marks a crate name with multiple declaration paths.
|
|
311
337
|
const cargoManifests = new Map();
|
|
@@ -321,7 +347,23 @@ async function finalize(projectDir, manifests, markers, allowedRootReal, nugetFi
|
|
|
321
347
|
}
|
|
322
348
|
for (const manifest of manifests) {
|
|
323
349
|
const absolute = path.join(projectDir, manifest.path);
|
|
324
|
-
if (manifest.format === "
|
|
350
|
+
if (manifest.format === "yarn.lock") {
|
|
351
|
+
const captured = await yarnCache.get(absolute, projectDir, packageFiles);
|
|
352
|
+
if ("reason" in captured) {
|
|
353
|
+
result.skippedFiles.push({ path: manifest.path, reason: captured.reason });
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
result.yarnCopies.push({ path: manifest.path, content: captured.content, relations: captured.lookup });
|
|
357
|
+
}
|
|
358
|
+
else if (manifest.format === "pnpm-lock.yaml") {
|
|
359
|
+
const captured = await pnpmCache.get(absolute, projectDir);
|
|
360
|
+
if ("reason" in captured) {
|
|
361
|
+
result.skippedFiles.push({ path: manifest.path, reason: captured.reason });
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
result.pnpmCopies.push({ path: manifest.path, content: captured.content });
|
|
365
|
+
}
|
|
366
|
+
else if (manifest.format === "composer.lock") {
|
|
325
367
|
const read = await readRegularFile(absolute, { maxBytes: Math.min(MAX_NPM_LOCK_BYTES, composerBytes), boundary: projectDir });
|
|
326
368
|
if (!read.ok) {
|
|
327
369
|
result.skippedFiles.push({ path: manifest.path, reason: capitalizeReason(read.message) });
|
|
@@ -330,8 +372,24 @@ async function finalize(projectDir, manifests, markers, allowedRootReal, nugetFi
|
|
|
330
372
|
composerBytes -= read.bytes.length;
|
|
331
373
|
try {
|
|
332
374
|
const parsed = composerLockCopy(read.bytes.toString("utf8"), composerBudget);
|
|
333
|
-
if (parsed.packageCount > 0)
|
|
334
|
-
|
|
375
|
+
if (parsed.packageCount > 0) {
|
|
376
|
+
let relations = null;
|
|
377
|
+
const companion = await readRegularFile(path.join(path.dirname(absolute), "composer.json"), {
|
|
378
|
+
maxBytes: Math.min(4 * 1024 * 1024, composerBytes), boundary: projectDir,
|
|
379
|
+
});
|
|
380
|
+
if (companion.ok) {
|
|
381
|
+
composerBytes -= companion.bytes.length;
|
|
382
|
+
try {
|
|
383
|
+
const roots = composerRootRequirements(companion.bytes.toString("utf8"), composerBudget);
|
|
384
|
+
if (roots !== null)
|
|
385
|
+
relations = composerRelations(parsed.relationPackages, roots, composerRelationBudget);
|
|
386
|
+
}
|
|
387
|
+
catch { /* A companion failure affects relationships, not the scanned lockfile. */ }
|
|
388
|
+
}
|
|
389
|
+
if (relations === null)
|
|
390
|
+
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`);
|
|
391
|
+
result.composerCopies.push({ path: manifest.path, content: parsed.content, relations });
|
|
392
|
+
}
|
|
335
393
|
result.skippedFiles.push(...parsed.issues.map((reason) => ({ path: manifest.path, reason })));
|
|
336
394
|
result.composerWarnings.push(...parsed.warnings.map((warning) => `${manifest.path}: ${warning}`));
|
|
337
395
|
result.composerLocalPackages += parsed.localPackages;
|