@@ -10,14 +10,12 @@ import (
10
10
"path"
11
11
"path/filepath"
12
12
"regexp"
13
- "sort"
14
13
"strings"
15
14
16
15
"github.com/hashicorp/go-multierror"
17
16
"github.com/samber/lo"
18
17
"golang.org/x/xerrors"
19
18
20
- "github.com/aquasecurity/trivy/pkg/dependency"
21
19
"github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/packagejson"
22
20
"github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/yarn"
23
21
"github.com/aquasecurity/trivy/pkg/detector/library/compare/npm"
@@ -47,7 +45,7 @@ var fragmentRegexp = regexp.MustCompile(`(\S+):(@?.*?)(@(.*?)|)$`)
47
45
type yarnAnalyzer struct {
48
46
logger * log.Logger
49
47
packageJsonParser * packagejson.Parser
50
- lockParser language .Parser
48
+ lockParser * yarn .Parser
51
49
comparer npm.Comparer
52
50
license * license.License
53
51
}
@@ -70,8 +68,18 @@ func (a yarnAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysis
70
68
}
71
69
72
70
err := fsutils .WalkDir (input .FS , "." , required , func (filePath string , d fs.DirEntry , r io.Reader ) error {
71
+ // Detect direct and direct dev dependencies to use them when parsing yarn lock file
72
+ packageJsonPath := path .Join (path .Dir (filePath ), types .NpmPkg )
73
+ directDeps , directDevDeps , err := a .parsePackageJsonDependencies (input .FS , packageJsonPath )
74
+ if errors .Is (err , fs .ErrNotExist ) {
75
+ a .logger .Debug ("package.json not found" , log .FilePath (packageJsonPath ))
76
+ } else if err != nil {
77
+ a .logger .Warn ("Unable to parse package.json to remove dev dependencies" ,
78
+ log .FilePath (packageJsonPath ), log .Err (err ))
79
+ }
80
+
73
81
// Parse yarn.lock
74
- app , err := a .parseYarnLock (filePath , r )
82
+ app , err := a .parseYarnLock (filePath , r , directDeps , directDevDeps )
75
83
if err != nil {
76
84
return xerrors .Errorf ("parse error: %w" , err )
77
85
} else if app == nil {
@@ -83,12 +91,6 @@ func (a yarnAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysis
83
91
a .logger .Debug ("Unable to traverse licenses" , log .Err (err ))
84
92
}
85
93
86
- // Parse package.json alongside yarn.lock to find direct deps and mark dev deps
87
- if err = a .analyzeDependencies (input .FS , path .Dir (filePath ), app ); err != nil {
88
- a .logger .Warn ("Unable to parse package.json to remove dev dependencies" ,
89
- log .FilePath (path .Join (path .Dir (filePath ), types .NpmPkg )), log .Err (err ))
90
- }
91
-
92
94
// Fill licenses
93
95
for i , lib := range app .Packages {
94
96
if l , ok := licenses [lib .ID ]; ok {
@@ -152,128 +154,21 @@ func (a yarnAnalyzer) Version() int {
152
154
return version
153
155
}
154
156
155
- func (a yarnAnalyzer ) parseYarnLock (filePath string , r io.Reader ) (* types.Application , error ) {
156
- return language .Parse (types .Yarn , filePath , r , a .lockParser )
157
+ type pkgJsonDeps struct {
158
+ directDeps map [string ]string
159
+ directDevDeps map [string ]string
157
160
}
158
161
159
- // analyzeDependencies analyzes the package.json file next to yarn.lock,
160
- // distinguishing between direct and transitive dependencies as well as production and development dependencies.
161
- func (a yarnAnalyzer ) analyzeDependencies (fsys fs.FS , dir string , app * types.Application ) error {
162
- packageJsonPath := path .Join (dir , types .NpmPkg )
163
- directDeps , directDevDeps , err := a .parsePackageJsonDependencies (fsys , packageJsonPath )
164
- if errors .Is (err , fs .ErrNotExist ) {
165
- a .logger .Debug ("package.json not found" , log .FilePath (packageJsonPath ))
166
- return nil
167
- } else if err != nil {
168
- return xerrors .Errorf ("unable to parse %s: %w" , dir , err )
169
- }
170
-
171
- // yarn.lock file can contain same packages with different versions
172
- // save versions separately for version comparison by comparator
173
- pkgIDs := lo .SliceToMap (app .Packages , func (pkg types.Package ) (string , types.Package ) {
174
- return pkg .ID , pkg
175
- })
176
-
177
- // Walk prod dependencies
178
- pkgs , err := a .walkDependencies (app .Packages , pkgIDs , directDeps , false )
179
- if err != nil {
180
- return xerrors .Errorf ("unable to walk dependencies: %w" , err )
181
- }
182
-
183
- // Walk dev dependencies
184
- devPkgs , err := a .walkDependencies (app .Packages , pkgIDs , directDevDeps , true )
185
- if err != nil {
186
- return xerrors .Errorf ("unable to walk dependencies: %w" , err )
187
- }
188
-
189
- // Merge prod and dev dependencies.
190
- // If the same package is found in both prod and dev dependencies, use the one in prod.
191
- pkgs = lo .Assign (devPkgs , pkgs )
192
-
193
- pkgSlice := lo .MapToSlice (pkgs , func (_ string , pkg types.Package ) types.Package {
194
- // Use Trivy ID format for dependencies with `latest` version in `ID` (`version` field contains the correct version)
195
- if verFromID (pkg .ID ) == latestVersion {
196
- pkg .ID = dependency .ID (types .Yarn , pkg .Name , pkg .Version )
197
- }
198
- return pkg
199
- })
200
- sort .Sort (types .Packages (pkgSlice ))
201
-
202
- // Save packages
203
- app .Packages = pkgSlice
204
- return nil
162
+ func (d pkgJsonDeps ) Parse (r xio.ReadSeekerAt ) ([]types.Package , []types.Dependency , error ) {
163
+ return yarn .NewParser ().Parse (r , d .directDeps , d .directDevDeps )
205
164
}
206
165
207
- func (a yarnAnalyzer ) walkDependencies (pkgs []types.Package , pkgIDs map [string ]types.Package ,
208
- directDeps map [string ]string , dev bool ) (map [string ]types.Package , error ) {
209
-
210
- // Identify direct dependencies
211
- directPkgs := make (map [string ]types.Package )
212
- for _ , pkg := range pkgs {
213
- constraint , ok := directDeps [pkg .Name ]
214
- if ! ok {
215
- continue
216
- }
217
-
218
- if constraint == latestVersion {
219
- // pkgID with `latest` version uses `<pkgName>@latest` format.
220
- if verFromID (pkg .ID ) != latestVersion {
221
- continue
222
- }
223
- } else {
224
- // Handle aliases
225
- // cf. https://classic.yarnpkg.com/lang/en/docs/cli/add/#toc-yarn-add-alias
226
- if m := fragmentRegexp .FindStringSubmatch (constraint ); len (m ) == 5 {
227
- pkg .Name = m [2 ] // original name
228
- constraint = m [4 ]
229
- }
230
-
231
- // npm has own comparer to compare versions
232
- if match , err := a .comparer .MatchVersion (pkg .Version , constraint ); err != nil {
233
- return nil , xerrors .Errorf ("unable to match version for %s" , pkg .Name )
234
- } else if ! match {
235
- continue
236
- }
237
- }
238
-
239
- // Mark as a direct dependency
240
- pkg .Indirect = false
241
- pkg .Relationship = types .RelationshipDirect
242
- pkg .Dev = dev
243
- directPkgs [pkg .ID ] = pkg
244
-
245
- }
246
-
247
- // Walk indirect dependencies
248
- for _ , pkg := range directPkgs {
249
- a .walkIndirectDependencies (pkg , pkgIDs , directPkgs )
250
- }
251
-
252
- return directPkgs , nil
253
- }
254
-
255
- func verFromID (id string ) string {
256
- _ , ver , _ := strings .Cut (id , "@" )
257
- return ver
258
- }
259
-
260
- func (a yarnAnalyzer ) walkIndirectDependencies (pkg types.Package , pkgIDs , deps map [string ]types.Package ) {
261
- for _ , pkgID := range pkg .DependsOn {
262
- if _ , ok := deps [pkgID ]; ok {
263
- continue
264
- }
265
-
266
- dep , ok := pkgIDs [pkgID ]
267
- if ! ok {
268
- continue
269
- }
270
-
271
- dep .Indirect = true
272
- dep .Relationship = types .RelationshipIndirect
273
- dep .Dev = pkg .Dev
274
- deps [dep .ID ] = dep
275
- a .walkIndirectDependencies (dep , pkgIDs , deps )
166
+ func (a yarnAnalyzer ) parseYarnLock (filePath string , r io.Reader , directDeps , directDevDeps map [string ]string ) (* types.Application , error ) {
167
+ pkgJson := pkgJsonDeps {
168
+ directDeps : directDeps ,
169
+ directDevDeps : directDevDeps ,
276
170
}
171
+ return language .Parse (types .Yarn , filePath , r , pkgJson )
277
172
}
278
173
279
174
func (a yarnAnalyzer ) parsePackageJsonDependencies (fsys fs.FS , filePath string ) (map [string ]string , map [string ]string , error ) {
0 commit comments