Repository navigation
bug(pnpm): direct dependencies with an aliased version in importers are marked as dev #11389
Description
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.13Result (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.8Result:
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 isrepro-shared@file:///…; - for the alias:
my-mkdirp@mkdirp@0.5.1, while the snapshot ID ismkdirp@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
versioncan 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 befile: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 forpnpm deploythey are the same. The same logic has to handle both cases. - Not only
Devis wrong but alsoRelationship: the direct dependency is reported asindirect. - 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
MannXo commentedon Oct 12, 2026
I'd like to give this a shot if it's unassigned.
DmitriyLewen commentedon Oct 12, 2026
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
MannXo commentedon Oct 12, 2026
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:
Step 4 looks up
packageID("my-mkdirp", "mkdirp@0.5.1"), misses, andsharedends up with no dependencies, somkdirpandminimiststayDev: true.Plan:
(name, reference)to a snapshot key, mirroring pnpm'srefToRelative.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.importersin step 1 and for snapshot dependencies in step 4.packageIDand the reported IDs stay as they are.pnpm deploywith a workspace package, and the nested case above.I'd keep this to v9. In v6 (checked with pnpm 8)
dev: falsecomes from the lockfile, so onlyRelationshipis wrong there. Happy to do that separately.dfbustosus commentedon Oct 12, 2026
I'd like to take this on. Proposing the following approach for agreement before I start:
Root cause recap
In
parseV9the importer-side ID ispackageID(<importer key>, <version>):pnpm deployworkspace case: importer keyrepro-shared, versionrepro-shared@file:///…→ IDrepro-shared@repro-shared@file:///…, but the snapshot ID isrepro-shared@file:///…my-mkdirp, versionmkdirp@0.5.1→ IDmy-mkdirp@mkdirp@0.5.1, but the snapshot ID ismkdirp@0.5.1Neither matches, so the dep never enters
directProdDeps, staysDev: true/indirect, andmarkRootPkgsnever walks its subtree.Proposed fix
Normalize the importer version before building the direct-dependency ID:
trimPeerDepsfirst, so1.0.0(react@18.0.0)reduces to1.0.0before the alias check.versionitself has the shape<name>@<rest>and<rest>starts with a version digit,file:,link:,http(s)://, ornpm:— i.e. it is a complete package reference rather than a bare version — use the trimmedversionas the package ID directly, bypassingpackageID(<importer key>, <version>):repro-shared@file:///…→ matches the snapshot ID exactlymkdirp@0.5.1→ matches the snapshot ID exactlypackageID(key, version)behavior (plain versions, scoped names unchanged since the alias shape check requiresname@-prefix with a non-empty rest).Relationship: the dep is found indirectProdDepsand is reporteddirect,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
pnpmV9MultipleDocumentsprecedent):pnpm deploycase (repro-shared +@xmldom/xmldomsubtree must beDev: false, production)my-mkdirp→mkdirp@0.5.1,minimistsubtree must beDev: false)DevandRelationship: directfor the aliased roots, and that vulns in their subtrees are reported without--include-dev-deps.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)
parseV5/parseV6) — I'll check whether the same alias forms can appear there, but unless they do I'd keep this PR v9-only.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.
dfbustosus commentedon Oct 12, 2026
@MannXo your plan looks right to me, and the nested-alias case is a good catch — a single
(name, reference) → snapshot keyhelper covering importers and snapshot dependencies is cleaner than the per-callsite normalization I sketched (it subsumes both, and keepingpackageIDand 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.DmitriyLewen commentedon Oct 12, 2026
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