Skip to content

Commit

Permalink
Add cataloger for Swift Package Manager.
Browse files Browse the repository at this point in the history
Signed-off-by: Tristan Farkas <[email protected]>
  • Loading branch information
Tristan Farkas authored and Tristan Farkas committed Jul 7, 2023
1 parent 376c428 commit 74ba142
Show file tree
Hide file tree
Showing 21 changed files with 481 additions and 73 deletions.
4 changes: 4 additions & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ The following Syft components were contributed by external authors/organizations
## GraalVM Native Image

A cataloger contributed by Oracle Corporation that extracts packages given within GraalVM Native Image SBOMs.

## Swift Package Manager

A cataloger contributed by Axis Communications that catalogs packages resolved by Swift Package Manager.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ For commercial support options with Syft or Grype, please [contact Anchore](http
- Red Hat (rpm)
- Ruby (gem)
- Rust (cargo.lock)
- Swift (cocoapods)
- Swift (cocoapods, swift-package-manager)

## Installation

Expand Down Expand Up @@ -211,6 +211,7 @@ You can override the list of enabled/disabled catalogers by using the "cataloger
- ruby-gemfile
- rust-cargo-lock
- sbom
- swift-package-manager

##### Non Default:
- cargo-auditable-binary
Expand Down Expand Up @@ -521,6 +522,7 @@ platform: ""
# - ruby-gemspec-cataloger
# - rust-cargo-lock-cataloger
# - sbom-cataloger
# - spm-cataloger
catalogers:

# cataloging packages is exposed through the packages and power-user subcommands
Expand Down
2 changes: 2 additions & 0 deletions syft/formats/common/spdxhelpers/source_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from nix store path"
case pkg.Rpkg:
answer = "acquired package info from R-package DESCRIPTION file"
case pkg.SwiftPkg:
answer = "acquired package info from resolved Swift package manifest"
default:
answer = "acquired package info from the following paths"
}
Expand Down
8 changes: 8 additions & 0 deletions syft/formats/common/spdxhelpers/source_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@ func Test_SourceInfo(t *testing.T) {
"acquired package info from R-package DESCRIPTION file",
},
},
{
input: pkg.Package{
Type: pkg.SwiftPkg,
},
expected: []string{
"from resolved Swift package manifest",
},
},
}
var pkgTypes []pkg.Type
for _, test := range tests {
Expand Down
2 changes: 2 additions & 0 deletions syft/pkg/cataloger/cataloger.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger {
rust.NewCargoLockCataloger(),
sbom.NewSBOMCataloger(),
swift.NewCocoapodsCataloger(),
swift.NewSwiftPackageManagerCataloger(),
}, cfg.Catalogers)
}

Expand Down Expand Up @@ -132,6 +133,7 @@ func AllCatalogers(cfg Config) []pkg.Cataloger {
rust.NewCargoLockCataloger(),
sbom.NewSBOMCataloger(),
swift.NewCocoapodsCataloger(),
swift.NewSwiftPackageManagerCataloger(),
}, cfg.Catalogers)
}

Expand Down
7 changes: 6 additions & 1 deletion syft/pkg/cataloger/swift/cataloger.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
/*
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

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").
Expand Down
40 changes: 37 additions & 3 deletions syft/pkg/cataloger/swift/package.go
Original file line number Diff line number Diff line change
@@ -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: 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: packageURL(name, version),
PURL: cocoaPodsPackageURL(name, version),
Locations: file.NewLocationSet(locations...),
Type: pkg.CocoapodsPkg,
Language: pkg.Swift,
Expand All @@ -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(
Expand All @@ -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()
}
4 changes: 2 additions & 2 deletions syft/pkg/cataloger/swift/package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
})
}
}
134 changes: 134 additions & 0 deletions syft/pkg/cataloger/swift/parse_package_resolved.go
Original file line number Diff line number Diff line change
@@ -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
}
82 changes: 82 additions & 0 deletions syft/pkg/cataloger/swift/parse_package_resolved_test.go
Original file line number Diff line number Diff line change
@@ -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/[email protected]",
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/[email protected]",
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/[email protected]",
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/[email protected]",
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/[email protected]",
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)
}
Loading

0 comments on commit 74ba142

Please sign in to comment.