diff --git a/Taskfile.yaml b/Taskfile.yaml index 5e7f2c000ee..09c5c2449cd 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -233,7 +233,7 @@ tasks: - cmd: "test -f {{ .SNAPSHOT_BIN }} || (find {{ .SNAPSHOT_DIR }} && echo '\nno snapshot found' && false)" silent: true - - "go test -count=1 -timeout=15m -v ./test/cli" + - "go test -count=1 -timeout=25m -v ./test/cli" env: SYFT_BINARY_LOCATION: "{{ .SNAPSHOT_BIN }}" diff --git a/go.mod b/go.mod index 38b4a830029..4551568dba2 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,7 @@ require ( github.com/moby/sys/mountinfo v0.7.2 github.com/olekukonko/tablewriter v0.0.5 github.com/opencontainers/go-digest v1.0.0 - github.com/pelletier/go-toml v1.9.5 + github.com/pelletier/go-toml v1.9.5 // indirect github.com/quasilyte/go-ruleguard/dsl v0.3.22 github.com/saferwall/pe v1.5.4 github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d @@ -91,6 +91,7 @@ require ( github.com/OneOfOne/xxhash v1.2.8 github.com/adrg/xdg v0.5.0 github.com/magiconair/properties v1.8.7 + github.com/pelletier/go-toml/v2 v2.1.0 golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 ) diff --git a/internal/cmptest/common_options.go b/internal/cmptest/common_options.go index ecfb54edfde..1f528ca9806 100644 --- a/internal/cmptest/common_options.go +++ b/internal/cmptest/common_options.go @@ -1,9 +1,12 @@ package cmptest import ( + "slices" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" ) @@ -24,7 +27,16 @@ func CommonOptions(licenseCmp LicenseComparer, locationCmp LocationComparer) []c return []cmp.Option{ cmpopts.IgnoreFields(pkg.Package{}, "id"), // note: ID is not deterministic for test purposes cmpopts.SortSlices(pkg.Less), - cmpopts.SortSlices(DefaultRelationshipComparer), + cmp.Comparer(func(x, y []artifact.Relationship) bool { + // copy here, because we shouldn't mutate the input in any way! + cpyX := make([]artifact.Relationship, len(x)) + copy(cpyX, x) + cpyY := make([]artifact.Relationship, len(y)) + copy(cpyY, x) + slices.SortStableFunc(cpyX, DefaultRelationshipComparer) + slices.SortStableFunc(cpyY, DefaultRelationshipComparer) + return slices.CompareFunc(cpyX, cpyY, DefaultRelationshipComparer) == 0 + }), cmp.Comparer( func(x, y file.LocationSet) bool { xs := x.ToSlice() diff --git a/internal/cmptest/relationship.go b/internal/cmptest/relationship.go index 583b28dca68..e51f48a5c2e 100644 --- a/internal/cmptest/relationship.go +++ b/internal/cmptest/relationship.go @@ -1,6 +1,8 @@ package cmptest import ( + "reflect" + "github.com/sanity-io/litter" "github.com/anchore/syft/syft/artifact" @@ -8,19 +10,83 @@ import ( type RelationshipComparer func(x, y artifact.Relationship) bool -var relationshipStringer = litter.Options{ +var dataStringer = litter.Options{ Compact: true, StripPackageNames: false, - HidePrivateFields: true, // we want to ignore package IDs - HideZeroValues: true, - StrictGo: true, + //HidePrivateFields: true, // we want to ignore package IDs + HideZeroValues: true, + StrictGo: true, //FieldExclusions: ... // these can be added for future values that need to be ignored //FieldFilter: ... } -func DefaultRelationshipComparer(x, y artifact.Relationship) bool { +func DefaultRelationshipComparer(x, y artifact.Relationship) int { + if x.Type < y.Type { + return -1 + } + if x.Type > y.Type { + return 1 + } + + if i := DefaultIdentifiableComparer(x.From, y.From); i != 0 { + return i + } + if i := DefaultIdentifiableComparer(x.To, y.To); i != 0 { + return i + } + + if x.Data == nil && y.Data == nil { + return 0 + } + if x.Data == nil { + return -1 + } + if y.Data == nil { + return 1 + } + + { + xData := reflect.ValueOf(x.Data).Type().Name() + yData := reflect.ValueOf(y.Data).Type().Name() + if xData < yData { + return -1 + } + if xData > yData { + return 1 + } + } // we just need a stable sort, the ordering does not need to be sensible - xStr := relationshipStringer.Sdump(x) - yStr := relationshipStringer.Sdump(y) - return xStr < yStr + xStr := dataStringer.Sdump(x.Data) + yStr := dataStringer.Sdump(y.Data) + if xStr < yStr { + return -1 + } + if xStr > yStr { + return 1 + } + return 0 +} + +func DefaultIdentifiableComparer(x, y artifact.Identifiable) int { + { + xTo := reflect.ValueOf(x).Type().Name() + yTo := reflect.ValueOf(y).Type().Name() + if xTo < yTo { + return -1 + } + if xTo > yTo { + return 1 + } + } + { + xFrom := x.ID() + yFrom := y.ID() + if xFrom < yFrom { + return -1 + } + if xFrom > yFrom { + return 1 + } + } + return 0 } diff --git a/internal/relationship/sort.go b/internal/relationship/sort.go index 88582b4e922..35e06a5d7e9 100644 --- a/internal/relationship/sort.go +++ b/internal/relationship/sort.go @@ -1,42 +1,15 @@ package relationship import ( - "sort" + "slices" + "github.com/anchore/syft/internal/cmptest" "github.com/anchore/syft/syft/artifact" - "github.com/anchore/syft/syft/pkg" ) // Sort takes a set of package-to-package relationships and sorts them in a stable order by name and version. // Note: this does not consider package-to-other, other-to-package, or other-to-other relationships. // TODO: ideally this should be replaced with a more type-agnostic sort function that resides in the artifact package. func Sort(rels []artifact.Relationship) { - sort.SliceStable(rels, func(i, j int) bool { - return less(rels[i], rels[j]) - }) -} - -func less(i, j artifact.Relationship) bool { - iFrom, ok1 := i.From.(pkg.Package) - iTo, ok2 := i.To.(pkg.Package) - jFrom, ok3 := j.From.(pkg.Package) - jTo, ok4 := j.To.(pkg.Package) - - if !(ok1 && ok2 && ok3 && ok4) { - return false - } - - if iFrom.Name != jFrom.Name { - return iFrom.Name < jFrom.Name - } - if iFrom.Version != jFrom.Version { - return iFrom.Version < jFrom.Version - } - if iTo.Name != jTo.Name { - return iTo.Name < jTo.Name - } - if iTo.Version != jTo.Version { - return iTo.Version < jTo.Version - } - return i.Type < j.Type + slices.SortStableFunc(rels, cmptest.DefaultRelationshipComparer) } diff --git a/internal/task/package_tasks.go b/internal/task/package_tasks.go index c38299f4dd6..61a43436d4c 100644 --- a/internal/task/package_tasks.go +++ b/internal/task/package_tasks.go @@ -107,7 +107,10 @@ func DefaultPackageTaskFactories() PackageTaskFactories { ), newSimplePackageTaskFactory(ruby.NewGemFileLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "ruby", "gem"), newSimplePackageTaskFactory(ruby.NewGemSpecCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "ruby", "gem", "gemspec"), - newSimplePackageTaskFactory(rust.NewCargoLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "rust", "cargo"), + newPackageTaskFactory( + func(cfg CatalogingFactoryConfig) pkg.Cataloger { + return rust.NewCargoLockCataloger(cfg.PackagesConfig.RustCargoLock) + }, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "rust", "cargo"), newSimplePackageTaskFactory(swift.NewCocoapodsCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "swift", "cocoapods"), newSimplePackageTaskFactory(swift.NewSwiftPackageManagerCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "swift", "spm"), newSimplePackageTaskFactory(swipl.NewSwiplPackCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "swipl", "pack"), diff --git a/syft/cataloging/pkgcataloging/config.go b/syft/cataloging/pkgcataloging/config.go index ac55de7efe5..fc1df2d1093 100644 --- a/syft/cataloging/pkgcataloging/config.go +++ b/syft/cataloging/pkgcataloging/config.go @@ -7,24 +7,27 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/javascript" "github.com/anchore/syft/syft/pkg/cataloger/kernel" "github.com/anchore/syft/syft/pkg/cataloger/python" + "github.com/anchore/syft/syft/pkg/cataloger/rust" ) type Config struct { - Binary binary.ClassifierCatalogerConfig `yaml:"binary" json:"binary" mapstructure:"binary"` - Golang golang.CatalogerConfig `yaml:"golang" json:"golang" mapstructure:"golang"` - JavaArchive java.ArchiveCatalogerConfig `yaml:"java-archive" json:"java-archive" mapstructure:"java-archive"` - JavaScript javascript.CatalogerConfig `yaml:"javascript" json:"javascript" mapstructure:"javascript"` - LinuxKernel kernel.LinuxKernelCatalogerConfig `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"` - Python python.CatalogerConfig `yaml:"python" json:"python" mapstructure:"python"` + Binary binary.ClassifierCatalogerConfig `yaml:"binary" json:"binary" mapstructure:"binary"` + Golang golang.CatalogerConfig `yaml:"golang" json:"golang" mapstructure:"golang"` + RustCargoLock rust.CargoLockCatalogerConfig `yaml:"cargo" json:"cargo" mapstructure:"cargo"` + JavaArchive java.ArchiveCatalogerConfig `yaml:"java-archive" json:"java-archive" mapstructure:"java-archive"` + JavaScript javascript.CatalogerConfig `yaml:"javascript" json:"javascript" mapstructure:"javascript"` + LinuxKernel kernel.LinuxKernelCatalogerConfig `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"` + Python python.CatalogerConfig `yaml:"python" json:"python" mapstructure:"python"` } func DefaultConfig() Config { return Config{ - Binary: binary.DefaultClassifierCatalogerConfig(), - Golang: golang.DefaultCatalogerConfig(), - LinuxKernel: kernel.DefaultLinuxKernelCatalogerConfig(), - Python: python.DefaultCatalogerConfig(), - JavaArchive: java.DefaultArchiveCatalogerConfig(), + Binary: binary.DefaultClassifierCatalogerConfig(), + Golang: golang.DefaultCatalogerConfig(), + RustCargoLock: rust.DefaultCargoLockCatalogerConfig(), + LinuxKernel: kernel.DefaultLinuxKernelCatalogerConfig(), + Python: python.DefaultCatalogerConfig(), + JavaArchive: java.DefaultArchiveCatalogerConfig(), } } @@ -57,3 +60,8 @@ func (c Config) WithJavaArchiveConfig(cfg java.ArchiveCatalogerConfig) Config { c.JavaArchive = cfg return c } + +func (c Config) WithRustCargoLockConfig(cfg rust.CargoLockCatalogerConfig) Config { + c.RustCargoLock = cfg + return c +} diff --git a/syft/format/common/spdxhelpers/to_format_model.go b/syft/format/common/spdxhelpers/to_format_model.go index 3f93883cf1b..06d96c6024f 100644 --- a/syft/format/common/spdxhelpers/to_format_model.go +++ b/syft/format/common/spdxhelpers/to_format_model.go @@ -777,6 +777,14 @@ func newPackageVerificationCode(rels *relationship.Index, p pkg.Package, sbom sb digests = append(digests, d) } + for _, r := range sbom.RelationshipsForPackage(p, artifact.ContainsRelationship) { + if digest, exists := r.Data.(file.Digest); exists { + if digest.Algorithm == "sha1" { + digests = append(digests, digest) + } + } + } + if len(digests) == 0 { return nil } diff --git a/syft/format/internal/cyclonedxutil/helpers/external_references_test.go b/syft/format/internal/cyclonedxutil/helpers/external_references_test.go index 6da98024e7b..19ce274f323 100644 --- a/syft/format/internal/cyclonedxutil/helpers/external_references_test.go +++ b/syft/format/internal/cyclonedxutil/helpers/external_references_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/rust/internal/cargo" ) func Test_encodeExternalReferences(t *testing.T) { @@ -62,7 +63,7 @@ func Test_encodeExternalReferences(t *testing.T) { Language: pkg.Rust, Type: pkg.RustPkg, Licenses: pkg.NewLicenseSet(), - Metadata: pkg.RustCargoLockEntry{ + Metadata: cargo.RustCargoLockEntry{ Name: "ansi_term", Version: "0.12.1", Source: "registry+https://github.com/rust-lang/crates.io-index", diff --git a/syft/format/internal/spdxutil/helpers/description.go b/syft/format/internal/spdxutil/helpers/description.go index 2749d11624f..02e1110d0e5 100644 --- a/syft/format/internal/spdxutil/helpers/description.go +++ b/syft/format/internal/spdxutil/helpers/description.go @@ -1,6 +1,8 @@ package helpers -import "github.com/anchore/syft/syft/pkg" +import ( + "github.com/anchore/syft/syft/pkg" +) func Description(p pkg.Package) string { if hasMetadata(p) { @@ -9,6 +11,10 @@ func Description(p pkg.Package) string { return metadata.Description case pkg.NpmPackage: return metadata.Description + case pkg.RustCargo: + if cargoEntry := metadata.CargoEntry; cargoEntry != nil { + return cargoEntry.Description + } } } return "" diff --git a/syft/format/internal/spdxutil/helpers/download_location.go b/syft/format/internal/spdxutil/helpers/download_location.go index b2bae8f968b..4a691113db0 100644 --- a/syft/format/internal/spdxutil/helpers/download_location.go +++ b/syft/format/internal/spdxutil/helpers/download_location.go @@ -1,6 +1,8 @@ package helpers -import "github.com/anchore/syft/syft/pkg" +import ( + "github.com/anchore/syft/syft/pkg" +) const NONE = "NONE" const NOASSERTION = "NOASSERTION" @@ -22,6 +24,14 @@ func DownloadLocation(p pkg.Package) string { return NoneIfEmpty(metadata.URL) case pkg.NpmPackageLockEntry: return NoneIfEmpty(metadata.Resolved) + case pkg.RustCargo: + if lockEntry := metadata.LockEntry; lockEntry != nil { + url, isRemote := lockEntry.SourceRemoteURL() + if isRemote { + return NoneIfEmpty(url) + } + return NOASSERTION + } case pkg.PhpComposerLockEntry: return NoneIfEmpty(metadata.Dist.URL) case pkg.PhpComposerInstalledEntry: diff --git a/syft/format/internal/spdxutil/helpers/homepage.go b/syft/format/internal/spdxutil/helpers/homepage.go index 61a43e1085f..335b13428f3 100644 --- a/syft/format/internal/spdxutil/helpers/homepage.go +++ b/syft/format/internal/spdxutil/helpers/homepage.go @@ -1,6 +1,8 @@ package helpers -import "github.com/anchore/syft/syft/pkg" +import ( + "github.com/anchore/syft/syft/pkg" +) func Homepage(p pkg.Package) string { if hasMetadata(p) { @@ -9,6 +11,13 @@ func Homepage(p pkg.Package) string { return metadata.Homepage case pkg.NpmPackage: return metadata.Homepage + case pkg.RustCargo: + if cargoEntry := metadata.CargoEntry; cargoEntry != nil { + if cargoEntry.Homepage != "" { + return cargoEntry.Homepage + } + return cargoEntry.Repository + } } } return "" diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go index 67abf1dc502..dcaf701c341 100644 --- a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go +++ b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go @@ -7,6 +7,7 @@ import ( "github.com/anchore/syft/syft/internal/packagemetadata" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/rust/internal/cargo" ) func Test_OriginatorSupplier(t *testing.T) { @@ -38,7 +39,7 @@ func Test_OriginatorSupplier(t *testing.T) { pkg.PythonRequirementsEntry{}, pkg.PythonPoetryLockEntry{}, pkg.RustBinaryAuditEntry{}, - pkg.RustCargoLockEntry{}, + cargo.RustCargoLockEntry{}, pkg.SwiftPackageManagerResolvedEntry{}, pkg.SwiplPackEntry{}, pkg.OpamPackage{}, diff --git a/syft/format/syftjson/model/package_test.go b/syft/format/syftjson/model/package_test.go index 778b7b237ed..5d84540de1a 100644 --- a/syft/format/syftjson/model/package_test.go +++ b/syft/format/syftjson/model/package_test.go @@ -10,6 +10,7 @@ import ( "github.com/anchore/syft/syft/license" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/rust/internal/cargo" ) func Test_UnmarshalJSON(t *testing.T) { @@ -355,7 +356,7 @@ func Test_UnmarshalJSON(t *testing.T) { }`), assert: func(p *Package) { assert.Equal(t, pkg.HackagePkg, p.Type) - assert.Equal(t, reflect.TypeOf(pkg.RustCargoLockEntry{}).Name(), reflect.TypeOf(p.Metadata).Name()) + assert.Equal(t, reflect.TypeOf(cargo.RustCargoLockEntry{}).Name(), reflect.TypeOf(p.Metadata).Name()) }, }, { diff --git a/syft/internal/packagemetadata/generated.go b/syft/internal/packagemetadata/generated.go index 24b96175301..2cdfde2ff8a 100644 --- a/syft/internal/packagemetadata/generated.go +++ b/syft/internal/packagemetadata/generated.go @@ -2,7 +2,9 @@ package packagemetadata -import "github.com/anchore/syft/syft/pkg" +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 { diff --git a/syft/internal/packagemetadata/names_test.go b/syft/internal/packagemetadata/names_test.go index ee73d843740..5d20b8dc22f 100644 --- a/syft/internal/packagemetadata/names_test.go +++ b/syft/internal/packagemetadata/names_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/rust/internal/cargo" ) func TestAllNames(t *testing.T) { @@ -40,22 +41,22 @@ func TestReflectTypeFromJSONName(t *testing.T) { { name: "exact match on ID", lookup: "rust-cargo-lock-entry", - wantRecord: reflect.TypeOf(pkg.RustCargoLockEntry{}), + wantRecord: reflect.TypeOf(cargo.RustCargoLockEntry{}), }, { name: "exact match on former name", lookup: "RustCargoPackageMetadata", - wantRecord: reflect.TypeOf(pkg.RustCargoLockEntry{}), + wantRecord: reflect.TypeOf(cargo.RustCargoLockEntry{}), }, { name: "case insensitive on ID", lookup: "RUST-CARGO-lock-entrY", - wantRecord: reflect.TypeOf(pkg.RustCargoLockEntry{}), + wantRecord: reflect.TypeOf(cargo.RustCargoLockEntry{}), }, { name: "case insensitive on alias", lookup: "rusTcArgopacKagEmEtadATa", - wantRecord: reflect.TypeOf(pkg.RustCargoLockEntry{}), + wantRecord: reflect.TypeOf(cargo.RustCargoLockEntry{}), }, { name: "consistent override", @@ -245,11 +246,11 @@ func TestReflectTypeFromJSONName_LegacyValues(t *testing.T) { expected: reflect.TypeOf(pkg.PhpComposerLockEntry{}), }, { - name: "map pkg.RustCargoLockEntry struct type", + name: "map rust.RustCargoLockEntry struct type", input: "RustCargoPackageMetadata", // this used to be shared as a use case for both RustCargoLockEntry and RustBinaryAuditEntry // neither of these is more correct over the other. - expected: reflect.TypeOf(pkg.RustCargoLockEntry{}), + expected: reflect.TypeOf(cargo.RustCargoLockEntry{}), }, } @@ -475,7 +476,7 @@ func Test_JSONName_JSONLegacyName(t *testing.T) { }, { name: "CargoPackageMetadata", - metadata: pkg.RustCargoLockEntry{}, + metadata: cargo.RustCargoLockEntry{}, expectedJSONName: "rust-cargo-lock-entry", expectedLegacyName: "RustCargoPackageMetadata", // note: maps to multiple entries (v11-12 breaking change) }, diff --git a/syft/pkg/cataloger/alpine/cataloger_test.go b/syft/pkg/cataloger/alpine/cataloger_test.go index 8ac6eaa8ed3..5f1d204feb4 100644 --- a/syft/pkg/cataloger/alpine/cataloger_test.go +++ b/syft/pkg/cataloger/alpine/cataloger_test.go @@ -136,6 +136,10 @@ func TestApkDBCataloger(t *testing.T) { readlinePkg, } + for _, e := range expectedPkgs { + e.SetID() + } + // # apk info --depends bash // bash-5.2.21-r0 depends on: // /bin/sh diff --git a/syft/pkg/cataloger/arch/cataloger_test.go b/syft/pkg/cataloger/arch/cataloger_test.go index 0badcb828fb..072bbef9e7e 100644 --- a/syft/pkg/cataloger/arch/cataloger_test.go +++ b/syft/pkg/cataloger/arch/cataloger_test.go @@ -275,6 +275,10 @@ func TestAlpmCataloger(t *testing.T) { gmpPkg, } + for _, e := range expectedPkgs { + e.SetID() + } + expectedRelationships := []artifact.Relationship{ { // exact spec lookup From: treeSitterPkg, diff --git a/syft/pkg/cataloger/rust/cataloger.go b/syft/pkg/cataloger/rust/cataloger.go index 8951f1b2864..a387be882ea 100644 --- a/syft/pkg/cataloger/rust/cataloger.go +++ b/syft/pkg/cataloger/rust/cataloger.go @@ -7,12 +7,30 @@ import ( "github.com/anchore/syft/internal/mimetype" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/generic" + "github.com/anchore/syft/syft/pkg/cataloger/internal/dependency" ) +type CargoLockCatalogerConfig struct { + // Todo: find a way to replicate cargo's mapping from repository source to their repository dir name + // When that's done we could enable LocalModCacheDir to point to cargo's cache and read directly from there + // SearchLocalModCacheLicenses bool `yaml:"search-local-mod-cache-licenses" json:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"` + // LocalModCacheDir string `yaml:"local-mod-cache-dir" json:"local-mod-cache-dir" mapstructure:"local-mod-cache-dir"` + SearchRemote bool `yaml:"search-remote" json:"search-remote" mapstructure:"search-remote"` +} + +func DefaultCargoLockCatalogerConfig() CargoLockCatalogerConfig { + return CargoLockCatalogerConfig{ + // SearchLocalModCacheLicenses: true, + // LocalModCacheDir: "~/.cargo/registry", + SearchRemote: false, + } +} + // NewCargoLockCataloger returns a new Rust Cargo lock file cataloger object. -func NewCargoLockCataloger() pkg.Cataloger { +func NewCargoLockCataloger(cfg CargoLockCatalogerConfig) pkg.Cataloger { return generic.NewCataloger("rust-cargo-lock-cataloger"). - WithParserByGlobs(parseCargoLock, "**/Cargo.lock") + WithParserByGlobs(newCargoModCataloger(cfg).parseCargoLock, "**/Cargo.lock"). + WithProcessors(dependency.Processor(cargoLockDependencySpecifier)) } // NewAuditBinaryCataloger returns a new Rust auditable binary cataloger object that can detect dependencies diff --git a/syft/pkg/cataloger/rust/cataloger_test.go b/syft/pkg/cataloger/rust/cataloger_test.go index 494b8cfbff5..524b07b00b0 100644 --- a/syft/pkg/cataloger/rust/cataloger_test.go +++ b/syft/pkg/cataloger/rust/cataloger_test.go @@ -68,7 +68,7 @@ func Test_CargoLockCataloger_Globs(t *testing.T) { pkgtest.NewCatalogTester(). FromDirectory(t, test.fixture). ExpectsResolverContentQueries(test.expected). - TestCataloger(t, NewCargoLockCataloger()) + TestCataloger(t, NewCargoLockCataloger(DefaultCargoLockCatalogerConfig())) }) } } diff --git a/syft/pkg/cataloger/rust/dependency.go b/syft/pkg/cataloger/rust/dependency.go new file mode 100644 index 00000000000..7eed5c28b6d --- /dev/null +++ b/syft/pkg/cataloger/rust/dependency.go @@ -0,0 +1,50 @@ +package rust + +import ( + "fmt" + "strings" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/internal/dependency" +) + +func cargoLockDependencySpecifier(p pkg.Package) dependency.Specification { + meta, ok := p.Metadata.(pkg.RustCargo) + if !ok { + log.Tracef("cataloger failed to extract rust cargo metadata for package %+v", p.Name) + return dependency.Specification{} + } + + if meta.LockEntry == nil { + log.Tracef("cataloger failed to extract rust cargo lock entry for package %+v", p.Name) + return dependency.Specification{} + } + + provides := []string{p.Name, p.Name + "@" + p.Version} + + var requires []string + for _, depSpecifier := range meta.LockEntry.Dependencies { + if depSpecifier == "" { + continue + } + requires = append(requires, splitPackageChoice(depSpecifier)...) + } + + return dependency.Specification{ + ProvidesRequires: dependency.ProvidesRequires{ + Provides: provides, + Requires: requires, + }, + } +} + +func splitPackageChoice(s string) (ret []string) { + name, versionString, found := strings.Cut(s, " ") + if found { + ret = append(ret, fmt.Sprintf("%s@%s", name, versionString)) + } else { + ret = append(ret, name) + } + return ret +} diff --git a/syft/pkg/cataloger/rust/internal/cargo/crate_resolver.go b/syft/pkg/cataloger/rust/internal/cargo/crate_resolver.go new file mode 100644 index 00000000000..89823c13a21 --- /dev/null +++ b/syft/pkg/cataloger/rust/internal/cargo/crate_resolver.go @@ -0,0 +1,168 @@ +package cargo + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto/sha1" //nolint:gosec // this is not a security issue since this is only used in the context of comparing hashes. + "crypto/sha256" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/pelletier/go-toml/v2" + + "github.com/anchore/syft/internal/cache" +) + +type CrateInfo struct { + DownloadLink string + DownloadSha string + Licenses []string + CargoToml + PathSha1Hashes map[string]string +} + +type crateResolver struct { + onlineEnabled bool + crateCache cache.Resolver[CrateInfo] + http httpGetter +} + +type CargoToml struct { // nolint:revive + Package TomlPackage `toml:"package"` +} +type TomlPackage struct { + Description string `toml:"description"` + Homepage string `toml:"homepage"` + Repository string `toml:"repository"` + License string `toml:"license"` + LicenseFile string `toml:"license-file"` +} + +func newCrateResolver(onlineEnabled bool) crateResolver { + return crateResolver{ + onlineEnabled: onlineEnabled, + crateCache: cache.GetResolverCachingErrors[CrateInfo]("cargo/crate", "v1"), + http: http.DefaultClient, + } +} + +func (r *crateResolver) resolve(entry LockEntry) (CrateInfo, error) { + if entry.CrateInfo != nil { + return *entry.CrateInfo, nil + } + if !r.onlineEnabled || entry.RegistryInfo == nil { + return CrateInfo{}, nil + } + + return r.crateCache.Resolve( + entry.Source, + sourceAdapter{ + entry: entry, + onlineEnabled: r.onlineEnabled, + http: r.http, + }.fetch) +} + +type sourceAdapter struct { + entry LockEntry + onlineEnabled bool + http httpGetter +} + +func (s sourceAdapter) fetch() (CrateInfo, error) { + if !s.onlineEnabled { + return CrateInfo{}, nil + } + + entry := s.entry + + genDepInfo := CrateInfo{ + PathSha1Hashes: make(map[string]string), + } + + content, link, err := s.fetchCargoArchiveContents() + genDepInfo.DownloadLink = link + if err != nil { + return genDepInfo, err + } + + // TODO chain stream of hasher and gzip reader instead of reading entire contents. + genDepInfo.DownloadSha = fmt.Sprintf("%x", sha256.Sum256(content)) + hashMatchesChecksum := strings.EqualFold(genDepInfo.DownloadSha, entry.Checksum) + if !hashMatchesChecksum { + return genDepInfo, fmt.Errorf("hash of the downloaded Source for crate %s@%s doesn't match the stored checksum. Got %s but expected %s", entry.Name, entry.Version, genDepInfo.DownloadSha, entry.Checksum) + } + + gzReader, err := gzip.NewReader(bytes.NewReader(content)) + if err != nil { + return genDepInfo, err + } + defer func(reader *gzip.Reader) { + _ = reader.Close() + }(gzReader) + tarReader := tar.NewReader(gzReader) + + for { + next, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return genDepInfo, fmt.Errorf("unable to read tar file for rust cargo package %s@%s: %w", entry.Name, entry.Version, err) + } + + // TODO: stream this, don't store it all in memory + c, err := io.ReadAll(tarReader) + if err != nil { + return genDepInfo, err + } + + genDepInfo.PathSha1Hashes[next.Name] = fmt.Sprintf("%x", sha1.Sum(c)) //nolint:gosec // this is not a security issue since this is only used in the context of comparing hashes. + + if next.Name == entry.Name+"-"+entry.Version+"/Cargo.toml" { + var cargoToml CargoToml + err = toml.Unmarshal(c, &cargoToml) + if err != nil { + return genDepInfo, err + } + + genDepInfo.CargoToml = cargoToml + genDepInfo.Licenses = append(genDepInfo.Licenses, cargoToml.Package.License) + } + } + return genDepInfo, nil +} + +func (s sourceAdapter) fetchCargoArchiveContents() ([]byte, string, error) { + var content []byte + var err error + var link, isLocal = s.entry.cargoArchiveDownloadLink() + if link == "" { + return content, link, nil + } + + if !isLocal { + var resp *http.Response + resp, err = s.http.Get(link) //nolint:gosec // this is required functionality + if err != nil { + return content, link, err + } + + // TODO stream contents instead of reading all of it + content, err = io.ReadAll(resp.Body) + _ = resp.Body.Close() + if err != nil { + return content, link, err + } + } else { + content, err = os.ReadFile(link) + if err != nil { + return content, link, err + } + } + return content, link, nil +} diff --git a/syft/pkg/cataloger/rust/internal/cargo/lock_entry.go b/syft/pkg/cataloger/rust/internal/cargo/lock_entry.go new file mode 100644 index 00000000000..d967a1f2aca --- /dev/null +++ b/syft/pkg/cataloger/rust/internal/cargo/lock_entry.go @@ -0,0 +1,171 @@ +package cargo + +import ( + "fmt" + "strings" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/pkg" +) + +// For JSON naming purposes, it is important, that the name stays the same here! + +type LockEntry struct { + pkg.RustCargoLockEntry + *RegistryInfo + *CrateInfo + Licenses pkg.LicenseSet +} + +type LockEntryHydrator struct { + crate crateResolver + registry registryResolver +} + +func NewLockEntryHydrator(onlineEnabled bool) LockEntryHydrator { + return LockEntryHydrator{ + crate: newCrateResolver(onlineEnabled), + registry: newRegistryResolver(onlineEnabled), + } +} + +func (r *LockEntryHydrator) hydrateLockEntry(entry *LockEntry, lockVersion int) error { // nolint:unparam // known-unknowns will need to return an error + entry.Licenses = pkg.NewLicenseSet() + entry.RustCargoLockEntry.CargoLockVersion = lockVersion + + reg, err := r.registry.resolve(*entry) + if err == nil { + entry.RegistryInfo = ® + } + + cra, err := r.crate.resolve(*entry) + + if err == nil { + entry.CrateInfo = &cra + for _, license := range cra.Licenses { + if entry.CrateInfo.DownloadLink != "" && license != "" { + entry.Licenses.Add(pkg.NewLicenseFromURLs(license, entry.CrateInfo.DownloadLink)) + } + } + } else { + // TODO: return known-unknown error + log.WithFields("pkg", fmt.Sprintf("%s@%s", entry.Name, entry.Version)).Tracef("unable to resolve rust cargo lock info remotely: %s", err) + } + + return nil +} + +func (r *LockEntry) sourceID() *sourceID { + if r.Source == "" { + //Todo: add handling for looking in the current workspace, finding all Cargo.toml's and checking if any matches. + // if a match is found license information could potentially still be added. + // In that scenario adding "path" or "directory" support might make sense. + return nil + } + var before, after, found = strings.Cut(r.Source, "+") + if !found { + return nil + } + + return &sourceID{ + kind: before, + url: after, + } +} + +func (r *LockEntry) cargoArchiveDownloadLink() (string, bool) { + if r.RegistryInfo == nil { + return "", false + } + + url := r.RegistryInfo.Download + + if !strings.Contains(url, crate) && + !strings.Contains(url, version) && + !strings.Contains(url, prefix) && + !strings.Contains(url, lowerPrefix) && + !strings.Contains(url, sha256Checksum) { + return fmt.Sprintf("%s/%s/%s/download", url, r.Name, r.Version), r.RegistryInfo.IsLocalFile + } + + // TODO: can we simply craft the URL instead of replacing placeholders? + var link = url + link = strings.ReplaceAll(link, crate, r.Name) + link = strings.ReplaceAll(link, version, r.Version) + link = strings.ReplaceAll(link, prefix, r.getPrefix()) + link = strings.ReplaceAll(link, lowerPrefix, strings.ToLower(r.getPrefix())) + link = strings.ReplaceAll(link, sha256Checksum, r.Checksum) + return link, r.RegistryInfo.IsLocalFile +} + +// getPrefix get {path} for https://doc.rust-lang.org/cargo/reference/registry-index.html +func (r *LockEntry) getPrefix() string { + switch len(r.Name) { + case 0: + return "" + case 1: + return fmt.Sprintf("1/%s", r.Name[0:1]) + case 2: + return fmt.Sprintf("2/%s", r.Name[0:2]) + case 3: + return fmt.Sprintf("3/%s", r.Name[0:1]) + default: + return fmt.Sprintf("%s/%s", r.Name[0:2], r.Name[2:4]) + } +} + +// Todo: Do we care about any metadata present in the rust repository index? +// +// type DependencyInformation struct { +// Name string `json:"name"` +// Version string `json:"vers"` +// Dependencies []DependencyDependencyInformation `json:"deps"` +// Checksum string `json:"cksum"` +// Features map[string]string `json:"features"` +// Yanked bool `json:"yanked"` +// Links string `json:"links"` +// StructVersion int `json:"v"` +// Features2 map[string]string `json:"features2"` +// RustVersion string `json:"rust_version"` +//} +// type DependencyDependencyInformation struct { +// Name string `json:"name"` +// Requirement string `json:"req"` +// Features []string `json:"features"` +// Optional bool `json:"optional"` +// DefaultTargets bool `json:"default_targets"` +// Target string `json:"target"` +// Kind string `json:"kind"` +// Registry string `json:"registry"` +// Package string `json:"package"` +//} +// +// func (r *LockEntry) getIndexPath() string { +// return fmt.Sprintf("%s/%s", strings.ToLower(r.getPrefix()), strings.ToLower(r.Name)) +// } +// +// func (r *LockEntry) getIndexContent() ([]DependencyInformation, []error) { +// var deps []DependencyInformation +// var sourceID, err = r.sourceID() +// if err != nil { +// return deps, []error{err} +// } +// var content []byte +// var errors []error +// content, err = sourceID.GetPath(r.getIndexPath()) +// if err != nil { +// return deps, []error{err} +// } +// for _, v := range bytes.Split(content, []byte("\n")) { +// var depInfo = DependencyInformation{ +// StructVersion: 1, +// } +// err = json.Unmarshal(v, &depInfo) +// if err == nil { +// deps = append(deps, depInfo) +// } else { +// errors = append(errors, err) +// } +// } +// return deps, errors +// } diff --git a/syft/pkg/cataloger/rust/internal/cargo/lock_file.go b/syft/pkg/cataloger/rust/internal/cargo/lock_file.go new file mode 100644 index 00000000000..9dd45d7476d --- /dev/null +++ b/syft/pkg/cataloger/rust/internal/cargo/lock_file.go @@ -0,0 +1,28 @@ +package cargo + +import ( + "fmt" + "io" + + "github.com/pelletier/go-toml/v2" +) + +type LockFile struct { + CargoLockVersion int `toml:"version"` + Packages []LockEntry `toml:"package"` +} + +func ParseLockToml(reader io.Reader, entryFactory LockEntryHydrator) (*LockFile, error) { + m := LockFile{} + err := toml.NewDecoder(reader).Decode(&m) + if err != nil { + return nil, fmt.Errorf("unable to load or parse Cargo.lock: %w", err) + } + + for i := range m.Packages { + if err := entryFactory.hydrateLockEntry(&m.Packages[i], m.CargoLockVersion); err != nil { + return nil, err + } + } + return &m, nil +} diff --git a/syft/pkg/cataloger/rust/internal/cargo/registry_resolver.go b/syft/pkg/cataloger/rust/internal/cargo/registry_resolver.go new file mode 100644 index 00000000000..2ab3b74dc32 --- /dev/null +++ b/syft/pkg/cataloger/rust/internal/cargo/registry_resolver.go @@ -0,0 +1,281 @@ +package cargo + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport" + "github.com/go-git/go-git/v5/storage/memory" + + "github.com/anchore/syft/internal/cache" + "github.com/anchore/syft/internal/log" +) + +// see https://github.com/rust-lang/cargo/blob/master/crates/cargo-util-schemas/src/core/source_kind.rs +const ( + sourceKindPath = "path" + sourceKindGit = "git" + sourceKindRegistry = "registry" + sourceKindLocalRegistry = "local-registry" + sourceKindSparse = "sparse" + sourceKindLocalDirectory = "directory" + + crate = "{crate}" + version = "{version}" + prefix = "{prefix}" + lowerPrefix = "{lowerprefix}" + sha256Checksum = "{sha256-checksum}" + + // registryConfigName see https://github.com/rust-lang/cargo/blob/b134eff5cedcaa4879f60035d62630400e7fd543/src/cargo/sources/registry/mod.rs#L962 + registryConfigName = "config.json" +) + +type RegistryInfo struct { + IsLocalFile bool + RepositoryConfig +} + +type RepositoryConfig struct { + Download string `json:"dl"` +} + +type registryResolver struct { + onlineEnabled bool + registryGitRepoObjects map[string]*memory.Storage + registryCache cache.Resolver[RegistryInfo] + http httpGetter +} + +type sourceID struct { + kind string + url string +} + +func newRegistryResolver(onlineEnabled bool) registryResolver { + return registryResolver{ + onlineEnabled: onlineEnabled, + registryGitRepoObjects: make(map[string]*memory.Storage), + registryCache: cache.GetResolverCachingErrors[RegistryInfo]("cargo/registry", "v1"), + http: http.DefaultClient, + } +} + +func (r *registryResolver) resolve(entry LockEntry) (RegistryInfo, error) { + if entry.RegistryInfo != nil { + return *entry.RegistryInfo, nil + } + + if !r.onlineEnabled { + return RegistryInfo{}, nil + } + + return r.registryCache.Resolve(entry.Source, registryAdapter{ + entry: entry, + registryGitRepoObjects: r.registryGitRepoObjects, + onlineEnabled: r.onlineEnabled, + http: r.http, + }.fetch) +} + +type registryAdapter struct { + entry LockEntry + registryGitRepoObjects map[string]*memory.Storage + onlineEnabled bool + http httpGetter +} + +type httpGetter interface { + Get(url string) (*http.Response, error) +} + +func (r registryAdapter) fetch() (RegistryInfo, error) { + if !r.onlineEnabled { + return RegistryInfo{}, nil + } + + sID := r.entry.sourceID() + if sID == nil { + return RegistryInfo{}, nil + } + + repoConfig, err := r.getRegistryConfig(*sID) + if err != nil { + return RegistryInfo{}, err + } + + return RegistryInfo{ + IsLocalFile: sID.kind == sourceKindLocalRegistry, + RepositoryConfig: *repoConfig, + }, nil +} + +func (r registryAdapter) getRegistryConfig(i sourceID) (*RepositoryConfig, error) { + if i.kind == sourceKindLocalRegistry { + // see https://github.com/rust-lang/cargo/blob/b134eff5cedcaa4879f60035d62630400e7fd543/src/cargo/sources/registry/local.rs#L14-L57 + return &RepositoryConfig{ + Download: fmt.Sprintf("%s/%s-%s.crate", i.url, crate, version), + }, nil + } + + content, err := r.fetchRegistryConfigContents(i) + if err != nil { + return nil, err + } + + var repoConfig RepositoryConfig + err = json.Unmarshal(content, &repoConfig) + + log.WithFields("url", i.url, "kind", i.kind, "repo-dl", repoConfig.Download, "error", err).Tracef("rust cargo repo config: %s", string(content)) + + if err != nil { + err = fmt.Errorf("failed to deserialize rust repository configuration: %w", err) + } + return &repoConfig, err +} + +func (r registryAdapter) fetchRegistryConfigContents(i sourceID) ([]byte, error) { + path := registryConfigName + var content []byte + switch i.kind { + case sourceKindLocalRegistry: + return nil, nil + + // if path == registryConfigName { + // return nil, nil + //} + + // TODO: when would this ever be true? + // return os.ReadFile(fmt.Sprintf("%s/index/%s", i.url, path)) + + case sourceKindSparse: + if !r.onlineEnabled { + return nil, nil + } + resp, err := r.http.Get(fmt.Sprintf("%s/%s", i.url, path)) + if err != nil { + return content, fmt.Errorf("could not get the path %s/%s from sparse registry: %w", i.url, path, err) + } + + content, err = io.ReadAll(resp.Body) + _ = resp.Body.Close() + if err != nil { + err = fmt.Errorf("failed to get contents of response %s: %w", path, err) + } + + return content, err + + case sourceKindRegistry: + if !r.onlineEnabled { + return nil, nil + } + + _, repo, err := r.getOrInitRepo(i.url) + if err != nil { + return content, err + } + tree, err := getTree(repo) + if err != nil { + return content, err + } + file, err := tree.File(path) + if err != nil { + return content, fmt.Errorf("failed to find path %s in tree: %w", path, err) + } + reader, err := file.Reader() + if err != nil { + return content, fmt.Errorf("failed to get reader for file %s: %w", path, err) + } + content, err = io.ReadAll(reader) + if err != nil { + return content, fmt.Errorf("failed to get contents of file %s: %w", path, err) + } + + return content, err + } + return content, fmt.Errorf("unsupported Remote") +} + +func (r registryAdapter) getOrInitRepo(url string) (*memory.Storage, *git.Repository, error) { + var repo *git.Repository + var err error + + // Todo: Should we use an on-disk storage? + var storage, ok = r.registryGitRepoObjects[url] + if !ok { + storage = memory.NewStorage() + r.registryGitRepoObjects[url] = storage + repo, err = git.Init(storage, memfs.New()) + if err != nil { + return storage, nil, fmt.Errorf("unable to initialise repo: %w", err) + } + err = updateRepo(repo, url) + if err != nil { + err = fmt.Errorf("unable to fetch registry information: %w", err) + } + } else { + repo, err = git.Open(storage, memfs.New()) + if err != nil { + err = fmt.Errorf("unable to open repository: %w", err) + } + } + + return storage, repo, err +} + +func updateRepo(repo *git.Repository, url string) error { + // Todo: cargo re-initialises the repo, if the fetch fails. Do we need to copy that? + // see https://github.com/rust-lang/cargo/blob/b134eff5cedcaa4879f60035d62630400e7fd543/src/cargo/sources/git/utils.rs#L1150 + remote, err := repo.CreateRemoteAnonymous(&config.RemoteConfig{ + Name: "anonymous", + URLs: []string{url}, + Mirror: false, + // see https://github.com/rust-lang/cargo/blob/b134eff5cedcaa4879f60035d62630400e7fd543/src/cargo/sources/git/utils.rs#L979 + Fetch: []config.RefSpec{"+HEAD:refs/remotes/origin/HEAD"}, + }) + if err != nil { + return fmt.Errorf("failed to create anonymous remote for url %s: %w", url, err) + } + err = remote.Fetch(&git.FetchOptions{ + RemoteName: "origin", + Depth: 1, + // Todo: support private repos by allowing auth information to be specified + Auth: nil, + Progress: nil, + Tags: git.NoTags, + Force: false, + InsecureSkipTLS: false, + CABundle: nil, + ProxyOptions: transport.ProxyOptions{}, + Prune: false, + }) + if err != nil { + return fmt.Errorf("failed to fetch registry information from url %s: %w", url, err) + } + return err +} + +func getTree(repo *git.Repository) (*object.Tree, error) { + ref, err := repo.Reference("refs/remotes/origin/HEAD", true) + if err != nil { + return nil, fmt.Errorf("failed to get reference to refs/remotes/origin/HEAD: %w", err) + } + + var hash = ref.Hash() + commit, err := repo.CommitObject(hash) + if err != nil { + return nil, fmt.Errorf("failed to get commit from repo head: %w", err) + } + + tree, err := commit.Tree() + if err != nil { + return nil, fmt.Errorf("failed to get Tree from Commit: %w", err) + } + + return tree, err +} diff --git a/syft/pkg/cataloger/rust/package.go b/syft/pkg/cataloger/rust/package.go index be67d96a936..2e4fd757c7f 100644 --- a/syft/pkg/cataloger/rust/package.go +++ b/syft/pkg/cataloger/rust/package.go @@ -1,23 +1,41 @@ package rust import ( + "fmt" + "github.com/microsoft/go-rustaudit" "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/rust/internal/cargo" ) // Pkg returns the standard `pkg.Package` representation of the package referenced within the Cargo.lock metadata. -func newPackageFromCargoMetadata(m pkg.RustCargoLockEntry, locations ...file.Location) pkg.Package { +func newPackageFromCargoMetadata(m cargo.LockEntry, locations ...file.Location) pkg.Package { + var cargoEntry *pkg.RustCargoEntry + if m.CrateInfo != nil { + cargoEntry = &pkg.RustCargoEntry{ + DownloadURL: m.CrateInfo.DownloadLink, + DownloadDigest: fmt.Sprintf("%x", m.CrateInfo.DownloadSha), + Description: m.CrateInfo.CargoToml.Package.Description, + Homepage: m.CrateInfo.CargoToml.Package.Homepage, + Repository: m.CrateInfo.CargoToml.Package.Repository, + } + } + p := pkg.Package{ Name: m.Name, Version: m.Version, Locations: file.NewLocationSet(locations...), + Licenses: m.Licenses, PURL: packageURL(m.Name, m.Version), Language: pkg.Rust, Type: pkg.RustPkg, - Metadata: m, + Metadata: pkg.RustCargo{ + CargoEntry: cargoEntry, + LockEntry: &m.RustCargoLockEntry, + }, } p.SetID() diff --git a/syft/pkg/cataloger/rust/parse_cargo_lock.go b/syft/pkg/cataloger/rust/parse_cargo_lock.go index d1ef3db1261..56d01a472cb 100644 --- a/syft/pkg/cataloger/rust/parse_cargo_lock.go +++ b/syft/pkg/cataloger/rust/parse_cargo_lock.go @@ -2,49 +2,66 @@ package rust import ( "context" - "fmt" - - "github.com/pelletier/go-toml" "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" + "github.com/anchore/syft/syft/pkg/cataloger/rust/internal/cargo" ) -var _ generic.Parser = parseCargoLock - -type cargoLockFile struct { - Packages []pkg.RustCargoLockEntry `toml:"package"` +type cargoModCataloger struct { + config CargoLockCatalogerConfig + lockEntryHydrator cargo.LockEntryHydrator } -// parseCargoLock is a parser function for Cargo.lock contents, returning all rust cargo crates discovered. -func parseCargoLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { - tree, err := toml.LoadReader(reader) - if err != nil { - return nil, nil, fmt.Errorf("unable to load Cargo.lock for parsing: %w", err) +func newCargoModCataloger(config CargoLockCatalogerConfig) *cargoModCataloger { + return &cargoModCataloger{ + config: config, + lockEntryHydrator: cargo.NewLockEntryHydrator(config.SearchRemote), } +} - m := cargoLockFile{} - err = tree.Unmarshal(&m) +// parseCargoLock is a parser function for Cargo.lock contents, returning all rust cargo crates discovered. +func (c cargoModCataloger) parseCargoLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + m, err := cargo.ParseLockToml(reader, c.lockEntryHydrator) if err != nil { - return nil, nil, fmt.Errorf("unable to parse Cargo.lock: %w", err) + return nil, nil, err } var pkgs []pkg.Package + var relationships []artifact.Relationship for _, p := range m.Packages { - if p.Dependencies == nil { - p.Dependencies = make([]string, 0) - } - pkgs = append( - pkgs, - newPackageFromCargoMetadata( - p, - reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), - ), + spkg := newPackageFromCargoMetadata( + p, + reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), ) + + // relationships = append(relationships, populatePackageContainsRelationships(spkg, p.CrateInfo)...) + + pkgs = append(pkgs, spkg) } - return pkgs, nil, nil + return pkgs, relationships, nil } + +// TODO: this is fundamentally breaking the assumptions of what a file coordinate is, which is always relative to the artifact in some way +// +// func populatePackageContainsRelationships(p pkg.Package, gen *cargo.CrateInfo) (relationships []artifact.Relationship) { +// if gen == nil { +// return nil +// } +// for path, h := range gen.PathSha1Hashes { +// relationships = append(relationships, artifact.Relationship{ +// From: p, +// To: file.NewCoordinates(path, gen.DownloadLink), +// Type: artifact.ContainsRelationship, +// Data: file.Digest{ +// Algorithm: "sha1", +// Value: strings.ToLower(hex.EncodeToString(h[:])), +// }, +// }) +// } +// return relationships +// } diff --git a/syft/pkg/cataloger/rust/parse_cargo_lock_test.go b/syft/pkg/cataloger/rust/parse_cargo_lock_test.go index 6e12fb809ee..736ca76a3ca 100644 --- a/syft/pkg/cataloger/rust/parse_cargo_lock_test.go +++ b/syft/pkg/cataloger/rust/parse_cargo_lock_test.go @@ -1,193 +1,463 @@ package rust import ( + "fmt" "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" + "github.com/anchore/syft/syft/pkg/cataloger/rust/internal/cargo" ) -func TestParseCargoLock(t *testing.T) { - fixture := "test-fixtures/Cargo.lock" - locations := file.NewLocationSet(file.NewLocation(fixture)) - expectedPkgs := []pkg.Package{ - { - Name: "ansi_term", - Version: "0.12.1", - PURL: "pkg:cargo/ansi_term@0.12.1", +type registryLink string + +const ( + officialRegistry registryLink = "registry+https://github.com/rust-lang/crates.io-index" + officialSparse registryLink = "sparse+https://index.crates.io" +) + +type packageInfo struct { + pkg.Package + RustMeta cargo.LockEntry + CoordinatePathPrepend string +} + +func newPackage( + t *testing.T, + name string, + version string, + locations file.LocationSet, + toml cargo.CargoToml, + registry registryLink, + checksum string, + dependencies []string, + pathSha1Hashes map[string]string, //#nosec G505 G401 -- sha1 is used as a required hash function for SPDX, not a crypto function +) packageInfo { + t.Helper() + + crateInfo := cargo.CrateInfo{ + DownloadLink: fmt.Sprintf("https://static.crates.io/crates/%s/%s/download", name, version), + DownloadSha: checksum, + Licenses: []string{toml.Package.License}, + CargoToml: toml, + PathSha1Hashes: pathSha1Hashes, + } + + lockEntry := cargo.LockEntry{ + RustCargoLockEntry: pkg.RustCargoLockEntry{ + Name: name, + Version: version, + Source: string(registry), + Checksum: checksum, + Dependencies: dependencies, + }, + RegistryInfo: &cargo.RegistryInfo{ + IsLocalFile: false, + RepositoryConfig: cargo.RepositoryConfig{ + Download: "https://static.crates.io/crates", + }, + }, + CrateInfo: &crateInfo, + } + return packageInfo{ + Package: pkg.Package{ + Name: name, + Version: version, + PURL: fmt.Sprintf("pkg:cargo/%s@%s", name, version), Locations: locations, Language: pkg.Rust, Type: pkg.RustPkg, - Licenses: pkg.NewLicenseSet(), - Metadata: pkg.RustCargoLockEntry{ - Name: "ansi_term", - Version: "0.12.1", - Source: "registry+https://github.com/rust-lang/crates.io-index", - Checksum: "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2", - Dependencies: []string{ - "winapi", + Licenses: pkg.NewLicenseSet(pkg.NewLicense(toml.Package.License)), + Metadata: pkg.RustCargo{ + CargoEntry: &pkg.RustCargoEntry{ + DownloadURL: crateInfo.DownloadLink, + DownloadDigest: checksum, + Description: crateInfo.CargoToml.Package.Description, + Homepage: crateInfo.CargoToml.Package.Homepage, + Repository: crateInfo.CargoToml.Package.Repository, }, + LockEntry: &lockEntry.RustCargoLockEntry, }, }, - { - Name: "matches", - Version: "0.1.8", - PURL: "pkg:cargo/matches@0.1.8", - Locations: locations, - Language: pkg.Rust, - Type: pkg.RustPkg, - Licenses: pkg.NewLicenseSet(), - Metadata: pkg.RustCargoLockEntry{ - Name: "matches", - Version: "0.1.8", - Source: "registry+https://github.com/rust-lang/crates.io-index", - Checksum: "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08", - Dependencies: []string{}, + RustMeta: lockEntry, + CoordinatePathPrepend: fmt.Sprintf("%s-%s/", name, version), + } +} + +// The dependencies in this test are not correct. +// They have been altered in a consistent way, to avoid having an excessive amount of relations. +func TestParseCargoLock(t *testing.T) { + fixture := "test-fixtures/Cargo.lock" + locations := file.NewLocationSet(file.NewLocation(fixture)) + + ansiTerm := newPackage( + t, + "ansi_term", + "0.12.1", + locations, + cargo.CargoToml{ + Package: cargo.TomlPackage{ + Description: "Library for ANSI terminal colours and styles (bold, underline)", + Homepage: "https://github.com/ogham/rust-ansi-term", + Repository: "https://github.com/ogham/rust-ansi-term", + License: "MIT", + LicenseFile: "", }, }, - { - Name: "memchr", - Version: "2.3.3", - PURL: "pkg:cargo/memchr@2.3.3", - Locations: locations, - Language: pkg.Rust, - Type: pkg.RustPkg, - Licenses: pkg.NewLicenseSet(), - Metadata: pkg.RustCargoLockEntry{ - Name: "memchr", - Version: "2.3.3", - Source: "registry+https://github.com/rust-lang/crates.io-index", - Checksum: "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400", - Dependencies: []string{}, + officialRegistry, + "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2", + nil, // see comment at the head of the function + map[string]string{ + "ansi_term-0.12.1/.appveyor.yml": "c42d6a3f7e5034faa6575ffb3fbdbbdff1c7ae36", + "ansi_term-0.12.1/.cargo_vcs_info.json": "216d8c4b73c5c920e50b9381799fabeeb6db9e2b", + "ansi_term-0.12.1/.gitignore": "a61a3e6e96c70bfd3e7317e273e4bc73966cd206", + "ansi_term-0.12.1/.rustfmt.toml": "c5764722079c8d29355b51c529d33aa987308d96", + "ansi_term-0.12.1/.travis.yml": "87b6300a2c64fd5c277b239ccf3a197ac93330a9", + "ansi_term-0.12.1/Cargo.lock": "4fe4f31ecf5587749ef36a0d737520505e2b738a", + "ansi_term-0.12.1/Cargo.toml": "0293cdba284ead161e8bb22810df81da7e0d4d46", + "ansi_term-0.12.1/Cargo.toml.orig": "028652d42e04c077101de1915442bc91b969b0b8", + "ansi_term-0.12.1/LICENCE": "7293920aac55f4d275cef83ba10d706585622a53", + "ansi_term-0.12.1/README.md": "0256097d83afe02e629b924538e64daa5cc96cfc", + "ansi_term-0.12.1/examples/256_colours.rs": "69e1803a4e8ceb7b9ac824105e132e36dac5f83d", + "ansi_term-0.12.1/examples/basic_colours.rs": "1b012d37d1821eb962781e3b70f8a1049568a684", + "ansi_term-0.12.1/examples/rgb_colours.rs": "fe54d6382de09f91056cd0f0e23ee0cf08f4a465", + "ansi_term-0.12.1/src/ansi.rs": "a44febd838c3c6a083ad7855f8f256120f5910e5", + "ansi_term-0.12.1/src/debug.rs": "9a144eac569faadf3476394b6ccb14c535d5a4b3", + "ansi_term-0.12.1/src/difference.rs": "b27f3d41bbaa70b427a6be965b203d14b02b461f", + "ansi_term-0.12.1/src/display.rs": "0c0a49ac7f10fed51312844f0736d9b27b21e289", + "ansi_term-0.12.1/src/lib.rs": "685f66c3d2fd0487dead77764d8d4a1d882aad38", + "ansi_term-0.12.1/src/style.rs": "30e0f9157760b374caff3ebcdcb0b932115fc49f", + "ansi_term-0.12.1/src/util.rs": "ec085dabb9f7103ecf9c3c150d1f57cf33a4c6eb", + "ansi_term-0.12.1/src/windows.rs": "a2271341a4248916eebaf907b27be2170c12d45c", + "ansi_term-0.12.1/src/write.rs": "ac7f435f78ef8c2ed733573c62c428c7a9794038", + }, + ) + matches := newPackage( + t, + "matches", + "0.1.8", + locations, + cargo.CargoToml{ + Package: cargo.TomlPackage{ + Description: "A macro to evaluate, as a boolean, whether an expression matches a pattern.", + Homepage: "", + Repository: "https://github.com/SimonSapin/rust-std-candidates", + License: "MIT", + LicenseFile: "", }, }, - { - Name: "natord", - Version: "1.0.9", - PURL: "pkg:cargo/natord@1.0.9", - Locations: locations, - Language: pkg.Rust, - Type: pkg.RustPkg, - Licenses: pkg.NewLicenseSet(), - Metadata: pkg.RustCargoLockEntry{ - Name: "natord", - Version: "1.0.9", - Source: "registry+https://github.com/rust-lang/crates.io-index", - Checksum: "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c", - Dependencies: []string{}, + officialSparse, + "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08", + nil, + map[string]string{ + "matches-0.1.8/Cargo.toml": "ae580adb71d8a07fe5865cd9951bc6886ab3e3a4", + "matches-0.1.8/Cargo.toml.orig": "818c35d1d78008a9d8e2e7b33bb316eea02d7711", + "matches-0.1.8/LICENSE": "1b0e913d41a66c988376898aa995d6c2f45bb50c", + "matches-0.1.8/lib.rs": "c6329ef2162b8b59dd2bcda7402151c47a7cf99f", + "matches-0.1.8/tests/macro_use_one.rs": "faad095b6182c15929020d79581661f1a331daa3", + }, + ) + memchr := newPackage( + t, + "memchr", + "2.3.3", + locations, + cargo.CargoToml{ + Package: cargo.TomlPackage{ + Description: "Safe interface to memchr.", + Homepage: "https://github.com/BurntSushi/rust-memchr", + Repository: "https://github.com/BurntSushi/rust-memchr", + License: "Unlicense/MIT", + LicenseFile: "", }, }, - { - Name: "nom", - Version: "4.2.3", - PURL: "pkg:cargo/nom@4.2.3", - Locations: locations, - Language: pkg.Rust, - Type: pkg.RustPkg, - Licenses: pkg.NewLicenseSet(), - Metadata: pkg.RustCargoLockEntry{ - Name: "nom", - Version: "4.2.3", - Source: "registry+https://github.com/rust-lang/crates.io-index", - Checksum: "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6", - Dependencies: []string{ - "memchr", - "version_check", - }, + officialRegistry, + "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400", + nil, + map[string]string{ + "memchr-2.3.3/.cargo_vcs_info.json": "b1ecb8751a0d53cccb6be606ede175736d51da04", + "memchr-2.3.3/.github/workflows/ci.yml": "987372cc6a27668c02b8e9a9fe68e767cd4c658c", + "memchr-2.3.3/.gitignore": "556d32e5cb6dfbdbfc67e2dd06f948b76fe8b9d3", + "memchr-2.3.3/.ignore": "e48305d030f8aebbe1a89fcb84f4ac19bb073975", + "memchr-2.3.3/COPYING": "dd445710e6e4caccc4f8a587a130eaeebe83f6f6", + "memchr-2.3.3/Cargo.toml": "395d46c2216bc3a5092b37c3dc7516566467dfd4", + "memchr-2.3.3/Cargo.toml.orig": "fe0739eacb9577f22fa4cd9c35096c2ac11ead76", + "memchr-2.3.3/LICENSE-MIT": "4c8990add9180fc59efa5b0d8faf643c9709501e", + "memchr-2.3.3/README.md": "802d3bfea6ff17d5f082ecceb511913021390699", + "memchr-2.3.3/UNLICENSE": "ff007ce11f3ff7964f1a5b04202c4e95b5c82c85", + "memchr-2.3.3/build.rs": "59fca6951275d4feba14c07109c4bb351da187d5", + "memchr-2.3.3/rustfmt.toml": "558a7c72e415544f0b8790cd8c752690d0bc05c6", + "memchr-2.3.3/src/c.rs": "c75095493e42affe48a23e7de9c77c95ec139c7c", + "memchr-2.3.3/src/fallback.rs": "7f13c3502f300646a24e58172d99d78033e339b2", + "memchr-2.3.3/src/iter.rs": "b01cc89987d9c2f61baa97f7465ca2b81ce80b52", + "memchr-2.3.3/src/lib.rs": "35fac0bea520bfeb99197cfd97056bed99582ce2", + "memchr-2.3.3/src/naive.rs": "fcb709375bf7a20ddd97388982050d5d5da5f15f", + "memchr-2.3.3/src/tests/iter.rs": "daaf6a0b800563deb45227b2e7fb6fdae464ae84", + "memchr-2.3.3/src/tests/memchr.rs": "37f44dc29c8efb1d19eea6f2924a19ba86c14b3b", + "memchr-2.3.3/src/tests/miri.rs": "c6569d55c18255a52f5a75256f95167101d9dbeb", + "memchr-2.3.3/src/tests/mod.rs": "cdd9c0085ceccf76090bc327840e6a9315499acc", + "memchr-2.3.3/src/x86/avx.rs": "4bc56ed4faa1b026b399c169790a678b6af6a941", + "memchr-2.3.3/src/x86/mod.rs": "be8644c7bad1427b23436e6d5992c16e5129c216", + "memchr-2.3.3/src/x86/sse2.rs": "d2b640c77a0223812fa6a6f550e61ff4269320f0", + "memchr-2.3.3/src/x86/sse42.rs": "f053482427712918edf50aea0cb7e2fb95a2ccc1", + }, + ) + natord := newPackage( + t, + "natord", + "1.0.9", + locations, + cargo.CargoToml{ + Package: cargo.TomlPackage{ + Description: "Natural ordering for Rust", + Homepage: "https://github.com/lifthrasiir/rust-natord", + Repository: "https://github.com/lifthrasiir/rust-natord", + License: "MIT", + LicenseFile: "", }, }, - { - Name: "unicode-bidi", - Version: "0.3.4", - PURL: "pkg:cargo/unicode-bidi@0.3.4", - Locations: locations, - Language: pkg.Rust, - Type: pkg.RustPkg, - Licenses: pkg.NewLicenseSet(), - Metadata: pkg.RustCargoLockEntry{ - Name: "unicode-bidi", - Version: "0.3.4", - Source: "registry+https://github.com/rust-lang/crates.io-index", - Checksum: "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5", - Dependencies: []string{ - "matches", - }, + officialSparse, + "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c", + nil, + map[string]string{ + "natord-1.0.9/.gitignore": "3254b5d5538166f1fd5a0bb41f7f3d3bbd455c56", + "natord-1.0.9/.travis.yml": "4eadee39324e1cc0e156d4c1632fc417f9ed8a7e", + "natord-1.0.9/Cargo.toml": "bffdbe1b6b2576ae1b17d4693545aa0145b435be", + "natord-1.0.9/LICENSE.txt": "bf18c5cc6c1db93eb4e2e95b11352e4660408fec", + "natord-1.0.9/README.md": "c2958854fdc10329e409906292506d6e24dd78b5", + "natord-1.0.9/lib.rs": "83320272b3d922f5bed408d04fb18954c34958b0", + }, + ) + nom := newPackage( + t, + "nom", + "4.2.3", + locations, + cargo.CargoToml{ + Package: cargo.TomlPackage{ + Description: "A byte-oriented, zero-copy, parser combinators library", + Homepage: "", + Repository: "https://github.com/Geal/nom", + License: "MIT", + LicenseFile: "", }, }, - { - Name: "version_check", - Version: "0.1.5", - PURL: "pkg:cargo/version_check@0.1.5", - Locations: locations, - Language: pkg.Rust, - Type: pkg.RustPkg, - Licenses: pkg.NewLicenseSet(), - Metadata: pkg.RustCargoLockEntry{ - Name: "version_check", - Version: "0.1.5", - Source: "registry+https://github.com/rust-lang/crates.io-index", - Checksum: "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd", - Dependencies: []string{}, + officialRegistry, + "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6", + []string{ + "memchr", + "version_check", + }, + map[string]string{ + "nom-4.2.3/.cargo_vcs_info.json": "93ede0fafa3ccca217787167b2a7a9c22cdf0b88", + "nom-4.2.3/CHANGELOG.md": "48ba21326a9a3bdf1504a642a42cf7f84a0076e8", + "nom-4.2.3/Cargo.toml": "e041d8ad32b719e9ab49c3f7f187d2902ea491f9", + "nom-4.2.3/Cargo.toml.orig": "c1d374941ff23392f409b933c799e849bd948b0e", + "nom-4.2.3/LICENSE": "e7b32657d4608cb4a57afa790801ecb9c2a037f5", + "nom-4.2.3/build.rs": "c59d60b5509a470ff9445c43ee414a029724504e", + "nom-4.2.3/src/bits.rs": "419bf0a257199204fbf7e98ca2904eafd99f2264", + "nom-4.2.3/src/branch.rs": "388f6ae6ce5d441dbe360bcc1be493315386e73a", + "nom-4.2.3/src/bytes.rs": "483cae38a9eb9129e6a7958f1ca40be9d9bb2571", + "nom-4.2.3/src/character.rs": "a7ec8cc1042501dced6c35f436342e865aac97be", + "nom-4.2.3/src/internal.rs": "2ffaf19df16c691da9d250840d9fbdf56e2403bb", + "nom-4.2.3/src/lib.rs": "39e4e967ce4a559fb08b3bb0d7b38f8c9429fe82", + "nom-4.2.3/src/macros.rs": "39a24461edd8adc74a28c36d14699b0728e90f9d", + "nom-4.2.3/src/methods.rs": "3b934d588ee14965b19d831efc7f0b63cf78e0a9", + "nom-4.2.3/src/multi.rs": "098ba3b2faccdf3485491d88e521a3c9f5667ddc", + "nom-4.2.3/src/nom.rs": "c74454752b17c2a7fa8abe398093617f8141f6f7", + "nom-4.2.3/src/regexp.rs": "2875009ce9d7df6787d794a4a807a677a5f1e600", + "nom-4.2.3/src/sequence.rs": "1a496dbd1094e93f1409bad30b31867a566de089", + "nom-4.2.3/src/simple_errors.rs": "585ba0d774a4d6c48229f61e051368333ba16bf9", + "nom-4.2.3/src/str.rs": "2e7303a42f0f31647c68c9b45c85e815c7c9d2f4", + "nom-4.2.3/src/traits.rs": "74ca43bd49a2b81799dfef4b8fbecfd1f77884e9", + "nom-4.2.3/src/types.rs": "3032dba26cddcd7018c0e48295e30ae403902d7e", + "nom-4.2.3/src/util.rs": "fc1a7dc1250b692c5f849f2bfb6b84241eff9d0a", + "nom-4.2.3/src/verbose_errors.rs": "4d5ee906c52b72080d8a6eee5a9227ab4e76506b", + "nom-4.2.3/src/whitespace.rs": "2d5cb62bf4c5e7107d122f120603cc6c38c747be", + "nom-4.2.3/tests/arithmetic.rs": "a85ef14df0e37e9455b2543ed0d43c5f7a600a7d", + "nom-4.2.3/tests/arithmetic_ast.rs": "1b752396f083d8ea500a04ea0ce799b78ff42098", + "nom-4.2.3/tests/blockbuf-arithmetic.rs": "8b41afbcc779ce0410a1295b987b4340d30798b9", + "nom-4.2.3/tests/complete_arithmetic.rs": "be31008788ba4ba2f901ecbf5337e55f6c20828e", + "nom-4.2.3/tests/complete_float.rs": "c1e4b10c80c261842e808517d85f98bff6ca006a", + "nom-4.2.3/tests/css.rs": "a6bf483ae364c9820428c1b830e28c0e4eeddec3", + "nom-4.2.3/tests/custom_errors.rs": "41d94a408dfb23eed0142d7b9c982a4fdfbd293a", + "nom-4.2.3/tests/float.rs": "cbe19c77cd0b149198d610cbdb825a06d27c1ea4", + "nom-4.2.3/tests/inference.rs": "cee94e4224a72ceed0c21549ed2ef1341657fc32", + "nom-4.2.3/tests/ini.rs": "6c51af96426032b9f442caeab542b900b1128719", + "nom-4.2.3/tests/ini_str.rs": "aa971f48e78b79766deba7217dfca78a9b960855", + "nom-4.2.3/tests/issues.rs": "5f61b69052fad5b413d9e588ffecab8004849788", + "nom-4.2.3/tests/json.rs": "b45c8b0db154180350c34c2d2086fa355b1b45e5", + "nom-4.2.3/tests/mp4.rs": "7288433829b9b3f305761b240c8c9b60529f66a1", + "nom-4.2.3/tests/multiline.rs": "270c16ebaf0c3839e9ebe66cca28803fe64bbc05", + "nom-4.2.3/tests/named_args.rs": "f67ef99a7df44705eecf64de45f200c296ec4648", + "nom-4.2.3/tests/overflow.rs": "1ad7e1bdc3b070bd336680f39294f7400bf50d61", + "nom-4.2.3/tests/reborrow_fold.rs": "4ba89f2b42dcf803a2687b370c835e2134f11af1", + "nom-4.2.3/tests/test1.rs": "f7c223208509242a9b66d9fbfaccc509d2a14bef", + }, + ) + + unicodeBidi := newPackage( + t, + "unicode-bidi", + "0.3.4", + locations, + cargo.CargoToml{ + Package: cargo.TomlPackage{ + Description: "Implementation of the Unicode Bidirectional Algorithm", + Homepage: "", + Repository: "https://github.com/servo/unicode-bidi", + License: "MIT / Apache-2.0", + LicenseFile: "", }, }, - { - Name: "winapi", - Version: "0.3.9", - PURL: "pkg:cargo/winapi@0.3.9", - Locations: locations, - Language: pkg.Rust, - Type: pkg.RustPkg, - Licenses: pkg.NewLicenseSet(), - Metadata: pkg.RustCargoLockEntry{ - Name: "winapi", - Version: "0.3.9", - Source: "registry+https://github.com/rust-lang/crates.io-index", - Checksum: "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419", - Dependencies: []string{ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", - }, + officialSparse, + "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5", + []string{ + "matches", + }, + map[string]string{ + "unicode-bidi-0.3.4/.appveyor.yml": "e6a4b71b2e461d29891f693815fb8d946a468dd5", + "unicode-bidi-0.3.4/.gitignore": "49b13a4aa553694185bb35d9ba508e55545ccb5c", + "unicode-bidi-0.3.4/.rustfmt.toml": "76a81d84fe870ade0d22f3e952a61d0d75eb5a87", + "unicode-bidi-0.3.4/.travis.yml": "e4acfec5e6f3afef42e8461a15a02348abd5d741", + "unicode-bidi-0.3.4/AUTHORS": "05bb8f0c2d2c480ab371dfbc7d58cc44b9184403", + "unicode-bidi-0.3.4/COPYRIGHT": "871b9912ab96cf7d79cb8ae83ca0b08cd5d0cbfd", + "unicode-bidi-0.3.4/Cargo.toml": "f6b0f63bf80d80eb4d5df44f3c6d77947a930acb", + "unicode-bidi-0.3.4/Cargo.toml.orig": "51dcdeeaa5e96a9b02471b6ba8a1fdd08ec6aa03", + "unicode-bidi-0.3.4/LICENSE-APACHE": "5798832c31663cedc1618d18544d445da0295229", + "unicode-bidi-0.3.4/LICENSE-MIT": "60c3522081bf15d7ac1d4c5a63de425ef253e87a", + "unicode-bidi-0.3.4/README.md": "78d6f25691fa623f950efdf9d2a9aae129e30e2d", + "unicode-bidi-0.3.4/src/char_data/mod.rs": "7a08a46193d71df15da9d900c339a4dbce0a5f45", + "unicode-bidi-0.3.4/src/char_data/tables.rs": "e495d79981929f5bb2f0457d452de8a4b52f6666", + "unicode-bidi-0.3.4/src/deprecated.rs": "43ac5028a8f1d5ddba76822147fb363ef0222601", + "unicode-bidi-0.3.4/src/explicit.rs": "eed6c9865990a33498339e5f0fe9ba63352d08f1", + "unicode-bidi-0.3.4/src/format_chars.rs": "468f7b50f5a290b6bdbb0707381dfb5a61e90012", + "unicode-bidi-0.3.4/src/implicit.rs": "1a47012f5da712a4f8d0418a54b93ac5e011cece", + "unicode-bidi-0.3.4/src/level.rs": "c9fabd87fb706e9ea6aa9bbfcff00cad11efae07", + "unicode-bidi-0.3.4/src/lib.rs": "7ea0fb0b66115b1ec766a9ad90b937496e6610ae", + "unicode-bidi-0.3.4/src/prepare.rs": "5030d5cbb328b1f04b79879ee8a455609b79f209", + }, + ) + + versionCheck := newPackage( + t, + "version_check", + "0.1.5", + locations, + cargo.CargoToml{ + Package: cargo.TomlPackage{ + Description: "Tiny crate to check the version of the installed/running rustc.", + Homepage: "", + Repository: "https://github.com/SergioBenitez/version_check", + License: "MIT/Apache-2.0", + LicenseFile: "", }, }, - { - Name: "winapi-i686-pc-windows-gnu", - Version: "0.4.0", - PURL: "pkg:cargo/winapi-i686-pc-windows-gnu@0.4.0", - Locations: locations, - Language: pkg.Rust, - Type: pkg.RustPkg, - Licenses: pkg.NewLicenseSet(), - Metadata: pkg.RustCargoLockEntry{ - Name: "winapi-i686-pc-windows-gnu", - Version: "0.4.0", - Source: "registry+https://github.com/rust-lang/crates.io-index", - Checksum: "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6", - Dependencies: []string{}, + officialRegistry, + "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd", + nil, + map[string]string{ + "version_check-0.1.5/.cargo_vcs_info.json": "c2cbaa20212e0f8eb1e2d1875be2a17d5ab223c2", + "version_check-0.1.5/.gitignore": "3254b5d5538166f1fd5a0bb41f7f3d3bbd455c56", + "version_check-0.1.5/Cargo.toml": "00d6f82ce931ab8de090f799be41afa120f23da7", + "version_check-0.1.5/Cargo.toml.orig": "fcdc047676e0857485f2834eeda27d97109e2438", + "version_check-0.1.5/LICENSE-APACHE": "5798832c31663cedc1618d18544d445da0295229", + "version_check-0.1.5/LICENSE-MIT": "cfcb552ef0afbe7ccb4128891c0de00685988a4b", + "version_check-0.1.5/README.md": "a88c576b3dc05d78012217f34a10fd9ae5a514da", + "version_check-0.1.5/src/lib.rs": "0b69c0dba2824a1bfd0b3f3da71c21ec2e2795fa", + }, + ) + + accesskitWinit := newPackage( + t, + "accesskit_winit", + "0.16.1", + locations, + cargo.CargoToml{ + Package: cargo.TomlPackage{ + Description: "AccessKit UI accessibility infrastructure: winit adapter", + Homepage: "", + Repository: "https://github.com/AccessKit/accesskit", + License: "Apache-2.0", + LicenseFile: "", }, }, + officialSparse, + "5284218aca17d9e150164428a0ebc7b955f70e3a9a78b4c20894513aabf98a67", + nil, // see comment at the head of the function + map[string]string{ + "accesskit_winit-0.16.1/.cargo_vcs_info.json": "41670cdd89868caeaf12ac151ba726121a7f2fcb", + "accesskit_winit-0.16.1/CHANGELOG.md": "736e12da4bf06f142c7815349fe4d9fc8d8a4e49", + "accesskit_winit-0.16.1/Cargo.lock": "8c0ff30a7c7d824d6b4ff72cbfcc47aeb5de42ba", + "accesskit_winit-0.16.1/Cargo.toml": "f6ee420b533c6a8b3682d1afe138e523c4cfb822", + "accesskit_winit-0.16.1/Cargo.toml.orig": "284301a6507e7701bad8c7b1606906ace5478600", + "accesskit_winit-0.16.1/README.md": "19f731ae277aad4a740b23e00354cb53ddb50864", + "accesskit_winit-0.16.1/examples/simple.rs": "7cd864d6183b46b0e21378aaae2ecb0abc8acdd3", + "accesskit_winit-0.16.1/src/lib.rs": "7d1e3bc6d8080e8f348d4690bdf6dc5d44b3c815", + "accesskit_winit-0.16.1/src/platform_impl/macos.rs": "b9cba69fa2902d2f6cc9b2319169fa5cf3975f05", + "accesskit_winit-0.16.1/src/platform_impl/mod.rs": "7aacdceafbe2c1bb920d6c84a685a354bacfd83d", + "accesskit_winit-0.16.1/src/platform_impl/null.rs": "ecd4209a6a4080a3c02bfd01c80f32a1c0251c66", + "accesskit_winit-0.16.1/src/platform_impl/unix.rs": "7010e6aad7fb9cbdc96bf384621816c0127276f8", + "accesskit_winit-0.16.1/src/platform_impl/windows.rs": "6d80687a1b62767930efb8dffb4fa96452a77916", + }, + ) + expectedPkgs := []pkg.Package{ + ansiTerm.Package, + matches.Package, + memchr.Package, + natord.Package, + nom.Package, + unicodeBidi.Package, + versionCheck.Package, + accesskitWinit.Package, + } + for _, p := range expectedPkgs { + p.SetID() + } + + var expectedRelationships = []artifact.Relationship{ { - Name: "winapi-x86_64-pc-windows-gnu", - Version: "0.4.0", - PURL: "pkg:cargo/winapi-x86_64-pc-windows-gnu@0.4.0", - Locations: locations, - Language: pkg.Rust, - Type: pkg.RustPkg, - Licenses: pkg.NewLicenseSet(), - Metadata: pkg.RustCargoLockEntry{ - Name: "winapi-x86_64-pc-windows-gnu", - Version: "0.4.0", - Source: "registry+https://github.com/rust-lang/crates.io-index", - Checksum: "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f", - Dependencies: []string{}, - }, + From: matches.Package, + To: unicodeBidi.Package, + Type: artifact.DependencyOfRelationship, + }, + { + From: memchr.Package, + To: nom.Package, + Type: artifact.DependencyOfRelationship, + }, + { + From: versionCheck.Package, + To: nom.Package, + Type: artifact.DependencyOfRelationship, }, } - // TODO: no relationships are under test yet - var expectedRelationships []artifact.Relationship + // TODO: this is invalid + //for _, p := range []packageInfo{ansiTerm, matches, + // memchr, + // natord, + // nom, + // unicodeBidi, + // versionCheck, + // accesskitWinit, + //} { + // for k, v := range p.RustMeta.CrateInfo.PathSha1Hashes { + // expectedRelationships = append(expectedRelationships, artifact.Relationship{ + // From: p.Package, + // To: file.NewCoordinates(k, p.RustMeta.CrateInfo.DownloadLink), + // Type: artifact.ContainsRelationship, + // Data: file.Digest{ + // Algorithm: "sha1", + // Value: v + // }, + // }) + // } + //} - pkgtest.TestFileParser(t, fixture, parseCargoLock, expectedPkgs, expectedRelationships) + pkgtest.TestFileParser(t, fixture, newCargoModCataloger(DefaultCargoLockCatalogerConfig()).parseCargoLock, expectedPkgs, expectedRelationships) + //pkgtest.NewCatalogTester().WithCompareOptions(cm).FromFile(t, fixture).Expects(expectedPkgs, expectedRelationships).TestParser(t, parseCargoLock) } diff --git a/syft/pkg/cataloger/rust/test-fixtures/Cargo.lock b/syft/pkg/cataloger/rust/test-fixtures/Cargo.lock index 5ee8fb7386f..15e90da69c8 100644 --- a/syft/pkg/cataloger/rust/test-fixtures/Cargo.lock +++ b/syft/pkg/cataloger/rust/test-fixtures/Cargo.lock @@ -5,14 +5,11 @@ name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] [[package]] name = "matches" version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" +source = "sparse+https://index.crates.io" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" [[package]] @@ -24,7 +21,7 @@ checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" [[package]] name = "natord" version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" +source = "sparse+https://index.crates.io" checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" [[package]] @@ -40,7 +37,7 @@ dependencies = [ [[package]] name = "unicode-bidi" version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" +source = "sparse+https://index.crates.io" checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" dependencies = [ "matches", @@ -53,24 +50,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" [[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - +name = "accesskit_winit" +version = "0.16.1" +source = "sparse+https://index.crates.io" +checksum = "5284218aca17d9e150164428a0ebc7b955f70e3a9a78b4c20894513aabf98a67" \ No newline at end of file diff --git a/syft/pkg/rust.go b/syft/pkg/rust.go index c06c65fe7bd..f2bbe5db642 100644 --- a/syft/pkg/rust.go +++ b/syft/pkg/rust.go @@ -1,11 +1,39 @@ package pkg +import "strings" + +type RustCargo struct { + CargoEntry *RustCargoEntry `json:"cargoEntry,omitempty"` + LockEntry *RustCargoLockEntry `json:"lockEntry,omitempty"` +} + +type RustCargoEntry struct { + DownloadURL string `json:"downloadURL,omitempty"` + DownloadDigest string `json:"downloadDigest,omitempty"` + Description string `json:"description"` + Homepage string `json:"homepage"` + Repository string `json:"repository"` +} + type RustCargoLockEntry struct { - Name string `toml:"name" json:"name"` - Version string `toml:"version" json:"version"` - Source string `toml:"source" json:"source"` - Checksum string `toml:"checksum" json:"checksum"` - Dependencies []string `toml:"dependencies" json:"dependencies"` + CargoLockVersion int `toml:"-" json:"cargoLockVersion,omitempty"` + Name string `toml:"name" json:"name"` + Version string `toml:"version" json:"version"` + Source string `toml:"source" json:"source"` + Checksum string `toml:"checksum" json:"checksum"` + Dependencies []string `toml:"dependencies" json:"dependencies,omitempty"` +} + +// SourceRemoteURL returns the remote URL based on the Source field of the RustCargoLockEntry. The second return value +// is true if the Source field represents a remote URL. +func (r RustCargoLockEntry) SourceRemoteURL() (string, bool) { + before, after, found := strings.Cut(r.Source, "+") + if !found { + return "", false + } + + // see https://github.com/rust-lang/cargo/blob/master/crates/cargo-util-schemas/src/core/source_kind.rs + return after, before != "local-registry" } type RustBinaryAuditEntry struct {