Skip to content

bug(pnpm): direct dependencies with an aliased version in importers are marked as dev #11389

Description

@DmitriyLewen

Description

For pnpm-lock.yaml v9, Trivy detects direct dependencies and their dev/prod status from the importers section.
When an importer's version is written in the alias form <name>@<version>, the direct dependency is not matched to its snapshot.
As a result, it is marked as indirect and Dev: true, and its whole production subtree stays dev as well.
Without --include-dev-deps, these packages and their vulnerabilities disappear from the result.

The alias form appears in at least two cases.

1. pnpm deploy with a workspace package

pnpm --config.inject-workspace-packages=true deploy --prod records the workspace package as a file: dependency:

importers:
  .:
    dependencies:
      repro-shared:
        specifier: repro-shared@file:///<path>/packages/shared
        version: repro-shared@file:///<path>/packages/shared

snapshots:
  '@xmldom/xmldom@0.8.13': {}

  repro-shared@file:///<path>/packages/shared:
    dependencies:
      '@xmldom/xmldom': 0.8.13

Result (trivy fs pnpm-lock.yaml --include-dev-deps -f json):

repro-shared@file:///<path>/packages/shared  Dev: true  indirect
@xmldom/xmldom@0.8.13                        Dev: true  indirect

Without --include-dev-deps, vulnerabilities in @xmldom/xmldom@0.8.13 are not reported, even though the package is part of the production deployment.

Reproduction: https://github.com/sergio-ferreira-jscrambler/trivy-pnpm-workspace-repro (pnpm 11 and 12).

2. npm alias

{ "dependencies": { "my-mkdirp": "npm:mkdirp@0.5.1" } }

pnpm install --lockfile-only (pnpm 11):

importers:
  .:
    dependencies:
      my-mkdirp:
        specifier: npm:mkdirp@0.5.1
        version: mkdirp@0.5.1

snapshots:
  minimist@0.0.8: {}

  mkdirp@0.5.1:
    dependencies:
      minimist: 0.0.8

Result:

mkdirp@0.5.1    Dev: true  indirect
minimist@0.0.8  Dev: true  indirect

trivy fs pnpm-lock.yaml without --include-dev-deps finds nothing.
With the flag, it reports CVE-2021-44906 and CVE-2020-7598 in minimist@0.0.8.

Root cause

In parseV9 (pkg/dependency/parser/nodejs/pnpm/parse.go), the ID of a direct dependency is built as packageID(<importer key>, <version>):

  • for pnpm deploy: repro-shared@repro-shared@file:///…, while the snapshot ID is repro-shared@file:///…;
  • for the alias: my-mkdirp@mkdirp@0.5.1, while the snapshot ID is mkdirp@0.5.1.

The IDs don't match, so the package never gets into directProdDeps, stays Dev: true, and markRootPkgs never walks its dependencies.

This is not a regression: before #10894, the comparison was by name and trimPeerDeps(version) == version, and it didn't match either.

Challenges

  • An importer's version can be a plain version (0.8.13), a version with a peer suffix (1.0.0(react@18.0.0)), link:…, or the alias form <name>@<version>, where <version> itself can be file: or a URL. The name can be scoped and contain @.
  • For an alias, the importer key (my-mkdirp) differs from the real package name (mkdirp), while for pnpm deploy they are the same. The same logic has to handle both cases.
  • Not only Dev is wrong but also Relationship: the direct dependency is reported as indirect.
  • It hasn't been checked how such entries look in lock files before v9 and in other scenarios (overrides, catalog:, git dependencies).
  • Test lock files should be generated by real pnpm, with the commands to reproduce them in a comment (as done for pnpmV9MultipleDocuments).

Before implementation

Before opening a PR, please describe the approach you plan to take in this issue so we can agree on it.

Discussed in #11385

