diff --git a/syft/pkg/cataloger/javascript/package.go b/syft/pkg/cataloger/javascript/package.go index 98006d65103..7a513c09675 100644 --- a/syft/pkg/cataloger/javascript/package.go +++ b/syft/pkg/cataloger/javascript/package.go @@ -43,15 +43,15 @@ func newPackageJSONPackage(u packageJSON, locations ...source.Location) pkg.Pack return p } -func newPackageLockV1Package(resolver source.FileResolver, location source.Location, name string, u lockDependency) pkg.Package { +func newPackageLockV1Package(resolver source.FileResolver, location source.Location, name, version string, u lockDependency) pkg.Package { return finalizeLockPkg( resolver, location, pkg.Package{ Name: name, - Version: u.Version, + Version: version, Locations: source.NewLocationSet(location), - PURL: packageURL(name, u.Version), + PURL: packageURL(name, version), Language: pkg.JavaScript, Type: pkg.NpmPkg, }, diff --git a/syft/pkg/cataloger/javascript/parse_package_lock.go b/syft/pkg/cataloger/javascript/parse_package_lock.go index 28fba68d064..bc71add9b11 100644 --- a/syft/pkg/cataloger/javascript/parse_package_lock.go +++ b/syft/pkg/cataloger/javascript/parse_package_lock.go @@ -39,6 +39,25 @@ type lockPackage struct { License string `json:"license"` } +// Handles type aliases https://github.com/npm/rfcs/blob/main/implemented/0001-package-aliases.md +// for npm package-lock.json version 1 +func resolvePackageLockNameAliases(name, version string) (string, string) { + aliasSeparator := "npm:" + + if !strings.HasPrefix(version, aliasSeparator) { + // not an alias + return name, version + } + + canonicalPackageAndVersion := version[len(aliasSeparator):] + versionSeparator := strings.LastIndex(canonicalPackageAndVersion, "@") + + canonicalName := canonicalPackageAndVersion[:versionSeparator] + canonicalVersion := canonicalPackageAndVersion[versionSeparator+1:] + + return canonicalName, canonicalVersion +} + // parsePackageLock parses a package-lock.json and returns the discovered JavaScript packages. func parsePackageLock(resolver source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { // in the case we find package-lock.json files in the node_modules directories, skip those @@ -61,7 +80,9 @@ func parsePackageLock(resolver source.FileResolver, _ *generic.Environment, read if lock.LockfileVersion == 1 { for name, pkgMeta := range lock.Dependencies { - pkgs = append(pkgs, newPackageLockV1Package(resolver, reader.Location, name, pkgMeta)) + canonicalName, canonicalVersion := resolvePackageLockNameAliases(name, pkgMeta.Version) + + pkgs = append(pkgs, newPackageLockV1Package(resolver, reader.Location, canonicalName, canonicalVersion, pkgMeta)) } } @@ -75,6 +96,11 @@ func parsePackageLock(resolver source.FileResolver, _ *generic.Environment, read } } + // handles alias names + if pkgMeta.Name != "" { + name = pkgMeta.Name + } + pkgs = append(pkgs, newPackageLockV2Package(resolver, reader.Location, getNameFromPath(name), pkgMeta)) } } diff --git a/syft/pkg/cataloger/javascript/parse_package_lock_test.go b/syft/pkg/cataloger/javascript/parse_package_lock_test.go index a78e3ef1278..1f3e7826950 100644 --- a/syft/pkg/cataloger/javascript/parse_package_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_package_lock_test.go @@ -3,12 +3,61 @@ package javascript import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" "github.com/anchore/syft/syft/source" ) +func Test_resolveNameAliases(t *testing.T) { + + tests := []struct { + testName string + name string + version string + expectedName string + expectedVersion string + }{ + { + testName: "alias", + name: "case-alias", + version: "npm:case@1.6.3", + expectedName: "case", + expectedVersion: "1.6.3", + }, + { + testName: "alias-with-@", + name: "case-alias", + version: "npm:@case@1.6.3", + expectedName: "@case", + expectedVersion: "1.6.3", + }, + { + testName: "simple-name-version", + name: "case", + version: "1.6.3", + expectedName: "case", + expectedVersion: "1.6.3", + }, + { + testName: "simple-@name-version", + name: "@case", + version: "1.6.3", + expectedName: "@case", + expectedVersion: "1.6.3", + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + resolvedName, resolvedVersion := resolvePackageLockNameAliases(tt.name, tt.version) + assert.Equal(t, tt.expectedName, resolvedName) + assert.Equal(t, tt.expectedVersion, resolvedVersion) + }) + } +} + func TestParsePackageLock(t *testing.T) { var expectedRelationships []artifact.Relationship expectedPkgs := []pkg.Package{ @@ -193,3 +242,42 @@ func TestParsePackageLockV3(t *testing.T) { } pkgtest.TestFileParser(t, fixture, parsePackageLock, expectedPkgs, expectedRelationships) } + +func TestParsePackageLockAlias(t *testing.T) { + var expectedRelationships []artifact.Relationship + expectedPkgs := []pkg.Package{ + { + Name: "case", + Version: "1.6.2", + PURL: "pkg:npm/case@1.6.2", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + { + Name: "case", + Version: "1.6.3", + PURL: "pkg:npm/case@1.6.3", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + { + Name: "@bundled-es-modules/chai", + Version: "4.2.2", + PURL: "pkg:npm/%40bundled-es-modules/chai@1.6.3", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + } + packageLockV1 := "test-fixtures/pkg-lock/alias-package-lock-1.json" + packageLockV2 := "test-fixtures/pkg-lock/alias-package-lock-2.json" + packageLocks := []string{packageLockV1, packageLockV2} + + for _, packageLock := range packageLocks { + expected := make([]pkg.Package, len(expectedPkgs)) + copy(expectedPkgs, expected) + for i := range expected { + expected[i].Locations.Add(source.NewLocation(packageLock)) + } + pkgtest.TestFileParser(t, packageLock, parsePackageLock, expected, expectedRelationships) + } +} diff --git a/syft/pkg/cataloger/javascript/test-fixtures/pkg-lock/alias-package-lock-1.json b/syft/pkg/cataloger/javascript/test-fixtures/pkg-lock/alias-package-lock-1.json new file mode 100644 index 00000000000..f7fc133000f --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/pkg-lock/alias-package-lock-1.json @@ -0,0 +1,23 @@ +{ + "name": "alias-check", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "case": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/case/-/case-1.6.2.tgz", + "integrity": "sha512-ll380ZRoraT7mUK2G92UbH+FJVD5AwdVIAYk9xhV1tauh0carDgYByUD1HhjCWsWgxrfQvCeHvtfj7IYR6TKeg==" + }, + "case-alias": { + "version": "npm:case@1.6.3", + "resolved": "https://registry.npmjs.org/case/-/case-1.6.3.tgz", + "integrity": "sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==" + }, + "chai": { + "version": "npm:@bundled-es-modules/chai@4.2.2", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/chai/-/chai-4.2.2.tgz", + "integrity": "sha512-iGmVYw2/zJCoqyKTtWEYCtFmMyi8WmACQKtky0lpNyEKWX0YIOpKWGD7saMXL+tPpllss0otilxV0SLwyi3Ytg==" + } + } +} diff --git a/syft/pkg/cataloger/javascript/test-fixtures/pkg-lock/alias-package-lock-2.json b/syft/pkg/cataloger/javascript/test-fixtures/pkg-lock/alias-package-lock-2.json new file mode 100644 index 00000000000..c9de6cffa4b --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/pkg-lock/alias-package-lock-2.json @@ -0,0 +1,58 @@ +{ + "name": "alias-check", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "alias-check", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "case": "1.6.2", + "case-alias": "npm:case@^1.6.3", + "chai": "npm:@bundled-es-modules/chai@^4.2.2" + } + }, + "node_modules/case": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/case/-/case-1.6.2.tgz", + "integrity": "sha512-ll380ZRoraT7mUK2G92UbH+FJVD5AwdVIAYk9xhV1tauh0carDgYByUD1HhjCWsWgxrfQvCeHvtfj7IYR6TKeg==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/case-alias": { + "name": "case", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/case/-/case-1.6.3.tgz", + "integrity": "sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/chai": { + "name": "@bundled-es-modules/chai", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/chai/-/chai-4.2.2.tgz", + "integrity": "sha512-iGmVYw2/zJCoqyKTtWEYCtFmMyi8WmACQKtky0lpNyEKWX0YIOpKWGD7saMXL+tPpllss0otilxV0SLwyi3Ytg==" + } + }, + "dependencies": { + "case": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/case/-/case-1.6.2.tgz", + "integrity": "sha512-ll380ZRoraT7mUK2G92UbH+FJVD5AwdVIAYk9xhV1tauh0carDgYByUD1HhjCWsWgxrfQvCeHvtfj7IYR6TKeg==" + }, + "case-alias": { + "version": "npm:case@1.6.3", + "resolved": "https://registry.npmjs.org/case/-/case-1.6.3.tgz", + "integrity": "sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==" + }, + "chai": { + "version": "npm:@bundled-es-modules/chai@4.2.2", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/chai/-/chai-4.2.2.tgz", + "integrity": "sha512-iGmVYw2/zJCoqyKTtWEYCtFmMyi8WmACQKtky0lpNyEKWX0YIOpKWGD7saMXL+tPpllss0otilxV0SLwyi3Ytg==" + } + } +}