From 9d8244bae6aab260d5ddb504bc1de278d0f4bf10 Mon Sep 17 00:00:00 2001 From: Rob Cresswell Date: Fri, 18 Nov 2022 17:41:31 +0000 Subject: [PATCH] feat: Add support for npm lockfile version 3 (#1206) This PR adds support for npm lockfile version 3, which drops the "dependencies" key and uses "packages" instead. I've refactored the lockfile parser to make the distinction between the versions explicit rather than the implicit behaviour before. It _might_ be worth splitting into separate files at some point, but the logic is so minimal that I haven't done it. Fixes #1203 Signed-off-by: Rob Cresswell --- syft/pkg/cataloger/javascript/package.go | 25 ++++++--- .../javascript/parse_package_lock.go | 35 ++++++++---- .../javascript/parse_package_lock_test.go | 53 +++++++++++++++++++ .../pkg-lock/package-lock-3.json | 40 ++++++++++++++ 4 files changed, 137 insertions(+), 16 deletions(-) create mode 100644 syft/pkg/cataloger/javascript/test-fixtures/pkg-lock/package-lock-3.json diff --git a/syft/pkg/cataloger/javascript/package.go b/syft/pkg/cataloger/javascript/package.go index 810647df4de..98006d65103 100644 --- a/syft/pkg/cataloger/javascript/package.go +++ b/syft/pkg/cataloger/javascript/package.go @@ -43,13 +43,26 @@ func newPackageJSONPackage(u packageJSON, locations ...source.Location) pkg.Pack return p } -func newPackageLockPackage(resolver source.FileResolver, location source.Location, name string, u lockDependency, licenseMap map[string]string) pkg.Package { - var sb strings.Builder - sb.WriteString(u.Resolved) - sb.WriteString(u.Integrity) +func newPackageLockV1Package(resolver source.FileResolver, location source.Location, name string, u lockDependency) pkg.Package { + return finalizeLockPkg( + resolver, + location, + pkg.Package{ + Name: name, + Version: u.Version, + Locations: source.NewLocationSet(location), + PURL: packageURL(name, u.Version), + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + ) +} + +func newPackageLockV2Package(resolver source.FileResolver, location source.Location, name string, u lockPackage) pkg.Package { var licenses []string - if l, exists := licenseMap[sb.String()]; exists { - licenses = append(licenses, l) + + if u.License != "" { + licenses = append(licenses, u.License) } return finalizeLockPkg( diff --git a/syft/pkg/cataloger/javascript/parse_package_lock.go b/syft/pkg/cataloger/javascript/parse_package_lock.go index 217020100b5..28fba68d064 100644 --- a/syft/pkg/cataloger/javascript/parse_package_lock.go +++ b/syft/pkg/cataloger/javascript/parse_package_lock.go @@ -32,10 +32,11 @@ type lockDependency struct { } type lockPackage struct { + Name string `json:"name"` // only present in the root package entry (named "") Version string `json:"version"` Resolved string `json:"resolved"` Integrity string `json:"integrity"` - License string `json:""` + License string `json:"license"` } // parsePackageLock parses a package-lock.json and returns the discovered JavaScript packages. @@ -49,23 +50,32 @@ func parsePackageLock(resolver source.FileResolver, _ *generic.Environment, read var pkgs []pkg.Package dec := json.NewDecoder(reader) + var lock packageLock for { - var lock packageLock if err := dec.Decode(&lock); errors.Is(err, io.EOF) { break } else if err != nil { return nil, nil, fmt.Errorf("failed to parse package-lock.json file: %w", err) } - licenseMap := make(map[string]string) - for _, pkgMeta := range lock.Packages { - var sb strings.Builder - sb.WriteString(pkgMeta.Resolved) - sb.WriteString(pkgMeta.Integrity) - licenseMap[sb.String()] = pkgMeta.License - } + } + if lock.LockfileVersion == 1 { for name, pkgMeta := range lock.Dependencies { - pkgs = append(pkgs, newPackageLockPackage(resolver, reader.Location, name, pkgMeta, licenseMap)) + pkgs = append(pkgs, newPackageLockV1Package(resolver, reader.Location, name, pkgMeta)) + } + } + + if lock.LockfileVersion == 2 || lock.LockfileVersion == 3 { + for name, pkgMeta := range lock.Packages { + if name == "" { + if pkgMeta.Name == "" { + continue + } else { + name = pkgMeta.Name + } + } + + pkgs = append(pkgs, newPackageLockV2Package(resolver, reader.Location, getNameFromPath(name), pkgMeta)) } } @@ -73,3 +83,8 @@ func parsePackageLock(resolver source.FileResolver, _ *generic.Environment, read return pkgs, nil, nil } + +func getNameFromPath(path string) string { + parts := strings.Split(path, "node_modules/") + return parts[len(parts)-1] +} diff --git a/syft/pkg/cataloger/javascript/parse_package_lock_test.go b/syft/pkg/cataloger/javascript/parse_package_lock_test.go index 74fc84ff16e..a78e3ef1278 100644 --- a/syft/pkg/cataloger/javascript/parse_package_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_package_lock_test.go @@ -102,6 +102,13 @@ func TestParsePackageLockV2(t *testing.T) { fixture := "test-fixtures/pkg-lock/package-lock-2.json" var expectedRelationships []artifact.Relationship expectedPkgs := []pkg.Package{ + { + Name: "npm", + Version: "6.14.6", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + PURL: "pkg:npm/npm@6.14.6", + }, { Name: "@types/prop-types", Version: "15.7.5", @@ -140,3 +147,49 @@ func TestParsePackageLockV2(t *testing.T) { } pkgtest.TestFileParser(t, fixture, parsePackageLock, expectedPkgs, expectedRelationships) } + +func TestParsePackageLockV3(t *testing.T) { + fixture := "test-fixtures/pkg-lock/package-lock-3.json" + var expectedRelationships []artifact.Relationship + expectedPkgs := []pkg.Package{ + { + Name: "lock-v3-fixture", + Version: "1.0.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + PURL: "pkg:npm/lock-v3-fixture@1.0.0", + }, + { + Name: "@types/prop-types", + Version: "15.7.5", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + PURL: "pkg:npm/%40types/prop-types@15.7.5", + }, + { + Name: "@types/react", + Version: "18.0.20", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + PURL: "pkg:npm/%40types/react@18.0.20", + }, + { + Name: "@types/scheduler", + Version: "0.16.2", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + PURL: "pkg:npm/%40types/scheduler@0.16.2", + }, + { + Name: "csstype", + Version: "3.1.1", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + PURL: "pkg:npm/csstype@3.1.1", + }, + } + for i := range expectedPkgs { + expectedPkgs[i].Locations.Add(source.NewLocation(fixture)) + } + pkgtest.TestFileParser(t, fixture, parsePackageLock, expectedPkgs, expectedRelationships) +} diff --git a/syft/pkg/cataloger/javascript/test-fixtures/pkg-lock/package-lock-3.json b/syft/pkg/cataloger/javascript/test-fixtures/pkg-lock/package-lock-3.json new file mode 100644 index 00000000000..68008c089a5 --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/pkg-lock/package-lock-3.json @@ -0,0 +1,40 @@ +{ + "name": "lock-v3-fixture", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lock-v3-fixture", + "version": "1.0.0", + "dependencies": { + "@types/react": "^18.0.9" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, + "node_modules/@types/react": { + "version": "18.0.20", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.20.tgz", + "integrity": "sha512-MWul1teSPxujEHVwZl4a5HxQ9vVNsjTchVA+xRqv/VYGCuKGAU6UhfrTdF5aBefwD1BHUD8i/zq+O/vyCm/FrA==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "node_modules/csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + } + } +}