Skip to content

Commit e8abab2

Browse files
SAY-5wagoodman
andauthored
swift: emit canonical purls without .git suffix or repeated name (#4785)
* swift: emit canonical purls without .git suffix or repeated name Reported in #3961: the purl syft emits for Swift packages parsed out of Package.resolved keeps the repository URLs .git suffix and then re-appends the package name, producing pkg:swift/github.com/apple/swift-nio-ssl.git/swift-nio-ssl@2.0.0 NVD / Grype cannot match that purl against the known swift-nio-ssl@2.0.0 CVE (GHSA-frg3-gpcx-968f), so every Swift SBOM produced by syft silently loses vulnerability coverage. cdxgen and the wider purl ecosystem use the shorter form pkg:swift/github.com/apple/swift-nio-ssl@2.0.0 which Grype does match. Replace the ad-hoc strings.Replace with swiftNamespaceFromSourceURL, which trims the common URL schemes, strips the ".git" suffix, and drops a trailing /<name> segment so the namespace is only the organisation path (e.g. github.com/apple). Existing test expectations are updated to the new purl shape. Fixes #3961 Signed-off-by: Sai Asish Y <say.apm35@gmail.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * Revert unrelated consul binary classifier changes The swift purl commit (ff5ffc3) accidentally bundled removal of the consul GitDescribe and NUL-wrapped version matchers plus the 1.12.9 and 1.7.14 fixtures/test cases. That drops version detection for consul binaries not carrying the CONSUL_VERSION string. Restore the binary cataloger to its pre-commit state so this branch is swift-only. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * swift: strip repeated name segment case-insensitively Package.resolved identities are lowercased while the repo URL path may be mixed-case (e.g. github.com/Apple/Swift-NIO vs identity swift-nio). The case-sensitive TrimSuffix left the repo segment in the namespace for such repos, reintroducing the duplicated-name purl. Compare the trailing segment with EqualFold and add unit coverage for swiftPackageManagerPackageURL. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Sai Asish Y <say.apm35@gmail.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
1 parent 002a326 commit e8abab2

3 files changed

Lines changed: 93 additions & 8 deletions

File tree

syft/pkg/cataloger/swift/package.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,44 @@ func swiftPackageManagerPackageURL(name, version, sourceURL string) string {
6262

6363
return packageurl.NewPackageURL(
6464
packageurl.TypeSwift,
65-
strings.Replace(sourceURL, "https://", "", 1),
65+
swiftNamespaceFromSourceURL(sourceURL, name),
6666
name,
6767
version,
6868
qualifiers,
6969
"",
7070
).ToString()
7171
}
72+
73+
// swiftNamespaceFromSourceURL derives the purl namespace (e.g.
74+
// "github.com/apple") from a Swift Package.resolved source URL like
75+
// "https://github.com/apple/swift-nio-ssl.git".
76+
//
77+
// Two bits of cleanup are needed compared with the previous
78+
// strings.Replace(sourceURL, "https://", "", 1) behaviour:
79+
//
80+
// 1. Strip the ".git" suffix. Swift Package.resolved always carries the
81+
// repository URL with a trailing ".git", but the purl spec uses the
82+
// plain web path.
83+
// 2. Drop the trailing /<name> segment. The previous code left the repo
84+
// name in the namespace and then appended the package name again, so
85+
// the emitted purl looked like
86+
// pkg:swift/github.com/apple/swift-nio-ssl.git/swift-nio-ssl@2.0.0
87+
// which Grype cannot match against NVD. The expected form is
88+
// pkg:swift/github.com/apple/swift-nio-ssl@2.0.0
89+
// which is also what cdxgen emits. See anchore/syft#3961.
90+
func swiftNamespaceFromSourceURL(sourceURL, name string) string {
91+
ns := sourceURL
92+
for _, prefix := range []string{"https://", "http://", "git+ssh://", "ssh://"} {
93+
ns = strings.TrimPrefix(ns, prefix)
94+
}
95+
ns = strings.TrimSuffix(ns, ".git")
96+
// drop a trailing "/<name>" segment. Package.resolved identities are lowercased
97+
// while the repo path may be mixed-case (e.g. github.com/Apple/Swift-NIO vs
98+
// identity swift-nio), so compare case-insensitively.
99+
if name != "" {
100+
if idx := strings.LastIndex(ns, "/"); idx >= 0 && strings.EqualFold(ns[idx+1:], name) {
101+
ns = ns[:idx]
102+
}
103+
}
104+
return ns
105+
}

syft/pkg/cataloger/swift/package_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,57 @@ import (
66
"github.com/stretchr/testify/assert"
77
)
88

9+
func Test_swiftPackageManagerPackageURL(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
pkgName string
13+
version string
14+
sourceURL string
15+
want string
16+
}{
17+
{
18+
name: "strips .git suffix and repeated name segment",
19+
pkgName: "swift-nio-ssl",
20+
version: "2.0.0",
21+
sourceURL: "https://github.com/apple/swift-nio-ssl.git",
22+
want: "pkg:swift/github.com/apple/swift-nio-ssl@2.0.0",
23+
},
24+
{
25+
name: "no .git suffix",
26+
pkgName: "swift-numerics",
27+
version: "1.0.2",
28+
sourceURL: "https://github.com/apple/swift-numerics",
29+
want: "pkg:swift/github.com/apple/swift-numerics@1.0.2",
30+
},
31+
{
32+
name: "mixed-case repo path with lowercased identity",
33+
pkgName: "swift-nio",
34+
version: "2.0.0",
35+
sourceURL: "https://github.com/Apple/Swift-NIO.git",
36+
want: "pkg:swift/github.com/Apple/swift-nio@2.0.0",
37+
},
38+
{
39+
name: "ssh scheme",
40+
pkgName: "swift-nio",
41+
version: "2.0.0",
42+
sourceURL: "git+ssh://github.com/apple/swift-nio.git",
43+
want: "pkg:swift/github.com/apple/swift-nio@2.0.0",
44+
},
45+
{
46+
name: "trailing segment differs from name is preserved",
47+
pkgName: "nio",
48+
version: "2.0.0",
49+
sourceURL: "https://github.com/apple/swift-nio.git",
50+
want: "pkg:swift/github.com/apple/swift-nio/nio@2.0.0",
51+
},
52+
}
53+
for _, tt := range tests {
54+
t.Run(tt.name, func(t *testing.T) {
55+
assert.Equal(t, tt.want, swiftPackageManagerPackageURL(tt.pkgName, tt.version, tt.sourceURL))
56+
})
57+
}
58+
}
59+
960
func Test_cocoaPodsPackageURL(t *testing.T) {
1061
type args struct {
1162
name string

syft/pkg/cataloger/swift/parse_package_resolved_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func TestParsePackageResolved(t *testing.T) {
2020
{
2121
Name: "swift-algorithms",
2222
Version: "1.0.0",
23-
PURL: "pkg:swift/github.com/apple/swift-algorithms.git/swift-algorithms@1.0.0",
23+
PURL: "pkg:swift/github.com/apple/swift-algorithms@1.0.0",
2424
Locations: locations,
2525
Language: pkg.Swift,
2626
Type: pkg.SwiftPkg,
@@ -31,7 +31,7 @@ func TestParsePackageResolved(t *testing.T) {
3131
{
3232
Name: "swift-async-algorithms",
3333
Version: "0.1.0",
34-
PURL: "pkg:swift/github.com/apple/swift-async-algorithms.git/swift-async-algorithms@0.1.0",
34+
PURL: "pkg:swift/github.com/apple/swift-async-algorithms@0.1.0",
3535
Locations: locations,
3636
Language: pkg.Swift,
3737
Type: pkg.SwiftPkg,
@@ -42,7 +42,7 @@ func TestParsePackageResolved(t *testing.T) {
4242
{
4343
Name: "swift-atomics",
4444
Version: "1.1.0",
45-
PURL: "pkg:swift/github.com/apple/swift-atomics.git/swift-atomics@1.1.0",
45+
PURL: "pkg:swift/github.com/apple/swift-atomics@1.1.0",
4646
Locations: locations,
4747
Language: pkg.Swift,
4848
Type: pkg.SwiftPkg,
@@ -53,7 +53,7 @@ func TestParsePackageResolved(t *testing.T) {
5353
{
5454
Name: "swift-collections",
5555
Version: "1.0.4",
56-
PURL: "pkg:swift/github.com/apple/swift-collections.git/swift-collections@1.0.4",
56+
PURL: "pkg:swift/github.com/apple/swift-collections@1.0.4",
5757
Locations: locations,
5858
Language: pkg.Swift,
5959
Type: pkg.SwiftPkg,
@@ -64,7 +64,7 @@ func TestParsePackageResolved(t *testing.T) {
6464
{
6565
Name: "swift-numerics",
6666
Version: "1.0.2",
67-
PURL: "pkg:swift/github.com/apple/swift-numerics/swift-numerics@1.0.2",
67+
PURL: "pkg:swift/github.com/apple/swift-numerics@1.0.2",
6868
Locations: locations,
6969
Language: pkg.Swift,
7070
Type: pkg.SwiftPkg,
@@ -87,7 +87,7 @@ func TestParsePackageResolvedV3(t *testing.T) {
8787
{
8888
Name: "swift-mmio",
8989
Version: "",
90-
PURL: "pkg:swift/github.com/apple/swift-mmio/swift-mmio",
90+
PURL: "pkg:swift/github.com/apple/swift-mmio",
9191
Locations: locations,
9292
Language: pkg.Swift,
9393
Type: pkg.SwiftPkg,
@@ -98,7 +98,7 @@ func TestParsePackageResolvedV3(t *testing.T) {
9898
{
9999
Name: "swift-syntax",
100100
Version: "509.1.1",
101-
PURL: "pkg:swift/github.com/apple/swift-syntax.git/swift-syntax@509.1.1",
101+
PURL: "pkg:swift/github.com/apple/swift-syntax@509.1.1",
102102
Locations: locations,
103103
Language: pkg.Swift,
104104
Type: pkg.SwiftPkg,

0 commit comments

Comments
 (0)