diff --git a/syft/pkg/apk_metadata.go b/syft/pkg/apk_metadata.go index 327de0ac8f1..09f29da082d 100644 --- a/syft/pkg/apk_metadata.go +++ b/syft/pkg/apk_metadata.go @@ -18,9 +18,12 @@ import ( const ApkDBGlob = "**/lib/apk/db/installed" var ( - _ FileOwner = (*ApkMetadata)(nil) - prefixes = []string{"py-", "py2-", "py3-", "ruby-"} - upstreamPattern = regexp.MustCompile(`^(?P[a-zA-Z][\w-]*?)\-?\d[\d\.]*$`) + _ FileOwner = (*ApkMetadata)(nil) + prefixesToPackageType = map[string]Type{ + "py-": PythonPkg, + "ruby-": GemPkg, + } + streamVersionPkgNamePattern = regexp.MustCompile(`^(?P[a-zA-Z][\w-]*?)(?P\-?\d[\d\.]*?)($|-(?P[a-zA-Z][\w-]*?)?)$`) ) // ApkMetadata represents all captured data for a Alpine DB package entry. @@ -121,23 +124,44 @@ func (m ApkMetadata) OwnedFiles() (result []string) { return result } -func (m ApkMetadata) Upstream() string { +type UpstreamCandidate struct { + Name string + Type Type +} + +func (m ApkMetadata) UpstreamCandidates() (candidates []UpstreamCandidate) { + name := m.Package if m.OriginPackage != "" && m.OriginPackage != m.Package { - return m.OriginPackage + candidates = append(candidates, UpstreamCandidate{Name: m.OriginPackage, Type: ApkPkg}) } - groups := internal.MatchNamedCaptureGroups(upstreamPattern, m.Package) + groups := internal.MatchNamedCaptureGroups(streamVersionPkgNamePattern, m.Package) + stream, ok := groups["stream"] - upstream, ok := groups["upstream"] - if !ok { - upstream = m.Package + if ok && stream != "" { + sub, ok := groups["subPackage"] + + if ok && sub != "" { + name = fmt.Sprintf("%s-%s", stream, sub) + } else { + name = stream + } } - for _, p := range prefixes { - if strings.HasPrefix(upstream, p) { - return strings.TrimPrefix(upstream, p) + for prefix, typ := range prefixesToPackageType { + if strings.HasPrefix(name, prefix) { + t := strings.TrimPrefix(name, prefix) + if t != "" { + candidates = append(candidates, UpstreamCandidate{Name: t, Type: typ}) + return candidates + } } } - return upstream + if name != "" { + candidates = append(candidates, UpstreamCandidate{Name: name, Type: UnknownPkg}) + return candidates + } + + return candidates } diff --git a/syft/pkg/apk_metadata_test.go b/syft/pkg/apk_metadata_test.go index 610d8321a49..1b7ac45b4b4 100644 --- a/syft/pkg/apk_metadata_test.go +++ b/syft/pkg/apk_metadata_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "testing" - "github.com/sergi/go-diff/diffmatchpatch" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -165,26 +164,30 @@ func TestSpaceDelimitedStringSlice_UnmarshalJSON(t *testing.T) { } } -func TestApkMetadata_Upstream(t *testing.T) { +func TestApkMetadata_UpstreamCandidates(t *testing.T) { tests := []struct { name string metadata ApkMetadata - expected string + expected []UpstreamCandidate }{ { name: "gocase", metadata: ApkMetadata{ Package: "p", }, - expected: "p", + expected: []UpstreamCandidate{ + {Name: "p", Type: UnknownPkg}, + }, }, { - name: "same package and origin", + name: "same package and origin simple case", metadata: ApkMetadata{ Package: "p", OriginPackage: "p", }, - expected: "p", + expected: []UpstreamCandidate{ + {Name: "p", Type: UnknownPkg}, + }, }, { name: "different package and origin", @@ -192,15 +195,30 @@ func TestApkMetadata_Upstream(t *testing.T) { Package: "p", OriginPackage: "origin", }, - expected: "origin", + expected: []UpstreamCandidate{ + {Name: "origin", Type: ApkPkg}, + {Name: "p", Type: UnknownPkg}, + }, }, { - name: "upstream python package information as qualifier", + name: "upstream python package information as qualifier py- prefix", + metadata: ApkMetadata{ + Package: "py-potatoes", + OriginPackage: "py-potatoes", + }, + expected: []UpstreamCandidate{ + {Name: "potatoes", Type: PythonPkg}, + }, + }, + { + name: "upstream python package information as qualifier py3- prefix", metadata: ApkMetadata{ Package: "py3-potatoes", OriginPackage: "py3-potatoes", }, - expected: "potatoes", + expected: []UpstreamCandidate{ + {Name: "potatoes", Type: PythonPkg}, + }, }, { name: "python package with distinct origin package", @@ -208,7 +226,10 @@ func TestApkMetadata_Upstream(t *testing.T) { Package: "py3-non-existant", OriginPackage: "abcdefg", }, - expected: "abcdefg", + expected: []UpstreamCandidate{ + {Name: "abcdefg", Type: ApkPkg}, + {Name: "non-existant", Type: PythonPkg}, + }, }, { name: "upstream ruby package information as qualifier", @@ -216,117 +237,171 @@ func TestApkMetadata_Upstream(t *testing.T) { Package: "ruby-something", OriginPackage: "ruby-something", }, - expected: "something", + expected: []UpstreamCandidate{ + {Name: "something", Type: GemPkg}, + }, }, { - name: "python package with distinct origin package", + name: "ruby package with distinct origin package", metadata: ApkMetadata{ Package: "ruby-something", OriginPackage: "1234567", }, - expected: "1234567", + expected: []UpstreamCandidate{ + {Name: "1234567", Type: ApkPkg}, + {Name: "something", Type: GemPkg}, + }, }, { name: "postgesql-15 upstream postgresql", metadata: ApkMetadata{ Package: "postgresql-15", }, - expected: "postgresql", + expected: []UpstreamCandidate{ + {Name: "postgresql", Type: UnknownPkg}, + }, }, { name: "postgesql15 upstream postgresql", metadata: ApkMetadata{ Package: "postgresql15", }, - expected: "postgresql", + expected: []UpstreamCandidate{ + {Name: "postgresql", Type: UnknownPkg}, + }, }, { name: "go-1.19 upstream go", metadata: ApkMetadata{ Package: "go-1.19", }, - expected: "go", + expected: []UpstreamCandidate{ + {Name: "go", Type: UnknownPkg}, + }, }, { name: "go1.143 upstream go", metadata: ApkMetadata{ Package: "go1.143", }, - expected: "go", + expected: []UpstreamCandidate{ + {Name: "go", Type: UnknownPkg}, + }, }, { name: "abc-101.191.23456 upstream abc", metadata: ApkMetadata{ Package: "abc-101.191.23456", }, - expected: "abc", + expected: []UpstreamCandidate{ + {Name: "abc", Type: UnknownPkg}, + }, }, { name: "abc101.191.23456 upstream abc", metadata: ApkMetadata{ Package: "abc101.191.23456", }, - expected: "abc", + expected: []UpstreamCandidate{ + {Name: "abc", Type: UnknownPkg}, + }, }, { name: "abc101-12345-1045 upstream abc101-12345", metadata: ApkMetadata{ Package: "abc101-12345-1045", }, - expected: "abc101-12345", + expected: []UpstreamCandidate{ + {Name: "abc101-12345", Type: UnknownPkg}, + }, }, { name: "abc101-a12345-1045 upstream abc101-a12345", metadata: ApkMetadata{ Package: "abc101-a12345-1045", }, - expected: "abc101-a12345", + expected: []UpstreamCandidate{ + {Name: "abc-a12345-1045", Type: UnknownPkg}, + }, }, { name: "package starting with single digit", metadata: ApkMetadata{ Package: "3proxy", }, - expected: "3proxy", + expected: []UpstreamCandidate{ + {Name: "3proxy", Type: UnknownPkg}, + }, }, { name: "package starting with multiple digits", metadata: ApkMetadata{ Package: "356proxy", }, - expected: "356proxy", + expected: []UpstreamCandidate{ + {Name: "356proxy", Type: UnknownPkg}, + }, }, { name: "package composed of only digits", metadata: ApkMetadata{ Package: "123456", }, - expected: "123456", + expected: []UpstreamCandidate{ + {Name: "123456", Type: UnknownPkg}, + }, }, { name: "ruby-3.6 upstream ruby", metadata: ApkMetadata{ Package: "ruby-3.6", }, - expected: "ruby", + expected: []UpstreamCandidate{ + {Name: "ruby", Type: UnknownPkg}, + }, }, { name: "ruby3.6 upstream ruby", metadata: ApkMetadata{ Package: "ruby3.6", }, - expected: "ruby", + expected: []UpstreamCandidate{ + {Name: "ruby", Type: UnknownPkg}, + }, + }, + { + name: "ruby3.6-tacos upstream tacos", + metadata: ApkMetadata{ + Package: "ruby3.6-tacos", + }, + expected: []UpstreamCandidate{ + {Name: "tacos", Type: GemPkg}, + }, + }, + { + name: "ruby-3.6-tacos upstream tacos", + metadata: ApkMetadata{ + Package: "ruby-3.6-tacos", + }, + expected: []UpstreamCandidate{ + {Name: "tacos", Type: GemPkg}, + }, + }, + { + name: "abc1234jksajflksa", + metadata: ApkMetadata{ + Package: "abc1234jksajflksa", + }, + expected: []UpstreamCandidate{ + {Name: "abc1234jksajflksa", Type: UnknownPkg}, + }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual := test.metadata.Upstream() - if actual != test.expected { - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(test.expected, actual, true) - t.Errorf("diff: %s", dmp.DiffPrettyText(diffs)) - } + actual := test.metadata.UpstreamCandidates() + assert.Equal(t, test.expected, actual) }) } } diff --git a/syft/pkg/cataloger/apkdb/package.go b/syft/pkg/cataloger/apkdb/package.go index 4c0d86582c6..08501c95ff2 100644 --- a/syft/pkg/cataloger/apkdb/package.go +++ b/syft/pkg/cataloger/apkdb/package.go @@ -36,9 +36,14 @@ func packageURL(m pkg.ApkMetadata, distro *linux.Release) string { pkg.PURLQualifierArch: m.Architecture, } - upstream := m.Upstream() - if upstream != "" && upstream != m.Package { - qualifiers[pkg.PURLQualifierUpstream] = upstream + upstreams := m.UpstreamCandidates() + if len(upstreams) > 0 { + // only room for one value so for now just take the first one + upstream := upstreams[0] + + if upstream.Name != "" && upstream.Name != m.Package { + qualifiers[pkg.PURLQualifierUpstream] = upstream.Name + } } return packageurl.NewPackageURL( diff --git a/syft/pkg/cataloger/apkdb/package_test.go b/syft/pkg/cataloger/apkdb/package_test.go index d9cc89b90da..0b12e5666ee 100644 --- a/syft/pkg/cataloger/apkdb/package_test.go +++ b/syft/pkg/cataloger/apkdb/package_test.go @@ -234,7 +234,7 @@ func Test_PackageURL(t *testing.T) { ID: "alpine", VersionID: "3.4.6", }, - expected: "pkg:apk/alpine/abc101-a12345-1045@101.191.23456?arch=a&upstream=abc101-a12345&distro=alpine-3.4.6", + expected: "pkg:apk/alpine/abc101-a12345-1045@101.191.23456?arch=a&upstream=abc-a12345-1045&distro=alpine-3.4.6", }, { name: "wolfi distro", diff --git a/syft/pkg/cataloger/common/cpe/apk.go b/syft/pkg/cataloger/common/cpe/apk.go index f91054478b7..a5a197b2990 100644 --- a/syft/pkg/cataloger/common/cpe/apk.go +++ b/syft/pkg/cataloger/common/cpe/apk.go @@ -1,159 +1,9 @@ package cpe import ( - "strings" - "github.com/anchore/syft/syft/pkg" ) -var ( - pythonPrefixes = []string{"py-", "py2-", "py3-"} - rubyPrefixes = []string{"ruby-"} -) - -func pythonCandidateVendorsFromName(v string) fieldCandidateSet { - vendors := newFieldCandidateSet() - vendors.addValue(v) - vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, pkg.PythonPkg, v, v)...) - vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, pkg.PythonPkg, v)...) - - for _, av := range additionalVendorsForPython(v) { - vendors.addValue(av) - vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, pkg.PythonPkg, av, av)...) - vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, pkg.PythonPkg, av)...) - } - - return vendors -} - -func pythonCandidateVendorsFromAPK(m pkg.ApkMetadata) fieldCandidateSet { - vendors := newFieldCandidateSet() - upstream := m.Upstream() - - for _, p := range pythonPrefixes { - if strings.HasPrefix(m.Package, p) { - t := strings.ToLower(strings.TrimPrefix(m.Package, p)) - vendors.union(pythonCandidateVendorsFromName(t)) - } - - if upstream != m.Package && strings.HasPrefix(upstream, p) { - t := strings.ToLower(strings.TrimPrefix(upstream, p)) - vendors.union(pythonCandidateVendorsFromName(t)) - } - } - - return vendors -} - -func pythonCandidateProductsFromName(p string) fieldCandidateSet { - products := newFieldCandidateSet() - products.addValue(p) - products.addValue(findAdditionalProducts(defaultCandidateAdditions, pkg.PythonPkg, p)...) - products.removeByValue(findProductsToRemove(defaultCandidateRemovals, pkg.PythonPkg, p)...) - return products -} - -func pythonCandidateProductsFromAPK(m pkg.ApkMetadata) fieldCandidateSet { - products := newFieldCandidateSet() - upstream := m.Upstream() - - for _, p := range pythonPrefixes { - if strings.HasPrefix(m.Package, p) { - t := strings.ToLower(strings.TrimPrefix(m.Package, p)) - products.union(pythonCandidateProductsFromName(t)) - } - - if upstream != m.Package && strings.HasPrefix(upstream, p) { - t := strings.ToLower(strings.TrimPrefix(upstream, p)) - products.union(pythonCandidateProductsFromName(t)) - } - } - - return products -} - -func rubyCandidateVendorsFromName(v string) fieldCandidateSet { - vendors := newFieldCandidateSet() - vendors.addValue(v) - vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, pkg.GemPkg, v, v)...) - vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, pkg.GemPkg, v)...) - return vendors -} - -func rubyCandidateVendorsFromAPK(m pkg.ApkMetadata) fieldCandidateSet { - vendors := newFieldCandidateSet() - upstream := m.Upstream() - - if upstream != "ruby" { - for _, p := range rubyPrefixes { - if strings.HasPrefix(m.Package, p) { - t := strings.ToLower(strings.TrimPrefix(m.Package, p)) - vendors.union(rubyCandidateVendorsFromName(t)) - } - - if upstream != "" && upstream != m.Package && strings.HasPrefix(upstream, p) { - t := strings.ToLower(strings.TrimPrefix(upstream, p)) - vendors.union(rubyCandidateVendorsFromName(t)) - } - } - } - - return vendors -} - -func rubyCandidateProductsFromName(p string) fieldCandidateSet { - products := newFieldCandidateSet() - products.addValue(p) - products.addValue(findAdditionalProducts(defaultCandidateAdditions, pkg.GemPkg, p)...) - products.removeByValue(findProductsToRemove(defaultCandidateRemovals, pkg.GemPkg, p)...) - return products -} - -func rubyCandidateProductsFromAPK(m pkg.ApkMetadata) fieldCandidateSet { - products := newFieldCandidateSet() - upstream := m.Upstream() - - if upstream != "ruby" { - for _, p := range rubyPrefixes { - if strings.HasPrefix(m.Package, p) { - t := strings.ToLower(strings.TrimPrefix(m.Package, p)) - products.union(rubyCandidateProductsFromName(t)) - } - - if upstream != "" && upstream != m.Package && strings.HasPrefix(upstream, p) { - t := strings.ToLower(strings.TrimPrefix(upstream, p)) - products.union(rubyCandidateProductsFromName(t)) - } - } - } - - return products -} - -func candidateVendorsFromAPKUpstream(m pkg.ApkMetadata) fieldCandidateSet { - vendors := newFieldCandidateSet() - upstream := m.Upstream() - if upstream != "" && upstream != m.Package { - vendors.addValue(upstream) - vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, pkg.ApkPkg, upstream, upstream)...) - vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, pkg.ApkPkg, upstream)...) - } - - return vendors -} - -func candidateProductsFromAPKUpstream(m pkg.ApkMetadata) fieldCandidateSet { - products := newFieldCandidateSet() - upstream := m.Upstream() - if upstream != "" { - products.addValue(upstream) - products.addValue(findAdditionalProducts(defaultCandidateAdditions, pkg.ApkPkg, upstream)...) - products.removeByValue(findProductsToRemove(defaultCandidateRemovals, pkg.ApkPkg, upstream)...) - } - - return products -} - func candidateVendorsForAPK(p pkg.Package) fieldCandidateSet { metadata, ok := p.Metadata.(pkg.ApkMetadata) if !ok { @@ -161,9 +11,30 @@ func candidateVendorsForAPK(p pkg.Package) fieldCandidateSet { } vendors := newFieldCandidateSet() - vendors.union(pythonCandidateVendorsFromAPK(metadata)) - vendors.union(rubyCandidateVendorsFromAPK(metadata)) - vendors.union(candidateVendorsFromAPKUpstream(metadata)) + candidates := metadata.UpstreamCandidates() + + for _, c := range candidates { + switch c.Type { + case pkg.UnknownPkg: + vendors.addValue(c.Name) + vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, pkg.ApkPkg, c.Name, c.Name)...) + vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, pkg.ApkPkg, c.Name)...) + case pkg.PythonPkg: + vendors.addValue(c.Name) + vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, c.Type, c.Name, c.Name)...) + vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, c.Type, c.Name)...) + for _, av := range additionalVendorsForPython(c.Name) { + vendors.addValue(av) + vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, pkg.PythonPkg, av, av)...) + vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, pkg.PythonPkg, av)...) + } + default: + vendors.addValue(c.Name) + vendors.addValue(findAdditionalVendors(defaultCandidateAdditions, c.Type, c.Name, c.Name)...) + vendors.removeByValue(findVendorsToRemove(defaultCandidateRemovals, c.Type, c.Name)...) + } + } + vendors.union(candidateVendorsFromURL(metadata.URL)) for v := range vendors { @@ -181,9 +52,20 @@ func candidateProductsForAPK(p pkg.Package) fieldCandidateSet { } products := newFieldCandidateSet() - products.union(pythonCandidateProductsFromAPK(metadata)) - products.union(rubyCandidateProductsFromAPK(metadata)) - products.union(candidateProductsFromAPKUpstream(metadata)) + candidates := metadata.UpstreamCandidates() + + for _, c := range candidates { + switch c.Type { + case pkg.UnknownPkg: + products.addValue(c.Name) + products.addValue(findAdditionalProducts(defaultCandidateAdditions, pkg.ApkPkg, c.Name)...) + products.removeByValue(findProductsToRemove(defaultCandidateRemovals, pkg.ApkPkg, c.Name)...) + default: + products.addValue(c.Name) + products.addValue(findAdditionalProducts(defaultCandidateAdditions, c.Type, c.Name)...) + products.removeByValue(findProductsToRemove(defaultCandidateRemovals, c.Type, c.Name)...) + } + } for p := range products { p.disallowDelimiterVariations = true diff --git a/syft/pkg/cataloger/common/cpe/apk_test.go b/syft/pkg/cataloger/common/cpe/apk_test.go index ed4efb1e02a..5fdb594dc34 100644 --- a/syft/pkg/cataloger/common/cpe/apk_test.go +++ b/syft/pkg/cataloger/common/cpe/apk_test.go @@ -69,7 +69,35 @@ func Test_candidateVendorsForAPK(t *testing.T) { URL: "https://www.gnu.org/software/make", }, }, - expected: []string{"gnu"}, + expected: []string{"gnu", "make"}, + }, + { + name: "ruby-rake with matching origin", + pkg: pkg.Package{ + Name: "ruby-rake", + Type: pkg.ApkPkg, + MetadataType: pkg.ApkMetadataType, + Metadata: pkg.ApkMetadata{ + Package: "ruby-rake", + URL: "https://github.com/ruby/rake", + OriginPackage: "ruby-rake", + }, + }, + expected: []string{"rake", "ruby-lang", "ruby"}, + }, + { + name: "ruby-rake with non-matching origin", + pkg: pkg.Package{ + Name: "ruby-rake", + Type: pkg.ApkPkg, + MetadataType: pkg.ApkMetadataType, + Metadata: pkg.ApkMetadata{ + Package: "ruby-rake", + URL: "https://www.ruby-lang.org/", + OriginPackage: "ruby", + }, + }, + expected: []string{"rake", "ruby-lang", "ruby"}, }, } for _, test := range tests { @@ -142,6 +170,31 @@ func Test_candidateProductsForAPK(t *testing.T) { }, expected: []string{"make"}, }, + { + name: "ruby-rake with matching origin", + pkg: pkg.Package{ + Metadata: pkg.ApkMetadata{ + Package: "ruby-rake", + URL: "https://github.com/ruby/rake", + OriginPackage: "ruby-rake", + }, + }, + expected: []string{"rake"}, + }, + { + name: "ruby-rake with non-matching origin", + pkg: pkg.Package{ + Name: "ruby-rake", + Type: pkg.ApkPkg, + MetadataType: pkg.ApkMetadataType, + Metadata: pkg.ApkMetadata{ + Package: "ruby-rake", + URL: "https://www.ruby-lang.org/", + OriginPackage: "ruby", + }, + }, + expected: []string{"rake", "ruby"}, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/syft/pkg/cataloger/common/cpe/generate_test.go b/syft/pkg/cataloger/common/cpe/generate_test.go index fe966e603c2..02b720caaa0 100644 --- a/syft/pkg/cataloger/common/cpe/generate_test.go +++ b/syft/pkg/cataloger/common/cpe/generate_test.go @@ -673,6 +673,48 @@ func TestGeneratePackageCPEs(t *testing.T) { "cpe:2.3:a:python_redis:redis:2.1.4:*:*:*:*:*:*:*", }, }, + { + name: "regression: ruby-rake apk missing expected ruby-lang:rake CPE", + p: pkg.Package{ + Name: "ruby-rake", + Version: "2.7.6-r0", + Type: pkg.ApkPkg, + FoundBy: "apk-db-analyzer", + Language: pkg.UnknownLanguage, + MetadataType: pkg.ApkMetadataType, + Metadata: pkg.ApkMetadata{ + Package: "ruby-rake", + URL: "https://www.ruby-lang.org/", + OriginPackage: "ruby", + }, + }, + expected: []string{ + "cpe:2.3:a:ruby-lang:rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:rake:rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:rake:ruby-rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:rake:ruby_rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-lang:ruby-rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-lang:ruby_rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-rake:rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-rake:ruby-rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-rake:ruby_rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby:rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby:ruby-rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby:ruby_rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_lang:rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_lang:ruby-rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_lang:ruby_rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_rake:rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_rake:ruby-rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_rake:ruby_rake:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:rake:ruby:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-lang:ruby:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby-rake:ruby:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby:ruby:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_lang:ruby:2.7.6-r0:*:*:*:*:*:*:*", + "cpe:2.3:a:ruby_rake:ruby:2.7.6-r0:*:*:*:*:*:*:*", + }, + }, } for _, test := range tests {