diff --git a/syft/pkg/cataloger/javascript/package.go b/syft/pkg/cataloger/javascript/package.go index 810647df4..98006d651 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 217020100..28fba68d0 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 74fc84ff1..a78e3ef12 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 000000000..68008c089 --- /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==" + } + } +}