Add cataloger for Swift Package Manager. (#1919)

Signed-off-by: Tristan Farkas <Tristan.Farkas@axis.com>
This commit is contained in:
Tristan Farkas
2023-07-25 14:35:21 -04:00
committed by GitHub
parent 9a73380f29
commit e1c1832f84
24 changed files with 2353 additions and 14 deletions
+6 -1
View File
@@ -1,5 +1,5 @@
/*
Package swift provides a concrete Cataloger implementation for Podfile.lock files.
Package swift provides a concrete Cataloger implementation for Podfile.lock and Package.resolved files.
*/
package swift
@@ -7,6 +7,11 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
func NewSwiftPackageManagerCataloger() *generic.Cataloger {
return generic.NewCataloger("spm-cataloger").
WithParserByGlobs(parsePackageResolved, "**/Package.resolved", "**/.package.resolved")
}
// NewCocoapodsCataloger returns a new Swift Cocoapods lock file cataloger object.
func NewCocoapodsCataloger() *generic.Cataloger {
return generic.NewCataloger("cocoapods-cataloger").
+37 -3
View File
@@ -1,16 +1,37 @@
package swift
import (
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
func newPackage(name, version, hash string, locations ...file.Location) pkg.Package {
func newSwiftPackageManagerPackage(name, version, sourceURL, revision string, locations ...file.Location) pkg.Package {
p := pkg.Package{
Name: name,
Version: version,
PURL: packageURL(name, version),
PURL: swiftPackageManagerPackageURL(name, version, sourceURL),
Locations: file.NewLocationSet(locations...),
Type: pkg.SwiftPkg,
Language: pkg.Swift,
MetadataType: pkg.SwiftPackageManagerMetadataType,
Metadata: pkg.SwiftPackageManagerMetadata{
Revision: revision,
},
}
p.SetID()
return p
}
func newCocoaPodsPackage(name, version, hash string, locations ...file.Location) pkg.Package {
p := pkg.Package{
Name: name,
Version: version,
PURL: cocoaPodsPackageURL(name, version),
Locations: file.NewLocationSet(locations...),
Type: pkg.CocoapodsPkg,
Language: pkg.Swift,
@@ -25,7 +46,7 @@ func newPackage(name, version, hash string, locations ...file.Location) pkg.Pack
return p
}
func packageURL(name, version string) string {
func cocoaPodsPackageURL(name, version string) string {
var qualifiers packageurl.Qualifiers
return packageurl.NewPackageURL(
@@ -37,3 +58,16 @@ func packageURL(name, version string) string {
"",
).ToString()
}
func swiftPackageManagerPackageURL(name, version, sourceURL string) string {
var qualifiers packageurl.Qualifiers
return packageurl.NewPackageURL(
packageurl.TypeSwift,
strings.Replace(sourceURL, "https://", "", 1),
name,
version,
qualifiers,
"",
).ToString()
}
+2 -2
View File
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_packageURL(t *testing.T) {
func Test_cocoaPodsPackageURL(t *testing.T) {
type args struct {
name string
version string
@@ -27,7 +27,7 @@ func Test_packageURL(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, packageURL(tt.args.name, tt.args.version))
assert.Equal(t, tt.want, cocoaPodsPackageURL(tt.args.name, tt.args.version))
})
}
}
@@ -0,0 +1,134 @@
package swift
import (
"encoding/json"
"errors"
"fmt"
"io"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
var _ generic.Parser = parsePackageResolved
// swift package manager has two versions (1 and 2) of the resolved files, the types below describes the serialization strategies for each version
// with its suffix indicating which version its specific to.
type packageResolvedV1 struct {
PackageObject packageObjectV1 `json:"object"`
Version int `json:"version"`
}
type packageObjectV1 struct {
Pins []packagePinsV1
}
type packagePinsV1 struct {
Name string `json:"package"`
RepositoryURL string `json:"repositoryURL"`
State packageState `json:"state"`
}
type packageResolvedV2 struct {
Pins []packagePinsV2
}
type packagePinsV2 struct {
Identity string `json:"identity"`
Kind string `json:"kind"`
Location string `json:"location"`
State packageState `json:"state"`
}
type packagePin struct {
Identity string
Location string
Revision string
Version string
}
type packageState struct {
Revision string `json:"revision"`
Version string `json:"version"`
}
// parsePackageResolved is a parser for the contents of a Package.resolved file, which is generated by Xcode after it's resolved Swift Package Manger packages.
func parsePackageResolved(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
dec := json.NewDecoder(reader)
var packageResolvedData map[string]interface{}
for {
if err := dec.Decode(&packageResolvedData); errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, nil, fmt.Errorf("failed to parse Package.resolved file: %w", err)
}
}
var pins, err = pinsForVersion(packageResolvedData, packageResolvedData["version"].(float64))
if err != nil {
return nil, nil, err
}
var pkgs []pkg.Package
for _, packagePin := range pins {
pkgs = append(
pkgs,
newSwiftPackageManagerPackage(
packagePin.Identity,
packagePin.Version,
packagePin.Location,
packagePin.Revision,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
),
)
}
return pkgs, nil, nil
}
func pinsForVersion(data map[string]interface{}, version float64) ([]packagePin, error) {
var genericPins []packagePin
switch version {
case 1:
t := packageResolvedV1{}
jsonString, err := json.Marshal(data)
if err != nil {
return nil, err
}
parseErr := json.Unmarshal(jsonString, &t)
if parseErr != nil {
return nil, parseErr
}
for _, pin := range t.PackageObject.Pins {
genericPins = append(genericPins, packagePin{
pin.Name,
pin.RepositoryURL,
pin.State.Revision,
pin.State.Version,
})
}
case 2:
t := packageResolvedV2{}
jsonString, err := json.Marshal(data)
if err != nil {
return nil, err
}
parseErr := json.Unmarshal(jsonString, &t)
if parseErr != nil {
return nil, parseErr
}
for _, pin := range t.Pins {
genericPins = append(genericPins, packagePin{
pin.Identity,
pin.Location,
pin.State.Revision,
pin.State.Version,
})
}
default:
return nil, fmt.Errorf("unknown swift package manager version, %f", version)
}
return genericPins, nil
}
@@ -0,0 +1,82 @@
package swift
import (
"testing"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func TestParsePackageResolved(t *testing.T) {
fixture := "test-fixtures/Package.resolved"
locations := file.NewLocationSet(file.NewLocation(fixture))
expectedPkgs := []pkg.Package{
{
Name: "swift-algorithms",
Version: "1.0.0",
PURL: "pkg:swift/github.com/apple/swift-algorithms.git/swift-algorithms@1.0.0",
Locations: locations,
Language: pkg.Swift,
Type: pkg.SwiftPkg,
MetadataType: pkg.SwiftPackageManagerMetadataType,
Metadata: pkg.SwiftPackageManagerMetadata{
Revision: "b14b7f4c528c942f121c8b860b9410b2bf57825e",
},
},
{
Name: "swift-async-algorithms",
Version: "0.1.0",
PURL: "pkg:swift/github.com/apple/swift-async-algorithms.git/swift-async-algorithms@0.1.0",
Locations: locations,
Language: pkg.Swift,
Type: pkg.SwiftPkg,
MetadataType: pkg.SwiftPackageManagerMetadataType,
Metadata: pkg.SwiftPackageManagerMetadata{
Revision: "9cfed92b026c524674ed869a4ff2dcfdeedf8a2a",
},
},
{
Name: "swift-atomics",
Version: "1.1.0",
PURL: "pkg:swift/github.com/apple/swift-atomics.git/swift-atomics@1.1.0",
Locations: locations,
Language: pkg.Swift,
Type: pkg.SwiftPkg,
MetadataType: pkg.SwiftPackageManagerMetadataType,
Metadata: pkg.SwiftPackageManagerMetadata{
Revision: "6c89474e62719ddcc1e9614989fff2f68208fe10",
},
},
{
Name: "swift-collections",
Version: "1.0.4",
PURL: "pkg:swift/github.com/apple/swift-collections.git/swift-collections@1.0.4",
Locations: locations,
Language: pkg.Swift,
Type: pkg.SwiftPkg,
MetadataType: pkg.SwiftPackageManagerMetadataType,
Metadata: pkg.SwiftPackageManagerMetadata{
Revision: "937e904258d22af6e447a0b72c0bc67583ef64a2",
},
},
{
Name: "swift-numerics",
Version: "1.0.2",
PURL: "pkg:swift/github.com/apple/swift-numerics/swift-numerics@1.0.2",
Locations: locations,
Language: pkg.Swift,
Type: pkg.SwiftPkg,
MetadataType: pkg.SwiftPackageManagerMetadataType,
Metadata: pkg.SwiftPackageManagerMetadata{
Revision: "0a5bc04095a675662cf24757cc0640aa2204253b",
},
},
}
// TODO: no relationships are under test yet
var expectedRelationships []artifact.Relationship
pkgtest.TestFileParser(t, fixture, parsePackageResolved, expectedPkgs, expectedRelationships)
}
@@ -61,7 +61,7 @@ func parsePodfileLock(_ file.Resolver, _ *generic.Environment, reader file.Locat
pkgs = append(
pkgs,
newPackage(
newCocoaPodsPackage(
podName,
podVersion,
pkgHash,
@@ -0,0 +1,50 @@
{
"pins" : [
{
"identity" : "swift-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-algorithms.git",
"state" : {
"revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e",
"version" : "1.0.0"
}
},
{
"identity" : "swift-async-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms.git",
"state" : {
"revision" : "9cfed92b026c524674ed869a4ff2dcfdeedf8a2a",
"version" : "0.1.0"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "6c89474e62719ddcc1e9614989fff2f68208fe10",
"version" : "1.1.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2",
"version" : "1.0.4"
}
},
{
"identity" : "swift-numerics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-numerics",
"state" : {
"revision" : "0a5bc04095a675662cf24757cc0640aa2204253b",
"version" : "1.0.2"
}
}
],
"version" : 2
}