diff --git a/syft/pkg/cataloger/swift/package.go b/syft/pkg/cataloger/swift/package.go index c4370f5dc..17870370a 100644 --- a/syft/pkg/cataloger/swift/package.go +++ b/syft/pkg/cataloger/swift/package.go @@ -62,10 +62,44 @@ func swiftPackageManagerPackageURL(name, version, sourceURL string) string { return packageurl.NewPackageURL( packageurl.TypeSwift, - strings.Replace(sourceURL, "https://", "", 1), + swiftNamespaceFromSourceURL(sourceURL, name), name, version, qualifiers, "", ).ToString() } + +// swiftNamespaceFromSourceURL derives the purl namespace (e.g. +// "github.com/apple") from a Swift Package.resolved source URL like +// "https://github.com/apple/swift-nio-ssl.git". +// +// Two bits of cleanup are needed compared with the previous +// strings.Replace(sourceURL, "https://", "", 1) behaviour: +// +// 1. Strip the ".git" suffix. Swift Package.resolved always carries the +// repository URL with a trailing ".git", but the purl spec uses the +// plain web path. +// 2. Drop the trailing / segment. The previous code left the repo +// name in the namespace and then appended the package name again, so +// the emitted purl looked like +// pkg:swift/github.com/apple/swift-nio-ssl.git/swift-nio-ssl@2.0.0 +// which Grype cannot match against NVD. The expected form is +// pkg:swift/github.com/apple/swift-nio-ssl@2.0.0 +// which is also what cdxgen emits. See anchore/syft#3961. +func swiftNamespaceFromSourceURL(sourceURL, name string) string { + ns := sourceURL + for _, prefix := range []string{"https://", "http://", "git+ssh://", "ssh://"} { + ns = strings.TrimPrefix(ns, prefix) + } + ns = strings.TrimSuffix(ns, ".git") + // drop a trailing "/" segment. Package.resolved identities are lowercased + // while the repo path may be mixed-case (e.g. github.com/Apple/Swift-NIO vs + // identity swift-nio), so compare case-insensitively. + if name != "" { + if idx := strings.LastIndex(ns, "/"); idx >= 0 && strings.EqualFold(ns[idx+1:], name) { + ns = ns[:idx] + } + } + return ns +} diff --git a/syft/pkg/cataloger/swift/package_test.go b/syft/pkg/cataloger/swift/package_test.go index daa3132cd..abc8f3145 100644 --- a/syft/pkg/cataloger/swift/package_test.go +++ b/syft/pkg/cataloger/swift/package_test.go @@ -6,6 +6,57 @@ import ( "github.com/stretchr/testify/assert" ) +func Test_swiftPackageManagerPackageURL(t *testing.T) { + tests := []struct { + name string + pkgName string + version string + sourceURL string + want string + }{ + { + name: "strips .git suffix and repeated name segment", + pkgName: "swift-nio-ssl", + version: "2.0.0", + sourceURL: "https://github.com/apple/swift-nio-ssl.git", + want: "pkg:swift/github.com/apple/swift-nio-ssl@2.0.0", + }, + { + name: "no .git suffix", + pkgName: "swift-numerics", + version: "1.0.2", + sourceURL: "https://github.com/apple/swift-numerics", + want: "pkg:swift/github.com/apple/swift-numerics@1.0.2", + }, + { + name: "mixed-case repo path with lowercased identity", + pkgName: "swift-nio", + version: "2.0.0", + sourceURL: "https://github.com/Apple/Swift-NIO.git", + want: "pkg:swift/github.com/Apple/swift-nio@2.0.0", + }, + { + name: "ssh scheme", + pkgName: "swift-nio", + version: "2.0.0", + sourceURL: "git+ssh://github.com/apple/swift-nio.git", + want: "pkg:swift/github.com/apple/swift-nio@2.0.0", + }, + { + name: "trailing segment differs from name is preserved", + pkgName: "nio", + version: "2.0.0", + sourceURL: "https://github.com/apple/swift-nio.git", + want: "pkg:swift/github.com/apple/swift-nio/nio@2.0.0", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, swiftPackageManagerPackageURL(tt.pkgName, tt.version, tt.sourceURL)) + }) + } +} + func Test_cocoaPodsPackageURL(t *testing.T) { type args struct { name string diff --git a/syft/pkg/cataloger/swift/parse_package_resolved_test.go b/syft/pkg/cataloger/swift/parse_package_resolved_test.go index cd2565924..494a164c1 100644 --- a/syft/pkg/cataloger/swift/parse_package_resolved_test.go +++ b/syft/pkg/cataloger/swift/parse_package_resolved_test.go @@ -20,7 +20,7 @@ func TestParsePackageResolved(t *testing.T) { { Name: "swift-algorithms", Version: "1.0.0", - PURL: "pkg:swift/github.com/apple/swift-algorithms.git/swift-algorithms@1.0.0", + PURL: "pkg:swift/github.com/apple/swift-algorithms@1.0.0", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg, @@ -31,7 +31,7 @@ func TestParsePackageResolved(t *testing.T) { { Name: "swift-async-algorithms", Version: "0.1.0", - PURL: "pkg:swift/github.com/apple/swift-async-algorithms.git/swift-async-algorithms@0.1.0", + PURL: "pkg:swift/github.com/apple/swift-async-algorithms@0.1.0", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg, @@ -42,7 +42,7 @@ func TestParsePackageResolved(t *testing.T) { { Name: "swift-atomics", Version: "1.1.0", - PURL: "pkg:swift/github.com/apple/swift-atomics.git/swift-atomics@1.1.0", + PURL: "pkg:swift/github.com/apple/swift-atomics@1.1.0", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg, @@ -53,7 +53,7 @@ func TestParsePackageResolved(t *testing.T) { { Name: "swift-collections", Version: "1.0.4", - PURL: "pkg:swift/github.com/apple/swift-collections.git/swift-collections@1.0.4", + PURL: "pkg:swift/github.com/apple/swift-collections@1.0.4", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg, @@ -64,7 +64,7 @@ func TestParsePackageResolved(t *testing.T) { { Name: "swift-numerics", Version: "1.0.2", - PURL: "pkg:swift/github.com/apple/swift-numerics/swift-numerics@1.0.2", + PURL: "pkg:swift/github.com/apple/swift-numerics@1.0.2", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg, @@ -87,7 +87,7 @@ func TestParsePackageResolvedV3(t *testing.T) { { Name: "swift-mmio", Version: "", - PURL: "pkg:swift/github.com/apple/swift-mmio/swift-mmio", + PURL: "pkg:swift/github.com/apple/swift-mmio", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg, @@ -98,7 +98,7 @@ func TestParsePackageResolvedV3(t *testing.T) { { Name: "swift-syntax", Version: "509.1.1", - PURL: "pkg:swift/github.com/apple/swift-syntax.git/swift-syntax@509.1.1", + PURL: "pkg:swift/github.com/apple/swift-syntax@509.1.1", Locations: locations, Language: pkg.Swift, Type: pkg.SwiftPkg,