diff --git a/docs/docs/coverage/language/nodejs.md b/docs/docs/coverage/language/nodejs.md index 1489c459cd7d..393491b34f46 100644 --- a/docs/docs/coverage/language/nodejs.md +++ b/docs/docs/coverage/language/nodejs.md @@ -42,7 +42,10 @@ By default, Trivy doesn't report development dependencies. Use the `--include-de ### Yarn Trivy parses `yarn.lock`, which doesn't contain information about development dependencies. -To exclude devDependencies, `package.json` also needs to be present next to `yarn.lock`. +Trivy also uses `package.json` file to handle [aliases](https://classic.yarnpkg.com/lang/en/docs/cli/add/#toc-yarn-add-alias). + +To exclude devDependencies and allow aliases, `package.json` also needs to be present next to `yarn.lock`. + Trivy analyzes `.yarn` (Yarn 2+) or `node_modules` (Yarn Classic) folder next to the yarn.lock file to detect licenses. By default, Trivy doesn't report development dependencies. Use the `--include-dev-deps` flag to include them. diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/alias/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/alias/package.json new file mode 100644 index 000000000000..caa6e0f3f637 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/alias/package.json @@ -0,0 +1,14 @@ +{ + "name": "test", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "devDependencies": { + "foo-json": "npm:@types/jsonstream@0.8.33", + "foo-uuid": "npm:@types/uuid" + }, + "dependencies": { + "foo-debug": "npm:debug@^4.3", + "foo-ms": "npm:ms" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/alias/yarn.lock b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/alias/yarn.lock new file mode 100644 index 000000000000..cb68abb657f9 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/alias/yarn.lock @@ -0,0 +1,44 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@*": + version "20.10.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.5.tgz#47ad460b514096b7ed63a1dae26fad0914ed3ab2" + integrity sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw== + dependencies: + undici-types "~5.26.4" + +"foo-debug@npm:debug@^4.3": + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +"foo-json@npm:@types/jsonstream@0.8.33": + version "0.8.33" + resolved "https://registry.yarnpkg.com/@types/jsonstream/-/jsonstream-0.8.33.tgz#7d37a16a78cf68a67858110dc1767023436fca23" + integrity sha512-yhg1SNOgJ8y2nOkvAQ1zZ1Z2xibxgFs7984+EeBPuWgo/TbuYo79+rj2wUVch3KF4GhhcwAi/AlJcehmLCXb3g== + dependencies: + "@types/node" "*" + +"foo-ms@npm:ms": + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +"foo-uuid@npm:@types/uuid": + version "9.0.7" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.7.tgz#b14cebc75455eeeb160d5fe23c2fcc0c64f724d8" + integrity sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/yarn.go b/pkg/fanal/analyzer/language/nodejs/yarn/yarn.go index d42c28e47128..3089b881e2e3 100644 --- a/pkg/fanal/analyzer/language/nodejs/yarn/yarn.go +++ b/pkg/fanal/analyzer/language/nodejs/yarn/yarn.go @@ -9,6 +9,7 @@ import ( "os" "path" "path/filepath" + "regexp" "sort" "strings" @@ -36,6 +37,10 @@ func init() { const version = 2 +// Taken from Yarn +// cf. https://github.com/yarnpkg/yarn/blob/328fd596de935acc6c3e134741748fcc62ec3739/src/resolvers/exotics/registry-resolver.js#L12 +var fragmentRegexp = regexp.MustCompile(`(\S+):(@?.*?)(@(.*?)|)$`) + type yarnAnalyzer struct { packageJsonParser *packagejson.Parser lockParser godeptypes.Parser @@ -193,17 +198,30 @@ func (a yarnAnalyzer) walkDependencies(libs []types.Package, pkgIDs map[string]t // Identify direct dependencies pkgs := make(map[string]types.Package) for _, pkg := range libs { - if constraint, ok := directDeps[pkg.Name]; ok { - // npm has own comparer to compare versions - if match, err := a.comparer.MatchVersion(pkg.Version, constraint); err != nil { - return nil, xerrors.Errorf("unable to match version for %s", pkg.Name) - } else if match { - // Mark as a direct dependency - pkg.Indirect = false - pkg.Dev = dev - pkgs[pkg.ID] = pkg - } + constraint, ok := directDeps[pkg.Name] + if !ok { + continue + } + + // Handle aliases + // cf. https://classic.yarnpkg.com/lang/en/docs/cli/add/#toc-yarn-add-alias + if m := fragmentRegexp.FindStringSubmatch(constraint); len(m) == 5 { + pkg.Name = m[2] // original name + constraint = m[4] } + + // npm has own comparer to compare versions + if match, err := a.comparer.MatchVersion(pkg.Version, constraint); err != nil { + return nil, xerrors.Errorf("unable to match version for %s", pkg.Name) + } else if !match { + continue + } + + // Mark as a direct dependency + pkg.Indirect = false + pkg.Dev = dev + pkgs[pkg.ID] = pkg + } // Walk indirect dependencies diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/yarn_test.go b/pkg/fanal/analyzer/language/nodejs/yarn/yarn_test.go index eb8be228afd5..081d06c5954f 100644 --- a/pkg/fanal/analyzer/language/nodejs/yarn/yarn_test.go +++ b/pkg/fanal/analyzer/language/nodejs/yarn/yarn_test.go @@ -318,6 +318,117 @@ func Test_yarnLibraryAnalyzer_Analyze(t *testing.T) { }, }, }, + { + name: "happy path with alias rewrite", + dir: "testdata/alias", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Yarn, + FilePath: "yarn.lock", + Libraries: types.Packages{ + { + ID: "foo-json@0.8.33", + Name: "@types/jsonstream", + Version: "0.8.33", + Indirect: false, + Dev: true, + Locations: []types.Location{ + { + StartLine: 19, + EndLine: 24, + }, + }, + DependsOn: []string{ + "@types/node@20.10.5", + }, + }, + { + ID: "@types/node@20.10.5", + Name: "@types/node", + Version: "20.10.5", + Indirect: true, + Dev: true, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 10, + }, + }, + DependsOn: []string{ + "undici-types@5.26.5", + }, + }, + { + ID: "foo-uuid@9.0.7", + Name: "@types/uuid", + Version: "9.0.7", + Indirect: false, + Dev: true, + Locations: []types.Location{ + { + StartLine: 31, + EndLine: 34, + }, + }, + }, + { + ID: "foo-debug@4.3.4", + Name: "debug", + Version: "4.3.4", + Indirect: false, + Locations: []types.Location{ + { + StartLine: 12, + EndLine: 17, + }, + }, + DependsOn: []string{ + "ms@2.1.2", + }, + }, + { + ID: "ms@2.1.2", + Name: "ms", + Version: "2.1.2", + Indirect: true, + Locations: []types.Location{ + { + StartLine: 36, + EndLine: 39, + }, + }, + }, + { + ID: "foo-ms@2.1.3", + Name: "ms", + Version: "2.1.3", + Indirect: false, + Locations: []types.Location{ + { + StartLine: 26, + EndLine: 29, + }, + }, + }, + { + ID: "undici-types@5.26.5", + Name: "undici-types", + Version: "5.26.5", + Indirect: true, + Dev: true, + Locations: []types.Location{ + { + StartLine: 41, + EndLine: 44, + }, + }, + }, + }, + }, + }, + }, + }, { name: "monorepo", dir: "testdata/monorepo",