Activity

  1. added theissue type on Oct 12, 2026
  2. added
    kind/bugCategorizes issue or PR as related to a bug.
    help wantedDenotes an issue that needs help from a contributor. Must meet "help wanted" guidelines.
    good first issueDenotes an issue ready for a new contributor, according to the "help wanted" guidelines.
    scan/vulnerabilityIssues relating to vulnerability scanning
    on Oct 12, 2026
  3. added this to the v0.77.0 milestone on Oct 12, 2026
  4. MannXo commented on Oct 12, 2026

    @MannXo

    I'd like to give this a shot if it's unassigned.

  5. DmitriyLewen commented on Oct 12, 2026

    @DmitriyLewen
    ContributorAuthor

    Hello @MannXo
    Thanks!

    Let me know once you have a work plan for this fix, so I can assign this issue to you.

    Regards, Dmitriy

  6. MannXo commented on Oct 12, 2026

    @MannXo

    Reproduced both cases with pnpm 11.28.2. There's a third one the same fix covers. An alias inside a snapshot's dependencies drops the graph edge:

    snapshots:
      shared@file:../shared:
        dependencies:
          my-mkdirp: mkdirp@0.5.1

    Step 4 looks up packageID("my-mkdirp", "mkdirp@0.5.1"), misses, and shared ends up with no dependencies, so mkdirp and minimist stay Dev: true.

    Plan:

    1. Add a helper that maps (name, reference) to a snapshot key, mirroring pnpm's refToRelative. link: has no snapshot, a reference starting with @ or with an @ before any : and ( is already a full key, anything else gets the name prepended.
    2. Use it for importers in step 1 and for snapshot dependencies in step 4. packageID and the reported IDs stay as they are.
    3. Tests: a table test for the helper, plus fixtures generated by real pnpm (with the commands in a comment) for the npm alias, pnpm deploy with a workspace package, and the nested case above.

    I'd keep this to v9. In v6 (checked with pnpm 8) dev: false comes from the lockfile, so only Relationship is wrong there. Happy to do that separately.

  7. dfbustosus commented on Oct 12, 2026

    @dfbustosus

    I'd like to take this on. Proposing the following approach for agreement before I start:

    Root cause recap

    In parseV9 the importer-side ID is packageID(<importer key>, <version>):

    • pnpm deploy workspace case: importer key repro-shared, version repro-shared@file:///… → ID repro-shared@repro-shared@file:///…, but the snapshot ID is repro-shared@file:///…
    • npm alias case: importer key my-mkdirp, version mkdirp@0.5.1 → ID my-mkdirp@mkdirp@0.5.1, but the snapshot ID is mkdirp@0.5.1

    Neither matches, so the dep never enters directProdDeps, stays Dev: true / indirect, and markRootPkgs never walks its subtree.

    Proposed fix

    Normalize the importer version before building the direct-dependency ID:

    1. Apply the existing trimPeerDeps first, so 1.0.0(react@18.0.0) reduces to 1.0.0 before the alias check.
    2. If the trimmed version itself has the shape <name>@<rest> and <rest> starts with a version digit, file:, link:, http(s)://, or npm: — i.e. it is a complete package reference rather than a bare version — use the trimmed version as the package ID directly, bypassing packageID(<importer key>, <version>):
      • workspace deploy: repro-shared@file:///… → matches the snapshot ID exactly
      • npm alias: mkdirp@0.5.1 → matches the snapshot ID exactly
    3. Otherwise keep today's packageID(key, version) behavior (plain versions, scoped names unchanged since the alias shape check requires name@-prefix with a non-empty rest).
    4. Since the snapshot is keyed by the real package name in the alias case, matching by ID (not by importer key) also fixes Relationship: the dep is found in directProdDeps and is reported direct, Dev: false.

    Detection heuristic for "complete package reference": the first @ must not be at index 0 (that's a scoped name), and the remainder must parse as version/file:/link:/npm:/URL. For scoped names (@scope/name@1.2.3) the second @ is the separator.

    Tests

    • Add lockfile fixtures generated by real pnpm (with repro commands in a comment, matching the pnpmV9MultipleDocuments precedent):
      • workspace pnpm deploy case (repro-shared + @xmldom/xmldom subtree must be Dev: false, production)
      • npm alias case (my-mkdirp → mkdirp@0.5.1, minimist subtree must be Dev: false)
    • Assert both Dev and Relationship: direct for the aliased roots, and that vulns in their subtrees are reported without --include-dev-deps.
    • I'll also check what overrides/catalog: entries look like in v9 lockfiles and add a fixture if they exercise the same path.

    Out of scope (flag if you disagree)

    • Behavior for lockfiles before v9 (parseV5/parseV6) — I'll check whether the same alias forms can appear there, but unless they do I'd keep this PR v9-only.
    • The link: importer case: link: versions point to workspace links that have no snapshot of their own; if the normalization makes these resolve differently I'll keep the old path for them and document why.

    Does this match what you had in mind? Happy to adjust before implementing.

  8. dfbustosus commented on Oct 12, 2026

    @dfbustosus

    @MannXo your plan looks right to me, and the nested-alias case is a good catch — a single (name, reference) → snapshot key helper covering importers and snapshot dependencies is cleaner than the per-callsite normalization I sketched (it subsumes both, and keeping packageID and reported IDs unchanged avoids any output churn). I\u0027m happy to step back and let you run with it — happy to review the PR or help test the fixtures if useful.

  9. DmitriyLewen commented on Oct 12, 2026

    @DmitriyLewen
    ContributorAuthor

    Thanks, @MannXo!

    The plan looks right to me, so I'm assigning this issue to you.

    Unfortunately, we are busy with higher-priority tasks right now, so the review may take a while.
    Please be patient once you open the PR.

    Regards, Dmitriy

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

good first issueDenotes an issue ready for a new contributor, according to the "help wanted" guidelines.help wantedDenotes an issue that needs help from a contributor. Must meet "help wanted" guidelines.kind/bugCategorizes issue or PR as related to a bug.scan/vulnerabilityIssues relating to vulnerability scanning

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions