osv-scanner-mcp 0.20.0 → 0.21.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
@@ -295,7 +295,7 @@ Detects the lockfiles and manifests in a project and scans the Java / JavaScript
295
295
  ```
296
296
 
297
297
  - **Always check `coverage`.** If `complete` is `false`, some dependencies were not scanned, so zero findings does not mean the project is safe
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
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 PDM locked local identity and captured workspace/path declarations can be confirmed (see below; source checkout), when Poetry locked path-package identity and exported child declarations can be confirmed (see below; source checkout), when uv local workspace identity and manifest membership can be confirmed (source checkout), when `bun.lock` contains its exact normalized workspace path (text formats 0/1/2; 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 other than `uv.lock` / `poetry.lock` / `pdm.lock`, and so on). Generate the lockfile as described in `hint` (in a trusted environment) and scan again
299
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)
300
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
301
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`)
@@ -313,13 +313,35 @@ Detects the lockfiles and manifests in a project and scans the Java / JavaScript
313
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`
314
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
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.
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. Each lockfile is parsed with its own budget sized from its length (1,024 operations per KiB, at least 2,000,000), so files within the size limits are not rejected because other lockfiles were parsed first; importer 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, a 16 MiB one about 3 seconds). Unsupported/malformed YAML, duplicate keys, anchors, aliases, tags, multi-document files, conservative nesting limits, more than 1,000,000 lines, 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
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`
318
+ - `bun.lock` (source checkout): text formats 0/1/2 support direct/transitive relations, `introduced_by`, `declared_in` and workspace coverage. Locked workspace declarations are direct; dependencies, optional dependencies and installed peers are followed through exact package placement. Nested versions, scoped names, npm aliases, cycles and direct/transitive overlap are supported. Intentionally unresolved optional dependencies are not guessed from another installed package.
319
+ - `uv.lock` (source checkout): version 1, revisions 0–5 support direct/transitive relations, `introduced_by`, `declared_in` and workspace coverage. Captured local workspace packages define roots from normal, optional and development-group dependencies. Registry identities resolve only when their normalized name and any locked version/source constraints identify a unique package. Optional edges are followed only for explicitly requested extras; unknown combinations of differing markers are not joined.
320
+ - `poetry.lock` (source checkout): lock versions 1.1/2.0/2.1 support direct/transitive relations, `introduced_by`, `declared_in` and conservative child coverage. Captured root and confirmed local child `pyproject.toml` declarations establish direct roots; unique locked package names and unconditional string dependency edges establish transitive origins. Modern project dependencies/optional dependencies, Poetry dependency/dev/group tables and standard dependency groups are supported. Ambiguous names, conditional/optional edges requiring a solver, nonregistry releases and unreachable packages remain unknown.
321
+ - `pdm.lock` (source checkout): 4.0–4.4 lock schemas and 4.5.0/4.5.1 support direct/transitive relations, `introduced_by`, `declared_in` and child coverage. Captured project dependencies, optional groups, standard dependency groups (including group includes) and legacy `tool.pdm.dev-dependencies` define roots, selected by `metadata.groups` when present. Explicit extras select exact locked extras identities; ambiguous names/versions/sources are not guessed. PDM-generated editable dev declarations (`-e file:///${PROJECT_ROOT}/...#egg=name`) use the explicit egg identity and exact safe locked local path. Unsupported declarations do not discard other known roots; an unsupported runtime declaration still prevents confirming child coverage.
322
+ - Other formats (`gradle.lockfile` and `Pipfile.lock`), npm lockfile version 1, and entries not reachable from the project are `unknown`. If lockfiles disagree, the value is `mixed`
323
+
324
+ **PDM origins and child coverage (source checkout):** native `[tool.pdm.workspace].members` patterns (exact paths, `*`, `?`, whole-segment `**`, including dot directories) are checked against exact safe locked `path` identities. Explicit reachable local path dependencies can also establish inclusion. A matching name alone does not cover a child. Captured child name/version and exported runtime/optional declarations must agree with unique locked dependency identities (specifier order is normalized; marker token types, boundaries and literal contents are preserved while ignoring whitespace outside literals and quote style; other marker or constraint differences are not solved); missing local directories are `missing`, while unreadable, unsupported, ambiguous or unverifiable declarations are `unconfirmed`. Unsupported glob syntax, child development groups and dynamic dependencies/optional-dependencies cannot establish complete child coverage. Other child markers (`setup.py`, `setup.cfg`, `Pipfile`) remain unconfirmed; same-directory locks retain the existing coverage convention. Group presence and membership do not constitute a full freshness check.
325
+
326
+ `declared_in` names captured root/confirmed-child `pyproject.toml` files. PEP 503 names, PEP 440 release comparison, multiple origins, cycles, direct/transitive overlap and distinct extras identities are supported. Dependency markers are not evaluated; differing expressions on the same traversal path are not joined. No PDM, plugin, build backend or project code runs during a scan. Local/Git/URL identities are not treated as registry releases by relationship analysis; local paths are never followed to read source files. Existing OSV communication and package-origin handling are unchanged. TOML parsing is bounded and locks are captured once for metadata and scanner input (16 MiB per file, 64 MiB total). Captured companions have separate 4 MiB/file, 64 MiB total limits. Coverage and relationships have separate shared 2,000,000-operation budgets; exhausting relationship analysis preserves confirmed membership. Unsupported metadata leaves relationships unknown while vulnerability scanning continues. `pylock.toml`, custom lock filenames and full constraint/environment solving are outside this support.
327
+
328
+ **Poetry origins and child coverage (source checkout):** Poetry path dependencies are used to check shared-lock monorepos. This does not assume uv-style workspace membership. A child must have an exact safe relative directory source in the ancestor lock, be reachable from captured root declarations, and match a unique locked local name/version. Its runtime/optional declarations must agree with unique locked dependency edges (matching constraint text or exact locked versions); unsupported constraints or source identities remain unconfirmed. A path dependency on another local package (including a sibling such as `../core`) agrees when the locked edge is an unconditional `{path, develop}` table resolving to that package's locked directory inside the lock directory. Child development groups are not exported by path packages and therefore make child coverage unconfirmed. Dynamic `project.dependencies` or `project.optional-dependencies` also leave child coverage unconfirmed because backend-generated declarations cannot be verified from captured TOML. An absent child directory is missing; unreadable/unsupported/ambiguous metadata is unconfirmed. Independent descendants with the same package name do not count as included. Legacy `setup.py`/`setup.cfg`/`Pipfile` children remain unconfirmed. A same-directory lock retains the existing coverage convention.
329
+
330
+ Root declarations include all supported development/optional groups, because scanning covers the universal lock. Unconditional string edges are followed even when the root or an intermediate package is marked optional; conditional or optional dependency tables still remain unresolved. Names use PEP 503 normalization and versions use PEP 440 comparison. No environment, extras or constraint solver is run; lock freshness is not globally verified. Declared paths come from safely captured companions, not from following arbitrary lockfile source paths. Poetry, plugins and build backends are never executed during scanning. Lock bytes are captured once for coverage, relationships and scanner input (16 MiB/file, 64 MiB total); companions have a separate 4 MiB/file, 64 MiB total budget. TOML preflight and independent shared 2,000,000-operation coverage/relationship budgets bound metadata analysis; exceeding only the relationship budget preserves confirmed coverage. Private-index exclusion is not established by this metadata analysis.
331
+
332
+ **uv origins and workspace coverage (source checkout):** local `editable`, `virtual` or `directory` paths are matched with `[manifest].members` names to confirm child workspace membership. A unique local root at `.` can be confirmed for single-project locks. A local path dependency alone does not establish workspace membership; a locked local package that is not a confirmed member (such as a path dependency), or missing or ambiguous membership evidence, remains unconfirmed, while a child absent from the lockfile is missing. Unicode/space paths and virtual projects are supported. Only `pyproject.toml` children are confirmed; legacy `setup.py`/`setup.cfg`/`Pipfile` markers remain unconfirmed against ancestor uv locks.
333
+
334
+ `declared_in` reflects locked workspace `pyproject.toml` paths; current source files and lockfile freshness are not compared. Names use PEP 503 normalization; explicit dependency versions use PEP 440 comparison and source identities are kept separate. No marker, Python-version, platform or package constraint solver is run. Different marker expressions on one path are conservatively left unresolved rather than joined, and unreachable/ambiguous packages remain unknown. Workspace groups and optional declarations are included because scanning covers the universal lockfile, not a selected environment. Local paths may be graph intermediates but are never followed to read or execute source files. uv and build backends are never executed during scanning.
335
+
336
+ The same safely captured bytes feed membership, relation metadata and the scanner. TOML parsing is bounded at 16 MiB per file / 64 MiB total with a conservative preflight limit of 128 container levels, 128 unquoted dots per line and a per-file structural-character budget sized from its length (1,024 per KiB, at least 2,000,000), so files within the size limits are not rejected for size alone; duplicate keys and unsupported schema are rejected for metadata analysis. Workspace indexing and graph traversal use separate shared 2,000,000-operation budgets, so relationship limits do not invalidate confirmed membership; a root declared by several members with the same extras and marker is traversed once. Unreadable/oversized files are skipped with a reason. Package origin and private index exclusion are not established by relationship analysis; existing OSV communication behavior is unchanged.
337
+
338
+ **Bun origins and workspace coverage (source checkout):** the exact normalized `workspaces` path in captured `bun.lock` establishes inclusion in an ancestor lockfile, including Unicode/space paths and empty entries. An absent child is `missing`; unsupported/malformed/duplicate/ambiguous or parse-limited data is `unconfirmed`. Declaration paths reflect the lockfile, not a comparison with current `package.json` contents, and do not establish freshness or compatibility. Package paths resolve from the declaring workspace/package toward the locked root while keeping scoped names intact; unsupported local/Git/tarball identities are barriers and are never followed to source files. Bun is not executed by the server.
339
+
340
+ JSONC parsing allows comments/trailing commas and rejects duplicate keys, syntax errors and nesting beyond 128 levels before constructing oversized structures. Captured lockfiles are limited to 16 MiB each / 64 MiB total. Each lockfile is parsed with its own budget sized from its length (1,024 operations per KiB, at least 2,000,000); workspace indexing and relation traversal have separate shared 2,000,000-operation budgets per scan; relation exhaustion leaves metadata unknown without changing confirmed coverage. Coverage and scanning use the same captured bytes. Unreadable/oversized files are skipped with a reason. Binary `bun.lockb` is unsupported; generate a text lockfile in a trusted environment with `bun install --save-text-lockfile --lockfile-only --ignore-scripts`. Only lockfiles inside the selected scan directory are considered.
319
341
 
