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 *.hpi
*.zip *.zip
.idea/ .idea/
*.iml
*.log *.log
.images .images
.tmp/ .tmp/

2
go.mod
View File

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

View File

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

View File

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

View File

@ -9,10 +9,12 @@ import (
"strings" "strings"
"time" "time"
"github.com/docker/distribution/reference"
"github.com/spdx/tools-golang/spdx" "github.com/spdx/tools-golang/spdx"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/spdxlicense" "github.com/anchore/syft/internal/spdxlicense"
@ -21,10 +23,20 @@ import (
"github.com/anchore/syft/syft/formats/common/util" "github.com/anchore/syft/syft/formats/common/util"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
) )
const ( const (
noAssertion = "NOASSERTION" 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 // ToFormatModel creates and populates a new SPDX document struct that follows the SPDX 2.3
@ -33,23 +45,37 @@ const (
//nolint:funlen //nolint:funlen
func ToFormatModel(s sbom.SBOM) *spdx.Document { func ToFormatModel(s sbom.SBOM) *spdx.Document {
name, namespace := DocumentNameAndNamespace(s.Source) name, namespace := DocumentNameAndNamespace(s.Source)
packages := toPackages(s.Artifacts.Packages, s)
relationships := toRelationships(s.RelationshipsSorted()) relationships := toRelationships(s.RelationshipsSorted())
// for valid SPDX we need a document describes relationship // for valid SPDX we need a document describes relationship
// TODO: remove this placeholder after deciding on correct behavior describesID := spdx.ElementID("DOCUMENT")
// for the primary package purpose field:
// https://spdx.github.io/spdx-spec/v2.3/package-information/#724-primary-package-purpose-field 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{ documentDescribesRelationship := &spdx.Relationship{
RefA: spdx.DocElementID{ RefA: spdx.DocElementID{
ElementRefID: "DOCUMENT", ElementRefID: "DOCUMENT",
}, },
Relationship: string(DescribesRelationship), Relationship: string(DescribesRelationship),
RefB: spdx.DocElementID{ RefB: spdx.DocElementID{
ElementRefID: "DOCUMENT", ElementRefID: describesID,
}, },
RelationshipComment: "",
} }
// add the root document relationship
relationships = append(relationships, documentDescribesRelationship) relationships = append(relationships, documentDescribesRelationship)
return &spdx.Document{ return &spdx.Document{
@ -123,19 +149,130 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document {
// Cardinality: optional, one // Cardinality: optional, one
CreatorComment: "", CreatorComment: "",
}, },
Packages: toPackages(s.Artifacts.Packages, s), Packages: packages,
Files: toFiles(s), Files: toFiles(s),
Relationships: relationships, Relationships: relationships,
OtherLicenses: toOtherLicenses(s.Artifacts.Packages), 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 { func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
maxLen := 40 maxLen := 40
id := "" id := ""
switch it := identifiable.(type) { switch it := identifiable.(type) {
case pkg.Package: 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: case file.Coordinates:
p := "" p := ""
parts := strings.Split(it.RealPath, "/") parts := strings.Split(it.RealPath, "/")
@ -150,12 +287,12 @@ func toSPDXID(identifiable artifact.Identifiable) spdx.ElementID {
} }
p = path.Join(part, p) 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: default:
id = string(identifiable.ID()) id = string(identifiable.ID())
} }
// NOTE: the spdx library prepend SPDXRef-, so we don't do it here // 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/) // 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 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 { func toChecksumAlgorithm(algorithm string) spdx.ChecksumAlgorithm {
// this needs to be an uppercase version of our algorithm // this needs to be an uppercase version of our algorithm
return spdx.ChecksumAlgorithm(strings.ToUpper(algorithm)) return spdx.ChecksumAlgorithm(strings.ToUpper(algorithm))

View File

@ -5,17 +5,247 @@ import (
"regexp" "regexp"
"testing" "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"
"github.com/spdx/tools-golang/spdx/v2/v2_3"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/sourcemetadata"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom" "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) { func Test_toPackageChecksums(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View File

@ -2,11 +2,15 @@ package spdxhelpers
import ( import (
"errors" "errors"
"fmt"
"net/url" "net/url"
"path"
"regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/spdx/tools-golang/spdx" "github.com/spdx/tools-golang/spdx"
"github.com/spdx/tools-golang/spdx/v2/common"
"github.com/anchore/packageurl-go" "github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log" "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") return nil, errors.New("cannot convert SPDX document to Syft model because document is nil")
} }
spdxIDMap := make(map[string]interface{}) spdxIDMap := make(map[string]any)
src := extractSourceFromNamespace(doc.DocumentNamespace)
s := &sbom.SBOM{ s := &sbom.SBOM{
Source: src, Source: extractSource(spdxIDMap, doc),
Artifacts: sbom.Artifacts{ Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(), Packages: pkg.NewCollection(),
FileMetadata: map[file.Coordinates]file.Metadata{}, 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) collectSyftFiles(s, spdxIDMap, doc)
@ -49,6 +51,166 @@ func ToSyftModel(doc *spdx.Document) (*sbom.SBOM, error) {
return s, nil 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, // NOTE(jonas): SPDX doesn't inform what an SBOM is about,
// image, directory, for example. This is our best effort to determine // image, directory, for example. This is our best effort to determine
// the scheme. Syft-generated SBOMs have in the namespace // the scheme. Syft-generated SBOMs have in the namespace
@ -114,15 +276,15 @@ func findLinuxReleaseByPURL(doc *spdx.Document) *linux.Release {
return nil return nil
} }
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document) { func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]any, packages []*spdx.Package) {
for _, p := range doc.Packages { for _, p := range packages {
syftPkg := toSyftPackage(p) syftPkg := toSyftPackage(p)
spdxIDMap[string(p.PackageSPDXIdentifier)] = syftPkg 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 { for _, f := range doc.Files {
l := toSyftLocation(f) l := toSyftLocation(f)
spdxIDMap[string(f.FileSPDXIdentifier)] = l 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) { func toFileDigests(f *spdx.File) (digests []file.Digest) {
for _, digest := range f.Checksums { for _, digest := range f.Checksums {
digests = append(digests, file.Digest{ digests = append(digests, file.Digest{
Algorithm: string(digest.Algorithm), Algorithm: fromChecksumAlgorithm(digest.Algorithm),
Value: digest.Value, Value: digest.Value,
}) })
} }
return digests return digests
} }
func fromChecksumAlgorithm(algorithm common.ChecksumAlgorithm) string {
return strings.ToLower(string(algorithm))
}
func toFileMetadata(f *spdx.File) (meta file.Metadata) { 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 // FIXME Syft is currently lossy due to the SPDX 2.2.1 spec not supporting arbitrary mimetypes
for _, typ := range f.FileTypes { for _, typ := range f.FileTypes {
@ -164,21 +330,21 @@ func toFileMetadata(f *spdx.File) (meta file.Metadata) {
return meta 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 var out []artifact.Relationship
for _, r := range doc.Relationships { 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) { if r.RefA.DocumentRefID != "" && requireAndTrimPrefix(r.RefA.DocumentRefID, "DocumentRef-") != string(doc.SPDXIdentifier) {
log.Debugf("ignoring relationship to external document: %+v", r) log.Debugf("ignoring relationship to external document: %+v", r)
continue continue
} }
a := spdxIDMap[string(r.RefA.ElementRefID)] a := spdxIDMap[string(r.RefA.ElementRefID)]
b := spdxIDMap[string(r.RefB.ElementRefID)] b := spdxIDMap[string(r.RefB.ElementRefID)]
from, fromOk := a.(*pkg.Package) from, fromOk := a.(pkg.Package)
toPackage, toPackageOk := b.(*pkg.Package) toPackage, toPackageOk := b.(pkg.Package)
toLocation, toLocationOk := b.(*file.Location) toLocation, toLocationOk := b.(file.Location)
if !fromOk || !(toPackageOk || toLocationOk) { 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 continue
} }
var to artifact.Identifiable 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) l := file.NewVirtualLocationFromCoordinates(toSyftCoordinates(f), f.FileName)
return &l return l
} }
func requireAndTrimPrefix(val interface{}, prefix string) string { 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) info := extractPkgInfo(p)
metadataType, metadata := extractMetadata(p, info) metadataType, metadata := extractMetadata(p, info)
sP := pkg.Package{ sP := &pkg.Package{
Type: info.typ, Type: info.typ,
Name: p.PackageName, Name: p.PackageName,
Version: p.PackageVersion, Version: p.PackageVersion,
Licenses: pkg.NewLicenseSet(parseSPDXLicenses(p)...), Licenses: pkg.NewLicenseSet(parseSPDXLicenses(p)...),
CPEs: extractCPEs(p), CPEs: extractCPEs(p),
PURL: info.purl.String(), PURL: purlValue(info.purl),
Language: info.lang, Language: info.lang,
MetadataType: metadataType, MetadataType: metadataType,
Metadata: metadata, Metadata: metadata,
@ -297,7 +463,15 @@ func toSyftPackage(p *spdx.Package) *pkg.Package {
sP.SetID() 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 { func parseSPDXLicenses(p *spdx.Package) []pkg.License {
@ -384,7 +558,7 @@ func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface
case pkg.JavaPkg: case pkg.JavaPkg:
var digests []file.Digest var digests []file.Digest
for _, value := range p.PackageChecksums { 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{ return pkg.JavaMetadataType, pkg.JavaMetadata{
ArchiveDigests: digests, ArchiveDigests: digests,
@ -392,7 +566,7 @@ func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface
case pkg.GoModulePkg: case pkg.GoModulePkg:
var h1Digest string var h1Digest string
for _, value := range p.PackageChecksums { 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 { if err != nil {
log.Debugf("invalid h1digest: %v %v", value, err) log.Debugf("invalid h1digest: %v %v", value, err)
continue continue

View File

@ -4,6 +4,8 @@ import (
"reflect" "reflect"
"testing" "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"
"github.com/spdx/tools-golang/spdx/v2/common" "github.com/spdx/tools-golang/spdx/v2/common"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -12,6 +14,7 @@ import (
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
@ -324,7 +327,7 @@ func TestH1Digest(t *testing.T) {
func Test_toSyftRelationships(t *testing.T) { func Test_toSyftRelationships(t *testing.T) {
type args struct { type args struct {
spdxIDMap map[string]interface{} spdxIDMap map[string]any
doc *spdx.Document doc *spdx.Document
} }
@ -361,9 +364,9 @@ func Test_toSyftRelationships(t *testing.T) {
{ {
name: "evident-by relationship", name: "evident-by relationship",
args: args{ args: args{
spdxIDMap: map[string]interface{}{ spdxIDMap: map[string]any{
string(toSPDXID(pkg1)): &pkg1, string(toSPDXID(pkg1)): pkg1,
string(toSPDXID(loc1)): &loc1, string(toSPDXID(loc1)): loc1,
}, },
doc: &spdx.Document{ doc: &spdx.Document{
Relationships: []*spdx.Relationship{ Relationships: []*spdx.Relationship{
@ -391,9 +394,9 @@ func Test_toSyftRelationships(t *testing.T) {
{ {
name: "ownership-by-file-overlap relationship", name: "ownership-by-file-overlap relationship",
args: args{ args: args{
spdxIDMap: map[string]interface{}{ spdxIDMap: map[string]any{
string(toSPDXID(pkg2)): &pkg2, string(toSPDXID(pkg2)): pkg2,
string(toSPDXID(pkg3)): &pkg3, string(toSPDXID(pkg3)): pkg3,
}, },
doc: &spdx.Document{ doc: &spdx.Document{
Relationships: []*spdx.Relationship{ 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: relationships:
for _, pkgName := range test.relationships { for _, pkgName := range test.relationships {
for _, rel := range sbom.Relationships { for _, rel := range sbom.Relationships {
p, ok := rel.From.(*pkg.Package) p, ok := rel.From.(pkg.Package)
if ok && p.Name == pkgName { if ok && p.Name == pkgName {
continue relationships continue relationships
} }

View File

@ -58,12 +58,29 @@
"referenceLocator": "pkg:deb/debian/package-2@2.0.1" "referenceLocator": "pkg:deb/debian/package-2@2.0.1"
} }
] ]
},
{
"name": "some/path",
"SPDXID": "SPDXRef-DocumentRoot-Directory-some-path",
"downloadLocation": "",
"filesAnalyzed": false,
"primaryPackagePurpose": "FILE"
} }
], ],
"relationships": [ "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", "spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-DOCUMENT", "relatedSpdxElement": "SPDXRef-DocumentRoot-Directory-some-path",
"relationshipType": "DESCRIBES" "relationshipType": "DESCRIBES"
} }
] ]

View File

@ -58,12 +58,43 @@
"referenceLocator": "pkg:deb/debian/package-2@2.0.1" "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": [ "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", "spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-DOCUMENT", "relatedSpdxElement": "SPDXRef-DocumentRoot-Image-user-image-input",
"relationshipType": "DESCRIBES" "relationshipType": "DESCRIBES"
} }
] ]

View File

@ -58,6 +58,27 @@
"referenceLocator": "pkg:deb/debian/package-2@2.0.1" "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": [ "files": [
@ -183,9 +204,19 @@
"relatedSpdxElement": "SPDXRef-File-f2-f9e49132a4b96ccd", "relatedSpdxElement": "SPDXRef-File-f2-f9e49132a4b96ccd",
"relationshipType": "CONTAINS" "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", "spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-DOCUMENT", "relatedSpdxElement": "SPDXRef-DocumentRoot-Image-user-image-input",
"relationshipType": "DESCRIBES" "relationshipType": "DESCRIBES"
} }
] ]

View File

@ -61,7 +61,8 @@ func TestSPDXJSONSPDXIDs(t *testing.T) {
}, },
Relationships: nil, Relationships: nil,
Source: source.Description{ 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{ Descriptor: sbom.Descriptor{
Name: "syft", Name: "syft",

View File

@ -8,10 +8,17 @@ Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus Creator: Tool: syft-v0.42.0-bogus
Created: redacted Created: redacted
##### Package: foobar/baz
PackageName: foobar/baz
SPDXID: SPDXRef-DocumentRoot-Directory-foobar-baz
PrimaryPackagePurpose: FILE
FilesAnalyzed: false
##### Package: @at-sign ##### Package: @at-sign
PackageName: @at-sign PackageName: @at-sign
SPDXID: SPDXRef-Package---at-sign-3732f7a5679bdec4 SPDXID: SPDXRef-Package--at-sign-3732f7a5679bdec4
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false FilesAnalyzed: false
PackageSourceInfo: acquired package info from the following paths: PackageSourceInfo: acquired package info from the following paths:
@ -22,7 +29,7 @@ PackageCopyrightText: NOASSERTION
##### Package: some/slashes ##### Package: some/slashes
PackageName: some/slashes PackageName: some/slashes
SPDXID: SPDXRef-Package--some-slashes-1345166d4801153b SPDXID: SPDXRef-Package-some-slashes-1345166d4801153b
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false FilesAnalyzed: false
PackageSourceInfo: acquired package info from the following paths: PackageSourceInfo: acquired package info from the following paths:
@ -33,7 +40,7 @@ PackageCopyrightText: NOASSERTION
##### Package: under_scores ##### Package: under_scores
PackageName: under_scores PackageName: under_scores
SPDXID: SPDXRef-Package--under-scores-290d5c77210978c1 SPDXID: SPDXRef-Package-under-scores-290d5c77210978c1
PackageDownloadLocation: NOASSERTION PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false FilesAnalyzed: false
PackageSourceInfo: acquired package info from the following paths: PackageSourceInfo: acquired package info from the following paths:
@ -43,5 +50,8 @@ PackageCopyrightText: NOASSERTION
##### Relationships ##### 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 FileChecksum: SHA1: 0000000000000000000000000000000000000000
LicenseConcluded: NOASSERTION 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 ##### Package: package-2
PackageName: 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-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-d1-f3-c6f5b29dca12661f
Relationship: SPDXRef-Package-python-package-1-125840abc1c66dd7 CONTAINS SPDXRef-File-f2-f9e49132a4b96ccd 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 Creator: Tool: syft-v0.42.0-bogus
Created: redacted Created: redacted
##### Package: some/path
PackageName: some/path
SPDXID: SPDXRef-DocumentRoot-Directory-some-path
PrimaryPackagePurpose: FILE
FilesAnalyzed: false
##### Package: package-2 ##### Package: package-2
PackageName: package-2 PackageName: package-2
@ -38,5 +45,7 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-2
##### Relationships ##### 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 Creator: Tool: syft-v0.42.0-bogus
Created: redacted 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 ##### Package: package-2
PackageName: package-2 PackageName: package-2
@ -38,5 +48,7 @@ ExternalRef: PACKAGE-MANAGER purl a-purl-1
##### Relationships ##### 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" "golang.org/x/exp/slices"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/linux"
@ -77,11 +78,18 @@ func (s SBOM) RelationshipsForPackage(p pkg.Package, rt ...artifact.Relationship
var relationships []artifact.Relationship var relationships []artifact.Relationship
for _, relationship := range s.Relationships { for _, relationship := range s.Relationships {
// check if the relationship is one we're searching for; rt is inclusive if relationship.From == nil || relationship.To == nil {
idx := slices.IndexFunc(rt, func(r artifact.RelationshipType) bool { return relationship.Type == r }) log.Debugf("relationship has nil edge, skipping: %#v", relationship)
if relationship.From.ID() == p.ID() && idx != -1 { continue
relationships = append(relationships, relationship)
} }
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 return relationships

View File

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

View File

@ -5,10 +5,12 @@ import (
"fmt" "fmt"
"github.com/bmatcuk/doublestar/v4" "github.com/bmatcuk/doublestar/v4"
"github.com/docker/distribution/reference"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
"github.com/anchore/stereoscope" "github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver" "github.com/anchore/syft/syft/internal/fileresolver"
@ -83,18 +85,45 @@ func (s StereoscopeImageSource) ID() artifact.ID {
} }
func (s StereoscopeImageSource) Describe() Description { func (s StereoscopeImageSource) Describe() Description {
name := s.metadata.UserInput
version := s.metadata.ManifestDigest
a := s.config.Alias 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{ return Description{
ID: string(s.id), ID: string(s.id),
Name: name, Name: name,