feat: SPDX 2.3 support (#1311)

This commit is contained in:
Keith Zantow
2022-11-18 08:54:39 -05:00
committed by GitHub
parent 0c4b99c1c2
commit 42cb0a47a4
71 changed files with 892 additions and 1339 deletions
@@ -11,6 +11,12 @@ import (
"github.com/anchore/syft/syft/source"
)
const (
inputImage = "image"
inputDirectory = "dir"
inputFile = "file"
)
func DocumentNameAndNamespace(srcMetadata source.Metadata) (string, string) {
name := DocumentName(srcMetadata)
return name, DocumentNamespace(name, srcMetadata)
@@ -20,11 +26,11 @@ func DocumentNamespace(name string, srcMetadata source.Metadata) string {
input := "unknown-source-type"
switch srcMetadata.Scheme {
case source.ImageScheme:
input = "image"
input = inputImage
case source.DirectoryScheme:
input = "dir"
input = inputDirectory
case source.FileScheme:
input = "file"
input = inputFile
}
uniqueID := uuid.Must(uuid.NewRandom())
@@ -4,7 +4,7 @@ type ReferenceCategory string
const (
SecurityReferenceCategory ReferenceCategory = "SECURITY"
PackageManagerReferenceCategory ReferenceCategory = "PACKAGE_MANAGER"
PackageManagerReferenceCategory ReferenceCategory = "PACKAGE-MANAGER"
OtherReferenceCategory ReferenceCategory = "OTHER"
)
@@ -109,7 +109,11 @@ func Test_Originator(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Originator(test.input))
typ, value := Originator(test.input)
if typ != "" {
value = typ + ": " + value
}
assert.Equal(t, test.expected, value)
})
}
}
+9 -6
View File
@@ -9,9 +9,11 @@ import (
// Originator needs to conform to the SPDX spec here:
// https://spdx.github.io/spdx-spec/package-information/#76-package-originator-field
// Available options are: <omit>, NOASSERTION, Person: <person>, Organization: <org>
func Originator(p pkg.Package) string {
// return values are: <type>, <value>
func Originator(p pkg.Package) (string, string) {
typ := ""
author := ""
if hasMetadata(p) {
author := ""
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
author = metadata.Maintainer
@@ -29,13 +31,14 @@ func Originator(p pkg.Package) string {
author = metadata.Authors[0]
}
case pkg.RpmMetadata:
return "Organization: " + metadata.Vendor
typ = "Organization"
author = metadata.Vendor
case pkg.DpkgMetadata:
author = metadata.Maintainer
}
if author != "" {
return "Person: " + author
if typ == "" && author != "" {
typ = "Person"
}
}
return ""
return typ, author
}
@@ -0,0 +1,461 @@
package spdxhelpers
import (
"fmt"
"sort"
"strings"
"time"
"github.com/spdx/tools-golang/spdx/common"
spdx "github.com/spdx/tools-golang/spdx/v2_3"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"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 (
spdxVersion = "SPDX-2.3"
noAssertion = "NOASSERTION"
)
// ToFormatModel creates and populates a new SPDX document struct that follows the SPDX 2.3
// spec from the given SBOM model.
//
//nolint:funlen
func ToFormatModel(s sbom.SBOM) *spdx.Document {
name, namespace := DocumentNameAndNamespace(s.Source)
return &spdx.Document{
// 6.1: SPDX Version; should be in the format "SPDX-x.x"
// Cardinality: mandatory, one
SPDXVersion: spdxVersion,
// 6.2: Data License; should be "CC0-1.0"
// Cardinality: mandatory, one
DataLicense: "CC0-1.0",
// 6.3: SPDX Identifier; should be "DOCUMENT" to represent mandatory identifier of SPDXRef-DOCUMENT
// Cardinality: mandatory, one
SPDXIdentifier: "DOCUMENT",
// 6.4: Document Name
// Cardinality: mandatory, one
DocumentName: name,
// 6.5: Document Namespace
// Cardinality: mandatory, one
// Purpose: Provide an SPDX document specific namespace as a unique absolute Uniform Resource
// Identifier (URI) as specified in RFC-3986, with the exception of the ‘#’ delimiter. The SPDX
// Document URI cannot contain a URI "part" (e.g. the "#" character), since the ‘#’ is used in SPDX
// element URIs (packages, files, snippets, etc) to separate the document namespace from the
// element’s SPDX identifier. Additionally, a scheme (e.g. “https:”) is required.
// The URI must be unique for the SPDX document including the specific version of the SPDX document.
// If the SPDX document is updated, thereby creating a new version, a new URI for the updated
// document must be used. There can only be one URI for an SPDX document and only one SPDX document
// for a given URI.
// Note that the URI does not have to be accessible. It is only intended to provide a unique ID.
// In many cases, the URI will point to a web accessible document, but this should not be assumed
// to be the case.
DocumentNamespace: namespace,
// 6.6: External Document References
// Cardinality: optional, one or many
ExternalDocumentReferences: nil,
// 6.11: Document Comment
// Cardinality: optional, one
DocumentComment: "",
CreationInfo: &spdx.CreationInfo{
// 6.7: License List Version
// Cardinality: optional, one
LicenseListVersion: spdxlicense.Version,
// 6.8: Creators: may have multiple keys for Person, Organization
// and/or Tool
// Cardinality: mandatory, one or many
Creators: []common.Creator{
{
Creator: "Anchore, Inc",
CreatorType: "Organization",
},
{
Creator: internal.ApplicationName + "-" + s.Descriptor.Version,
CreatorType: "Tool",
},
},
// 6.9: Created: data format YYYY-MM-DDThh:mm:ssZ
// Cardinality: mandatory, one
Created: time.Now().UTC().Format(time.RFC3339),
// 6.10: Creator Comment
// Cardinality: optional, one
CreatorComment: "",
},
Packages: toPackages(s.Artifacts.PackageCatalog),
Files: toFiles(s),
Relationships: toRelationships(s.Relationships),
}
}
func toSPDXID(identifiable artifact.Identifiable) common.ElementID {
id := ""
if p, ok := identifiable.(pkg.Package); ok {
id = SanitizeElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.ID()))
} else {
id = string(identifiable.ID())
}
// NOTE: the spdx libraries prepend SPDXRef-, so we don't do it here
return common.ElementID(id)
}
// packages populates all Package Information from the package Catalog (see https://spdx.github.io/spdx-spec/3-package-information/)
//
//nolint:funlen
func toPackages(catalog *pkg.Catalog) (results []*spdx.Package) {
for _, p := range catalog.Sorted() {
// name should be guaranteed to be unique, but semantically useful and stable
id := toSPDXID(p)
// If the Concluded License is not the same as the Declared License, a written explanation should be provided
// in the Comments on License field (section 7.16). With respect to NOASSERTION, a written explanation in
// the Comments on License field (section 7.16) is preferred.
license := License(p)
checksums, filesAnalyzed := toPackageChecksums(p)
results = append(results, &spdx.Package{
// NOT PART OF SPEC
// flag: does this "package" contain files that were in fact "unpackaged",
// e.g. included directly in the Document without being in a Package?
IsUnpackaged: false,
// 7.1: Package Name
// Cardinality: mandatory, one
PackageName: p.Name,
// 7.2: Package SPDX Identifier: "SPDXRef-[idstring]"
// Cardinality: mandatory, one
PackageSPDXIdentifier: id,
// 7.3: Package Version
// Cardinality: optional, one
PackageVersion: p.Version,
// 7.4: Package File Name
// Cardinality: optional, one
PackageFileName: "",
// 7.5: Package Supplier: may have single result for either Person or Organization,
// or NOASSERTION
// Cardinality: optional, one
// 7.6: Package Originator: may have single result for either Person or Organization,
// or NOASSERTION
// Cardinality: optional, one
PackageSupplier: nil,
PackageOriginator: toPackageOriginator(p),
// 7.7: Package Download Location
// Cardinality: mandatory, one
// NONE if there is no download location whatsoever.
// NOASSERTION if:
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
// (ii) the SPDX file creator has made no attempt to determine this field; or
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
PackageDownloadLocation: DownloadLocation(p),
// 7.8: FilesAnalyzed
// Cardinality: optional, one; default value is "true" if omitted
// Purpose: Indicates whether the file content of this package has been available for or subjected to
// analysis when creating the SPDX document. If false, indicates packages that represent metadata or
// URI references to a project, product, artifact, distribution or a component. If false, the package
// must not contain any files.
// Intent: A package can refer to a project, product, artifact, distribution or a component that is
// external to the SPDX document.
FilesAnalyzed: filesAnalyzed,
// NOT PART OF SPEC: did FilesAnalyzed tag appear?
IsFilesAnalyzedTagPresent: true,
// 7.9: Package Verification Code
// Cardinality: optional, one if filesAnalyzed is true / omitted;
// zero (must be omitted) if filesAnalyzed is false
PackageVerificationCode: nil,
// 7.10: Package Checksum: may have keys for SHA1, SHA256 and/or MD5
// Cardinality: optional, one or many
// 7.10.1 Purpose: Provide an independently reproducible mechanism that permits unique identification of
// a specific package that correlates to the data in this SPDX file. This identifier enables a recipient
// to determine if any file in the original package has been changed. If the SPDX file is to be included
// in a package, this value should not be calculated. The SHA-1 algorithm will be used to provide the
// checksum by default.
PackageChecksums: checksums,
// 7.11: Package Home Page
// Cardinality: optional, one
PackageHomePage: Homepage(p),
// 7.12: Source Information
// Cardinality: optional, one
PackageSourceInfo: SourceInfo(p),
// 7.13: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one
// Purpose: Contain the license the SPDX file creator has concluded as governing the
// package or alternative values, if the governing license cannot be determined.
PackageLicenseConcluded: license,
// 7.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one or many if filesAnalyzed is true / omitted;
// zero (must be omitted) if filesAnalyzed is false
PackageLicenseInfoFromFiles: nil,
// 7.15: Declared License: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one
// Purpose: List the licenses that have been declared by the authors of the package.
// Any license information that does not originate from the package authors, e.g. license
// information from a third party repository, should not be included in this field.
PackageLicenseDeclared: license,
// 7.16: Comments on License
// Cardinality: optional, one
PackageLicenseComments: "",
// 7.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one
// Purpose: IdentifyFormat the copyright holders of the package, as well as any dates present. This will be a free form text field extracted from package information files. The options to populate this field are limited to:
//
// Any text related to a copyright notice, even if not complete;
// NONE if the package contains no copyright information whatsoever; or
// NOASSERTION, if
// (i) the SPDX document creator has made no attempt to determine this field; or
// (ii) the SPDX document creator has intentionally provided no information (no meaning should be implied by doing so).
//
PackageCopyrightText: noAssertion,
// 7.18: Package Summary Description
// Cardinality: optional, one
PackageSummary: "",
// 7.19: Package Detailed Description
// Cardinality: optional, one
PackageDescription: Description(p),
// 7.20: Package Comment
// Cardinality: optional, one
PackageComment: "",
// 7.21: Package External Reference
// Cardinality: optional, one or many
PackageExternalReferences: formatSPDXExternalRefs(p),
// 7.22: Package External Reference Comment
// Cardinality: conditional (optional, one) for each External Reference
// contained within PackageExternalReference2_1 struct, if present
// 7.23: Package Attribution Text
// Cardinality: optional, one or many
PackageAttributionTexts: nil,
})
}
return results
}
func toPackageOriginator(p pkg.Package) *common.Originator {
kind, originator := Originator(p)
if kind == "" || originator == "" {
return nil
}
return &common.Originator{
Originator: originator,
OriginatorType: kind,
}
}
func toPackageChecksums(p pkg.Package) ([]common.Checksum, bool) {
filesAnalyzed := false
var checksums []common.Checksum
switch meta := p.Metadata.(type) {
// we generate digest for some Java packages
// spdx.github.io/spdx-spec/package-information/#710-package-checksum-field
case pkg.JavaMetadata:
if len(meta.ArchiveDigests) > 0 {
filesAnalyzed = true
for _, digest := range meta.ArchiveDigests {
checksums = append(checksums, common.Checksum{
Algorithm: common.ChecksumAlgorithm(digest.Algorithm),
Value: digest.Value,
})
}
}
case pkg.GolangBinMetadata:
algo, hexStr, err := util.HDigestToSHA(meta.H1Digest)
if err != nil {
log.Debugf("invalid h1digest: %s: %v", meta.H1Digest, err)
break
}
algo = strings.ToUpper(algo)
checksums = append(checksums, common.Checksum{
Algorithm: common.ChecksumAlgorithm(algo),
Value: hexStr,
})
}
return checksums, filesAnalyzed
}
func formatSPDXExternalRefs(p pkg.Package) (refs []*spdx.PackageExternalReference) {
for _, ref := range ExternalRefs(p) {
refs = append(refs, &spdx.PackageExternalReference{
Category: string(ref.ReferenceCategory),
RefType: string(ref.ReferenceType),
Locator: ref.ReferenceLocator,
ExternalRefComment: ref.Comment,
})
}
return refs
}
func toRelationships(relationships []artifact.Relationship) (result []*spdx.Relationship) {
for _, r := range relationships {
exists, relationshipType, comment := lookupRelationship(r.Type)
if !exists {
log.Debugf("unable to convert relationship to SPDX, dropping: %+v", r)
continue
}
// FIXME: we are only currently including Package -> * relationships
if _, ok := r.From.(pkg.Package); !ok {
log.Debugf("skipping non-package relationship: %+v", r)
continue
}
result = append(result, &spdx.Relationship{
RefA: common.DocElementID{
ElementRefID: toSPDXID(r.From),
},
Relationship: string(relationshipType),
RefB: common.DocElementID{
ElementRefID: toSPDXID(r.To),
},
RelationshipComment: comment,
})
}
return result
}
func lookupRelationship(ty artifact.RelationshipType) (bool, RelationshipType, string) {
switch ty {
case artifact.ContainsRelationship:
return true, ContainsRelationship, ""
case artifact.OwnershipByFileOverlapRelationship:
return true, OtherRelationship, fmt.Sprintf("%s: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by", ty)
}
return false, "", ""
}
func toFiles(s sbom.SBOM) (results []*spdx.File) {
artifacts := s.Artifacts
for _, coordinates := range s.AllCoordinates() {
var metadata *source.FileMetadata
if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists {
metadata = &metadataForLocation
}
var digests []file.Digest
if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists {
digests = digestsForLocation
}
// TODO: add file classifications (?) and content as a snippet
var comment string
if coordinates.FileSystemID != "" {
comment = fmt.Sprintf("layerID: %s", coordinates.FileSystemID)
}
results = append(results, &spdx.File{
FileSPDXIdentifier: toSPDXID(coordinates),
FileComment: comment,
// required, no attempt made to determine license information
LicenseConcluded: noAssertion,
Checksums: toFileChecksums(digests),
FileName: coordinates.RealPath,
FileTypes: toFileTypes(metadata),
})
}
// sort by real path then virtual path to ensure the result is stable across multiple runs
sort.SliceStable(results, func(i, j int) bool {
if results[i].FileName == results[j].FileName {
return results[i].FileSPDXIdentifier < results[j].FileSPDXIdentifier
}
return results[i].FileName < results[j].FileName
})
return results
}
func toFileChecksums(digests []file.Digest) (checksums []common.Checksum) {
for _, digest := range digests {
checksums = append(checksums, common.Checksum{
Algorithm: toChecksumAlgorithm(digest.Algorithm),
Value: digest.Value,
})
}
return checksums
}
func toChecksumAlgorithm(algorithm string) common.ChecksumAlgorithm {
// this needs to be an uppercase version of our algorithm
return common.ChecksumAlgorithm(strings.ToUpper(algorithm))
}
func toFileTypes(metadata *source.FileMetadata) (ty []string) {
if metadata == nil {
return nil
}
mimeTypePrefix := strings.Split(metadata.MIMEType, "/")[0]
switch mimeTypePrefix {
case "image":
ty = append(ty, string(ImageFileType))
case "video":
ty = append(ty, string(VideoFileType))
case "application":
ty = append(ty, string(ApplicationFileType))
case "text":
ty = append(ty, string(TextFileType))
case "audio":
ty = append(ty, string(AudioFileType))
}
if internal.IsExecutable(metadata.MIMEType) {
ty = append(ty, string(BinaryFileType))
}
if internal.IsArchive(metadata.MIMEType) {
ty = append(ty, string(ArchiveFileType))
}
// TODO: add support for source, spdx, and documentation file types
if len(ty) == 0 {
ty = append(ty, string(OtherFileType))
}
return ty
}
@@ -0,0 +1,337 @@
package spdxhelpers
import (
"fmt"
"testing"
"github.com/spdx/tools-golang/spdx/common"
spdx "github.com/spdx/tools-golang/spdx/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/pkg"
"github.com/anchore/syft/syft/source"
)
func Test_toFileTypes(t *testing.T) {
tests := []struct {
name string
metadata source.FileMetadata
expected []string
}{
{
name: "application",
metadata: source.FileMetadata{
MIMEType: "application/vnd.unknown",
},
expected: []string{
string(ApplicationFileType),
},
},
{
name: "archive",
metadata: source.FileMetadata{
MIMEType: "application/zip",
},
expected: []string{
string(ApplicationFileType),
string(ArchiveFileType),
},
},
{
name: "audio",
metadata: source.FileMetadata{
MIMEType: "audio/ogg",
},
expected: []string{
string(AudioFileType),
},
},
{
name: "video",
metadata: source.FileMetadata{
MIMEType: "video/3gpp",
},
expected: []string{
string(VideoFileType),
},
},
{
name: "text",
metadata: source.FileMetadata{
MIMEType: "text/html",
},
expected: []string{
string(TextFileType),
},
},
{
name: "image",
metadata: source.FileMetadata{
MIMEType: "image/png",
},
expected: []string{
string(ImageFileType),
},
},
{
name: "binary",
metadata: source.FileMetadata{
MIMEType: "application/x-sharedlib",
},
expected: []string{
string(ApplicationFileType),
string(BinaryFileType),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, toFileTypes(&test.metadata))
})
}
}
func Test_lookupRelationship(t *testing.T) {
tests := []struct {
input artifact.RelationshipType
exists bool
ty RelationshipType
comment string
}{
{
input: artifact.ContainsRelationship,
exists: true,
ty: ContainsRelationship,
},
{
input: artifact.OwnershipByFileOverlapRelationship,
exists: true,
ty: OtherRelationship,
comment: "ownership-by-file-overlap: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by",
},
{
input: "made-up",
exists: false,
},
}
for _, test := range tests {
t.Run(string(test.input), func(t *testing.T) {
exists, ty, comment := lookupRelationship(test.input)
assert.Equal(t, exists, test.exists)
assert.Equal(t, ty, test.ty)
assert.Equal(t, comment, test.comment)
})
}
}
func Test_toFileChecksums(t *testing.T) {
tests := []struct {
name string
digests []file.Digest
expected []common.Checksum
}{
{
name: "empty",
},
{
name: "has digests",
digests: []file.Digest{
{
Algorithm: "SHA256",
Value: "deadbeefcafe",
},
{
Algorithm: "md5",
Value: "meh",
},
},
expected: []common.Checksum{
{
Algorithm: "SHA256",
Value: "deadbeefcafe",
},
{
Algorithm: "MD5",
Value: "meh",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, toFileChecksums(test.digests))
})
}
}
func Test_fileIDsForPackage(t *testing.T) {
p := pkg.Package{
Name: "bogus",
}
c := source.Coordinates{
RealPath: "/path",
FileSystemID: "nowhere",
}
docElementId := func(identifiable artifact.Identifiable) common.DocElementID {
return common.DocElementID{
ElementRefID: toSPDXID(identifiable),
}
}
tests := []struct {
name string
relationships []artifact.Relationship
expected []*spdx.Relationship
}{
{
name: "package-to-file contains relationships",
relationships: []artifact.Relationship{
{
From: p,
To: c,
Type: artifact.ContainsRelationship,
},
},
expected: []*spdx.Relationship{
{
Relationship: "CONTAINS",
RefA: docElementId(p),
RefB: docElementId(c),
},
},
},
{
name: "package-to-package",
relationships: []artifact.Relationship{
{
From: p,
To: p,
Type: artifact.ContainsRelationship,
},
},
expected: []*spdx.Relationship{
{
Relationship: "CONTAINS",
RefA: docElementId(p),
RefB: docElementId(p),
},
},
},
{
name: "ignore file-to-file",
relationships: []artifact.Relationship{
{
From: c,
To: c,
Type: artifact.ContainsRelationship,
},
},
expected: nil,
},
{
name: "ignore file-to-package",
relationships: []artifact.Relationship{
{
From: c,
To: p,
Type: artifact.ContainsRelationship,
},
},
expected: nil,
},
{
name: "include package-to-file overlap relationships",
relationships: []artifact.Relationship{
{
From: p,
To: c,
Type: artifact.OwnershipByFileOverlapRelationship,
},
},
expected: []*spdx.Relationship{
{
Relationship: "OTHER",
RefA: docElementId(p),
RefB: docElementId(c),
RelationshipComment: "ownership-by-file-overlap: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
relationships := toRelationships(test.relationships)
assert.Equal(t, test.expected, relationships)
})
}
}
func Test_H1Digest(t *testing.T) {
tests := []struct {
name string
pkg pkg.Package
expectedDigest string
}{
{
name: "valid h1digest",
pkg: pkg.Package{
Name: "github.com/googleapis/gnostic",
Version: "v0.5.5",
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
},
},
expectedDigest: "SHA256:f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
},
{
name: "invalid h1digest",
pkg: pkg.Package{
Name: "github.com/googleapis/gnostic",
Version: "v0.5.5",
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
H1Digest: "h1:9fHAtK0uzzz",
},
},
expectedDigest: "",
},
{
name: "unsupported h-digest",
pkg: pkg.Package{
Name: "github.com/googleapis/gnostic",
Version: "v0.5.5",
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
H1Digest: "h12:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
},
},
expectedDigest: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
catalog := pkg.NewCatalog(test.pkg)
pkgs := toPackages(catalog)
require.Len(t, pkgs, 1)
for _, p := range pkgs {
if test.expectedDigest == "" {
require.Len(t, p.PackageChecksums, 0)
} else {
require.Len(t, p.PackageChecksums, 1)
for _, c := range p.PackageChecksums {
require.Equal(t, test.expectedDigest, fmt.Sprintf("%s:%s", c.Algorithm, c.Value))
}
}
}
})
}
}
@@ -6,7 +6,7 @@ import (
"strconv"
"strings"
"github.com/spdx/tools-golang/spdx"
spdx "github.com/spdx/tools-golang/spdx/v2_3"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
@@ -19,7 +19,7 @@ import (
"github.com/anchore/syft/syft/source"
)
func ToSyftModel(doc *spdx.Document2_2) (*sbom.SBOM, error) {
func ToSyftModel(doc *spdx.Document) (*sbom.SBOM, error) {
if doc == nil {
return nil, errors.New("cannot convert SPDX document to Syft model because document is nil")
}
@@ -27,9 +27,7 @@ func ToSyftModel(doc *spdx.Document2_2) (*sbom.SBOM, error) {
spdxIDMap := make(map[string]interface{})
src := source.Metadata{Scheme: source.UnknownScheme}
if doc.CreationInfo != nil {
src.Scheme = extractSchemeFromNamespace(doc.CreationInfo.DocumentNamespace)
}
src.Scheme = extractSchemeFromNamespace(doc.DocumentNamespace)
s := &sbom.SBOM{
Source: src,
@@ -63,18 +61,18 @@ func extractSchemeFromNamespace(ns string) source.Scheme {
parts := strings.Split(u.Path, "/")
for _, p := range parts {
switch p {
case "file":
case inputFile:
return source.FileScheme
case "image":
case inputImage:
return source.ImageScheme
case "dir":
case inputDirectory:
return source.DirectoryScheme
}
}
return source.UnknownScheme
}
func findLinuxReleaseByPURL(doc *spdx.Document2_2) *linux.Release {
func findLinuxReleaseByPURL(doc *spdx.Document) *linux.Release {
for _, p := range doc.Packages {
purlValue := findPURLValue(p)
if purlValue == "" {
@@ -107,7 +105,7 @@ func findLinuxReleaseByPURL(doc *spdx.Document2_2) *linux.Release {
return nil
}
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document2_2) {
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document) {
for _, p := range doc.Packages {
syftPkg := toSyftPackage(p)
spdxIDMap[string(p.PackageSPDXIdentifier)] = syftPkg
@@ -115,8 +113,8 @@ func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *sp
}
}
func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document2_2) {
for _, f := range doc.UnpackagedFiles {
func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.Document) {
for _, f := range doc.Files {
l := toSyftLocation(f)
spdxIDMap[string(f.FileSPDXIdentifier)] = l
@@ -125,8 +123,8 @@ func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]interface{}, doc *spdx.
}
}
func toFileDigests(f *spdx.File2_2) (digests []file.Digest) {
for _, digest := range f.FileChecksums {
func toFileDigests(f *spdx.File) (digests []file.Digest) {
for _, digest := range f.Checksums {
digests = append(digests, file.Digest{
Algorithm: string(digest.Algorithm),
Value: digest.Value,
@@ -135,9 +133,9 @@ func toFileDigests(f *spdx.File2_2) (digests []file.Digest) {
return digests
}
func toFileMetadata(f *spdx.File2_2) (meta source.FileMetadata) {
func toFileMetadata(f *spdx.File) (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 {
for _, typ := range f.FileTypes {
switch FileType(typ) {
case ImageFileType:
meta.MIMEType = "image/"
@@ -157,11 +155,11 @@ func toFileMetadata(f *spdx.File2_2) (meta source.FileMetadata) {
return meta
}
func toSyftRelationships(spdxIDMap map[string]interface{}, doc *spdx.Document2_2) []artifact.Relationship {
func toSyftRelationships(spdxIDMap map[string]interface{}, 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
if r.RefA.DocumentRefID != "" && requireAndTrimPrefix(r.RefA.DocumentRefID, "DocumentRef-") != string(doc.CreationInfo.SPDXIdentifier) {
if r.RefA.DocumentRefID != "" && requireAndTrimPrefix(r.RefA.DocumentRefID, "DocumentRef-") != string(doc.SPDXIdentifier) {
log.Debugf("ignoring relationship to external document: %+v", r)
continue
}
@@ -205,7 +203,7 @@ func toSyftRelationships(spdxIDMap map[string]interface{}, doc *spdx.Document2_2
return out
}
func toSyftCoordinates(f *spdx.File2_2) source.Coordinates {
func toSyftCoordinates(f *spdx.File) source.Coordinates {
const layerIDPrefix = "layerID: "
var fileSystemID string
if strings.Index(f.FileComment, layerIDPrefix) == 0 {
@@ -220,7 +218,7 @@ func toSyftCoordinates(f *spdx.File2_2) source.Coordinates {
}
}
func toSyftLocation(f *spdx.File2_2) *source.Location {
func toSyftLocation(f *spdx.File) *source.Location {
return &source.Location{
Coordinates: toSyftCoordinates(f),
VirtualPath: f.FileName,
@@ -255,7 +253,7 @@ func findQualifierValue(purl packageurl.PackageURL, qualifier string) string {
return ""
}
func extractPkgInfo(p *spdx.Package2_2) pkgInfo {
func extractPkgInfo(p *spdx.Package) pkgInfo {
pu := findPURLValue(p)
purl, err := packageurl.FromString(pu)
if err != nil {
@@ -268,7 +266,7 @@ func extractPkgInfo(p *spdx.Package2_2) pkgInfo {
}
}
func toSyftPackage(p *spdx.Package2_2) *pkg.Package {
func toSyftPackage(p *spdx.Package) *pkg.Package {
info := extractPkgInfo(p)
metadataType, metadata := extractMetadata(p, info)
sP := pkg.Package{
@@ -289,7 +287,7 @@ func toSyftPackage(p *spdx.Package2_2) *pkg.Package {
}
//nolint:funlen
func extractMetadata(p *spdx.Package2_2, info pkgInfo) (pkg.MetadataType, interface{}) {
func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface{}) {
arch := info.qualifierValue(pkg.PURLQualifierArch)
upstreamValue := info.qualifierValue(pkg.PURLQualifierUpstream)
upstream := strings.SplitN(upstreamValue, "@", 2)
@@ -298,12 +296,20 @@ func extractMetadata(p *spdx.Package2_2, info pkgInfo) (pkg.MetadataType, interf
if len(upstream) > 1 {
upstreamVersion = upstream[1]
}
supplier := ""
if p.PackageSupplier != nil {
supplier = p.PackageSupplier.Supplier
}
originator := ""
if p.PackageOriginator != nil {
originator = p.PackageOriginator.Originator
}
switch info.typ {
case pkg.ApkPkg:
return pkg.ApkMetadataType, pkg.ApkMetadata{
Package: p.PackageName,
OriginPackage: upstreamName,
Maintainer: p.PackageSupplierPerson,
Maintainer: supplier,
Version: p.PackageVersion,
License: p.PackageLicenseDeclared,
Architecture: arch,
@@ -329,7 +335,7 @@ func extractMetadata(p *spdx.Package2_2, info pkgInfo) (pkg.MetadataType, interf
Arch: arch,
SourceRpm: upstreamValue,
License: license,
Vendor: p.PackageOriginatorOrganization,
Vendor: originator,
}
case pkg.DebPkg:
return pkg.DpkgMetadataType, pkg.DpkgMetadata{
@@ -338,12 +344,12 @@ func extractMetadata(p *spdx.Package2_2, info pkgInfo) (pkg.MetadataType, interf
Version: p.PackageVersion,
SourceVersion: upstreamVersion,
Architecture: arch,
Maintainer: p.PackageOriginatorPerson,
Maintainer: originator,
}
case pkg.JavaPkg:
var digests []file.Digest
for algorithm, value := range p.PackageChecksums {
digests = append(digests, file.Digest{Algorithm: string(algorithm), Value: value.Value})
for _, value := range p.PackageChecksums {
digests = append(digests, file.Digest{Algorithm: string(value.Algorithm), Value: value.Value})
}
return pkg.JavaMetadataType, pkg.JavaMetadata{
ArchiveDigests: digests,
@@ -366,7 +372,7 @@ func extractMetadata(p *spdx.Package2_2, info pkgInfo) (pkg.MetadataType, interf
return pkg.UnknownMetadataType, nil
}
func findPURLValue(p *spdx.Package2_2) string {
func findPURLValue(p *spdx.Package) string {
for _, r := range p.PackageExternalReferences {
if r.RefType == string(PurlExternalRefType) {
return r.Locator
@@ -375,7 +381,7 @@ func findPURLValue(p *spdx.Package2_2) string {
return ""
}
func extractCPEs(p *spdx.Package2_2) (cpes []pkg.CPE) {
func extractCPEs(p *spdx.Package) (cpes []pkg.CPE) {
for _, r := range p.PackageExternalReferences {
if r.RefType == string(Cpe23ExternalRefType) {
cpe, err := pkg.NewCPE(r.Locator)
@@ -3,7 +3,8 @@ package spdxhelpers
import (
"testing"
"github.com/spdx/tools-golang/spdx"
"github.com/spdx/tools-golang/spdx/common"
spdx "github.com/spdx/tools-golang/spdx/v2_3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -12,32 +13,27 @@ import (
)
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: "",
sbom, err := ToSyftModel(&spdx.Document{
SPDXVersion: "1",
DataLicense: "GPL",
SPDXIdentifier: "id-doc-1",
DocumentName: "docName",
DocumentNamespace: "docNamespace",
ExternalDocumentReferences: nil,
DocumentComment: "",
CreationInfo: &spdx.CreationInfo{
LicenseListVersion: "",
Created: "",
CreatorComment: "",
},
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{
Packages: []*spdx.Package{
{
PackageName: "pkg-1",
PackageSPDXIdentifier: "id-pkg-1",
PackageVersion: "5.4.3",
PackageLicenseDeclared: "",
PackageDescription: "",
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: "SECURITY",
Locator: "cpe:2.3:a:pkg-1:pkg-1:5.4.3:*:*:*:*:*:*:*",
@@ -49,22 +45,20 @@ func TestToSyftModel(t *testing.T) {
RefType: "cpe23Type",
},
{
Category: "PACKAGE_MANAGER",
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{
{
PackageName: "pkg-2",
PackageSPDXIdentifier: "id-pkg-2",
PackageVersion: "7.3.1",
PackageLicenseDeclared: "",
PackageDescription: "",
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: "SECURITY",
Locator: "cpe:2.3:a:pkg-2:pkg-2:7.3.1:*:*:*:*:*:*:*",
@@ -81,7 +75,7 @@ func TestToSyftModel(t *testing.T) {
RefType: "cpe23Type",
},
{
Category: "PACKAGE_MANAGER",
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",
},
@@ -89,8 +83,7 @@ func TestToSyftModel(t *testing.T) {
Files: nil,
},
},
UnpackagedFiles: map[spdx.ElementID]*spdx.File2_2{},
Relationships: []*spdx.Relationship2_2{},
Relationships: []*spdx.Relationship{},
})
assert.NoError(t, err)
@@ -120,17 +113,17 @@ func TestToSyftModel(t *testing.T) {
func Test_extractMetadata(t *testing.T) {
oneTwoThreeFour := 1234
tests := []struct {
pkg spdx.Package2_2
pkg spdx.Package
metaType pkg.MetadataType
meta interface{}
}{
{
pkg: spdx.Package2_2{
pkg: spdx.Package{
PackageName: "SomeDebPkg",
PackageVersion: "43.1.235",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: "PACKAGE_MANAGER",
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",
},
@@ -146,12 +139,12 @@ func Test_extractMetadata(t *testing.T) {
},
},
{
pkg: spdx.Package2_2{
pkg: spdx.Package{
PackageName: "SomeApkPkg",
PackageVersion: "3.2.9",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: "PACKAGE_MANAGER",
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",
},
@@ -166,12 +159,12 @@ func Test_extractMetadata(t *testing.T) {
},
},
{
pkg: spdx.Package2_2{
pkg: spdx.Package{
PackageName: "SomeRpmPkg",
PackageVersion: "13.2.79",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: "PACKAGE_MANAGER",
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",
},
@@ -238,24 +231,24 @@ func TestExtractSourceFromNamespaces(t *testing.T) {
func TestH1Digest(t *testing.T) {
tests := []struct {
name string
pkg spdx.Package2_2
pkg spdx.Package
expectedDigest string
}{
{
name: "valid h1digest",
pkg: spdx.Package2_2{
pkg: spdx.Package{
PackageName: "github.com/googleapis/gnostic",
PackageVersion: "v0.5.5",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: "PACKAGE_MANAGER",
Category: "PACKAGE-MANAGER",
Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5",
RefType: "purl",
},
},
PackageChecksums: map[spdx.ChecksumAlgorithm]spdx.Checksum{
spdx.SHA256: {
Algorithm: spdx.SHA256,
PackageChecksums: []common.Checksum{
{
Algorithm: common.SHA256,
Value: "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
},
},
@@ -264,19 +257,19 @@ func TestH1Digest(t *testing.T) {
},
{
name: "invalid h1digest algorithm",
pkg: spdx.Package2_2{
pkg: spdx.Package{
PackageName: "github.com/googleapis/gnostic",
PackageVersion: "v0.5.5",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: "PACKAGE_MANAGER",
Category: "PACKAGE-MANAGER",
Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5",
RefType: "purl",
},
},
PackageChecksums: map[spdx.ChecksumAlgorithm]spdx.Checksum{
spdx.SHA256: {
Algorithm: spdx.SHA1,
PackageChecksums: []common.Checksum{
{
Algorithm: common.SHA1,
Value: "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
},
},
@@ -285,19 +278,19 @@ func TestH1Digest(t *testing.T) {
},
{
name: "invalid h1digest digest",
pkg: spdx.Package2_2{
pkg: spdx.Package{
PackageName: "github.com/googleapis/gnostic",
PackageVersion: "v0.5.5",
PackageExternalReferences: []*spdx.PackageExternalReference2_2{
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: "PACKAGE_MANAGER",
Category: "PACKAGE-MANAGER",
Locator: "pkg:golang/github.com/googleapis/gnostic@v0.5.5",
RefType: "purl",
},
},
PackageChecksums: map[spdx.ChecksumAlgorithm]spdx.Checksum{
spdx.SHA256: {
Algorithm: spdx.SHA256,
PackageChecksums: []common.Checksum{
{
Algorithm: common.SHA256,
Value: "",
},
},