feat: support top-level SPDX package and graph (#1934)

Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
Keith Zantow 2023-07-26 13:54:32 -04:00 committed by GitHub
parent 1e4d26f526
commit 9480f10ccd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 918 additions and 76 deletions

1
.gitignore vendored
View File

@ -20,6 +20,7 @@ VERSION
*.hpi
*.zip
.idea/
*.iml
*.log
.images
.tmp/

2
go.mod
View File

@ -59,6 +59,7 @@ require (
github.com/charmbracelet/lipgloss v0.7.1
github.com/dave/jennifer v1.6.1
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
github.com/docker/distribution v2.8.2+incompatible
github.com/docker/docker v24.0.5+incompatible
github.com/github/go-spdx/v2 v2.1.2
github.com/gkampitakis/go-snaps v0.4.7
@ -99,7 +100,6 @@ require (
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v23.0.5+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect

View File

@ -4,12 +4,12 @@ import (
"github.com/anchore/syft/syft/source"
)
func DocumentName(srcMetadata source.Description) string {
if srcMetadata.Name != "" {
return srcMetadata.Name
func DocumentName(src source.Description) string {
if src.Name != "" {
return src.Name
}
switch metadata := srcMetadata.Metadata.(type) {
switch metadata := src.Metadata.(type) {
case source.StereoscopeImageSourceMetadata:
return metadata.UserInput
case source.DirectorySourceMetadata:

View File

@ -17,13 +17,11 @@ func Test_DocumentName(t *testing.T) {
tests := []struct {
name string
inputName string
srcMetadata source.Description
expected string
}{
{
name: "image",
inputName: "my-name",
name: "image",
srcMetadata: source.Description{
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "image-repo/name:tag",
@ -34,21 +32,27 @@ func Test_DocumentName(t *testing.T) {
expected: "image-repo/name:tag",
},
{
name: "directory",
inputName: "my-name",
name: "directory",
srcMetadata: source.Description{
Metadata: source.DirectorySourceMetadata{Path: "some/path/to/place"},
},
expected: "some/path/to/place",
},
{
name: "file",
inputName: "my-name",
name: "file",
srcMetadata: source.Description{
Metadata: source.FileSourceMetadata{Path: "some/path/to/place"},
},
expected: "some/path/to/place",
},
{
name: "named",
srcMetadata: source.Description{
Name: "some/name",
Metadata: source.FileSourceMetadata{Path: "some/path/to/place"},
},
expected: "some/name",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {

View File

@ -9,10 +9,12 @@ import (
"strings"
"time"
"github.com/docker/distribution/reference"
"github.com/spdx/tools-golang/spdx"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/spdxlicense"
@ -21,10 +23,20 @@ import (
"github.com/anchore/syft/syft/formats/common/util"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
const (
noAssertion = "NOASSERTION"
spdxPrimaryPurposeContainer = "CONTAINER"
spdxPrimaryPurposeFile = "FILE"
spdxPrimaryPurposeOther = "OTHER"
prefixImage = "Image"
prefixDirectory = "Directory"
prefixFile = "File"
prefixUnknown = "Unknown"
)
// ToFormatModel creates and populates a new SPDX document struct that follows the SPDX 2.3
@ -33,23 +45,37 @@ const (
//nolint:funlen
func ToFormatModel(s sbom.SBOM) *spdx.Document {
name, namespace := DocumentNameAndNamespace(s.Source)
packages := toPackages(s.Artifacts.Packages, s)
relationships := toRelationships(s.RelationshipsSorted())
// for valid SPDX we need a document describes relationship
// TODO: remove this placeholder after deciding on correct behavior
// for the primary package purpose field:
// https://spdx.github.io/spdx-spec/v2.3/package-information/#724-primary-package-purpose-field
describesID := spdx.ElementID("DOCUMENT")
rootPackage := toRootPackage(s.Source)
if rootPackage != nil {
describesID = rootPackage.PackageSPDXIdentifier
// add all relationships from the document root to all other packages
relationships = append(relationships, toRootRelationships(rootPackage, packages)...)
// append the root package
packages = append(packages, rootPackage)
}
// add a relationship for the package the document describes
documentDescribesRelationship := &spdx.Relationship{
RefA: spdx.DocElementID{
ElementRefID: "DOCUMENT",
},
Relationship: string(DescribesRelationship),
RefB: spdx.DocElementID{
ElementRefID: "DOCUMENT",
ElementRefID: describesID,
},
RelationshipComment: "",
}
// add the root document relationship
relationships = append(relationships, documentDescribesRelationship)
return &spdx.Document{
@ -123,19 +149,130 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document {
// Cardinality: optional, one
CreatorComment: "",
},
Packages: toPackages(s.Artifacts.Packages, s),
Packages: packages,
Files: toFiles(s),
Relationships: relationships,
OtherLicenses: toOtherLicenses(s.Artifacts.Packages),
}
}
func toRootRelationships(rootPackage *spdx.Package, packages []*spdx.Package) (out []*spdx.Relationship) {
for _, p := range packages {
out = append(out, &spdx.Relationship{
RefA: spdx.DocElementID{
ElementRefID: rootPackage.PackageSPDXIdentifier,
},
Relationship: string(ContainsRelationship),
RefB: spdx.DocElementID{
ElementRefID: p.PackageSPDXIdentifier,
},
})
}
return
}
//nolint:funlen
func toRootPackage(s source.Description) *spdx.Package {
var prefix string
name := s.Name
version := s.Version
var purl *packageurl.PackageURL
purpose := ""
var checksums []spdx.Checksum
switch m := s.Metadata.(type) {
case source.StereoscopeImageSourceMetadata:
prefix = prefixImage
purpose = spdxPrimaryPurposeContainer
qualifiers := packageurl.Qualifiers{
{
Key: "arch",
Value: m.Architecture,
},
}
ref, _ := reference.Parse(m.UserInput)
if ref, ok := ref.(reference.NamedTagged); ok {
qualifiers = append(qualifiers, packageurl.Qualifier{
Key: "tag",
Value: ref.Tag(),
})
}
c := toChecksum(m.ManifestDigest)
if c != nil {
checksums = append(checksums, *c)
purl = &packageurl.PackageURL{
Type: "oci",
Name: s.Name,
Version: m.ManifestDigest,
Qualifiers: qualifiers,
}
}
case source.DirectorySourceMetadata:
prefix = prefixDirectory
purpose = spdxPrimaryPurposeFile
case source.FileSourceMetadata:
prefix = prefixFile
purpose = spdxPrimaryPurposeFile
for _, d := range m.Digests {
checksums = append(checksums, spdx.Checksum{
Algorithm: toChecksumAlgorithm(d.Algorithm),
Value: d.Value,
})
}
default:
prefix = prefixUnknown
purpose = spdxPrimaryPurposeOther
if name == "" {
name = s.ID
}
}
p := &spdx.Package{
PackageName: name,
PackageSPDXIdentifier: spdx.ElementID(SanitizeElementID(fmt.Sprintf("DocumentRoot-%s-%s", prefix, name))),
PackageVersion: version,
PackageChecksums: checksums,
PackageSupplier: nil,
PackageExternalReferences: nil,
PrimaryPackagePurpose: purpose,
}
if purl != nil {
p.PackageExternalReferences = []*spdx.PackageExternalReference{
{
Category: string(PackageManagerReferenceCategory),
RefType: string(PurlExternalRefType),
Locator: purl.String(),
},
}
}
return p
}
func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
maxLen := 40
id := ""
switch it := identifiable.(type) {
case pkg.Package:
id = SanitizeElementID(fmt.Sprintf("Package-%s-%s-%s", it.Type, it.Name, it.ID()))
switch {
case it.Type != "" && it.Name != "":
id = fmt.Sprintf("Package-%s-%s-%s", it.Type, it.Name, it.ID())
case it.Name != "":
id = fmt.Sprintf("Package-%s-%s", it.Name, it.ID())
case it.Type != "":
id = fmt.Sprintf("Package-%s-%s", it.Type, it.ID())
default:
id = fmt.Sprintf("Package-%s", it.ID())
}
case file.Coordinates:
p := ""
parts := strings.Split(it.RealPath, "/")
@ -150,12 +287,12 @@ func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
}
p = path.Join(part, p)
}
id = SanitizeElementID(fmt.Sprintf("File-%s-%s", p, it.ID()))
id = fmt.Sprintf("File-%s-%s", p, it.ID())
default:
id = string(identifiable.ID())
}
// NOTE: the spdx library prepend SPDXRef-, so we don't do it here
return spdx.ElementID(id)
return spdx.ElementID(SanitizeElementID(id))
}
// packages populates all Package Information from the package Collection (see https://spdx.github.io/spdx-spec/3-package-information/)
@ -494,6 +631,18 @@ func toFileChecksums(digests []file.Digest) (checksums []spdx.Checksum) {
return checksums
}
// toChecksum takes a checksum in the format <algorithm>:<hash> and returns an spdx.Checksum or nil if the string is invalid
func toChecksum(algorithmHash string) *spdx.Checksum {
parts := strings.Split(algorithmHash, ":")
if len(parts) < 2 {
return nil
}
return &spdx.Checksum{
Algorithm: toChecksumAlgorithm(parts[0]),
Value: parts[1],
}
}
func toChecksumAlgorithm(algorithm string) spdx.ChecksumAlgorithm {
// this needs to be an uppercase version of our algorithm
return spdx.ChecksumAlgorithm(strings.ToUpper(algorithm))

View File

@ -5,17 +5,247 @@ import (
"regexp"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/spdx/tools-golang/spdx"
"github.com/spdx/tools-golang/spdx/v2/v2_3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/sourcemetadata"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
// TODO: Add ToFormatModel tests
func Test_toFormatModel(t *testing.T) {
tracker := sourcemetadata.NewCompletionTester(t)
tests := []struct {
name string
in sbom.SBOM
expected *spdx.Document
}{
{
name: "container",
in: sbom.SBOM{
Source: source.Description{
Name: "alpine",
Version: "sha256:d34db33f",
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: "alpine:latest",
ManifestDigest: "sha256:d34db33f",
},
},
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(pkg.Package{
Name: "pkg-1",
Version: "version-1",
}),
},
},
expected: &spdx.Document{
SPDXIdentifier: "DOCUMENT",
SPDXVersion: spdx.Version,
DataLicense: spdx.DataLicense,
DocumentName: "alpine",
Packages: []*spdx.Package{
{
PackageSPDXIdentifier: "Package-pkg-1-pkg-1",
PackageName: "pkg-1",
PackageVersion: "version-1",
},
{
PackageSPDXIdentifier: "DocumentRoot-Image-alpine",
PackageName: "alpine",
PackageVersion: "sha256:d34db33f",
PrimaryPackagePurpose: "CONTAINER",
PackageChecksums: []spdx.Checksum{{Algorithm: "SHA256", Value: "d34db33f"}},
PackageExternalReferences: []*v2_3.PackageExternalReference{
{
Category: "PACKAGE-MANAGER",
RefType: "purl",
Locator: "pkg:oci/alpine@sha256:d34db33f?arch=&tag=latest",
},
},
},
},
Relationships: []*spdx.Relationship{
{
RefA: spdx.DocElementID{
ElementRefID: "DocumentRoot-Image-alpine",
},
RefB: spdx.DocElementID{
ElementRefID: "Package-pkg-1-pkg-1",
},
Relationship: spdx.RelationshipContains,
},
{
RefA: spdx.DocElementID{
ElementRefID: "DOCUMENT",
},
RefB: spdx.DocElementID{
ElementRefID: "DocumentRoot-Image-alpine",
},
Relationship: spdx.RelationshipDescribes,
},
},
},
},
{
name: "directory",
in: sbom.SBOM{
Source: source.Description{
Name: "some/directory",
Metadata: source.DirectorySourceMetadata{
Path: "some/directory",
},
},
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(pkg.Package{
Name: "pkg-1",
Version: "version-1",
}),
},
},
expected: &spdx.Document{
SPDXIdentifier: "DOCUMENT",
SPDXVersion: spdx.Version,
DataLicense: spdx.DataLicense,
DocumentName: "some/directory",
Packages: []*spdx.Package{
{
PackageSPDXIdentifier: "Package-pkg-1-pkg-1",
PackageName: "pkg-1",
PackageVersion: "version-1",
},
{
PackageSPDXIdentifier: "DocumentRoot-Directory-some-directory",
PackageName: "some/directory",
PackageVersion: "",
PrimaryPackagePurpose: "FILE",
},
},
Relationships: []*spdx.Relationship{
{
RefA: spdx.DocElementID{
ElementRefID: "DocumentRoot-Directory-some-directory",
},
RefB: spdx.DocElementID{
ElementRefID: "Package-pkg-1-pkg-1",
},
Relationship: spdx.RelationshipContains,
},
{
RefA: spdx.DocElementID{
ElementRefID: "DOCUMENT",
},
RefB: spdx.DocElementID{
ElementRefID: "DocumentRoot-Directory-some-directory",
},
Relationship: spdx.RelationshipDescribes,
},
},
},
},
{
name: "file",
in: sbom.SBOM{
Source: source.Description{
Name: "path/to/some.file",
Version: "sha256:d34db33f",
Metadata: source.FileSourceMetadata{
Path: "path/to/some.file",
Digests: []file.Digest{
{
Algorithm: "sha256",
Value: "d34db33f",
},
},
},
},
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(pkg.Package{
Name: "pkg-1",
Version: "version-1",
}),
},
},
expected: &spdx.Document{
SPDXIdentifier: "DOCUMENT",
SPDXVersion: spdx.Version,
DataLicense: spdx.DataLicense,
DocumentName: "path/to/some.file",
Packages: []*spdx.Package{
{
PackageSPDXIdentifier: "Package-pkg-1-pkg-1",
PackageName: "pkg-1",
PackageVersion: "version-1",
},
{
PackageSPDXIdentifier: "DocumentRoot-File-path-to-some.file",
PackageName: "path/to/some.file",
PackageVersion: "sha256:d34db33f",
PrimaryPackagePurpose: "FILE",
PackageChecksums: []spdx.Checksum{{Algorithm: "SHA256", Value: "d34db33f"}},
},
},
Relationships: []*spdx.Relationship{
{
RefA: spdx.DocElementID{
ElementRefID: "DocumentRoot-File-path-to-some.file",
},
RefB: spdx.DocElementID{
ElementRefID: "Package-pkg-1-pkg-1",
},
Relationship: spdx.RelationshipContains,
},
{
RefA: spdx.DocElementID{
ElementRefID: "DOCUMENT",
},
RefB: spdx.DocElementID{
ElementRefID: "DocumentRoot-File-path-to-some.file",
},
Relationship: spdx.RelationshipDescribes,
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
tracker.Tested(t, test.in.Source.Metadata)
// replace IDs with package names
var pkgs []pkg.Package
for p := range test.in.Artifacts.Packages.Enumerate() {
p.OverrideID(artifact.ID(p.Name))
pkgs = append(pkgs, p)
}
test.in.Artifacts.Packages = pkg.NewCollection(pkgs...)
// convert
got := ToFormatModel(test.in)
// check differences
if diff := cmp.Diff(test.expected, got,
cmpopts.IgnoreUnexported(spdx.Document{}, spdx.Package{}),
cmpopts.IgnoreFields(spdx.Document{}, "CreationInfo", "DocumentNamespace"),
cmpopts.IgnoreFields(spdx.Package{}, "PackageDownloadLocation", "IsFilesAnalyzedTagPresent", "PackageSourceInfo", "PackageLicenseConcluded", "PackageLicenseDeclared", "PackageCopyrightText"),
); diff != "" {
t.Error(diff)
}
})
}
}
func Test_toPackageChecksums(t *testing.T) {
tests := []struct {
name string

View File

@ -2,11 +2,15 @@ package spdxhelpers
import (
"errors"
"fmt"
"net/url"
"path"
"regexp"
"strconv"
"strings"
"github.com/spdx/tools-golang/spdx"
"github.com/spdx/tools-golang/spdx/v2/common"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
@ -26,12 +30,10 @@ func ToSyftModel(doc *spdx.Document) (*sbom.SBOM, error) {
return nil, errors.New("cannot convert SPDX document to Syft model because document is nil")
}
spdxIDMap := make(map[string]interface{})
src := extractSourceFromNamespace(doc.DocumentNamespace)
spdxIDMap := make(map[string]any)
s := &sbom.SBOM{
Source: src,
Source: extractSource(spdxIDMap, doc),
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(),
FileMetadata: map[file.Coordinates]file.Metadata{},
@ -40,7 +42,7 @@ func ToSyftModel(doc *spdx.Document) (*sbom.SBOM, error) {
},
}
collectSyftPackages(s, spdxIDMap, doc)
collectSyftPackages(s, spdxIDMap, doc.Packages)
collectSyftFiles(s, spdxIDMap, doc)
@ -49,6 +51,166 @@ func ToSyftModel(doc *spdx.Document) (*sbom.SBOM, error) {
return s, nil
}
func isDirectory(name string) bool {
if name == "." || name == ".." || strings.HasSuffix(name, "/") || !strings.Contains(path.Base(name), ".") {
return true
}
return false
}
func removePackage(packages []*spdx.Package, remove *spdx.Package) (pkgs []*spdx.Package) {
for _, p := range packages {
if p == remove {
continue
}
pkgs = append(pkgs, p)
}
return
}
func removeRelationships(relationships []*spdx.Relationship, spdxID spdx.ElementID) (relations []*spdx.Relationship) {
for _, r := range relationships {
if r.RefA.ElementRefID == spdxID || r.RefB.ElementRefID == spdxID {
continue
}
relations = append(relations, r)
}
return
}
func findRootPackages(doc *spdx.Document) (out []*spdx.Package) {
for _, p := range doc.Packages {
for _, r := range doc.Relationships {
describes := r.RefA.ElementRefID == "DOCUMENT" &&
r.Relationship == spdx.RelationshipDescribes &&
r.RefB.ElementRefID == p.PackageSPDXIdentifier
describedBy := r.RefB.ElementRefID == "DOCUMENT" &&
r.Relationship == spdx.RelationshipDescribedBy &&
r.RefA.ElementRefID == p.PackageSPDXIdentifier
if !describes && !describedBy {
continue
}
out = append(out, p)
}
}
return
}
func extractSource(spdxIDMap map[string]any, doc *spdx.Document) source.Description {
src := extractSourceFromNamespace(doc.DocumentNamespace)
rootPackages := findRootPackages(doc)
if len(rootPackages) != 1 {
return src
}
p := rootPackages[0]
switch p.PrimaryPackagePurpose {
case spdxPrimaryPurposeContainer:
src = containerSource(p)
case spdxPrimaryPurposeFile:
src = fileSource(p)
default:
return src
}
spdxIDMap[string(p.PackageSPDXIdentifier)] = src
doc.Packages = removePackage(doc.Packages, p)
doc.Relationships = removeRelationships(doc.Relationships, p.PackageSPDXIdentifier)
return src
}
func containerSource(p *spdx.Package) source.Description {
id := string(p.PackageSPDXIdentifier)
container := p.PackageName
v := p.PackageVersion
if v != "" {
container += ":" + v
}
digest := ""
if len(p.PackageChecksums) > 0 {
c := p.PackageChecksums[0]
digest = fmt.Sprintf("%s:%s", fromChecksumAlgorithm(c.Algorithm), c.Value)
}
return source.Description{
ID: id,
Name: p.PackageName,
Version: p.PackageVersion,
Metadata: source.StereoscopeImageSourceMetadata{
UserInput: container,
ID: id,
Layers: nil, // TODO handle formats with nested layer packages like Tern and K8s BOM tool
ManifestDigest: digest,
},
}
}
func fileSource(p *spdx.Package) source.Description {
typeRegex := regexp.MustCompile("^DocumentRoot-([^-]+)-.*$")
typeName := typeRegex.ReplaceAllString(string(p.PackageSPDXIdentifier), "$1")
var version string
var metadata any
switch {
case typeName == prefixDirectory:
// is a Syft SBOM, explicitly a directory source
metadata, version = directorySourceMetadata(p)
case typeName == prefixFile:
// is a Syft SBOM, explicitly a file source
metadata, version = fileSourceMetadata(p)
case isDirectory(p.PackageName):
// is a non-Syft SBOM, which looks like a directory
metadata, version = directorySourceMetadata(p)
default:
// is a non-Syft SBOM, which is probably a file
metadata, version = fileSourceMetadata(p)
}
return source.Description{
ID: string(p.PackageSPDXIdentifier),
Name: p.PackageName,
Version: version,
Metadata: metadata,
}
}
func fileSourceMetadata(p *spdx.Package) (any, string) {
version := p.PackageVersion
m := source.FileSourceMetadata{
Path: p.PackageName,
}
// if this is a Syft SBOM, we might have output a digest as the version
checksum := toChecksum(p.PackageVersion)
for _, d := range p.PackageChecksums {
if checksum != nil && checksum.Value == d.Value {
version = ""
}
m.Digests = append(m.Digests, file.Digest{
Algorithm: fromChecksumAlgorithm(d.Algorithm),
Value: d.Value,
})
}
return m, version
}
func directorySourceMetadata(p *spdx.Package) (any, string) {
return source.DirectorySourceMetadata{
Path: p.PackageName,
Base: "",
}, p.PackageVersion
}
// NOTE(jonas): SPDX doesn't inform what an SBOM is about,
// image, directory, for example. This is our best effort to determine
// the scheme. Syft-generated SBOMs have in the namespace
@ -114,15 +276,15 @@ func findLinuxReleaseByPURL(doc *spdx.Document) *linux.Release {
return nil
}
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document) {
for _, p := range doc.Packages {
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]any, packages []*spdx.Package) {
for _, p := range packages {
syftPkg := toSyftPackage(p)
spdxIDMap[string(p.PackageSPDXIdentifier)] = syftPkg
s.Artifacts.Packages.Add(*syftPkg)
s.Artifacts.Packages.Add(syftPkg)
}
}
func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document) {
func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]any, doc *spdx.Document) {
for _, f := range doc.Files {
l := toSyftLocation(f)
spdxIDMap[string(f.FileSPDXIdentifier)] = l
@ -135,13 +297,17 @@ func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.
func toFileDigests(f *spdx.File) (digests []file.Digest) {
for _, digest := range f.Checksums {
digests = append(digests, file.Digest{
Algorithm: string(digest.Algorithm),
Algorithm: fromChecksumAlgorithm(digest.Algorithm),
Value: digest.Value,
})
}
return digests
}
func fromChecksumAlgorithm(algorithm common.ChecksumAlgorithm) string {
return strings.ToLower(string(algorithm))
}
func toFileMetadata(f *spdx.File) (meta file.Metadata) {
// FIXME Syft is currently lossy due to the SPDX 2.2.1 spec not supporting arbitrary mimetypes
for _, typ := range f.FileTypes {
@ -164,21 +330,21 @@ func toFileMetadata(f *spdx.File) (meta file.Metadata) {
return meta
}
func toSyftRelationships(spdxIDMap map[string]interface{}, doc *spdx.Document) []artifact.Relationship {
func toSyftRelationships(spdxIDMap map[string]any, doc *spdx.Document) []artifact.Relationship {
var out []artifact.Relationship
for _, r := range doc.Relationships {
// FIXME what to do with r.RefA.DocumentRefID and r.RefA.SpecialID
// FIXME what to do with r.RefA.DocumentRefID and r.RefA.SpecialID
if r.RefA.DocumentRefID != "" && requireAndTrimPrefix(r.RefA.DocumentRefID, "DocumentRef-") != string(doc.SPDXIdentifier) {
log.Debugf("ignoring relationship to external document: %+v", r)
continue
}
a := spdxIDMap[string(r.RefA.ElementRefID)]
b := spdxIDMap[string(r.RefB.ElementRefID)]
from, fromOk := a.(*pkg.Package)
toPackage, toPackageOk := b.(*pkg.Package)
toLocation, toLocationOk := b.(*file.Location)
from, fromOk := a.(pkg.Package)
toPackage, toPackageOk := b.(pkg.Package)
toLocation, toLocationOk := b.(file.Location)
if !fromOk || !(toPackageOk || toLocationOk) {
log.Debugf("unable to find valid relationship mapping from SPDX 2.2 JSON, ignoring: (from: %+v) (to: %+v)", a, b)
log.Debugf("unable to find valid relationship mapping from SPDX, ignoring: (from: %+v) (to: %+v)", a, b)
continue
}
var to artifact.Identifiable
@ -234,9 +400,9 @@ func toSyftCoordinates(f *spdx.File) file.Coordinates {
}
}
func toSyftLocation(f *spdx.File) *file.Location {
func toSyftLocation(f *spdx.File) file.Location {
l := file.NewVirtualLocationFromCoordinates(toSyftCoordinates(f), f.FileName)
return &l
return l
}
func requireAndTrimPrefix(val interface{}, prefix string) string {
@ -280,16 +446,16 @@ func extractPkgInfo(p *spdx.Package) pkgInfo {
}
}
func toSyftPackage(p *spdx.Package) *pkg.Package {
func toSyftPackage(p *spdx.Package) pkg.Package {
info := extractPkgInfo(p)
metadataType, metadata := extractMetadata(p, info)
sP := pkg.Package{
sP := &pkg.Package{
Type: info.typ,
Name: p.PackageName,
Version: p.PackageVersion,
Licenses: pkg.NewLicenseSet(parseSPDXLicenses(p)...),
CPEs: extractCPEs(p),
PURL: info.purl.String(),
PURL: purlValue(info.purl),
Language: info.lang,
MetadataType: metadataType,
Metadata: metadata,
@ -297,7 +463,15 @@ func toSyftPackage(p *spdx.Package) *pkg.Package {
sP.SetID()
return &sP
return *sP
}
func purlValue(purl packageurl.PackageURL) string {
p := purl.String()
if p == "pkg:/" {
return ""
}
return p
}
func parseSPDXLicenses(p *spdx.Package) []pkg.License {
@ -384,7 +558,7 @@ func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface
case pkg.JavaPkg:
var digests []file.Digest
for _, value := range p.PackageChecksums {
digests = append(digests, file.Digest{Algorithm: string(value.Algorithm), Value: value.Value})
digests = append(digests, file.Digest{Algorithm: fromChecksumAlgorithm(value.Algorithm), Value: value.Value})
}
return pkg.JavaMetadataType, pkg.JavaMetadata{
ArchiveDigests: digests,
@ -392,7 +566,7 @@ func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface
case pkg.GoModulePkg:
var h1Digest string
for _, value := range p.PackageChecksums {
digest, err := util.HDigestFromSHA(string(value.Algorithm), value.Value)
digest, err := util.HDigestFromSHA(fromChecksumAlgorithm(value.Algorithm), value.Value)
if err != nil {
log.Debugf("invalid h1digest: %v %v", value, err)
continue

View File

@ -4,6 +4,8 @@ import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/spdx/tools-golang/spdx"
"github.com/spdx/tools-golang/spdx/v2/common"
"github.com/stretchr/testify/assert"
@ -12,6 +14,7 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
@ -324,7 +327,7 @@ func TestH1Digest(t *testing.T) {
func Test_toSyftRelationships(t *testing.T) {
type args struct {
spdxIDMap map[string]interface{}
spdxIDMap map[string]any
doc *spdx.Document
}
@ -361,9 +364,9 @@ func Test_toSyftRelationships(t *testing.T) {
{
name: "evident-by relationship",
args: args{
spdxIDMap: map[string]interface{}{
string(toSPDXID(pkg1)): &pkg1,
string(toSPDXID(loc1)): &loc1,
spdxIDMap: map[string]any{
string(toSPDXID(pkg1)): pkg1,
string(toSPDXID(loc1)): loc1,
},
doc: &spdx.Document{
Relationships: []*spdx.Relationship{
@ -391,9 +394,9 @@ func Test_toSyftRelationships(t *testing.T) {
{
name: "ownership-by-file-overlap relationship",
args: args{
spdxIDMap: map[string]interface{}{
string(toSPDXID(pkg2)): &pkg2,
string(toSPDXID(pkg3)): &pkg3,
spdxIDMap: map[string]any{
string(toSPDXID(pkg2)): pkg2,
string(toSPDXID(pkg3)): pkg3,
},
doc: &spdx.Document{
Relationships: []*spdx.Relationship{
@ -431,3 +434,121 @@ func Test_toSyftRelationships(t *testing.T) {
})
}
}
func Test_convertToAndFromFormat(t *testing.T) {
packages := []pkg.Package{
{
Name: "pkg1",
MetadataType: pkg.UnknownMetadataType,
},
{
Name: "pkg2",
MetadataType: pkg.UnknownMetadataType,
},
}
for i := range packages {
(&packages[i]).SetID()
}
relationships := []artifact.Relationship{
{
From: packages[0],
To: packages[1],
Type: artifact.ContainsRelationship,
},
}
tests := []struct {
name string
source source.Description
packages []pkg.Package
relationships []artifact.Relationship
}{
{
name: "image source",
source: source.Description{
ID: "DocumentRoot-Image-some-image",
Metadata: source.StereoscopeImageSourceMetadata{
ID: "DocumentRoot-Image-some-image",
UserInput: "some-image:some-tag",
ManifestDigest: "sha256:ab8b83234bc28f28d8e",
},
Name: "some-image",
Version: "some-tag",
},
packages: packages,
relationships: relationships,
},
{
name: ". directory source",
source: source.Description{
ID: "DocumentRoot-Directory-.",
Name: ".",
Metadata: source.DirectorySourceMetadata{
Path: ".",
},
},
packages: packages,
relationships: relationships,
},
{
name: "directory source",
source: source.Description{
ID: "DocumentRoot-Directory-my-app",
Name: "my-app",
Metadata: source.DirectorySourceMetadata{
Path: "my-app",
},
},
packages: packages,
relationships: relationships,
},
{
name: "file source",
source: source.Description{
ID: "DocumentRoot-File-my-app.exe",
Metadata: source.FileSourceMetadata{
Path: "my-app.exe",
Digests: []file.Digest{
{
Algorithm: "sha256",
Value: "3723cae0b8b83234bc28f28d8e",
},
},
},
Name: "my-app.exe",
},
packages: packages,
relationships: relationships,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
src := &test.source
s := sbom.SBOM{
Source: *src,
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(test.packages...),
},
Relationships: test.relationships,
}
doc := ToFormatModel(s)
got, err := ToSyftModel(doc)
require.NoError(t, err)
if diff := cmp.Diff(&s, got,
cmpopts.IgnoreUnexported(artifact.Relationship{}),
cmpopts.IgnoreUnexported(file.LocationSet{}),
cmpopts.IgnoreUnexported(pkg.Collection{}),
cmpopts.IgnoreUnexported(pkg.Package{}),
cmpopts.IgnoreUnexported(pkg.LicenseSet{}),
cmpopts.IgnoreFields(pkg.Package{}, "MetadataType"),
cmpopts.IgnoreFields(sbom.Artifacts{}, "FileMetadata", "FileDigests"),
); diff != "" {
t.Fatalf("packages do not match:\n%s", diff)
}
})
}
}

View File

@ -92,7 +92,7 @@ func TestSPDXJSONDecoder(t *testing.T) {
relationships:
for _, pkgName := range test.relationships {
for _, rel := range sbom.Relationships {
p, ok := rel.From.(*pkg.Package)
p, ok := rel.From.(pkg.Package)
if ok && p.Name == pkgName {
continue relationships
}

View File

@ -58,12 +58,29 @@
"referenceLocator": "pkg:deb/debian/package-2@2.0.1"
}
]
},
{
"name": "some/path",
"SPDXID": "SPDXRef-DocumentRoot-Directory-some-path",
"downloadLocation": "",
"filesAnalyzed": false,
"primaryPackagePurpose": "FILE"
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-DocumentRoot-Directory-some-path",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-9265397e5e15168a",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DocumentRoot-Directory-some-path",
"relatedSpdxElement": "SPDXRef-Package-deb-package-2-db4abfe497c180d3",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-DocumentRoot-Directory-some-path",
"relationshipType": "DESCRIBES"
}
]

View File

@ -58,12 +58,43 @@
"referenceLocator": "pkg:deb/debian/package-2@2.0.1"
}
]
},
{
"name": "user-image-input",
"SPDXID": "SPDXRef-DocumentRoot-Image-user-image-input",
"versionInfo": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"downloadLocation": "",
"filesAnalyzed": false,
"checksums": [
{
"algorithm": "SHA256",
"checksumValue": "2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
}
],
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:oci/user-image-input@sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch="
}
],
"primaryPackagePurpose": "CONTAINER"
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input",
"relatedSpdxElement": "SPDXRef-Package-deb-package-2-958443e2d9304af4",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-DocumentRoot-Image-user-image-input",
"relationshipType": "DESCRIBES"
}
]

View File

@ -58,6 +58,27 @@
"referenceLocator": "pkg:deb/debian/package-2@2.0.1"
}
]
},
{
"name": "user-image-input",
"SPDXID": "SPDXRef-DocumentRoot-Image-user-image-input",
"versionInfo": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"downloadLocation": "",
"filesAnalyzed": false,
"checksums": [
{
"algorithm": "SHA256",
"checksumValue": "2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
}
],
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:oci/user-image-input@sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch="
}
],
"primaryPackagePurpose": "CONTAINER"
}
],
"files": [
@ -183,9 +204,19 @@
"relatedSpdxElement": "SPDXRef-File-f2-f9e49132a4b96ccd",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input",
"relatedSpdxElement": "SPDXRef-Package-python-package-1-125840abc1c66dd7",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DocumentRoot-Image-user-image-input",
"relatedSpdxElement": "SPDXRef-Package-deb-package-2-958443e2d9304af4",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-DocumentRoot-Image-user-image-input",
"relationshipType": "DESCRIBES"
}
]

View File

@ -61,7 +61,8 @@ func TestSPDXJSONSPDXIDs(t *testing.T) {
},
Relationships: nil,
Source: source.Description{
Metadata: source.DirectorySourceMetadata{Path: "foobar/baz"}, // in this case, foobar is used as the spdx docment name
Name: "foobar/baz", // in this case, foobar is used as the spdx document name
Metadata: source.DirectorySourceMetadata{},
},
Descriptor: sbom.Descriptor{
Name: "syft",

View File

@ -8,10 +8,17 @@ Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus
Created: redacted
##### Package: foobar/baz
PackageName: foobar/baz
SPDXID: SPDXRef-DocumentRoot-Directory-foobar-baz
PrimaryPackagePurpose: FILE
FilesAnalyzed: false
##### Package: @at-sign
PackageName: @at-sign
SPDXID: SPDXRef-Package---at-sign-3732f7a5679bdec4
SPDXID: SPDXRef-Package--at-sign-3732f7a5679bdec4
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageSourceInfo: acquired package info from the following paths:
@ -22,7 +29,7 @@ PackageCopyrightText: NOASSERTION
##### Package: some/slashes
PackageName: some/slashes
SPDXID: SPDXRef-Package--some-slashes-1345166d4801153b
SPDXID: SPDXRef-Package-some-slashes-1345166d4801153b
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageSourceInfo: acquired package info from the following paths:
@ -33,7 +40,7 @@ PackageCopyrightText: NOASSERTION
##### Package: under_scores
PackageName: under_scores
SPDXID: SPDXRef-Package--under-scores-290d5c77210978c1
SPDXID: SPDXRef-Package-under-scores-290d5c77210978c1
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageSourceInfo: acquired package info from the following paths:
@ -43,5 +50,8 @@ PackageCopyrightText: NOASSERTION
##### Relationships
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DOCUMENT
Relationship: SPDXRef-DocumentRoot-Directory-foobar-baz CONTAINS SPDXRef-Package--at-sign-3732f7a5679bdec4
Relationship: SPDXRef-DocumentRoot-Directory-foobar-baz CONTAINS SPDXRef-Package-some-slashes-1345166d4801153b
Relationship: SPDXRef-DocumentRoot-Directory-foobar-baz CONTAINS SPDXRef-Package-under-scores-290d5c77210978c1
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Directory-foobar-baz

View File

@ -46,6 +46,16 @@ FileType: OTHER
FileChecksum: SHA1: 0000000000000000000000000000000000000000
LicenseConcluded: NOASSERTION
##### Package: user-image-input
PackageName: user-image-input
SPDXID: SPDXRef-DocumentRoot-Image-user-image-input
PackageVersion: sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368
PrimaryPackagePurpose: CONTAINER
FilesAnalyzed: false
PackageChecksum: SHA256: 2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368
ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=
##### Package: package-2
PackageName: package-2
@ -82,5 +92,7 @@ Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef
Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-d2-f4-c641caa71518099f
Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-d1-f3-c6f5b29dca12661f
Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DOCUMENT
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-125840abc1c66dd7
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-958443e2d9304af4
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input

View File

@ -8,6 +8,13 @@ Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus
Created: redacted
##### Package: some/path
PackageName: some/path
SPDXID: SPDXRef-DocumentRoot-Directory-some-path
PrimaryPackagePurpose: FILE
FilesAnalyzed: false
##### Package: package-2
PackageName: package-2
@ -38,5 +45,7 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-2
##### Relationships
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DOCUMENT
Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-python-package-1-9265397e5e15168a
Relationship: SPDXRef-DocumentRoot-Directory-some-path CONTAINS SPDXRef-Package-deb-package-2-db4abfe497c180d3
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Directory-some-path

View File

@ -8,6 +8,16 @@ Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus
Created: redacted
##### Package: user-image-input
PackageName: user-image-input
SPDXID: SPDXRef-DocumentRoot-Image-user-image-input
PackageVersion: sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368
PrimaryPackagePurpose: CONTAINER
FilesAnalyzed: false
PackageChecksum: SHA256: 2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368
ExternalRef: PACKAGE-MANAGER purl pkg:oci/user-image-input@sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368?arch=
##### Package: package-2
PackageName: package-2
@ -38,5 +48,7 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-1
##### Relationships
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DOCUMENT
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-python-package-1-125840abc1c66dd7
Relationship: SPDXRef-DocumentRoot-Image-user-image-input CONTAINS SPDXRef-Package-deb-package-2-958443e2d9304af4
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DocumentRoot-Image-user-image-input

View File

@ -5,6 +5,7 @@ import (
"golang.org/x/exp/slices"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
@ -77,11 +78,18 @@ func (s SBOM) RelationshipsForPackage(p pkg.Package, rt ...artifact.Relationship
var relationships []artifact.Relationship
for _, relationship := range s.Relationships {
// check if the relationship is one we're searching for; rt is inclusive
idx := slices.IndexFunc(rt, func(r artifact.RelationshipType) bool { return relationship.Type == r })
if relationship.From.ID() == p.ID() && idx != -1 {
relationships = append(relationships, relationship)
if relationship.From == nil || relationship.To == nil {
log.Debugf("relationship has nil edge, skipping: %#v", relationship)
continue
}
if relationship.From.ID() != p.ID() {
continue
}
// check if the relationship is one we're searching for; rt is inclusive
if !slices.ContainsFunc(rt, func(r artifact.RelationshipType) bool { return relationship.Type == r }) {
continue
}
relationships = append(relationships, relationship)
}
return relationships

View File

@ -120,6 +120,9 @@ func (s DirectorySource) Describe() Description {
if a.Name != "" {
name = a.Name
}
if a.Version != "" {
version = a.Version
}
}
return Description{
ID: string(s.id),

View File

@ -5,10 +5,12 @@ import (
"fmt"
"github.com/bmatcuk/doublestar/v4"
"github.com/docker/distribution/reference"
"github.com/opencontainers/go-digest"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver"
@ -83,18 +85,45 @@ func (s StereoscopeImageSource) ID() artifact.ID {
}
func (s StereoscopeImageSource) Describe() Description {
name := s.metadata.UserInput
version := s.metadata.ManifestDigest
a := s.config.Alias
if a.Name != "" {
name = a.Name
name := a.Name
nameIfUnset := func(n string) {
if name != "" {
return
}
name = n
}
if a.Version != "" {
version = a.Version
version := a.Version
versionIfUnset := func(v string) {
if version != "" && version != "latest" {
return
}
version = v
}
ref, err := reference.Parse(s.metadata.UserInput)
if err != nil {
log.Debugf("unable to parse image ref: %s", s.config.Reference)
} else {
if ref, ok := ref.(reference.Named); ok {
nameIfUnset(ref.Name())
} else {
nameIfUnset(s.metadata.UserInput)
}
if ref, ok := ref.(reference.NamedTagged); ok {
versionIfUnset(ref.Tag())
}
if ref, ok := ref.(reference.Digested); ok {
versionIfUnset(ref.Digest().String())
}
}
versionIfUnset(s.metadata.ManifestDigest)
return Description{
ID: string(s.id),
Name: name,