mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
feat: support top-level SPDX package and graph (#1934)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
parent
1e4d26f526
commit
9480f10ccd
1
.gitignore
vendored
1
.gitignore
vendored
@ -20,6 +20,7 @@ VERSION
|
||||
*.hpi
|
||||
*.zip
|
||||
.idea/
|
||||
*.iml
|
||||
*.log
|
||||
.images
|
||||
.tmp/
|
||||
|
||||
2
go.mod
2
go.mod
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user