@@ -18,44 +18,6 @@ import (
1818 xio "github.com/aquasecurity/trivy/pkg/x/io"
1919)
2020
21- type PackageResolution struct {
22- Tarball string `yaml:"tarball,omitempty"`
23- }
24-
25- type PackageInfo struct {
26- Resolution PackageResolution `yaml:"resolution"`
27- Dependencies map [string ]string `yaml:"dependencies,omitempty"`
28- DevDependencies map [string ]string `yaml:"devDependencies,omitempty"`
29- IsDev bool `yaml:"dev,omitempty"`
30- Name string `yaml:"name,omitempty"`
31- Version string `yaml:"version,omitempty"`
32- }
33-
34- type LockFile struct {
35- LockfileVersion any `yaml:"lockfileVersion"`
36- Dependencies map [string ]any `yaml:"dependencies,omitempty"`
37- DevDependencies map [string ]any `yaml:"devDependencies,omitempty"`
38- Packages map [string ]PackageInfo `yaml:"packages,omitempty"`
39-
40- // V9
41- Importers map [string ]Importer `yaml:"importers,omitempty"`
42- Snapshots map [string ]Snapshot `yaml:"snapshots,omitempty"`
43- }
44-
45- type Importer struct {
46- Dependencies map [string ]ImporterDepVersion `yaml:"dependencies,omitempty"`
47- DevDependencies map [string ]ImporterDepVersion `yaml:"devDependencies,omitempty"`
48- }
49-
50- type ImporterDepVersion struct {
51- Version string `yaml:"version,omitempty"`
52- }
53-
54- type Snapshot struct {
55- Dependencies map [string ]string `yaml:"dependencies,omitempty"`
56- OptionalDependencies map [string ]string `yaml:"optionalDependencies,omitempty"`
57- }
58-
5921type Parser struct {
6022 logger * log.Logger
6123}
@@ -96,7 +58,7 @@ func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, []
9658
9759 // Dependency path is a path to a dependency with a specific set of resolved subdependencies.
9860 // cf. https://github.com/pnpm/spec/blob/ad27a225f81d9215becadfa540ef05fa4ad6dd60/dependency-path.md
99- for depPath , info := range lockFile .Packages {
61+ for pkgKey , info := range lockFile .Packages {
10062 if info .IsDev {
10163 continue
10264 }
@@ -109,9 +71,10 @@ func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, []
10971 var ref string
11072
11173 if name == "" {
112- name , version , ref = p .parseDepPath ( depPath , lockVer )
113- version = p .parseVersion (depPath , version , lockVer )
74+ name , version , ref = p .parsePnpmKey ( string ( pkgKey ) , lockVer )
75+ version = p .parseVersion (string ( pkgKey ) , version , lockVer )
11476 }
77+ // Create Trivy's internal package ID
11578 pkgID := packageID (name , version )
11679
11780 dependencies := make ([]string , 0 , len (info .Dependencies ))
@@ -139,34 +102,19 @@ func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, []
139102 return pkgs , deps
140103}
141104
105+ // parseV9 parses pnpm-lock.yaml version 9.x format and returns packages and their dependencies.
106+ // Version 9 introduced "snapshots" where each snapshot represents a package with its exact resolved dependencies.
142107func (p * Parser ) parseV9 (lockFile LockFile ) ([]ftypes.Package , []ftypes.Dependency ) {
143108 lockVer := 9.0
144- resolvedPkgs := make (map [string ]ftypes.Package )
145- resolvedDeps := make (map [string ]ftypes.Dependency )
146-
147- // Check all snapshots and save with resolved versions
148- resolvedSnapshots := make (map [string ][]string )
149- for depPath , snapshot := range lockFile .Snapshots {
150- name , version , _ := p .parseDepPath (depPath , lockVer )
151-
152- var dependsOn []string
153- for depName , depVer := range lo .Assign (snapshot .OptionalDependencies , snapshot .Dependencies ) {
154- depVer = p .trimPeerDeps (depVer , lockVer ) // pnpm has already separated dep name. therefore, we only need to separate peer deps.
155- depVer = p .parseVersion (depPath , depVer , lockVer )
156- id := packageID (depName , depVer )
157- if _ , ok := lockFile .Packages [id ]; ok {
158- dependsOn = append (dependsOn , id )
159- }
160- }
161- if len (dependsOn ) > 0 {
162- resolvedSnapshots [packageID (name , version )] = dependsOn
163- }
164-
165- }
166-
167- // Parse `Importers` to find all direct dependencies
168- devDeps := make (map [string ]string )
169- deps := make (map [string ]string )
109+ resolvedPkgs := make (map [SnapshotKey ]ftypes.Package )
110+ resolvedDeps := make (map [SnapshotKey ]ftypes.Dependency )
111+
112+ // Step 1: Extract direct dependencies from the "importers" section.
113+ // The "importers" section contains the dependencies defined in package.json files.
114+ // We need to identify which packages are direct dependencies (vs transitive)
115+ // and which are development dependencies (vs production dependencies).
116+ devDeps := make (map [string ]string ) // name -> version for dev dependencies
117+ deps := make (map [string ]string ) // name -> version for production dependencies
170118 for _ , importer := range lockFile .Importers {
171119 for n , v := range importer .DevDependencies {
172120 devDeps [n ] = v .Version
@@ -176,74 +124,109 @@ func (p *Parser) parseV9(lockFile LockFile) ([]ftypes.Package, []ftypes.Dependen
176124 }
177125 }
178126
179- for depPath , pkgInfo := range lockFile .Packages {
180- name , ver , ref := p .parseDepPath (depPath , lockVer )
181- parsedVer := p .parseVersion (depPath , ver , lockVer )
182-
183- if pkgInfo .Version != "" {
127+ // Step 2: Process each snapshot to create package entries.
128+ // Each snapshot represents a unique package installation with specific peer dependencies.
129+ // The snapshotKey is the key that uniquely identifies this package instance,
130+ // including any peer dependency information (e.g., "[email protected] ([email protected] )"). 131+ for snapshotKey , snapshot := range lockFile .Snapshots {
132+ name , version , ref := p .parsePnpmKey (string (snapshotKey ), lockVer )
133+ // Clean and validate the version string (remove file: or http: prefixes if invalid)
134+ parsedVer := p .parseVersion (string (snapshotKey ), version , lockVer )
135+
136+ // Try to get the exact version from the "packages" section if available.
137+ // The "packages" section may contain more accurate version information
138+ // for packages installed from non-standard sources (git, local files, etc.).
139+ pkgKey := PackageKey (packageID (name , version ))
140+ if pkgInfo , ok := lockFile .Packages [pkgKey ]; ok && pkgInfo .Version != "" {
184141 parsedVer = pkgInfo .Version
185142 }
186143
187- // By default, pkg is dev pkg.
188- // We will update `Dev` field later.
144+ // Step 3: Determine if this package is a direct or transitive dependency,
145+ // and whether it's a development or production dependency.
146+ // By default, assume it's a development dependency (will be corrected later if needed).
189147 dev := true
190- relationship := ftypes .RelationshipIndirect
191- if v , ok := devDeps [name ]; ok && p .trimPeerDeps (v , lockVer ) == ver {
148+ relationship := ftypes .RelationshipIndirect // Assume transitive by default
149+
150+ // Check if this package matches a direct dev dependency
151+ if v , ok := devDeps [name ]; ok && p .trimPeerDeps (v , lockVer ) == version {
192152 relationship = ftypes .RelationshipDirect
193153 }
194- if v , ok := deps [name ]; ok && p .trimPeerDeps (v , lockVer ) == ver {
154+ // Check if this package matches a direct production dependency
155+ if v , ok := deps [name ]; ok && p .trimPeerDeps (v , lockVer ) == version {
195156 relationship = ftypes .RelationshipDirect
196- dev = false // mark root direct deps to update ` dev` field of their child deps.
157+ dev = false // This is a production dependency, not a dev dependency
197158 }
198159
199- id := packageID (name , parsedVer )
200- resolvedPkgs [id ] = ftypes.Package {
201- ID : id ,
160+ // Create the package entry with all extracted information.
161+ pkg := ftypes.Package {
162+ // ID is the full snapshotKey which uniquely identifies this package instance
163+ // including any peer dependency context.
164+ ID : string (snapshotKey ),
202165 Name : name ,
203166 Version : parsedVer ,
204167 Relationship : relationship ,
205168 Dev : dev ,
206169 ExternalReferences : toExternalRefs (ref ),
207170 }
171+ resolvedPkgs [snapshotKey ] = pkg
208172
209- // Save child deps
210- if dependsOn , ok := resolvedSnapshots [depPath ]; ok {
211- sort .Strings (dependsOn )
212- resolvedDeps [id ] = ftypes.Dependency {
213- ID : id ,
214- DependsOn : dependsOn , // Deps from dependsOn has been resolved when parsing snapshots
173+ // Step 4: Build the dependency graph by recording what this package depends on.
174+ var dependsOn []string // List of snapshot keys this package depends on
175+ for depName , depVer := range lo .Assign (snapshot .OptionalDependencies , snapshot .Dependencies ) {
176+ normalizedDepVer := p .trimPeerDeps (depVer , lockVer )
177+ // Only include dependencies that are actually installed (exist in "packages" section).
178+ if _ , ok := lockFile .Packages [PackageKey (packageID (depName , normalizedDepVer ))]; ok {
179+ // Use the original name/version string (with peer deps) to build the snapshot key correctly.
180+ dependsOn = append (dependsOn , packageID (depName , depVer ))
181+ }
182+ }
183+ if len (dependsOn ) > 0 {
184+ resolvedDeps [snapshotKey ] = ftypes.Dependency {
185+ ID : string (snapshotKey ),
186+ DependsOn : dependsOn ,
215187 }
216188 }
217189 }
218190
219- visited := set .New [string ]()
220- // Overwrite the `Dev` field for dev deps and their child dependencies.
191+ // Step 5: Propagate the "production" status to all transitive dependencies.
192+ // If a package is a production dependency (Dev=false), all packages it depends on
193+ // should also be marked as production dependencies, even if they were initially
194+ // marked as dev dependencies. This ensures we correctly identify which packages
195+ // are needed for production vs only for development.
196+ visited := set .New [SnapshotKey ]()
221197 for _ , pkg := range resolvedPkgs {
222- if ! pkg .Dev {
223- p .markRootPkgs (pkg .ID , resolvedPkgs , resolvedDeps , visited )
198+ if ! pkg .Dev { // If this is a production dependency
199+ // Recursively mark this package and all its dependencies as production
200+ p .markRootPkgs (SnapshotKey (pkg .ID ), resolvedPkgs , resolvedDeps , visited )
224201 }
225202 }
226203
227204 return lo .Values (resolvedPkgs ), lo .Values (resolvedDeps )
228205}
229206
230- // markRootPkgs sets `Dev` to false for non dev dependency.
231- func (p * Parser ) markRootPkgs (id string , pkgs map [string ]ftypes.Package , deps map [string ]ftypes.Dependency , visited set.Set [string ]) {
207+ // markRootPkgs recursively marks a package and all its dependencies as production dependencies.
208+ // This is used to propagate the production status from direct production dependencies
209+ // to all their transitive dependencies, ensuring that any package required for production
210+ // is correctly identified, even if it's also listed as a dev dependency elsewhere.
211+ func (p * Parser ) markRootPkgs (id SnapshotKey , pkgs map [SnapshotKey ]ftypes.Package , deps map [SnapshotKey ]ftypes.Dependency , visited set.Set [SnapshotKey ]) {
212+ // Avoid infinite recursion in case of circular dependencies
232213 if visited .Contains (id ) {
233214 return
234215 }
216+ // Get the package; skip if not found
235217 pkg , ok := pkgs [id ]
236218 if ! ok {
237219 return
238220 }
239221
222+ // Mark this package as a production dependency
240223 pkg .Dev = false
241224 pkgs [id ] = pkg
242- visited .Append (id )
225+ visited .Append (id ) // Track that we've processed this package
243226
244- // Update child deps
227+ // Recursively process all dependencies of this package
245228 for _ , depID := range deps [id ].DependsOn {
246- p .markRootPkgs (depID , pkgs , deps , visited )
229+ p .markRootPkgs (SnapshotKey ( depID ) , pkgs , deps , visited )
247230 }
248231}
249232
@@ -267,8 +250,14 @@ func (p *Parser) parseLockfileVersion(lockFile LockFile) float64 {
267250 }
268251}
269252
270- func (p * Parser ) parseDepPath (depPath string , lockVer float64 ) (string , string , string ) {
271- dPath , nonDefaultRegistry := p .trimRegistry (depPath , lockVer )
253+ // parsePnpmKey parses a pnpm package key (either PackageKey or SnapshotKey)
254+ // and extracts the package name, version, and optional registry reference.
255+ // The key format varies between pnpm versions:
256+ // - v5: "registry.npmjs.org/@babel/generator/7.21.9"
257+ // - v6+: "@babel/[email protected] " 258+ // - v9+: "@babel/[email protected] ([email protected] )" (SnapshotKey with peers) 259+ func (p * Parser ) parsePnpmKey (pnpmKey string , lockVer float64 ) (string , string , string ) {
260+ dPath , nonDefaultRegistry := p .trimRegistry (pnpmKey , lockVer )
272261
273262 var scope string
274263 scope , dPath = p .separateScope (dPath )
@@ -283,43 +272,43 @@ func (p *Parser) parseDepPath(depPath string, lockVer float64) (string, string,
283272
284273 ver := p .trimPeerDeps (dPath , lockVer )
285274
286- return name , ver , lo .Ternary (nonDefaultRegistry , depPath , "" )
275+ return name , ver , lo .Ternary (nonDefaultRegistry , pnpmKey , "" )
287276}
288277
289- // trimRegistry trims registry (or `/` prefix) for depPath .
278+ // trimRegistry trims registry (or `/` prefix) from a pnpm key .
290279// It returns true if non-default registry has been trimmed.
291280// e.g.
292281// - "registry.npmjs.org/lodash/4.17.10" => "lodash/4.17.10", false
293282// - "registry.npmjs.org/@babel/generator/7.21.9" => "@babel/generator/7.21.9", false
294283// - "private.npm.org/@babel/generator/7.21.9" => "@babel/generator/7.21.9", true
295284// - "/lodash/4.17.10" => "lodash/4.17.10", false
296285297- func (p * Parser ) trimRegistry (depPath string , lockVer float64 ) (string , bool ) {
286+ func (p * Parser ) trimRegistry (pnpmKey string , lockVer float64 ) (string , bool ) {
298287 var nonDefaultRegistry bool
299288 // lock file v9 doesn't use registry prefix
300289 if lockVer < 9 {
301290 var registry string
302- registry , depPath , _ = strings .Cut (depPath , "/" )
291+ registry , pnpmKey , _ = strings .Cut (pnpmKey , "/" )
303292 if registry != "" && registry != "registry.npmjs.org" {
304293 nonDefaultRegistry = true
305294 }
306295 }
307- return depPath , nonDefaultRegistry
296+ return pnpmKey , nonDefaultRegistry
308297}
309298
310- // separateScope separates the scope (if set) from the rest of the depPath .
299+ // separateScope separates the scope (if set) from the rest of the pnpm key .
311300// e.g.
312301// - v5: "@babel/generator/7.21.9" => {"babel", "generator/7.21.9"}
313- // - v6+: "@babel/[email protected] " => " {"babel", "[email protected] "} 314- func (p * Parser ) separateScope (depPath string ) (string , string ) {
302+ // - v6+: "@babel/[email protected] " => {"babel", "[email protected] "} 303+ func (p * Parser ) separateScope (pnpmKey string ) (string , string ) {
315304 var scope string
316- if strings .HasPrefix (depPath , "@" ) {
317- scope , depPath , _ = strings .Cut (depPath , "/" )
305+ if strings .HasPrefix (pnpmKey , "@" ) {
306+ scope , pnpmKey , _ = strings .Cut (pnpmKey , "/" )
318307 }
319- return scope , depPath
308+ return scope , pnpmKey
320309}
321310
322- // separateName separates pkg name and version.
311+ // separateName separates package name and version from a pnpm key .
323312// e.g.
324313// - v5: "generator/7.21.9" => {"generator", "7.21.9"}
325314// - v6+: "7.21.5(@babel/[email protected] )" => "7.21.5" @@ -330,26 +319,26 @@ func (p *Parser) separateScope(depPath string) (string, string) {
330319//
331320// Also version can contain peer deps:
332321333- func (p * Parser ) separateName (depPath string , lockVer float64 ) (string , string ) {
322+ func (p * Parser ) separateName (pnpmKey string , lockVer float64 ) (string , string ) {
334323 sep := "@"
335324 if lockVer < 6 {
336325 sep = "/"
337326 }
338- name , version , _ := strings .Cut (depPath , sep )
327+ name , version , _ := strings .Cut (pnpmKey , sep )
339328 return name , version
340329}
341330
342- // Trim peer deps
331+ // trimPeerDeps removes peer dependency suffixes from a version string.
343332// e.g.
344333// - v5: "7.21.5_@[email protected] " => "7.21.5" 345334// - v6+: "7.21.5(@babel/[email protected] )" => "7.21.5" 346- func (p * Parser ) trimPeerDeps (depPath string , lockVer float64 ) string {
335+ func (p * Parser ) trimPeerDeps (version string , lockVer float64 ) string {
347336 sep := "("
348337 if lockVer < 6 {
349338 sep = "_"
350339 }
351- version , _ , _ := strings .Cut (depPath , sep )
352- return version
340+ v , _ , _ := strings .Cut (version , sep )
341+ return v
353342}
354343
355344// parseVersion parses version.
0 commit comments