320
342
  **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
343
 
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.
344
+ 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 limits and a 1,000,000-line limit apply. Each lockfile is parsed with its own size-based budget (1,024 operations per KiB, at least 2,000,000); workspace coverage shares 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
345
 
324
346
  **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
347
 
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.20.0";
69
+ const SERVER_VERSION = "0.21.0";
70
70
  const server = new McpServer({
71
71
  name: "osv-scanner-mcp",
72
72
  version: SERVER_VERSION,
@@ -88,7 +88,7 @@ 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. 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, 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. " +
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, bun.lock text formats 0/1/2, uv.lock v1 revisions 0-5, 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. uv uses captured workspace identities and manifest members for coverage, and unique version/source references plus requested extras for origins/declarations; ambiguous markers are not joined and uv/build backends are not executed. Bun uses captured workspace declarations and scoped package placement for origins/declarations and exact workspace paths for coverage; unresolvable identities stay unknown and Bun is never executed. 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
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: {
@@ -90,10 +90,10 @@ function updateHint(pkg) {
90
90
  return undefined; // gradle.lockfile等: 判定できないため具体的な案内はしない(従来どおり)
91
91
  case "PyPI":
92
92
  if (relation === "direct") {
93
- return "Direct dependency. Update the version in requirements.txt, pyproject.toml, or Pipfile, and regenerate the lockfile";
93
+ return `Direct dependency. Update the version in ${pkg.declared_in?.join(", ") ?? "requirements.txt, pyproject.toml, or Pipfile"}, and regenerate the lockfile`;
94
94
  }
95
95
  if (relation === "transitive") {
96
- return "Transitive dependency. Update the direct dependency that requires it, or set the version with a pip constraints file (-c) or the override settings of uv, Poetry, and similar tools";
96
+ return `Transitive dependency. Update ${by ? `the direct dependencies that require it (${by})` : "the direct dependency that requires it"}, or set the version with a pip constraints file (-c) or the override settings of uv, Poetry, and similar tools`;
97
97
  }
98
98
  return UPDATE_HINTS.PyPI;
99
99
  default:
@@ -169,7 +169,7 @@ export async function buildRelationLookups(copies, maxTotalBytes = MAX_RELATION_
169
169
  // pnpmの関係の解析の作業量はスキャン全体で共有する(Composer・NuGetと同じ)
170
170
  const pnpmWorkBudget = createPnpmWorkBudget();
171
171
  let remaining = maxTotalBytes;
172
- for (const { copy, format, originalRelative, entries, cargoPackages, declaringManifests, nugetPackages, nugetLookup, composerLookup, yarnLookup } of copies) {
172
+ for (const { copy, format, originalRelative, entries, cargoPackages, declaringManifests, nugetPackages, nugetLookup, composerLookup, yarnLookup, bunLookup, uvLookup, poetryLookup, pdmLookup } of copies) {
173
173
  const lockDir = originalRelative === null ? "" : path.dirname(originalRelative);
174
174
  let lookup = null;
175
175
  try {
@@ -182,6 +182,18 @@ export async function buildRelationLookups(copies, maxTotalBytes = MAX_RELATION_
182
182
  lookup = format === "go.mod" ? goModRelations(text) : format === "pnpm-lock.yaml" ? pnpmLockRelations(text, pnpmWorkBudget) : npmLockRelations(parseNpmLock(text, npmWorkBudget));
183
183
  }
184
184
  }
185
+ else if (format === "pdm.lock") {
186
+ lookup = pdmLookup ?? null;
187
+ }
188
+ else if (format === "poetry.lock") {
189
+ lookup = poetryLookup ?? null;
190
+ }
191
+ else if (format === "uv.lock") {
192
+ lookup = uvLookup ?? null;
193
+ }
194
+ else if (format === "bun.lock") {
195
+ lookup = bunLookup ?? null;
196
+ }
185
197
  else if (format === "yarn.lock") {
186
198
  lookup = yarnLookup ?? null;
187
199
  }
@@ -310,6 +322,26 @@ export async function scanFromSnapshot(project, options) {
310
322
  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(" / ")})`);
311
323
  }
312
324
  }
325
+ for (const copy of project.pdmCopies ?? []) {
326
+ const generated = await snapshot.writeGenerated(copy.content);
327
+ targets.push({ path: generated, format: "pdm.lock" });
328
+ relationSources.push({ copy: generated, format: "pdm.lock", originalRelative: copy.path, pdmLookup: copy.relations });
329
+ }
330
+ for (const copy of project.poetryCopies ?? []) {
331
+ const generated = await snapshot.writeGenerated(copy.content);
332
+ targets.push({ path: generated, format: "poetry.lock" });
333
+ relationSources.push({ copy: generated, format: "poetry.lock", originalRelative: copy.path, poetryLookup: copy.relations });
334
+ }
335
+ for (const copy of project.uvCopies ?? []) {
336
+ const generated = await snapshot.writeGenerated(copy.content);
337
+ targets.push({ path: generated, format: "uv.lock" });
338
+ relationSources.push({ copy: generated, format: "uv.lock", originalRelative: copy.path, uvLookup: copy.relations });
339
+ }
340
+ for (const copy of project.bunCopies ?? []) {
341
+ const generated = await snapshot.writeGenerated(copy.content);
342
+ targets.push({ path: generated, format: "bun.lock" });
343
+ relationSources.push({ copy: generated, format: "bun.lock", originalRelative: copy.path, bunLookup: copy.relations });
344
+ }
313
345
  for (const copy of project.yarnCopies ?? []) {
314
346
  const generated = await snapshot.writeGenerated(copy.content);
315
347
  targets.push({ path: generated, format: "yarn.lock" });
@@ -0,0 +1,74 @@
1
+ import { parse } from "smol-toml";
2
+ import { createParseBudget } from "./dependencyRelations.js";
3
+ const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
4
+ /** Bound TOML containers and structural work before asking the parser to allocate them. */
5
+ export function parseBoundedToml(source) {
6
+ try {
7
+ if (Buffer.byteLength(source) > 16 * 1024 * 1024)
8
+ return null;
9
+ // Structural work is bounded per file in proportion to its size, as for the other lockfile parsers.
10
+ const maxStructure = createParseBudget(source).remaining;
11
+ let quote = "", triple = false, escaped = false, comment = false, depth = 0, structure = 0, dots = 0;
12
+ for (let i = 0; i < source.length; i++) {
13
+ const c = source[i];
14
+ if (comment) {
15
+ if (c === "\n") {
16
+ comment = false;
17
+ dots = 0;
18
+ }
19
+ continue;
20
+ }
21
+ if (quote) {
22
+ if (escaped) {
23
+ escaped = false;
24
+ continue;
25
+ }
26
+ if (quote === '"' && c === "\\") {
27
+ escaped = true;
28
+ continue;
29
+ }
30
+ if (c === quote && (!triple || source.slice(i, i + 3) === quote.repeat(3))) {
31
+ if (triple)
32
+ i += 2;
33
+ quote = "";
34
+ triple = false;
35
+ }
36
+ continue;
37
+ }
38
+ if (c === "#") {
39
+ comment = true;
40
+ continue;
41
+ }
42
+ if (c === '"' || c === "'") {
43
+ quote = c;
44
+ triple = source.slice(i, i + 3) === c.repeat(3);
45
+ if (triple)
46
+ i += 2;
47
+ continue;
48
+ }
49
+ if (c === "\n")
50
+ dots = 0;
51
+ if (c === "." && ++dots > 128)
52
+ return null;
53
+ if (c === "[" || c === "{") {
54
+ if (++depth > 128)
55
+ return null;
56
+ }
57
+ else if (c === "]" || c === "}") {
58
+ if (--depth < 0)
59
+ return null;
60
+ }
61
+ if ("[]{}=,.".includes(c) && ++structure > maxStructure)
62
+ return null;
63
+ }
64
+ if (depth || quote)
65
+ return null;
66
+ const value = parse(source, { unsafeKeyBehaviour: "throw" });
67
+ if (!record(value))
68
+ return null;
69
+ return value;
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ }
@@ -0,0 +1,249 @@
1
+ import path from "node:path";
2
+ import { visit } from "jsonc-parser";
3
+ import { valid } from "semver";
4
+ import { MAX_DEPENDENCY_EDGES } from "./dependencyRelations.js";
5
+ export const createBunWorkBudget = () => ({ remaining: MAX_DEPENDENCY_EDGES });
6
+ const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
7
+ const fields = ["dependencies", "optionalDependencies", "peerDependencies"];
8
+ const rootFields = [...fields, "devDependencies"];
9
+ const safePath = (p) => !path.posix.isAbsolute(p) && !path.win32.isAbsolute(p) && !/^[A-Za-z]:|[\\\x00-\x1f\x7f]/.test(p) && !p.split("/").includes("..");
10
+ const identity = (name, version) => JSON.stringify([name, version]);
11
+ /** JSONC visitor bounds allocation before constructing values, rejects duplicate keys. */
12
+ export function parseBunLock(source, budget = createBunWorkBudget()) {
13
+ try {
14
+ if (Buffer.byteLength(source) > 16 * 1024 * 1024)
15
+ return null;
16
+ const tick = () => { if (--budget.remaining < 0)
17
+ throw new Error("Bun parse limit"); };
18
+ const stack = [];
19
+ let result;
20
+ const add = (value) => {
21
+ tick();
22
+ const parent = stack.at(-1);
23
+ if (!parent) {
24
+ if (result !== undefined)
25
+ throw new Error("multiple values");
26
+ result = value;
27
+ }
28
+ else if (Array.isArray(parent.value))
29
+ parent.value.push(value);
30
+ else {
31
+ if (parent.pending === undefined)
32
+ throw new Error("missing key");
33
+ parent.value[parent.pending] = value;
34
+ parent.pending = undefined;
35
+ }
36
+ };
37
+ const begin = (value) => {
38
+ if (stack.length >= 128)
39
+ throw new Error("Bun nesting limit");
40
+ add(value);
41
+ stack.push({ value, keys: new Set() });
42
+ };
43
+ visit(source, {
44
+ onObjectBegin: () => begin(Object.create(null)),
45
+ onObjectProperty: name => { tick(); const parent = stack.at(-1); if (parent.keys.has(name))
46
+ throw new Error("duplicate key"); parent.keys.add(name); parent.pending = name; },
47
+ onObjectEnd: () => { tick(); stack.pop(); },
48
+ onArrayBegin: () => begin([]),
49
+ onArrayEnd: () => { tick(); stack.pop(); },
50
+ onLiteralValue: add,
51
+ onSeparator: tick,
52
+ onComment: tick,
53
+ onError: () => { throw new Error("invalid JSONC"); },
54
+ }, { allowTrailingComma: true, disallowComments: false });
55
+ if (!record(result) || ![0, 1, 2].includes(result.lockfileVersion))
56
+ return null;
57
+ if (result.configVersion !== undefined && result.configVersion !== 0 && result.configVersion !== 1)
58
+ return null;
59
+ return result;
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ /** Coverage is independent of relationship traversal; empty workspace entries are valid. */
66
+ export function bunWorkspaceKeys(data, budget = createBunWorkBudget()) {
67
+ if (!record(data.workspaces))
68
+ return null;
69
+ const keys = new Set();
70
+ for (const [raw, workspace] of Object.entries(data.workspaces)) {
71
+ if (--budget.remaining < 0 || !safePath(raw) || !record(workspace))
72
+ return null;
73
+ const normalized = raw === "" ? "" : path.posix.normalize(raw);
74
+ if ((normalized === "." && raw !== "") || keys.has(normalized))
75
+ return null;
76
+ for (const field of rootFields) {
77
+ const deps = workspace[field];
78
+ if (deps !== undefined && !record(deps))
79
+ return null;
80
+ if (record(deps))
81
+ for (const ref of Object.values(deps)) {
82
+ if (--budget.remaining < 0 || typeof ref !== "string" || ref.length === 0)
83
+ return null;
84
+ }
85
+ }
86
+ keys.add(normalized);
87
+ }
88
+ return keys;
89
+ }
90
+ /** Bun package paths omit node_modules; scoped names must remain single path components. */
91
+ function packagePath(key) {
92
+ const tokens = key.split("/"), parts = [];
93
+ for (let i = 0; i < tokens.length; i++) {
94
+ const token = tokens[i];
95
+ if (token.startsWith("@")) {
96
+ const name = tokens[++i];
97
+ if (!/^@[A-Za-z0-9_.-]+$/.test(token) || !name || !/^[A-Za-z0-9_.-]+$/.test(name))
98
+ return null;
99
+ parts.push(`${token}/${name}`);
100
+ }
101
+ else {
102
+ if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(token) || token === "..")
103
+ return null;
104
+ parts.push(token);
105
+ }
106
+ }
107
+ return parts;
108
+ }
109
+ /** Resolve only the locked placement; never solve a range or follow local/Git source paths. */
110
+ export function bunLockRelations(data, budget = createBunWorkBudget()) {
111
+ const tick = () => { if (--budget.remaining < 0)
112
+ throw new Error("Bun relationship limit"); };
113
+ try {
114
+ if (!record(data.packages) || !record(data.workspaces))
115
+ return null;
116
+ const workspaces = new Map();
117
+ for (const [rawPath, value] of Object.entries(data.workspaces)) {
118
+ tick();
119
+ if (!record(value) || !safePath(rawPath))
120
+ return null;
121
+ const location = rawPath === "" ? "" : path.posix.normalize(rawPath);
122
+ if (location === "." || workspaces.has(location))
123
+ return null;
124
+ workspaces.set(location, value);
125
+ }
126
+ const nodes = new Map(), anchors = new Map();
127
+ for (const [key, raw] of Object.entries(data.packages)) {
128
+ tick();
129
+ const parts = packagePath(key);
130
+ if (!parts || !Array.isArray(raw) || typeof raw[0] !== "string")
131
+ return null;
132
+ const match = /^(@[^/]+\/[^@]+|[^@]+)@(.+)$/.exec(raw[0]);
133
+ if (!match)
134
+ return null;
135
+ const ref = match[2], registry = valid(ref) !== null && raw.length === 4 && typeof raw[1] === "string" && record(raw[2]) && typeof raw[3] === "string";
136
+ const info = registry ? raw[2] : record(raw[1]) ? raw[1] : {};
137
+ const node = { parts, name: match[1], version: ref, info, registry };
138
+ if (ref.startsWith("workspace:")) {
139
+ const location = ref.slice("workspace:".length);
140
+ if (safePath(location)) {
141
+ node.workspace = location === "." ? "" : path.posix.normalize(location);
142
+ anchors.set(node.workspace, anchors.has(node.workspace) ? null : parts);
143
+ }
144
+ }
145
+ nodes.set(key, node);
146
+ }
147
+ const resolve = (from, name, ref) => {
148
+ tick();
149
+ const nameParts = packagePath(name);
150
+ if (typeof ref !== "string" || ref.length === 0 || !nameParts || nameParts.length !== 1)
151
+ return null;
152
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(ref) && !/^(?:npm:|workspace:|catalog:)/.test(ref))
153
+ return null;
154
+ if (ref.includes("/") && !/^(?:npm:|workspace:|catalog:)/.test(ref))
155
+ return null;
156
+ const context = [...from];
157
+ for (;;) {
158
+ tick();
159
+ const candidate = [...context, name].join("/");
160
+ if (nodes.has(candidate)) {
161
+ const node = nodes.get(candidate);
162
+ return ref.startsWith("workspace:") && node.workspace === undefined ? null : node;
163
+ } // An unsupported local identity is a barrier, not a fallback.
164
+ if (!context.length)
165
+ return null;
166
+ context.pop();
167
+ }
168
+ };
169
+ const declared = new Map(), introduced = new Map();
170
+ const edges = (value, list) => {
171
+ const out = [];
172
+ const optional = record(value.optionalDependencies) ? value.optionalDependencies : {};
173
+ if (value.unresolved !== undefined && (!Array.isArray(value.unresolved) || !value.unresolved.every(v => typeof v === "string")))
174
+ throw new Error("invalid unresolved dependencies");
175
+ const unresolved = new Set(Array.isArray(value.unresolved) ? value.unresolved : []);
176
+ for (const field of list) {
177
+ const deps = value[field];
178
+ if (deps === undefined)
179
+ continue;
180
+ if (!record(deps))
181
+ throw new Error("invalid dependencies");
182
+ for (const [name, ref] of Object.entries(deps)) {
183
+ tick();
184
+ if (field !== "optionalDependencies" && Object.hasOwn(optional, name))
185
+ continue;
186
+ if (field === "optionalDependencies" && unresolved.has(name))
187
+ continue;
188
+ out.push([name, ref]);
189
+ }
190
+ }
191
+ return out;
192
+ };
193
+ for (const [location, value] of workspaces) {
194
+ tick();
195
+ const context = location === "" ? [] : anchors.get(location);
196
+ if (!context)
197
+ continue;
198
+ for (const [name, ref] of edges(value, rootFields)) {
199
+ const target = resolve(context, name, ref);
200
+ if (!target?.registry)
201
+ continue;
202
+ const paths = declared.get(target) ?? new Set();
203
+ paths.add(location === "" ? "package.json" : `${location}/package.json`);
204
+ declared.set(target, paths);
205
+ }
206
+ }
207
+ for (const origin of declared.keys()) {
208
+ const seen = new Set([origin]), queue = [origin];
209
+ while (queue.length) {
210
+ tick();
211
+ const node = queue.pop();
212
+ for (const [name, ref] of edges(node.info, fields)) {
213
+ const target = resolve(node.parts, name, ref);
214
+ if (!target?.registry || seen.has(target))
215
+ continue;
216
+ seen.add(target);
217
+ queue.push(target);
218
+ const roots = introduced.get(target) ?? new Set();
219
+ roots.add(origin.name);
220
+ introduced.set(target, roots);
221
+ }
222
+ }
223
+ }
224
+ const merged = new Map();
225
+ for (const node of nodes.values()) {
226
+ tick();
227
+ if (!node.registry)
228
+ continue;
229
+ const key = identity(node.name, node.version), info = merged.get(key) ?? { declaredIn: new Set(), introducedBy: new Set() };
230
+ for (const p of declared.get(node) ?? []) {
231
+ tick();
232
+ info.declaredIn.add(p);
233
+ }
234
+ for (const p of introduced.get(node) ?? []) {
235
+ tick();
236
+ info.introducedBy.add(p);
237
+ }
238
+ merged.set(key, info);
239
+ }
240
+ return (name, version) => {
241
+ const info = merged.get(identity(name, version));
242
+ return info ? { relation: info.declaredIn.size ? "direct" : info.introducedBy.size ? "transitive" : "unknown",
243
+ ...(info.declaredIn.size ? { declaredIn: info.declaredIn } : {}), ...(info.introducedBy.size ? { introducedBy: info.introducedBy } : {}) } : { relation: "unknown" };
244
+ };
245
+ }
246
+ catch {
247
+ return null;
248
+ }
249
+ }
@@ -0,0 +1,33 @@
1
+ import { MAX_NPM_LOCK_BYTES, MAX_NPM_LOCK_TOTAL_BYTES } from "./npmLockCopy.js";
2
+ import { readRegularFile, capitalizeReason } from "./safeRead.js";
3
+ import { parseBunLock, bunWorkspaceKeys, bunLockRelations, createBunWorkBudget } from "./bunLock.js";
4
+ import { createParseBudget } from "./dependencyRelations.js";
5
+ export class BunLockCache {
6
+ remaining;
7
+ locks = new Map();
8
+ coverageBudget = createBunWorkBudget();
9
+ relationBudget = createBunWorkBudget();
10
+ constructor(remaining = MAX_NPM_LOCK_TOTAL_BYTES) {
11
+ this.remaining = remaining;
12
+ }
13
+ async get(file, projectDir) {
14
+ const cached = this.locks.get(file);
15
+ if (cached)
16
+ return cached;
17
+ let captured;
18
+ if (this.remaining <= 0)
19
+ captured = { reason: "Bun lockfiles exceeded the total input size limit" };
20
+ else {
21
+ const read = await readRegularFile(file, { boundary: projectDir, maxBytes: Math.min(MAX_NPM_LOCK_BYTES, this.remaining) });
22
+ if (!read.ok)
23
+ captured = { reason: capitalizeReason(read.message) };
24
+ else {
25
+ this.remaining -= read.bytes.length;
26
+ const content = read.bytes.toString("utf8"), data = parseBunLock(content, createParseBudget(content));
27
+ captured = { content, workspaces: data === null ? null : bunWorkspaceKeys(data, this.coverageBudget), relations: data === null ? null : bunLockRelations(data, this.relationBudget) };
28
+ }
29
+ }
30
+ this.locks.set(file, captured);
31
+ return captured;
32
+ }
33
+ }
@@ -12,6 +12,17 @@
12
12
  import { cargoSourceKind, parsePackageId } from "./cargoLock.js";
13
13
  /** たどる依存の辺の上限(巨大・悪意あるlockfileで処理が膨らまないように) */
14
14
  export const MAX_DEPENDENCY_EDGES = 2_000_000;
15
+ /** Lockfile parsing operations allowed per KiB (dense pnpm lockfiles measured at about 440). */
16
+ export const PARSE_OPERATIONS_PER_KIB = 1024;
17
+ /** Line limit for YAML-like lockfiles; 200,000 lines rejected pnpm/Yarn lockfiles of about 6 MiB, below the 16 MiB limit. */
18
+ export const MAX_LOCKFILE_LINES = 1_000_000;
19
+ /**
20
+ * Parsing is linear and already bounded by the byte limits, so each lockfile gets its own budget sized from its length.
21
+ * A shared parse budget ran out at about 7.5 MiB of pnpm/Yarn input in a scan, well below the 16 MiB / 64 MiB read limits.
22
+ */
23
+ export function createParseBudget(source) {
24
+ return { remaining: Math.max(MAX_DEPENDENCY_EDGES, Math.ceil(Buffer.byteLength(source) / 1024) * PARSE_OPERATIONS_PER_KIB) };
25
+ }
15
26
  const UNKNOWN = { relation: "unknown" };
16
27
  /** Cargoの省略されたpackage IDは、一意に解決できるときだけたどる。 */
17
28
  export function cargoLockRelations(packages, manifests = new Map(), maxEdges = MAX_DEPENDENCY_EDGES) {