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 20:35:21 +02:00 committed by GitHub
parent 9a73380f29
commit e1c1832f84
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 2353 additions and 14 deletions

View file

@ -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.

View file

@ -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
@ -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
@ -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

View file

@ -6,5 +6,5 @@ const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "9.0.1"
JSONSchemaVersion = "9.0.2"
)

File diff suppressed because it is too large Load diff

View file

@ -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"
}

View file

@ -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 {

View file

@ -6,5 +6,5 @@ import "github.com/anchore/syft/syft/pkg"
// AllTypes returns a list of all pkg metadata types that syft supports (that are represented in the pkg.Package.Metadata field).
func AllTypes() []any {
return []any{pkg.AlpmMetadata{}, pkg.ApkMetadata{}, pkg.BinaryMetadata{}, pkg.CargoPackageMetadata{}, pkg.CocoapodsMetadata{}, pkg.ConanLockMetadata{}, pkg.ConanMetadata{}, pkg.DartPubMetadata{}, pkg.DotnetDepsMetadata{}, pkg.DotnetPortableExecutableMetadata{}, pkg.DpkgMetadata{}, pkg.GemMetadata{}, pkg.GolangBinMetadata{}, pkg.GolangModMetadata{}, pkg.HackageMetadata{}, pkg.JavaMetadata{}, pkg.KbPackageMetadata{}, pkg.LinuxKernelMetadata{}, pkg.LinuxKernelModuleMetadata{}, pkg.MixLockMetadata{}, pkg.NixStoreMetadata{}, pkg.NpmPackageJSONMetadata{}, pkg.NpmPackageLockJSONMetadata{}, pkg.PhpComposerJSONMetadata{}, pkg.PortageMetadata{}, pkg.PythonPackageMetadata{}, pkg.PythonPipfileLockMetadata{}, pkg.PythonRequirementsMetadata{}, pkg.RDescriptionFileMetadata{}, pkg.RebarLockMetadata{}, pkg.RpmMetadata{}}
return []any{pkg.AlpmMetadata{}, pkg.ApkMetadata{}, pkg.BinaryMetadata{}, pkg.CargoPackageMetadata{}, pkg.CocoapodsMetadata{}, pkg.ConanLockMetadata{}, pkg.ConanMetadata{}, pkg.DartPubMetadata{}, pkg.DotnetDepsMetadata{}, pkg.DotnetPortableExecutableMetadata{}, pkg.DpkgMetadata{}, pkg.GemMetadata{}, pkg.GolangBinMetadata{}, pkg.GolangModMetadata{}, pkg.HackageMetadata{}, pkg.JavaMetadata{}, pkg.KbPackageMetadata{}, pkg.LinuxKernelMetadata{}, pkg.LinuxKernelModuleMetadata{}, pkg.MixLockMetadata{}, pkg.NixStoreMetadata{}, pkg.NpmPackageJSONMetadata{}, pkg.NpmPackageLockJSONMetadata{}, pkg.PhpComposerJSONMetadata{}, pkg.PortageMetadata{}, pkg.PythonPackageMetadata{}, pkg.PythonPipfileLockMetadata{}, pkg.PythonRequirementsMetadata{}, pkg.RDescriptionFileMetadata{}, pkg.RebarLockMetadata{}, pkg.RpmMetadata{}, pkg.SwiftPackageManagerMetadata{}}
}

View file

@ -93,6 +93,7 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger {
rust.NewCargoLockCataloger(),
sbom.NewSBOMCataloger(),
swift.NewCocoapodsCataloger(),
swift.NewSwiftPackageManagerCataloger(),
}, cfg.Catalogers)
}
@ -134,6 +135,7 @@ func AllCatalogers(cfg Config) []pkg.Cataloger {
rust.NewCargoLockCataloger(),
sbom.NewSBOMCataloger(),
swift.NewCocoapodsCataloger(),
swift.NewSwiftPackageManagerCataloger(),
}, cfg.Catalogers)
}

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").

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()
}

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))
})
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -61,7 +61,7 @@ func parsePodfileLock(_ file.Resolver, _ *generic.Environment, reader file.Locat
pkgs = append(
pkgs,
newPackage(
newCocoaPodsPackage(
podName,
podVersion,
pkgHash,

View file

@ -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
}

View file

@ -82,7 +82,7 @@ func LanguageByName(name string) Language {
return Dart
case packageurl.TypeDotnet:
return Dotnet
case packageurl.TypeCocoapods, packageurl.TypeSwift, string(CocoapodsPkg):
case packageurl.TypeCocoapods, packageurl.TypeSwift, string(CocoapodsPkg), string(SwiftPkg):
return Swift
case packageurl.TypeConan, string(CPP):
return CPP

View file

@ -70,9 +70,13 @@ func TestLanguageFromPURL(t *testing.T) {
purl: "pkg:cran/base@4.3.0",
want: R,
},
{
purl: "pkg:swift/github.com/apple/swift-numerics/swift-numerics@1.0.2",
want: Swift,
},
}
var languages []string
var languages = strset.New()
var expectedLanguages = strset.New()
for _, ty := range AllLanguages {
expectedLanguages.Add(string(ty))
@ -87,14 +91,14 @@ func TestLanguageFromPURL(t *testing.T) {
actual := LanguageFromPURL(tt.purl)
if actual != "" {
languages = append(languages, string(actual))
languages.Add(string(actual))
}
assert.Equalf(t, tt.want, actual, "LanguageFromPURL(%v)", tt.purl)
})
}
assert.ElementsMatch(t, expectedLanguages.List(), languages, "missing one or more languages to test against (maybe a package type was added?)")
assert.ElementsMatch(t, expectedLanguages.List(), languages.List(), "missing one or more languages to test against (maybe a package type was added?)")
}

View file

@ -42,6 +42,7 @@ const (
RDescriptionFileMetadataType MetadataType = "RDescriptionFileMetadataType"
RpmMetadataType MetadataType = "RpmMetadata"
RustCargoPackageMetadataType MetadataType = "RustCargoPackageMetadata"
SwiftPackageManagerMetadataType MetadataType = "SwiftPackageManagerMetadata"
)
var AllMetadataTypes = []MetadataType{
@ -76,6 +77,7 @@ var AllMetadataTypes = []MetadataType{
RebarLockMetadataType,
RpmMetadataType,
RustCargoPackageMetadataType,
SwiftPackageManagerMetadataType,
}
var MetadataTypeByName = map[MetadataType]reflect.Type{
@ -110,6 +112,7 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{
RebarLockMetadataType: reflect.TypeOf(RebarLockMetadata{}),
RpmMetadataType: reflect.TypeOf(RpmMetadata{}),
RustCargoPackageMetadataType: reflect.TypeOf(CargoPackageMetadata{}),
SwiftPackageManagerMetadataType: reflect.TypeOf(SwiftPackageManagerMetadata{}),
}
func CleanMetadataType(typ MetadataType) MetadataType {

View file

@ -0,0 +1,5 @@
package pkg
type SwiftPackageManagerMetadata struct {
Revision string `mapstructure:"revision" json:"revision"`
}

View file

@ -36,6 +36,7 @@ const (
Rpkg Type = "R-package"
RpmPkg Type = "rpm"
RustPkg Type = "rust-crate"
SwiftPkg Type = "swift"
)
// AllPkgs represents all supported package types
@ -65,6 +66,7 @@ var AllPkgs = []Type{
Rpkg,
RpmPkg,
RustPkg,
SwiftPkg,
}
// PackageURLType returns the PURL package type for the current package.
@ -114,6 +116,8 @@ func (t Type) PackageURLType() string {
return packageurl.TypeRPM
case RustPkg:
return "cargo"
case SwiftPkg:
return packageurl.TypeSwift
default:
// TODO: should this be a "generic" purl type instead?
return ""
@ -179,6 +183,8 @@ func TypeByName(name string) Type {
return NixPkg
case packageurl.TypeCran:
return Rpkg
case packageurl.TypeSwift:
return SwiftPkg
default:
return UnknownPkg
}

View file

@ -95,6 +95,10 @@ func TestTypeFromPURL(t *testing.T) {
purl: "pkg:cran/base@4.3.0",
expected: Rpkg,
},
{
purl: "pkg:swift/github.com/apple/swift-numerics/swift-numerics@1.0.2",
expected: SwiftPkg,
},
}
var pkgTypes []string

View file

@ -356,6 +356,18 @@ var dirOnlyTestCases = []testCase{
"unicode_util_compat": "0.7.0",
},
},
{
name: "find swift package manager packages",
pkgType: pkg.SwiftPkg,
pkgLanguage: pkg.Swift,
pkgInfo: map[string]string{
"swift-algorithms": "1.0.0",
"swift-async-algorithms": "0.1.0",
"swift-atomics": "1.1.0",
"swift-collections": "1.0.4",
"swift-numerics": "1.0.2",
},
},
}
var commonTestCases = []testCase{

View file

@ -95,6 +95,7 @@ func TestPkgCoverageImage(t *testing.T) {
definedPkgs.Remove(string(pkg.HexPkg))
definedPkgs.Remove(string(pkg.LinuxKernelPkg))
definedPkgs.Remove(string(pkg.LinuxKernelModulePkg))
definedPkgs.Remove(string(pkg.SwiftPkg))
var cases []testCase
cases = append(cases, commonTestCases...)

View file

@ -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
}