From a3c555021746fca1eaeca466245d70c506b74613 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Tue, 23 May 2023 10:24:25 -0400 Subject: [PATCH] fix: duplicate packages, support pnpm lockfile v6 (#1778) --- .../cataloger/javascript/parse_pnpm_lock.go | 63 ++++++++- .../javascript/parse_pnpm_lock_test.go | 92 +++++++++++++ .../test-fixtures/pnpm-v6/pnpm-lock.yaml | 127 ++++++++++++++++++ 3 files changed, 277 insertions(+), 5 deletions(-) create mode 100644 syft/pkg/cataloger/javascript/test-fixtures/pnpm-v6/pnpm-lock.yaml diff --git a/syft/pkg/cataloger/javascript/parse_pnpm_lock.go b/syft/pkg/cataloger/javascript/parse_pnpm_lock.go index 071334b466e..418f6286285 100644 --- a/syft/pkg/cataloger/javascript/parse_pnpm_lock.go +++ b/syft/pkg/cataloger/javascript/parse_pnpm_lock.go @@ -3,10 +3,13 @@ package javascript import ( "fmt" "io" + "regexp" + "strconv" "strings" "gopkg.in/yaml.v3" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/generic" @@ -17,8 +20,9 @@ import ( var _ generic.Parser = parsePnpmLock type pnpmLockYaml struct { - Dependencies map[string]string `json:"dependencies"` - Packages map[string]interface{} `json:"packages"` + Version string `json:"lockfileVersion" yaml:"lockfileVersion"` + Dependencies map[string]interface{} `json:"dependencies" yaml:"dependencies"` + Packages map[string]interface{} `json:"packages" yaml:"packages"` } func parsePnpmLock(resolver source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { @@ -34,19 +38,55 @@ func parsePnpmLock(resolver source.FileResolver, _ *generic.Environment, reader return nil, nil, fmt.Errorf("failed to parse pnpm-lock.yaml file: %w", err) } - for name, version := range lockFile.Dependencies { + lockVersion, _ := strconv.ParseFloat(lockFile.Version, 64) + + for name, info := range lockFile.Dependencies { + version := "" + + switch info := info.(type) { + case string: + version = info + case map[string]interface{}: + v, ok := info["version"] + if !ok { + break + } + ver, ok := v.(string) + if ok { + version = parseVersion(ver) + } + default: + log.Tracef("unsupported pnpm dependency type: %+v", info) + continue + } + + if hasPkg(pkgs, name, version) { + continue + } + pkgs = append(pkgs, newPnpmPackage(resolver, reader.Location, name, version)) } + packageNameRegex := regexp.MustCompile(`^/?([^(]*)(?:\(.*\))*$`) + splitChar := "/" + if lockVersion >= 6.0 { + splitChar = "@" + } + // parse packages from packages section of pnpm-lock.yaml for nameVersion := range lockFile.Packages { - nameVersionSplit := strings.Split(strings.TrimPrefix(nameVersion, "/"), "/") + nameVersion = packageNameRegex.ReplaceAllString(nameVersion, "$1") + nameVersionSplit := strings.Split(strings.TrimPrefix(nameVersion, "/"), splitChar) // last element in split array is version version := nameVersionSplit[len(nameVersionSplit)-1] // construct name from all array items other than last item (version) - name := strings.Join(nameVersionSplit[:len(nameVersionSplit)-1], "/") + name := strings.Join(nameVersionSplit[:len(nameVersionSplit)-1], splitChar) + + if hasPkg(pkgs, name, version) { + continue + } pkgs = append(pkgs, newPnpmPackage(resolver, reader.Location, name, version)) } @@ -55,3 +95,16 @@ func parsePnpmLock(resolver source.FileResolver, _ *generic.Environment, reader return pkgs, nil, nil } + +func hasPkg(pkgs []pkg.Package, name, version string) bool { + for _, p := range pkgs { + if p.Name == name && p.Version == version { + return true + } + } + return false +} + +func parseVersion(version string) string { + return strings.SplitN(version, "(", 2)[0] +} diff --git a/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go b/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go index 275cc0439a6..bcf1fe40ad0 100644 --- a/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go @@ -52,3 +52,95 @@ func TestParsePnpmLock(t *testing.T) { pkgtest.TestFileParser(t, fixture, parsePnpmLock, expectedPkgs, expectedRelationships) } + +func TestParsePnpmV6Lock(t *testing.T) { + var expectedRelationships []artifact.Relationship + fixture := "test-fixtures/pnpm-v6/pnpm-lock.yaml" + + locationSet := source.NewLocationSet(source.NewLocation(fixture)) + + expectedPkgs := []pkg.Package{ + { + Name: "@testing-library/jest-dom", + Version: "5.16.5", + PURL: "pkg:npm/%40testing-library/jest-dom@5.16.5", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + { + Name: "@testing-library/react", + Version: "13.4.0", + PURL: "pkg:npm/%40testing-library/react@13.4.0", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + { + Name: "@testing-library/user-event", + Version: "13.5.0", + PURL: "pkg:npm/%40testing-library/user-event@13.5.0", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + { + Name: "react", + Version: "18.2.0", + PURL: "pkg:npm/react@18.2.0", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + { + Name: "react-dom", + Version: "18.2.0", + PURL: "pkg:npm/react-dom@18.2.0", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + { + Name: "web-vitals", + Version: "2.1.4", + PURL: "pkg:npm/web-vitals@2.1.4", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + { + Name: "@babel/core", + Version: "7.21.4", + PURL: "pkg:npm/%40babel/core@7.21.4", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + { + Name: "@types/eslint", + Version: "8.37.0", + PURL: "pkg:npm/%40types/eslint@8.37.0", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + { + Name: "read-cache", + Version: "1.0.0", + PURL: "pkg:npm/read-cache@1.0.0", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + { + Name: "schema-utils", + Version: "3.1.2", + PURL: "pkg:npm/schema-utils@3.1.2", + Locations: locationSet, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + } + + pkgtest.TestFileParser(t, fixture, parsePnpmLock, expectedPkgs, expectedRelationships) +} diff --git a/syft/pkg/cataloger/javascript/test-fixtures/pnpm-v6/pnpm-lock.yaml b/syft/pkg/cataloger/javascript/test-fixtures/pnpm-v6/pnpm-lock.yaml new file mode 100644 index 00000000000..5098519b66e --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/pnpm-v6/pnpm-lock.yaml @@ -0,0 +1,127 @@ +lockfileVersion: '6.0' + +dependencies: + '@testing-library/jest-dom': + specifier: ^5.16.5 + version: 5.16.5 + '@testing-library/react': + specifier: ^13.4.0 + version: 13.4.0(react-dom@18.2.0)(react@18.2.0) + '@testing-library/user-event': + specifier: ^13.5.0 + version: 13.5.0(@testing-library/dom@9.2.0) + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + web-vitals: + specifier: ^2.1.4 + version: 2.1.4 + +packages: + /@babel/core@7.21.4: + resolution: {integrity: sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.21.4 + '@babel/helper-compilation-targets': 7.21.4(@babel/core@7.21.4) + '@babel/helper-module-transforms': 7.21.2 + '@babel/helpers': 7.21.0 + '@babel/parser': 7.21.4 + '@babel/template': 7.20.7 + '@babel/traverse': 7.21.4 + '@babel/types': 7.21.4 + convert-source-map: 1.9.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@testing-library/jest-dom@5.16.5: + resolution: {integrity: sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==} + engines: {node: '>=8', npm: '>=6', yarn: '>=1'} + dependencies: + '@adobe/css-tools': 4.2.0 + '@babel/runtime': 7.21.0 + '@types/testing-library__jest-dom': 5.14.5 + aria-query: 5.1.3 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.5.16 + lodash: 4.17.21 + redent: 3.0.0 + dev: false + + /@testing-library/react@13.4.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==} + engines: {node: '>=12'} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + '@babel/runtime': 7.21.0 + '@testing-library/dom': 8.20.0 + '@types/react-dom': 18.2.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@testing-library/user-event@13.5.0(@testing-library/dom@9.2.0): + resolution: {integrity: sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + dependencies: + '@babel/runtime': 7.21.0 + '@testing-library/dom': 9.2.0 + dev: false + + /@types/eslint@8.37.0: + resolution: {integrity: sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==} + dependencies: + '@types/estree': 1.0.1 + '@types/json-schema': 7.0.11 + dev: false + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + dev: false + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: false + + /schema-utils@3.1.2: + resolution: {integrity: sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.11 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: false + + /web-vitals@2.1.4: + resolution: {integrity: sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==} + dev: false