Skip to content

Commit

Permalink
feat: support top-level SPDX package and graph (#1934)
Browse files Browse the repository at this point in the history
Signed-off-by: Keith Zantow <kzantow@gmail.com>
  • Loading branch information
kzantow authored Jul 26, 2023
1 parent 1e4d26f commit 9480f10
Show file tree
Hide file tree
Showing 20 changed files with 917 additions and 75 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ VERSION
*.hpi
*.zip
.idea/
*.iml
*.log
.images
.tmp/
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions syft/formats/common/spdxhelpers/document_name.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 11 additions & 7 deletions syft/formats/common/spdxhelpers/document_name_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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) {
Expand Down
167 changes: 158 additions & 9 deletions syft/formats/common/spdxhelpers/to_format_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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{
Expand Down Expand Up @@ -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, "/")
Expand All @@ -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/)
Expand Down Expand Up @@ -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))
Expand Down
Loading

0 comments on commit 9480f10

Please sign in to comment.