diff --git a/syft/pkg/cataloger/javascript/parse_yarn_lock.go b/syft/pkg/cataloger/javascript/parse_yarn_lock.go index 7c849672a..7c53bc556 100644 --- a/syft/pkg/cataloger/javascript/parse_yarn_lock.go +++ b/syft/pkg/cataloger/javascript/parse_yarn_lock.go @@ -6,9 +6,7 @@ import ( "encoding/json" "fmt" "io" - "maps" "regexp" - "slices" "strings" "github.com/goccy/go-yaml" @@ -250,7 +248,9 @@ func parseYarnLockYaml(reader io.ReadCloser) ([]yarnPackage, error) { return nil, fmt.Errorf("failed to unmarshal yarn v2 lockfile: %w", err) } - packages := make(map[string]yarnPackage) + var seenPkgs = strset.New() + + pkgs := []yarnPackage{} for key, value := range lockfile { packageName := findPackageName(key) if packageName == "" { @@ -258,10 +258,16 @@ func parseYarnLockYaml(reader io.ReadCloser) ([]yarnPackage, error) { continue } - packages[packageName] = yarnPackage{Name: packageName, Version: value.Version, Resolved: value.Resolution, Integrity: value.Checksum, Dependencies: value.Dependencies} + var pkg = yarnPackage{Name: packageName, Version: value.Version, Resolved: value.Resolution, Integrity: value.Checksum, Dependencies: value.Dependencies} + var nameVersion = pkg.Name + "@" + pkg.Version + + if !seenPkgs.Has(nameVersion) { + seenPkgs.Add(nameVersion) + pkgs = append(pkgs, pkg) + } } - return slices.Collect(maps.Values(packages)), nil + return pkgs, nil } func (a genericYarnLockAdapter) parseYarnLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { diff --git a/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go b/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go index 06f675b31..93a24c725 100644 --- a/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go @@ -204,6 +204,42 @@ func TestParseYarnBerry(t *testing.T) { pkgtest.TestFileParser(t, fixture, adapter.parseYarnLock, expectedPkgs, expectedRelationships) } +func TestParseYarnBerryWithDuplicates(t *testing.T) { + var expectedRelationships []artifact.Relationship + fixture := "testdata/yarn-berry-dups/yarn.lock" + locations := file.NewLocationSet(file.NewLocation(fixture)) + + expectedPkgs := []pkg.Package{ + { + Name: "async", + Version: "3.2.6", + Locations: locations, + PURL: "pkg:npm/async@3.2.6", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.YarnLockEntry{ + Resolved: "async@npm:3.2.6", + Integrity: "10c0/36484bb15ceddf07078688d95e27076379cc2f87b10c03b6dd8a83e89475a3c8df5848859dd06a4c95af1e4c16fc973de0171a77f18ea00be899aca2a4f85e70", + }, + }, + { + Name: "async", + Version: "0.9.2", + Locations: locations, + PURL: "pkg:npm/async@0.9.2", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + Metadata: pkg.YarnLockEntry{ + Resolved: "async@npm:0.9.2", + Integrity: "10c0/22ac816db119a9b84ac7182fa969b2cceacfcfa278c3efb0ac6a94d1210a4429e42c8cf6e704039aa7662e4ba62f26cecf039c91d41ceb91355dc9672c9b9ac1", + }, + }, + } + + adapter := newGenericYarnLockAdapter(CatalogerConfig{}) + pkgtest.TestFileParser(t, fixture, adapter.parseYarnLock, expectedPkgs, expectedRelationships) +} + func TestParseYarnLock(t *testing.T) { var expectedRelationships []artifact.Relationship fixture := "testdata/yarn/yarn.lock" diff --git a/syft/pkg/cataloger/javascript/testdata/yarn-berry-dups/yarn.lock b/syft/pkg/cataloger/javascript/testdata/yarn-berry-dups/yarn.lock new file mode 100644 index 000000000..5faf5aaf4 --- /dev/null +++ b/syft/pkg/cataloger/javascript/testdata/yarn-berry-dups/yarn.lock @@ -0,0 +1,20 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"async@npm:^3.2.3": + version: 3.2.6 + resolution: "async@npm:3.2.6" + checksum: 10c0/36484bb15ceddf07078688d95e27076379cc2f87b10c03b6dd8a83e89475a3c8df5848859dd06a4c95af1e4c16fc973de0171a77f18ea00be899aca2a4f85e70 + languageName: node + linkType: hard + +"async@npm:0.9.2": + version: 0.9.2 + resolution: "async@npm:0.9.2" + checksum: 10c0/22ac816db119a9b84ac7182fa969b2cceacfcfa278c3efb0ac6a94d1210a4429e42c8cf6e704039aa7662e4ba62f26cecf039c91d41ceb91355dc9672c9b9ac1 + languageName: node + linkType: hard