Suport SPDX SBOM decoding (#738)

This commit is contained in:
Keith Zantow
2022-02-09 14:11:20 -05:00
committed by GitHub
parent ca081ae5e0
commit 76f8205936
46 changed files with 1679 additions and 297 deletions
@@ -2,6 +2,9 @@ package spdxhelpers
import "github.com/anchore/syft/syft/pkg"
const NONE = "NONE"
const NOASSERTION = "NOASSERTION"
func DownloadLocation(p pkg.Package) string {
// 3.7: Package Download Location
// Cardinality: mandatory, one
@@ -19,5 +22,5 @@ func DownloadLocation(p pkg.Package) string {
return NoneIfEmpty(metadata.URL)
}
}
return "NOASSERTION"
return NOASSERTION
}
@@ -16,7 +16,7 @@ func Test_DownloadLocation(t *testing.T) {
{
name: "no metadata",
input: pkg.Package{},
expected: "NOASSERTION",
expected: NOASSERTION,
},
{
name: "from apk",
@@ -43,7 +43,7 @@ func Test_DownloadLocation(t *testing.T) {
URL: "",
},
},
expected: "NONE",
expected: NONE,
},
}
for _, test := range tests {
@@ -0,0 +1,43 @@
package spdxhelpers
type ReferenceCategory string
const (
SecurityReferenceCategory ReferenceCategory = "SECURITY"
PackageManagerReferenceCategory ReferenceCategory = "PACKAGE_MANAGER"
OtherReferenceCategory ReferenceCategory = "OTHER"
)
// source: https://spdx.github.io/spdx-spec/appendix-VI-external-repository-identifiers/
type ExternalRefType string
const (
// see https://nvd.nist.gov/cpe
Cpe22ExternalRefType ExternalRefType = "cpe22Type"
// see https://nvd.nist.gov/cpe
Cpe23ExternalRefType ExternalRefType = "cpe23Type"
// see http://repo1.maven.org/maven2/
MavenCentralExternalRefType ExternalRefType = "maven-central"
// see https://www.npmjs.com/
NpmExternalRefType ExternalRefType = "npm"
// see https://www.nuget.org/
NugetExternalRefType ExternalRefType = "nuget"
// see http://bower.io/
BowerExternalRefType ExternalRefType = "bower"
// see https://github.com/package-url/purl-spec
PurlExternalRefType ExternalRefType = "purl"
// These point to objects present in the Software Heritage archive by the means of SoftWare Heritage persistent Identifiers (SWHID)
SwhExternalRefType ExternalRefType = "swh"
)
type ExternalRef struct {
Comment string `json:"comment,omitempty"`
// Category for the external reference.
ReferenceCategory ReferenceCategory `json:"referenceCategory"`
// The unique string with no spaces necessary to access the package-specific information, metadata, or content
// within the target location. The format of the locator is subject to constraints defined by the <type>.
ReferenceLocator string `json:"referenceLocator"`
// Type of the external reference. These are defined in an appendix in the SPDX specification.
ReferenceType ExternalRefType `json:"referenceType"`
}
@@ -1,51 +1,26 @@
package spdxhelpers
import (
"github.com/anchore/syft/internal/formats/spdx22json/model"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
)
func ExternalRefs(p pkg.Package) (externalRefs []model.ExternalRef) {
externalRefs = make([]model.ExternalRef, 0)
func ExternalRefs(p pkg.Package) (externalRefs []ExternalRef) {
externalRefs = make([]ExternalRef, 0)
for _, c := range p.CPEs {
externalRefs = append(externalRefs, model.ExternalRef{
ReferenceCategory: model.SecurityReferenceCategory,
externalRefs = append(externalRefs, ExternalRef{
ReferenceCategory: SecurityReferenceCategory,
ReferenceLocator: pkg.CPEString(c),
ReferenceType: model.Cpe23ExternalRefType,
ReferenceType: Cpe23ExternalRefType,
})
}
if p.PURL != "" {
externalRefs = append(externalRefs, model.ExternalRef{
ReferenceCategory: model.PackageManagerReferenceCategory,
externalRefs = append(externalRefs, ExternalRef{
ReferenceCategory: PackageManagerReferenceCategory,
ReferenceLocator: p.PURL,
ReferenceType: model.PurlExternalRefType,
ReferenceType: PurlExternalRefType,
})
}
return externalRefs
}
func ExtractPURL(refs []model.ExternalRef) string {
for _, r := range refs {
if r.ReferenceType == model.PurlExternalRefType {
return r.ReferenceLocator
}
}
return ""
}
func ExtractCPEs(refs []model.ExternalRef) (cpes []pkg.CPE) {
for _, r := range refs {
if r.ReferenceType == model.Cpe23ExternalRefType {
cpe, err := pkg.NewCPE(r.ReferenceLocator)
if err != nil {
log.Warnf("unable to extract SPDX CPE=%q: %+v", r.ReferenceLocator, err)
continue
}
cpes = append(cpes, cpe)
}
}
return cpes
}
@@ -3,7 +3,6 @@ package spdxhelpers
import (
"testing"
"github.com/anchore/syft/internal/formats/spdx22json/model"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
@@ -13,7 +12,7 @@ func Test_ExternalRefs(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected []model.ExternalRef
expected []ExternalRef
}{
{
name: "cpe + purl",
@@ -23,16 +22,16 @@ func Test_ExternalRefs(t *testing.T) {
},
PURL: "a-purl",
},
expected: []model.ExternalRef{
expected: []ExternalRef{
{
ReferenceCategory: model.SecurityReferenceCategory,
ReferenceCategory: SecurityReferenceCategory,
ReferenceLocator: pkg.CPEString(testCPE),
ReferenceType: model.Cpe23ExternalRefType,
ReferenceType: Cpe23ExternalRefType,
},
{
ReferenceCategory: model.PackageManagerReferenceCategory,
ReferenceCategory: PackageManagerReferenceCategory,
ReferenceLocator: "a-purl",
ReferenceType: model.PurlExternalRefType,
ReferenceType: PurlExternalRefType,
},
},
},
@@ -0,0 +1,17 @@
package spdxhelpers
type FileType string
const (
DocumentationFileType FileType = "DOCUMENTATION" // if the file serves as documentation
ImageFileType FileType = "IMAGE" // if the file is associated with a picture image file (MIME type of image/*, e.g., .jpg, .gif)
VideoFileType FileType = "VIDEO" // if the file is associated with a video file type (MIME type of video/*)
ArchiveFileType FileType = "ARCHIVE" // if the file represents an archive (.tar, .jar, etc.)
SpdxFileType FileType = "SPDX" // if the file is an SPDX document
ApplicationFileType FileType = "APPLICATION" // if the file is associated with a specific application type (MIME type of application/*)
SourceFileType FileType = "SOURCE" // if the file is human readable source code (.c, .html, etc.)
BinaryFileType FileType = "BINARY" // if the file is a compiled object, target image or binary executable (.o, .a, etc.)
TextFileType FileType = "TEXT" // if the file is human readable text file (MIME type of text/*)
AudioFileType FileType = "AUDIO" // if the file is associated with an audio file (MIME type of audio/* , e.g. .mp3)
OtherFileType FileType = "OTHER" // if the file doesn't fit into the above categories (generated artifacts, data files, etc.)
)
@@ -18,7 +18,7 @@ func License(p pkg.Package) string {
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
if len(p.Licenses) == 0 {
return "NONE"
return NONE
}
// take all licenses and assume an AND expression; for information about license expressions see https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/
@@ -30,7 +30,7 @@ func License(p pkg.Package) string {
}
if len(parsedLicenses) == 0 {
return "NOASSERTION"
return NOASSERTION
}
return strings.Join(parsedLicenses, " AND ")
@@ -16,7 +16,7 @@ func Test_License(t *testing.T) {
{
name: "no licenses",
input: pkg.Package{},
expected: "NONE",
expected: NONE,
},
{
name: "no SPDX licenses",
@@ -25,7 +25,7 @@ func Test_License(t *testing.T) {
"made-up",
},
},
expected: "NOASSERTION",
expected: NOASSERTION,
},
{
name: "with SPDX license",
@@ -6,7 +6,7 @@ import (
func NoneIfEmpty(value string) string {
if strings.TrimSpace(value) == "" {
return "NONE"
return NONE
}
return value
}
@@ -20,17 +20,17 @@ func Test_noneIfEmpty(t *testing.T) {
{
name: "empty",
value: "",
expected: "NONE",
expected: NONE,
},
{
name: "space",
value: " ",
expected: "NONE",
expected: NONE,
},
{
name: "tab",
value: "\t",
expected: "NONE",
expected: NONE,
},
}
for _, test := range tests {
@@ -0,0 +1,173 @@
package spdxhelpers
// source: https://spdx.github.io/spdx-spec/7-relationships-between-SPDX-elements/
type RelationshipType string
const (
// DescribedByRelationship is to be used when SPDXRef-A is described by SPDXREF-Document.
// Example: The package 'WildFly' is described by SPDX document WildFly.spdx.
DescribedByRelationship RelationshipType = "DESCRIBED_BY"
// ContainsRelationship is to be used when SPDXRef-A contains SPDXRef-B.
// Example: An ARCHIVE file bar.tgz contains a SOURCE file foo.c.
ContainsRelationship RelationshipType = "CONTAINS"
// ContainedByRelationship is to be used when SPDXRef-A is contained by SPDXRef-B.
// Example: A SOURCE file foo.c is contained by ARCHIVE file bar.tgz
ContainedByRelationship RelationshipType = "CONTAINED_BY"
// DependsOnRelationship is to be used when SPDXRef-A depends on SPDXRef-B.
// Example: Package A depends on the presence of package B in order to build and run
DependsOnRelationship RelationshipType = "DEPENDS_ON"
// DependencyOfRelationship is to be used when SPDXRef-A is dependency of SPDXRef-B.
// Example: A is explicitly stated as a dependency of B in a machine-readable file. Use when a package manager does not define scopes.
DependencyOfRelationship RelationshipType = "DEPENDENCY_OF"
// DependencyManifestOfRelationship is to be used when SPDXRef-A is a manifest file that lists a set of dependencies for SPDXRef-B.
// Example: A file package.json is the dependency manifest of a package foo. Note that only one manifest should be used to define the same dependency graph.
DependencyManifestOfRelationship RelationshipType = "DEPENDENCY_MANIFEST_OF"
// BuildDependencyOfRelationship is to be used when SPDXRef-A is a build dependency of SPDXRef-B.
// Example: A is in the compile scope of B in a Maven project.
BuildDependencyOfRelationship RelationshipType = "BUILD_DEPENDENCY_OF"
// DevDependencyOfRelationship is to be used when SPDXRef-A is a development dependency of SPDXRef-B.
// Example: A is in the devDependencies scope of B in a Maven project.
DevDependencyOfRelationship RelationshipType = "DEV_DEPENDENCY_OF"
// OptionalDependencyOfRelationship is to be used when SPDXRef-A is an optional dependency of SPDXRef-B.
// Example: Use when building the code will proceed even if a dependency cannot be found, fails to install, or is only installed on a specific platform. For example, A is in the optionalDependencies scope of npm project B.
OptionalDependencyOfRelationship RelationshipType = "OPTIONAL_DEPENDENCY_OF"
// ProvidedDependencyOfRelationship is to be used when SPDXRef-A is a to be provided dependency of SPDXRef-B.
// Example: A is in the provided scope of B in a Maven project, indicating that the project expects it to be provided, for instance, by the container or JDK.
ProvidedDependencyOfRelationship RelationshipType = "PROVIDED_DEPENDENCY_OF"
// TestDependencyOfRelationship is to be used when SPDXRef-A is a test dependency of SPDXRef-B.
// Example: A is in the test scope of B in a Maven project.
TestDependencyOfRelationship RelationshipType = "TEST_DEPENDENCY_OF"
// RuntimeDependencyOfRelationship is to be used when SPDXRef-A is a dependency required for the execution of SPDXRef-B.
// Example: A is in the runtime scope of B in a Maven project.
RuntimeDependencyOfRelationship RelationshipType = "RUNTIME_DEPENDENCY_OF"
// ExampleOfRelationship is to be used when SPDXRef-A is an example of SPDXRef-B.
// Example: The file or snippet that illustrates how to use an application or library.
ExampleOfRelationship RelationshipType = "EXAMPLE_OF"
// GeneratesRelationship is to be used when SPDXRef-A generates SPDXRef-B.
// Example: A SOURCE file makefile.mk generates a BINARY file a.out
GeneratesRelationship RelationshipType = "GENERATES"
// GeneratedFromRelationship is to be used when SPDXRef-A was generated from SPDXRef-B.
// Example: A BINARY file a.out has been generated from a SOURCE file makefile.mk. A BINARY file foolib.a is generated from a SOURCE file bar.c.
GeneratedFromRelationship RelationshipType = "GENERATED_FROM"
// AncestorOfRelationship is to be used when SPDXRef-A is an ancestor (same lineage but pre-dates) SPDXRef-B.
// Example: A SOURCE file makefile.mk is a version of the original ancestor SOURCE file 'makefile2.mk'
AncestorOfRelationship RelationshipType = "ANCESTOR_OF"
// DescendantOfRelationship is to be used when SPDXRef-A is a descendant of (same lineage but postdates) SPDXRef-B.
// Example: A SOURCE file makefile2.mk is a descendant of the original SOURCE file 'makefile.mk'
DescendantOfRelationship RelationshipType = "DESCENDANT_OF"
// VariantOfRelationship is to be used when SPDXRef-A is a variant of (same lineage but not clear which came first) SPDXRef-B.
// Example: A SOURCE file makefile2.mk is a variant of SOURCE file makefile.mk if they differ by some edit, but there is no way to tell which came first (no reliable date information).
VariantOfRelationship RelationshipType = "VARIANT_OF"
// DistributionArtifactRelationship is to be used when distributing SPDXRef-A requires that SPDXRef-B also be distributed.
// Example: A BINARY file foo.o requires that the ARCHIVE file bar-sources.tgz be made available on distribution.
DistributionArtifactRelationship RelationshipType = "DISTRIBUTION_ARTIFACT"
// PatchForRelationship is to be used when SPDXRef-A is a patch file for (to be applied to) SPDXRef-B.
// Example: A SOURCE file foo.diff is a patch file for SOURCE file foo.c.
PatchForRelationship RelationshipType = "PATCH_FOR"
// PatchAppliedRelationship is to be used when SPDXRef-A is a patch file that has been applied to SPDXRef-B.
// Example: A SOURCE file foo.diff is a patch file that has been applied to SOURCE file 'foo-patched.c'.
PatchAppliedRelationship RelationshipType = "PATCH_APPLIED"
// CopyOfRelationship is to be used when SPDXRef-A is an exact copy of SPDXRef-B.
// Example: A BINARY file alib.a is an exact copy of BINARY file a2lib.a.
CopyOfRelationship RelationshipType = "COPY_OF"
// FileAddedRelationship is to be used when SPDXRef-A is a file that was added to SPDXRef-B.
// Example: A SOURCE file foo.c has been added to package ARCHIVE bar.tgz.
FileAddedRelationship RelationshipType = "FILE_ADDED"
// FileDeletedRelationship is to be used when SPDXRef-A is a file that was deleted from SPDXRef-B.
// Example: A SOURCE file foo.diff has been deleted from package ARCHIVE bar.tgz.
FileDeletedRelationship RelationshipType = "FILE_DELETED"
// FileModifiedRelationship is to be used when SPDXRef-A is a file that was modified from SPDXRef-B.
// Example: A SOURCE file foo.c has been modified from SOURCE file foo.orig.c.
FileModifiedRelationship RelationshipType = "FILE_MODIFIED"
// ExpandedFromArchiveRelationship is to be used when SPDXRef-A is expanded from the archive SPDXRef-B.
// Example: A SOURCE file foo.c, has been expanded from the archive ARCHIVE file xyz.tgz.
ExpandedFromArchiveRelationship RelationshipType = "EXPANDED_FROM_ARCHIVE"
// DynamicLinkRelationship is to be used when SPDXRef-A dynamically links to SPDXRef-B.
// Example: An APPLICATION file 'myapp' dynamically links to BINARY file zlib.so.
DynamicLinkRelationship RelationshipType = "DYNAMIC_LINK"
// StaticLinkRelationship is to be used when SPDXRef-A statically links to SPDXRef-B.
// Example: An APPLICATION file 'myapp' statically links to BINARY zlib.a.
StaticLinkRelationship RelationshipType = "STATIC_LINK"
// DataFileOfRelationship is to be used when SPDXRef-A is a data file used in SPDXRef-B.
// Example: An IMAGE file 'kitty.jpg' is a data file of an APPLICATION 'hellokitty'.
DataFileOfRelationship RelationshipType = "DATA_FILE_OF"
// TestCaseOfRelationship is to be used when SPDXRef-A is a test case used in testing SPDXRef-B.
// Example: A SOURCE file testMyCode.java is a unit test file used to test an APPLICATION MyPackage.
TestCaseOfRelationship RelationshipType = "TEST_CASE_OF"
// BuildToolOfRelationship is to be used when SPDXRef-A is used to build SPDXRef-B.
// Example: A SOURCE file makefile.mk is used to build an APPLICATION 'zlib'.
BuildToolOfRelationship RelationshipType = "BUILD_TOOL_OF"
// DevToolOfRelationship is to be used when SPDXRef-A is used as a development tool for SPDXRef-B.
// Example: Any tool used for development such as a code debugger.
DevToolOfRelationship RelationshipType = "DEV_TOOL_OF"
// TestOfRelationship is to be used when SPDXRef-A is used for testing SPDXRef-B.
// Example: Generic relationship for cases where it's clear that something is used for testing but unclear whether it's TEST_CASE_OF or TEST_TOOL_OF.
TestOfRelationship RelationshipType = "TEST_OF"
// TestToolOfRelationship is to be used when SPDXRef-A is used as a test tool for SPDXRef-B.
// Example: Any tool used to test the code such as ESlint.
TestToolOfRelationship RelationshipType = "TEST_TOOL_OF"
// DocumentationOfRelationship is to be used when SPDXRef-A provides documentation of SPDXRef-B.
// Example: A DOCUMENTATION file readme.txt documents the APPLICATION 'zlib'.
DocumentationOfRelationship RelationshipType = "DOCUMENTATION_OF"
// OptionalComponentOfRelationship is to be used when SPDXRef-A is an optional component of SPDXRef-B.
// Example: A SOURCE file fool.c (which is in the contributors directory) may or may not be included in the build of APPLICATION 'atthebar'.
OptionalComponentOfRelationship RelationshipType = "OPTIONAL_COMPONENT_OF"
// MetafileOfRelationship is to be used when SPDXRef-A is a metafile of SPDXRef-B.
// Example: A SOURCE file pom.xml is a metafile of the APPLICATION 'Apache Xerces'.
MetafileOfRelationship RelationshipType = "METAFILE_OF"
// PackageOfRelationship is to be used when SPDXRef-A is used as a package as part of SPDXRef-B.
// Example: A Linux distribution contains an APPLICATION package gawk as part of the distribution MyLinuxDistro.
PackageOfRelationship RelationshipType = "PACKAGE_OF"
// AmendsRelationship is to be used when (current) SPDXRef-DOCUMENT amends the SPDX information in SPDXRef-B.
// Example: (Current) SPDX document A version 2 contains a correction to a previous version of the SPDX document A version 1. Note the reserved identifier SPDXRef-DOCUMENT for the current document is required.
AmendsRelationship RelationshipType = "AMENDS"
// PrerequisiteForRelationship is to be used when SPDXRef-A is a prerequisite for SPDXRef-B.
// Example: A library bar.dll is a prerequisite or dependency for APPLICATION foo.exe
PrerequisiteForRelationship RelationshipType = "PREREQUISITE_FOR"
// HasPrerequisiteRelationship is to be used when SPDXRef-A has as a prerequisite SPDXRef-B.
// Example: An APPLICATION foo.exe has prerequisite or dependency on bar.dll
HasPrerequisiteRelationship RelationshipType = "HAS_PREREQUISITE"
// OtherRelationship is to be used for a relationship which has not been defined in the formal SPDX specification. A description of the relationship should be included in the Relationship comments field.
OtherRelationship RelationshipType = "OTHER"
)
@@ -0,0 +1,339 @@
package spdxhelpers
import (
"strconv"
"strings"
"github.com/spdx/tools-golang/spdx"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
func ToSyftModel(doc *spdx.Document2_2) (*sbom.SBOM, error) {
spdxIDMap := make(map[string]interface{})
s := &sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: pkg.NewCatalog(),
FileMetadata: map[source.Coordinates]source.FileMetadata{},
FileDigests: map[source.Coordinates][]file.Digest{},
LinuxDistribution: findLinuxReleaseByPURL(doc),
},
}
collectSyftPackages(s, spdxIDMap, doc)
collectSyftFiles(s, spdxIDMap, doc)
s.Relationships = toSyftRelationships(spdxIDMap, doc)
return s, nil
}
func findLinuxReleaseByPURL(doc *spdx.Document2_2) *linux.Release {
for _, p := range doc.Packages {
purlValue := findPURLValue(p)
if purlValue == "" {
continue
}
purl, err := packageurl.FromString(purlValue)
if err != nil {
log.Warnf("unable to parse purl: %s", purlValue)
continue
}
distro := findQualifierValue(purl, pkg.PURLQualifierDistro)
if distro != "" {
parts := strings.Split(distro, "-")
name := parts[0]
version := ""
if len(parts) > 1 {
version = parts[1]
}
return &linux.Release{
PrettyName: name,
Name: name,
ID: name,
IDLike: []string{name},
Version: version,
VersionID: version,
}
}
}
return nil
}
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document2_2) {
for _, p := range doc.Packages {
syftPkg := toSyftPackage(p)
spdxIDMap[string(p.PackageSPDXIdentifier)] = syftPkg
s.Artifacts.PackageCatalog.Add(*syftPkg)
}
}
func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document2_2) {
for _, f := range doc.UnpackagedFiles {
l := toSyftLocation(f)
spdxIDMap[string(f.FileSPDXIdentifier)] = l
s.Artifacts.FileMetadata[l.Coordinates] = toFileMetadata(f)
s.Artifacts.FileDigests[l.Coordinates] = toFileDigests(f)
}
}
func toFileDigests(f *spdx.File2_2) (digests []file.Digest) {
for _, digest := range f.FileChecksums {
digests = append(digests, file.Digest{
Algorithm: string(digest.Algorithm),
Value: digest.Value,
})
}
return digests
}
func toFileMetadata(f *spdx.File2_2) (meta source.FileMetadata) {
// FIXME Syft is currently lossy due to the SPDX 2.2.1 spec not supporting arbitrary mimetypes
for _, typ := range f.FileType {
switch FileType(typ) {
case ImageFileType:
meta.MIMEType = "image/"
case VideoFileType:
meta.MIMEType = "video/"
case ApplicationFileType:
meta.MIMEType = "application/"
case TextFileType:
meta.MIMEType = "text/"
case AudioFileType:
meta.MIMEType = "audio/"
case BinaryFileType:
case ArchiveFileType:
case OtherFileType:
}
}
return meta
}
func toSyftRelationships(spdxIDMap map[string]interface{}, doc *spdx.Document2_2) []artifact.Relationship {
var out []artifact.Relationship
for _, r := range doc.Relationships {
// FIXME what to do with r.RefA.DocumentRefID and r.RefA.SpecialID
if r.RefA.DocumentRefID != "" && requireAndTrimPrefix(r.RefA.DocumentRefID, "DocumentRef-") != string(doc.CreationInfo.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.(*source.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)
continue
}
var to artifact.Identifiable
var typ artifact.RelationshipType
if toLocationOk {
if r.Relationship == string(ContainsRelationship) {
typ = artifact.ContainsRelationship
to = toLocation
}
} else {
switch RelationshipType(r.Relationship) {
case ContainsRelationship:
typ = artifact.ContainsRelationship
to = toPackage
case BuildDependencyOfRelationship:
typ = artifact.BuildDependencyOfRelationship
to = toPackage
case RuntimeDependencyOfRelationship:
typ = artifact.RuntimeDependencyOfRelationship
to = toPackage
case OtherRelationship:
// Encoding uses a specifically formatted comment...
if strings.Index(r.RelationshipComment, string(artifact.OwnershipByFileOverlapRelationship)) == 0 {
typ = artifact.RuntimeDependencyOfRelationship
to = toPackage
}
}
}
if typ != "" && to != nil {
out = append(out, artifact.Relationship{
From: from,
To: to,
Type: typ,
})
}
}
return out
}
func toSyftCoordinates(f *spdx.File2_2) source.Coordinates {
const layerIDPrefix = "layerID: "
var fileSystemID string
if strings.Index(f.FileComment, layerIDPrefix) == 0 {
fileSystemID = strings.TrimPrefix(f.FileComment, layerIDPrefix)
}
if strings.Index(string(f.FileSPDXIdentifier), layerIDPrefix) == 0 {
fileSystemID = strings.TrimPrefix(string(f.FileSPDXIdentifier), layerIDPrefix)
}
return source.Coordinates{
RealPath: f.FileName,
FileSystemID: fileSystemID,
}
}
func toSyftLocation(f *spdx.File2_2) *source.Location {
return &source.Location{
Coordinates: toSyftCoordinates(f),
VirtualPath: f.FileName,
}
}
func requireAndTrimPrefix(val interface{}, prefix string) string {
if v, ok := val.(string); ok {
if i := strings.Index(v, prefix); i == 0 {
return strings.Replace(v, prefix, "", 1)
}
}
return ""
}
type pkgInfo struct {
purl packageurl.PackageURL
typ pkg.Type
lang pkg.Language
}
func (p *pkgInfo) qualifierValue(name string) string {
return findQualifierValue(p.purl, name)
}
func findQualifierValue(purl packageurl.PackageURL, qualifier string) string {
for _, q := range purl.Qualifiers {
if q.Key == qualifier {
return q.Value
}
}
return ""
}
func extractPkgInfo(p *spdx.Package2_2) pkgInfo {
pu := findPURLValue(p)
purl, err := packageurl.FromString(pu)
if err != nil {
return pkgInfo{}
}
return pkgInfo{
purl,
pkg.TypeByName(purl.Type),
pkg.LanguageByName(purl.Type),
}
}
func toSyftPackage(p *spdx.Package2_2) *pkg.Package {
info := extractPkgInfo(p)
metadataType, metadata := extractMetadata(p, info)
sP := pkg.Package{
Type: info.typ,
Name: p.PackageName,
Version: p.PackageVersion,
Licenses: parseLicense(p.PackageLicenseDeclared),
CPEs: extractCPEs(p),
PURL: info.purl.String(),
Language: info.lang,
MetadataType: metadataType,
Metadata: metadata,
}
sP.SetID()
return &sP
}
func extractMetadata(p *spdx.Package2_2, info pkgInfo) (pkg.MetadataType, interface{}) {
arch := info.qualifierValue(pkg.PURLQualifierArch)
upstreamValue := info.qualifierValue(pkg.PURLQualifierUpstream)
upstream := strings.SplitN(upstreamValue, "@", 2)
upstreamName := upstream[0]
upstreamVersion := ""
if len(upstream) > 1 {
upstreamVersion = upstream[1]
}
switch info.typ {
case pkg.ApkPkg:
return pkg.ApkMetadataType, pkg.ApkMetadata{
Package: p.PackageName,
OriginPackage: upstreamName,
Maintainer: p.PackageSupplierPerson,
Version: p.PackageVersion,
License: p.PackageLicenseDeclared,
Architecture: arch,
URL: p.PackageHomePage,
Description: p.PackageDescription,
}
case pkg.RpmPkg:
converted, err := strconv.Atoi(info.qualifierValue(pkg.PURLQualifierEpoch))
var epoch *int
if err != nil {
epoch = nil
} else {
epoch = &converted
}
return pkg.RpmdbMetadataType, pkg.RpmdbMetadata{
Name: p.PackageName,
Version: p.PackageVersion,
Epoch: epoch,
Arch: arch,
SourceRpm: upstreamValue,
License: p.PackageLicenseConcluded,
Vendor: p.PackageOriginatorOrganization,
}
case pkg.DebPkg:
return pkg.DpkgMetadataType, pkg.DpkgMetadata{
Package: p.PackageName,
Source: upstreamName,
Version: p.PackageVersion,
SourceVersion: upstreamVersion,
Architecture: arch,
Maintainer: p.PackageOriginatorPerson,
}
}
return pkg.UnknownMetadataType, nil
}
func findPURLValue(p *spdx.Package2_2) string {
for _, r := range p.PackageExternalReferences {
if r.RefType == string(PurlExternalRefType) {
return r.Locator
}
}
return ""
}
func extractCPEs(p *spdx.Package2_2) (cpes []pkg.CPE) {
for _, r := range p.PackageExternalReferences {
if r.RefType == string(Cpe23ExternalRefType) {
cpe, err := pkg.NewCPE(r.Locator)
if err != nil {
log.Warnf("unable to extract SPDX CPE=%q: %+v", r.Locator, err)
continue
}
cpes = append(cpes, cpe)
}
}
return cpes
}
func parseLicense(l string) []string {
if l == NOASSERTION || l == NONE {
return nil
}
return strings.Split(l, " AND ")
}
@@ -0,0 +1,197 @@
package spdxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/spdx/tools-golang/spdx"
"github.com/stretchr/testify/assert"
)
func TestToSyftModel(t *testing.T) {
sbom, err := ToSyftModel(&spdx.Document2_2{
CreationInfo: &spdx.CreationInfo2_2{
SPDXVersion: "1",
DataLicense: "GPL",
SPDXIdentifier: "id-doc-1",
DocumentName: "docName",
DocumentNamespace: "docNamespace",
ExternalDocumentReferences: nil,
LicenseListVersion: "",
CreatorPersons: nil,
CreatorOrganizations: nil,
CreatorTools: nil,
Created: "",
CreatorComment: "",
DocumentComment: "",
},
Packages: map[spdx.ElementID]*spdx.Package2_2{
"id-pkg-1": {
PackageName: "pkg-1",
PackageSPDXIdentifier: "id-pkg-1",
PackageVersion: "5.4.3",
PackageSupplierPerson: "",
PackageSupplierOrganization: "",
PackageLicenseDeclared: "",
PackageDescription: "",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
{
Category: "SECURITY",
Locator: "cpe:2.3:a:pkg-1:pkg-1:5.4.3:*:*:*:*:*:*:*",
RefType: "cpe23Type",
},
{
Category: "SECURITY",
Locator: "cpe:2.3:a:pkg_1:pkg_1:5.4.3:*:*:*:*:*:*:*",
RefType: "cpe23Type",
},
{
Category: "PACKAGE_MANAGER",
Locator: "pkg:alpine/pkg-1@5.4.3?arch=x86_64&upstream=p1-origin&distro=alpine-3.10.9",
RefType: "purl",
},
},
Files: nil,
},
"id-pkg-2": {
PackageName: "pkg-2",
PackageSPDXIdentifier: "id-pkg-2",
PackageVersion: "7.3.1",
PackageSupplierPerson: "",
PackageSupplierOrganization: "",
PackageLicenseDeclared: "",
PackageDescription: "",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
{
Category: "SECURITY",
Locator: "cpe:2.3:a:pkg-2:pkg-2:7.3.1:*:*:*:*:*:*:*",
RefType: "cpe23Type",
},
{
Category: "SECURITY",
Locator: "cpe:2.3:a:pkg_2:pkg_2:7.3.1:*:*:*:*:*:*:*",
RefType: "cpe23Type",
},
{
Category: "SECURITY",
Locator: "cpe:2.3:a:pkg-2:pkg_2:7.3.1:*:*:*:*:*:*:*",
RefType: "cpe23Type",
},
{
Category: "PACKAGE_MANAGER",
Locator: "pkg:deb/pkg-2@7.3.1?arch=x86_64&upstream=p2-origin@9.1.3&distro=debian-3.10.9",
RefType: "purl",
},
},
Files: nil,
},
},
UnpackagedFiles: map[spdx.ElementID]*spdx.File2_2{},
Relationships: []*spdx.Relationship2_2{},
})
assert.NoError(t, err)
assert.NotNil(t, sbom)
pkgs := sbom.Artifacts.PackageCatalog.Sorted()
assert.Len(t, pkgs, 2)
p1 := pkgs[0]
assert.Equal(t, p1.Name, "pkg-1")
assert.Equal(t, p1.MetadataType, pkg.ApkMetadataType)
p1meta := p1.Metadata.(pkg.ApkMetadata)
assert.Equal(t, p1meta.OriginPackage, "p1-origin")
assert.Len(t, p1.CPEs, 2)
p2 := pkgs[1]
assert.Equal(t, p2.Name, "pkg-2")
assert.Equal(t, p2.MetadataType, pkg.DpkgMetadataType)
p2meta := p2.Metadata.(pkg.DpkgMetadata)
assert.Equal(t, p2meta.Source, "p2-origin")
assert.Equal(t, p2meta.SourceVersion, "9.1.3")
assert.Len(t, p2.CPEs, 3)
}
func Test_extractMetadata(t *testing.T) {
oneTwoThreeFour := 1234
tests := []struct {
pkg spdx.Package2_2
metaType pkg.MetadataType
meta interface{}
}{
{
pkg: spdx.Package2_2{
PackageName: "SomeDebPkg",
PackageVersion: "43.1.235",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
{
Category: "PACKAGE_MANAGER",
Locator: "pkg:deb/pkg-2@7.3.1?arch=x86_64&upstream=somedebpkg-origin@9.1.3&distro=debian-3.10.9",
RefType: "purl",
},
},
},
metaType: pkg.DpkgMetadataType,
meta: pkg.DpkgMetadata{
Package: "SomeDebPkg",
Source: "somedebpkg-origin",
Version: "43.1.235",
SourceVersion: "9.1.3",
Architecture: "x86_64",
},
},
{
pkg: spdx.Package2_2{
PackageName: "SomeApkPkg",
PackageVersion: "3.2.9",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
{
Category: "PACKAGE_MANAGER",
Locator: "pkg:alpine/pkg-2@7.3.1?arch=x86_64&upstream=apk-origin@9.1.3&distro=alpine-3.10.9",
RefType: "purl",
},
},
},
metaType: pkg.ApkMetadataType,
meta: pkg.ApkMetadata{
Package: "SomeApkPkg",
OriginPackage: "apk-origin",
Version: "3.2.9",
Architecture: "x86_64",
},
},
{
pkg: spdx.Package2_2{
PackageName: "SomeRpmPkg",
PackageVersion: "13.2.79",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
{
Category: "PACKAGE_MANAGER",
Locator: "pkg:rpm/pkg-2@7.3.1?arch=x86_64&epoch=1234&upstream=some-rpm-origin-1.16.3&distro=alpine-3.10.9",
RefType: "purl",
},
},
},
metaType: pkg.RpmdbMetadataType,
meta: pkg.RpmdbMetadata{
Name: "SomeRpmPkg",
Version: "13.2.79",
Epoch: &oneTwoThreeFour,
Arch: "x86_64",
Release: "",
SourceRpm: "some-rpm-origin-1.16.3",
},
},
}
for _, test := range tests {
t.Run(test.pkg.PackageName, func(t *testing.T) {
info := extractPkgInfo(&test.pkg)
metaType, meta := extractMetadata(&test.pkg, info)
assert.Equal(t, test.metaType, metaType)
assert.EqualValues(t, test.meta, meta)
})
}
}