@@ -2,6 +2,7 @@ package yarn
22
33import (
44 "archive/zip"
5+ "cmp"
56 "context"
67 "errors"
78 "io"
@@ -27,6 +28,7 @@ import (
2728 "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/nodejs/license"
2829 "github.com/aquasecurity/trivy/pkg/fanal/types"
2930 "github.com/aquasecurity/trivy/pkg/log"
31+ "github.com/aquasecurity/trivy/pkg/set"
3032 "github.com/aquasecurity/trivy/pkg/utils/fsutils"
3133 xio "github.com/aquasecurity/trivy/pkg/x/io"
3234)
@@ -162,36 +164,28 @@ func (a yarnAnalyzer) Version() int {
162164// distinguishing between direct and transitive dependencies as well as production and development dependencies.
163165func (a yarnAnalyzer ) analyzeDependencies (fsys fs.FS , dir string , app * types.Application , patterns map [string ][]string ) error {
164166 packageJsonPath := path .Join (dir , types .NpmPkg )
165- directDeps , directDevDeps , err := a .parsePackageJsonDependencies (fsys , packageJsonPath )
167+ root , workspaces , err := a .parsePackageJSON (fsys , packageJsonPath )
166168 if errors .Is (err , fs .ErrNotExist ) {
167169 a .logger .Debug ("package.json not found" , log .FilePath (packageJsonPath ))
168170 return nil
169171 } else if err != nil {
170- return xerrors .Errorf ("unable to parse %s : %w" , dir , err )
172+ return xerrors .Errorf ("unable to parse root package.json : %w" , err )
171173 }
172174
173- // yarn.lock file can contain same packages with different versions
174- // save versions separately for version comparison by comparator
175- pkgIDs := lo .SliceToMap (app .Packages , func (pkg types.Package ) (string , types.Package ) {
175+ // Since yarn.lock file can contain same packages with different versions
176+ // we need to save versions separately for version comparison.
177+ pkgs := lo .SliceToMap (app .Packages , func (pkg types.Package ) (string , types.Package ) {
176178 return pkg .ID , pkg
177179 })
178180
179- // Walk prod dependencies
180- pkgs , err := a .walkDependencies (app .Packages , pkgIDs , directDeps , patterns , false )
181- if err != nil {
182- return xerrors .Errorf ("unable to walk dependencies: %w" , err )
181+ if err := a .resolveRootDependencies (& root , pkgs , patterns ); err != nil {
182+ return xerrors .Errorf ("unable to resolve root dependencies: %w" , err )
183183 }
184184
185- // Walk dev dependencies
186- devPkgs , err := a .walkDependencies (app .Packages , pkgIDs , directDevDeps , patterns , true )
187- if err != nil {
188- return xerrors .Errorf ("unable to walk dependencies: %w" , err )
185+ if err := a .resolveWorkspaceDependencies (workspaces , pkgs , patterns ); err != nil {
186+ return xerrors .Errorf ("unable to resolve workspace dependencies: %w" , err )
189187 }
190188
191- // Merge prod and dev dependencies.
192- // If the same package is found in both prod and dev dependencies, use the one in prod.
193- pkgs = lo .Assign (devPkgs , pkgs )
194-
195189 pkgSlice := lo .Values (pkgs )
196190 sort .Sort (types .Packages (pkgSlice ))
197191
@@ -200,11 +194,94 @@ func (a yarnAnalyzer) analyzeDependencies(fsys fs.FS, dir string, app *types.App
200194 return nil
201195}
202196
203- func (a yarnAnalyzer ) walkDependencies (pkgs []types.Package , pkgIDs map [string ]types.Package ,
204- directDeps map [string ]string , patterns map [string ][]string , dev bool ) (map [string ]types.Package , error ) {
197+ func (a yarnAnalyzer ) parsePackageJSON (fsys fs.FS , filePath string ) (packagejson.Package , []packagejson.Package , error ) {
198+ // Parse package.json
199+ f , err := fsys .Open (filePath )
200+ if err != nil {
201+ return packagejson.Package {}, nil , xerrors .Errorf ("file open error: %w" , err )
202+ }
203+ defer func () { _ = f .Close () }()
204+
205+ root , err := a .packageJsonParser .Parse (f )
206+ if err != nil {
207+ return packagejson.Package {}, nil , xerrors .Errorf ("parse error: %w" , err )
208+ }
209+
210+ root .Package .ID = cmp .Or (root .Package .ID , filePath ) // In case the package.json doesn't have a name or version
211+ root .Package .Relationship = types .RelationshipRoot
212+
213+ workspaces , err := a .traverseWorkspaces (fsys , path .Dir (filePath ), root .Workspaces )
214+ if err != nil {
215+ return packagejson.Package {}, nil , xerrors .Errorf ("traverse workspaces error: %w" , err )
216+ }
217+ for i := range workspaces {
218+ workspaces [i ].Package .Relationship = types .RelationshipWorkspace
219+
220+ // Add workspace as a child of root
221+ root .DependsOn = append (root .DependsOn , workspaces [i ].ID )
222+ }
223+
224+ return root , workspaces , nil
225+ }
226+
227+ func (a yarnAnalyzer ) resolveRootDependencies (root * packagejson.Package , pkgs map [string ]types.Package ,
228+ patterns map [string ][]string ) error {
229+ if err := a .resolveDependencies (root , pkgs , patterns ); err != nil {
230+ return xerrors .Errorf ("unable to resolve dependencies: %w" , err )
231+ }
232+
233+ // Add root package to the package map
234+ slices .Sort (root .Package .DependsOn )
235+ pkgs [root .Package .ID ] = root .Package
236+
237+ return nil
238+ }
239+
240+ func (a yarnAnalyzer ) resolveWorkspaceDependencies (workspaces []packagejson.Package , pkgs map [string ]types.Package ,
241+ patterns map [string ][]string ) error {
242+ if len (workspaces ) == 0 {
243+ return nil
244+ }
245+
246+ for _ , workspace := range workspaces {
247+ if err := a .resolveDependencies (& workspace , pkgs , patterns ); err != nil {
248+ return xerrors .Errorf ("unable to resolve dependencies: %w" , err )
249+ }
250+
251+ // Add workspace to the package map
252+ slices .Sort (workspace .Package .DependsOn )
253+ pkgs [workspace .ID ] = workspace .Package
254+ }
255+
256+ return nil
257+ }
258+
259+ // resolveDependencies resolves production and development dependencies from direct dependencies and patterns.
260+ // It also flags dependencies as direct or indirect and updates the dependencies of the parent package.
261+ func (a yarnAnalyzer ) resolveDependencies (pkg * packagejson.Package , pkgs map [string ]types.Package , patterns map [string ][]string ) error {
262+ // Recursively walk dependencies and flags development dependencies.
263+ // Walk development dependencies first to avoid overwriting production dependencies.
264+ directDevDeps := pkg .DevDependencies
265+ if err := a .walkDependencies (& pkg .Package , pkgs , directDevDeps , patterns , true ); err != nil {
266+ return xerrors .Errorf ("unable to walk dependencies: %w" , err )
267+ }
268+
269+ // Recursively walk dependencies and flags production dependencies.
270+ directProdDeps := lo .Assign (pkg .Dependencies , pkg .OptionalDependencies )
271+ if err := a .walkDependencies (& pkg .Package , pkgs , directProdDeps , patterns , false ); err != nil {
272+ return xerrors .Errorf ("unable to walk dependencies: %w" , err )
273+ }
274+
275+ return nil
276+ }
277+
278+ // walkDependencies recursively walk dependencies and flags them as direct or indirect.
279+ // Note that it overwrites the existing package map.
280+ func (a yarnAnalyzer ) walkDependencies (parent * types.Package , pkgs map [string ]types.Package , directDeps map [string ]string ,
281+ patterns map [string ][]string , dev bool ) error {
205282
206283 // Identify direct dependencies
207- directPkgs := make ( map [string ]types. Package )
284+ seenIDs := set . New [string ]( )
208285 for _ , pkg := range pkgs {
209286 constraint , ok := directDeps [pkg .Name ]
210287 if ! ok {
@@ -224,76 +301,59 @@ func (a yarnAnalyzer) walkDependencies(pkgs []types.Package, pkgIDs map[string]t
224301 if pkgPatterns , found := patterns [pkg .ID ]; ! found || ! slices .Contains (pkgPatterns , dependency .ID (types .Yarn , pkg .Name , constraint )) {
225302 // npm has own comparer to compare versions
226303 if match , err := a .comparer .MatchVersion (pkg .Version , constraint ); err != nil {
227- return nil , xerrors .Errorf ("unable to match version for %s" , pkg .Name )
304+ return xerrors .Errorf ("unable to match version for %s" , pkg .Name )
228305 } else if ! match {
229306 continue
230307 }
231308 }
232309
310+ // If the package is already marked as a production dependency, skip overwriting it.
311+ // Since the dev field is boolean, it cannot determine if the package is already processed,
312+ // so we need to check the relationship field.
313+ if pkg .Relationship == types .RelationshipUnknown || pkg .Dev {
314+ pkg .Dev = dev
315+ }
316+
233317 // Mark as a direct dependency
234318 pkg .Indirect = false
235319 pkg .Relationship = types .RelationshipDirect
236- pkg .Dev = dev
237- directPkgs [pkg .ID ] = pkg
238320
239- }
321+ pkgs [pkg .ID ] = pkg
322+ seenIDs .Append (pkg .ID )
240323
241- // Walk indirect dependencies
242- for _ , pkg := range directPkgs {
243- a .walkIndirectDependencies (pkg , pkgIDs , directPkgs )
324+ // Add a direct dependency to the parent package
325+ parent .DependsOn = append (parent .DependsOn , pkg .ID )
326+
327+ // Walk indirect dependencies
328+ a .walkIndirectDependencies (pkg , pkgs , seenIDs )
244329 }
245330
246- return directPkgs , nil
331+ return nil
247332}
248333
249- func (a yarnAnalyzer ) walkIndirectDependencies (pkg types.Package , pkgIDs , deps map [string ]types.Package ) {
334+ func (a yarnAnalyzer ) walkIndirectDependencies (pkg types.Package , pkgs map [string ]types.Package , seenIDs set. Set [ string ] ) {
250335 for _ , pkgID := range pkg .DependsOn {
251- if _ , ok := deps [ pkgID ]; ok {
252- continue
336+ if seenIDs . Contains ( pkgID ) {
337+ continue // Skip if we've already seen this package
253338 }
254339
255- dep , ok := pkgIDs [pkgID ]
340+ dep , ok := pkgs [pkgID ]
256341 if ! ok {
257342 continue
258343 }
259344
345+ if dep .Relationship == types .RelationshipUnknown || dep .Dev {
346+ dep .Dev = pkg .Dev
347+ }
260348 dep .Indirect = true
261349 dep .Relationship = types .RelationshipIndirect
262- dep .Dev = pkg .Dev
263- deps [dep .ID ] = dep
264- a .walkIndirectDependencies (dep , pkgIDs , deps )
265- }
266- }
350+ pkgs [dep .ID ] = dep
267351
268- func (a yarnAnalyzer ) parsePackageJsonDependencies (fsys fs.FS , filePath string ) (map [string ]string , map [string ]string , error ) {
269- // Parse package.json
270- f , err := fsys .Open (filePath )
271- if err != nil {
272- return nil , nil , xerrors .Errorf ("file open error: %w" , err )
273- }
274- defer func () { _ = f .Close () }()
352+ seenIDs .Append (dep .ID )
275353
276- rootPkg , err := a .packageJsonParser .Parse (f )
277- if err != nil {
278- return nil , nil , xerrors .Errorf ("parse error: %w" , err )
279- }
280-
281- // Merge dependencies and optionalDependencies
282- dependencies := lo .Assign (rootPkg .Dependencies , rootPkg .OptionalDependencies )
283- devDependencies := rootPkg .DevDependencies
284-
285- if len (rootPkg .Workspaces ) > 0 {
286- pkgs , err := a .traverseWorkspaces (fsys , path .Dir (filePath ), rootPkg .Workspaces )
287- if err != nil {
288- return nil , nil , xerrors .Errorf ("traverse workspaces error: %w" , err )
289- }
290- for _ , pkg := range pkgs {
291- dependencies = lo .Assign (dependencies , pkg .Dependencies , pkg .OptionalDependencies )
292- devDependencies = lo .Assign (devDependencies , pkg .DevDependencies )
293- }
354+ // Recursively walk dependencies
355+ a .walkIndirectDependencies (dep , pkgs , seenIDs )
294356 }
295-
296- return dependencies , devDependencies , nil
297357}
298358
299359func (a yarnAnalyzer ) traverseWorkspaces (fsys fs.FS , dir string , workspaces []string ) ([]packagejson.Package , error ) {
@@ -308,6 +368,7 @@ func (a yarnAnalyzer) traverseWorkspaces(fsys fs.FS, dir string, workspaces []st
308368 if err != nil {
309369 return xerrors .Errorf ("unable to parse %q: %w" , path , err )
310370 }
371+ pkg .Package .ID = cmp .Or (pkg .Package .ID , path ) // In case the package.json doesn't have a name or version
311372 pkgs = append (pkgs , pkg )
312373 return nil
313374 }
0 commit comments