diff --git a/go.sum b/go.sum index 020c825dd4d6..b715e8e589c0 100644 --- a/go.sum +++ b/go.sum @@ -301,6 +301,7 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66D github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git v1.0.0/go.mod h1:6+421e08gnZWn30y26Vchf7efgYLe4dl5OQbBSUXShE= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= diff --git a/syft/format/common/spdxhelpers/to_format_model.go b/syft/format/common/spdxhelpers/to_format_model.go index 767ffdcbe620..b385b8e5dc65 100644 --- a/syft/format/common/spdxhelpers/to_format_model.go +++ b/syft/format/common/spdxhelpers/to_format_model.go @@ -503,6 +503,11 @@ func toPackageChecksums(p pkg.Package) ([]spdx.Checksum, bool) { Algorithm: spdx.ChecksumAlgorithm(algo), Value: hexStr, }) + case pkg.RustCargoLockEntry: + checksums = append(checksums, spdx.Checksum{ + Algorithm: meta.GetChecksumType(), + Value: meta.Checksum, + }) } return checksums, filesAnalyzed } diff --git a/syft/format/internal/spdxutil/helpers/download_location.go b/syft/format/internal/spdxutil/helpers/download_location.go index 894306dea0cc..8ec79f542d9d 100644 --- a/syft/format/internal/spdxutil/helpers/download_location.go +++ b/syft/format/internal/spdxutil/helpers/download_location.go @@ -1,6 +1,9 @@ package helpers -import "github.com/anchore/syft/syft/pkg" +import ( + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/pkg" +) const NONE = "NONE" const NOASSERTION = "NOASSERTION" @@ -22,6 +25,14 @@ func DownloadLocation(p pkg.Package) string { return NoneIfEmpty(metadata.URL) case pkg.NpmPackageLockEntry: return NoneIfEmpty(metadata.Resolved) + case pkg.RustCargoLockEntry: + var url, err = metadata.GetDownloadLink() + if err != nil { + log.Info(err) + return NONE + } else { + return NoneIfEmpty(url) + } } } return NOASSERTION diff --git a/syft/pkg/cataloger/rust/parse_cargo_lock.go b/syft/pkg/cataloger/rust/parse_cargo_lock.go index d1ef3db1261d..be3a0170555b 100644 --- a/syft/pkg/cataloger/rust/parse_cargo_lock.go +++ b/syft/pkg/cataloger/rust/parse_cargo_lock.go @@ -15,6 +15,7 @@ import ( var _ generic.Parser = parseCargoLock type cargoLockFile struct { + Version int `toml:"version"` Packages []pkg.RustCargoLockEntry `toml:"package"` } @@ -34,6 +35,7 @@ func parseCargoLock(_ context.Context, _ file.Resolver, _ *generic.Environment, var pkgs []pkg.Package for _, p := range m.Packages { + p.CargoLockVersion = m.Version if p.Dependencies == nil { p.Dependencies = make([]string, 0) } diff --git a/syft/pkg/rust.go b/syft/pkg/rust.go index c06c65fe7bdf..653427ed5a0b 100644 --- a/syft/pkg/rust.go +++ b/syft/pkg/rust.go @@ -1,11 +1,26 @@ package pkg +import ( + "encoding/json" + "fmt" + "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" + "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/spdx/tools-golang/spdx" + "strings" +) + 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:"-"` + 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"` } type RustBinaryAuditEntry struct { @@ -13,3 +28,216 @@ type RustBinaryAuditEntry struct { Version string `toml:"version" json:"version"` Source string `toml:"source" json:"source"` } + +type RustRepositoryConfig struct { + Download string `json:"dl"` + API string `json:"api"` + AuthRequired bool `json:"auth-required"` +} + +type SourceId struct { + kind string + url string +} + +type DependencyInformation struct { +} + +// 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" +) + +var RegistryRepos = make(map[string]*memory.Storage) +var RegistryConfig = make(map[string]RustRepositoryConfig) + +// GetChecksumType This exists, to made adopting new potential cargo.lock versions easier +func (r *RustCargoLockEntry) GetChecksumType() spdx.ChecksumAlgorithm { + //Cargo currently always uses Sha256: https://github.com/rust-lang/cargo/blob/a9ee3e82b57df019dfc0385f844bc6928150ee63/src/cargo/sources/registry/download.rs#L125 + return spdx.SHA256 +} + +func (r *RustCargoLockEntry) getSourceId() (*SourceId, error) { + var before, after, found = strings.Cut(r.Source, "+") + if !found { + return nil, fmt.Errorf("did not find \"+\" in source field of dependency: Name: %s, Version: %s, Source: %s", r.Name, r.Version, r.Source) + } + + return &SourceId{ + kind: before, + url: after, + }, nil +} + +// GetPrefix get {path} for https://doc.rust-lang.org/cargo/reference/registry-index.html +func (r *RustCargoLockEntry) 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]) + } +} + +func (r *RustCargoLockEntry) GetDownloadLink() (string, error) { + var sourceId, err = r.getSourceId() + if err != nil { + return "", err + } + var repoConfig *RustRepositoryConfig = nil + repoConfig, err = sourceId.GetConfig() + if err != nil { + return "", err + } + return r.getDownloadLink(repoConfig.Download), err +} + +func (r *RustCargoLockEntry) getDownloadLink(url string) string { + const Crate = "{crate}" + const Version = "{version}" + const Prefix = "{prefix}" + const LowerPrefix = "{lowerprefix}" + const Sha256Checksum = "{sha256-checksum}" + if !strings.Contains(url, Crate) && + !strings.Contains(url, Version) && + !strings.Contains(url, Prefix) && + !strings.Contains(url, LowerPrefix) && + !strings.Contains(url, Sha256Checksum) { + return url + fmt.Sprintf("/%s/%s/download", r.Name, r.Version) + } + + 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 +} +func (r *RustCargoLockEntry) getIndexPath() string { + return fmt.Sprintf("%s/%s", strings.ToLower(r.GetPrefix()), strings.ToLower(r.Name)) +} + +// RepositoryConfigName see https://github.com/rust-lang/cargo/blob/b134eff5cedcaa4879f60035d62630400e7fd543/src/cargo/sources/registry/mod.rs#L962 +const RepositoryConfigName = "config.json" + +func (i *SourceId) GetConfig() (*RustRepositoryConfig, error) { + if repoConfig, ok := RegistryConfig[i.url]; ok { + return &repoConfig, nil + } + content, err := i.GetPath(RepositoryConfigName) + if err != nil { + return nil, err + } + var repoConfig = RustRepositoryConfig{} + err = json.Unmarshal([]byte(content), &repoConfig) + RegistryConfig[i.url] = repoConfig + return &repoConfig, err +} + +func (i *SourceId) GetPath(path string) (string, error) { + switch i.kind { + case SourceKindRegistry: + var content = "" + var tree, err = getTree(i.url) + if err != nil { + return content, err + } + var file *object.File = nil + file, err = tree.File(path) + if err != nil { + return content, err + } + content, err = file.Contents() + return content, err + } + return "", fmt.Errorf("unsupported Remote") +} + +func getOrInitRepo(url string) (*memory.Storage, *git.Repository, error) { + var repo *git.Repository = nil + var err error = nil + + var storage, ok = RegistryRepos[url] + //Todo: Should we use an on-disk storage? + if !ok { + storage = memory.NewStorage() + RegistryRepos[url] = storage + repo, err = git.Init(storage, memfs.New()) + if err != nil { + return storage, nil, err + } + err = updateRepo(repo, url) + } else { + repo, err = git.Open(storage, memfs.New()) + } + 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 + var 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 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, + }) + return err +} + +func getTree(url string) (*object.Tree, error) { + var _, repo, err = getOrInitRepo(url) + if err != nil { + return nil, err + } + + var ref *plumbing.Reference = nil + 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: %s", err) + } + + var hash = ref.Hash() + var commit *object.Commit = nil + commit, err = repo.CommitObject(hash) + if err != nil { + return nil, fmt.Errorf("failed to get commit from repo head: %s", err) + } + + var tree *object.Tree = nil + tree, err = commit.Tree() + if err != nil { + return nil, fmt.Errorf("failed to get Tree from Commit: %s", err) + } + + return tree, err +}