refactor(sbom): use intermediate representation for SPDX (#6310)

Signed-off-by: knqyf263 <knqyf263@gmail.com>
Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
This commit is contained in:
Teppei Fukuda
2024-03-18 12:52:11 +04:00
committed by GitHub
parent 71da44f7e1
commit ab74caa87f
21 changed files with 1113 additions and 947 deletions

View File

@@ -3,7 +3,7 @@
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "testdata/fixtures/repo/conda",
"documentNamespace": "http://aquasecurity.github.io/trivy/filesystem/testdata/fixtures/repo/conda-3ff14136-e09f-4df9-80ea-000000000001",
"documentNamespace": "http://aquasecurity.github.io/trivy/filesystem/testdata/fixtures/repo/conda-3ff14136-e09f-4df9-80ea-000000000004",
"creationInfo": {
"creators": [
"Organization: aquasecurity",
@@ -12,17 +12,9 @@
"created": "2021-08-25T12:20:30Z"
},
"packages": [
{
"name": "conda-pkg",
"SPDXID": "SPDXRef-Application-ee5ef1aa4ac89125",
"downloadLocation": "NONE",
"filesAnalyzed": false,
"sourceInfo": "Conda",
"primaryPackagePurpose": "APPLICATION"
},
{
"name": "openssl",
"SPDXID": "SPDXRef-Package-20b95c21bfbf9fc4",
"SPDXID": "SPDXRef-Package-b8061a5279413d55",
"versionInfo": "1.1.1q",
"supplier": "NOASSERTION",
"downloadLocation": "NONE",
@@ -39,11 +31,14 @@
"referenceLocator": "pkg:conda/openssl@1.1.1q"
}
],
"attributionTexts": [
"PkgType: conda-pkg"
],
"primaryPackagePurpose": "LIBRARY"
},
{
"name": "pip",
"SPDXID": "SPDXRef-Package-11a429ec3bd01d80",
"SPDXID": "SPDXRef-Package-84198b3828050c11",
"versionInfo": "22.2.2",
"supplier": "NOASSERTION",
"downloadLocation": "NONE",
@@ -60,6 +55,9 @@
"referenceLocator": "pkg:conda/pip@22.2.2"
}
],
"attributionTexts": [
"PkgType: conda-pkg"
],
"primaryPackagePurpose": "LIBRARY"
},
{
@@ -105,28 +103,23 @@
},
{
"spdxElementId": "SPDXRef-Filesystem-2e2426fd0f2580ef",
"relatedSpdxElement": "SPDXRef-Application-ee5ef1aa4ac89125",
"relatedSpdxElement": "SPDXRef-Package-84198b3828050c11",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Application-ee5ef1aa4ac89125",
"relatedSpdxElement": "SPDXRef-Package-20b95c21bfbf9fc4",
"spdxElementId": "SPDXRef-Filesystem-2e2426fd0f2580ef",
"relatedSpdxElement": "SPDXRef-Package-b8061a5279413d55",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-20b95c21bfbf9fc4",
"relatedSpdxElement": "SPDXRef-File-600e5e0110a84891",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Application-ee5ef1aa4ac89125",
"relatedSpdxElement": "SPDXRef-Package-11a429ec3bd01d80",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-11a429ec3bd01d80",
"spdxElementId": "SPDXRef-Package-84198b3828050c11",
"relatedSpdxElement": "SPDXRef-File-7eb62e2a3edddc0a",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-b8061a5279413d55",
"relatedSpdxElement": "SPDXRef-File-600e5e0110a84891",
"relationshipType": "CONTAINS"
}
]
}

View File

@@ -286,7 +286,7 @@
"bom-ref": "pkg:deb/debian/bsdutils@2.33.1-0.1?arch=amd64&distro=debian-10.2&epoch=1",
"type": "library",
"name": "bsdutils",
"version": "2.33.1-0.1",
"version": "1:2.33.1-0.1",
"licenses": [
{
"license": {
@@ -628,7 +628,7 @@
"bom-ref": "pkg:deb/debian/diffutils@3.7-3?arch=amd64&distro=debian-10.2&epoch=1",
"type": "library",
"name": "diffutils",
"version": "3.7-3",
"version": "1:3.7-3",
"licenses": [
{
"license": {
@@ -1338,7 +1338,7 @@
"bom-ref": "pkg:deb/debian/libattr1@2.4.48-4?arch=amd64&distro=debian-10.2&epoch=1",
"type": "library",
"name": "libattr1",
"version": "2.4.48-4",
"version": "1:2.4.48-4",
"licenses": [
{
"license": {
@@ -1396,7 +1396,7 @@
"bom-ref": "pkg:deb/debian/libaudit-common@2.8.4-3?arch=all&distro=debian-10.2&epoch=1",
"type": "library",
"name": "libaudit-common",
"version": "2.8.4-3",
"version": "1:2.8.4-3",
"licenses": [
{
"license": {
@@ -1454,7 +1454,7 @@
"bom-ref": "pkg:deb/debian/libaudit1@2.8.4-3?arch=amd64&distro=debian-10.2&epoch=1",
"type": "library",
"name": "libaudit1",
"version": "2.8.4-3",
"version": "1:2.8.4-3",
"licenses": [
{
"license": {
@@ -2091,7 +2091,7 @@
"bom-ref": "pkg:deb/debian/libgcc1@8.3.0-6?arch=amd64&distro=debian-10.2&epoch=1",
"type": "library",
"name": "libgcc1",
"version": "8.3.0-6",
"version": "1:8.3.0-6",
"purl": "pkg:deb/debian/libgcc1@8.3.0-6?arch=amd64&distro=debian-10.2&epoch=1",
"properties": [
{
@@ -2285,7 +2285,7 @@
"bom-ref": "pkg:deb/debian/libgmp10@6.1.2%2Bdfsg-4?arch=amd64&distro=debian-10.2&epoch=2",
"type": "library",
"name": "libgmp10",
"version": "6.1.2+dfsg-4",
"version": "2:6.1.2+dfsg-4",
"licenses": [
{
"license": {
@@ -3286,7 +3286,7 @@
"bom-ref": "pkg:deb/debian/libpcre3@8.39-12?arch=amd64&distro=debian-10.2&epoch=2",
"type": "library",
"name": "libpcre3",
"version": "8.39-12",
"version": "2:8.39-12",
"purl": "pkg:deb/debian/libpcre3@8.39-12?arch=amd64&distro=debian-10.2&epoch=2",
"properties": [
{
@@ -4450,7 +4450,7 @@
"bom-ref": "pkg:deb/debian/login@4.5-1.1?arch=amd64&distro=debian-10.2&epoch=1",
"type": "library",
"name": "login",
"version": "4.5-1.1",
"version": "1:4.5-1.1",
"licenses": [
{
"license": {
@@ -4742,7 +4742,7 @@
"bom-ref": "pkg:deb/debian/passwd@4.5-1.1?arch=amd64&distro=debian-10.2&epoch=1",
"type": "library",
"name": "passwd",
"version": "4.5-1.1",
"version": "1:4.5-1.1",
"licenses": [
{
"license": {
@@ -5338,7 +5338,7 @@
"bom-ref": "pkg:deb/debian/ruby@2.5.1?arch=amd64&distro=debian-10.2&epoch=1",
"type": "library",
"name": "ruby",
"version": "2.5.1",
"version": "1:2.5.1",
"licenses": [
{
"license": {
@@ -5690,7 +5690,7 @@
"bom-ref": "pkg:deb/debian/zlib1g@1.2.11.dfsg-1?arch=amd64&distro=debian-10.2&epoch=1",
"type": "library",
"name": "zlib1g",
"version": "1.2.11.dfsg-1",
"version": "1:1.2.11.dfsg-1",
"licenses": [
{
"license": {

View File

@@ -31,6 +31,7 @@ func Test_sbomAnalyzer_Analyze(t *testing.T) {
Type: types.Jar,
Libraries: types.Packages{
{
ID: "co.elastic.apm:apm-agent:1.36.0",
Name: "co.elastic.apm:apm-agent",
Version: "1.36.0",
FilePath: "opt/bitnami/elasticsearch",
@@ -44,6 +45,7 @@ func Test_sbomAnalyzer_Analyze(t *testing.T) {
},
},
{
ID: "co.elastic.apm:apm-agent-cached-lookup-key:1.36.0",
Name: "co.elastic.apm:apm-agent-cached-lookup-key",
Version: "1.36.0",
FilePath: "opt/bitnami/elasticsearch",
@@ -57,6 +59,7 @@ func Test_sbomAnalyzer_Analyze(t *testing.T) {
},
},
{
ID: "co.elastic.apm:apm-agent-common:1.36.0",
Name: "co.elastic.apm:apm-agent-common",
Version: "1.36.0",
FilePath: "opt/bitnami/elasticsearch",
@@ -70,6 +73,7 @@ func Test_sbomAnalyzer_Analyze(t *testing.T) {
},
},
{
ID: "co.elastic.apm:apm-agent-core:1.36.0",
Name: "co.elastic.apm:apm-agent-core",
Version: "1.36.0",
FilePath: "opt/bitnami/elasticsearch",
@@ -89,7 +93,8 @@ func Test_sbomAnalyzer_Analyze(t *testing.T) {
FilePath: "opt/bitnami/elasticsearch",
Libraries: types.Packages{
{
Name: "elasticsearch",
ID: "Elasticsearch@8.9.1",
Name: "Elasticsearch",
Version: "8.9.1",
Arch: "arm64",
Licenses: []string{"Elastic-2.0"},
@@ -169,7 +174,8 @@ func Test_sbomAnalyzer_Analyze(t *testing.T) {
FilePath: "opt/bitnami/postgresql",
Libraries: types.Packages{
{
Name: "gdal",
ID: "GDAL@3.7.1",
Name: "GDAL",
Version: "3.7.1",
Licenses: []string{"MIT"},
Identifier: types.PkgIdentifier{
@@ -181,7 +187,8 @@ func Test_sbomAnalyzer_Analyze(t *testing.T) {
},
},
{
Name: "geos",
ID: "GEOS@3.8.3",
Name: "GEOS",
Version: "3.8.3",
Licenses: []string{"LGPL-2.1-only"},
Identifier: types.PkgIdentifier{
@@ -193,7 +200,8 @@ func Test_sbomAnalyzer_Analyze(t *testing.T) {
},
},
{
Name: "postgresql",
ID: "PostgreSQL@15.3.0",
Name: "PostgreSQL",
Version: "15.3.0",
Licenses: []string{"PostgreSQL"},
Identifier: types.PkgIdentifier{
@@ -203,9 +211,15 @@ func Test_sbomAnalyzer_Analyze(t *testing.T) {
Version: "15.3.0",
},
},
DependsOn: []string{
"GEOS@3.8.3",
"Proj@6.3.2",
"GDAL@3.7.1",
},
},
{
Name: "proj",
ID: "Proj@6.3.2",
Name: "Proj",
Version: "6.3.2",
Licenses: []string{"MIT"},
Identifier: types.PkgIdentifier{

View File

@@ -263,12 +263,9 @@ func newPURL(pkgType ftypes.TargetType, metadata types.Metadata, pkg ftypes.Pack
func aggregate(detail *ftypes.ArtifactDetail) {
var apps []ftypes.Application
aggregatedApps := map[ftypes.LangType]*ftypes.Application{
ftypes.PythonPkg: {Type: ftypes.PythonPkg},
ftypes.CondaPkg: {Type: ftypes.CondaPkg},
ftypes.GemSpec: {Type: ftypes.GemSpec},
ftypes.NodePkg: {Type: ftypes.NodePkg},
ftypes.Jar: {Type: ftypes.Jar},
aggregatedApps := make(map[ftypes.LangType]*ftypes.Application)
for _, t := range ftypes.AggregatingTypes {
aggregatedApps[t] = &ftypes.Application{Type: t}
}
for _, app := range detail.Applications {

View File

@@ -81,6 +81,14 @@ const (
OCP LangType = "ocp" // Red Hat OpenShift Container Platform
)
var AggregatingTypes = []LangType{
PythonPkg,
CondaPkg,
GemSpec,
NodePkg,
Jar,
}
// Config files
const (
JSON ConfigType = "json"

View File

@@ -375,7 +375,9 @@ func (s *Scanner) clusterInfoToReportResources(allArtifact []*artifacts.Artifact
return nil, fmt.Errorf("failed to find node name")
}
kbom := core.NewBOM()
kbom := core.NewBOM(core.Options{
GenerateBOMRef: true,
})
for _, artifact := range allArtifact {
switch artifact.Kind {
case controlPlaneComponents:
@@ -413,7 +415,7 @@ func (s *Scanner) clusterInfoToReportResources(allArtifact []*artifacts.Artifact
}
imageComponent := &core.Component{
Type: core.TypeContainer,
Type: core.TypeContainerImage,
Name: name,
Version: cDigest,
PkgID: core.PkgID{

View File

@@ -155,7 +155,7 @@ func TestScanner_Scan(t *testing.T) {
},
},
{
Type: core.TypeContainer,
Type: core.TypeContainerImage,
Name: "k8s.gcr.io/kube-apiserver",
Version: "sha256:18e61c783b41758dd391ab901366ec3546b26fae00eef7e223d1f94da808e02f",
PkgID: core.PkgID{

View File

@@ -30,7 +30,7 @@ func NewWriter(output io.Writer, version string, spdxFormat types.Format) Writer
}
func (w Writer) Write(ctx context.Context, report types.Report) error {
spdxDoc, err := w.marshaler.Marshal(ctx, report)
spdxDoc, err := w.marshaler.MarshalReport(ctx, report)
if err != nil {
return xerrors.Errorf("failed to marshal spdx: %w", err)
}

View File

@@ -11,11 +11,14 @@ import (
)
const (
TypeApplication ComponentType = "application"
TypeContainer ComponentType = "container"
TypeLibrary ComponentType = "library"
TypeOS ComponentType = "os"
TypePlatform ComponentType = "platform"
TypeFilesystem ComponentType = "filesystem"
TypeRepository ComponentType = "repository"
TypeContainerImage ComponentType = "container_image"
TypeVM ComponentType = "vm"
TypeApplication ComponentType = "application"
TypeLibrary ComponentType = "library"
TypeOS ComponentType = "os"
TypePlatform ComponentType = "platform"
// Metadata properties
PropertySchemaVersion = "SchemaVersion"
@@ -59,7 +62,7 @@ type BOM struct {
components map[uuid.UUID]*Component
relationships map[uuid.UUID][]Relationship
// Vulnerabilities is a list of vulnerabilities that affect the component
// Vulnerabilities is a list of vulnerabilities that affect the component.
// CycloneDX: vulnerabilities
// SPDX: N/A
vulnerabilities map[uuid.UUID][]Vulnerability
@@ -67,6 +70,9 @@ type BOM struct {
// purls is a map of package URLs to UUIDs
// This is used to ensure that each package URL is only represented once in the BOM.
purls map[string][]uuid.UUID
// opts is a set of options for the BOM.
opts Options
}
type Component struct {
@@ -98,6 +104,21 @@ type Component struct {
// SPDX: package.versionInfo
Version string
// SrcName is the name of the source component
// CycloneDX: N/A
// SPDX: package.sourceInfo
SrcName string
// SrcVersion is the version of the source component
// CycloneDX: N/A
// SPDX: package.sourceInfo
SrcVersion string
// SrcFile is the file path where the component is found.
// CycloneDX: N/A
// SPDX: package.sourceInfo
SrcFile string
// Licenses is a list of licenses that apply to the component
// CycloneDX: component.licenses
// SPDX: package.licenseConcluded, package.licenseDeclared
@@ -139,9 +160,10 @@ type File struct {
Path string
// Hash is a hash that uniquely identify the component.
// A file can have several digests with different algorithms, like SHA1, SHA256, etc.
// CycloneDX: component.hashes
// SPDX: package.files[].checksum
Hash digest.Digest
// SPDX: package.files[].checksums
Digests []digest.Digest
}
type Property struct {
@@ -182,12 +204,17 @@ type Vulnerability struct {
DataSource *dtypes.DataSource
}
func NewBOM() *BOM {
type Options struct {
GenerateBOMRef bool
}
func NewBOM(opts Options) *BOM {
return &BOM{
components: make(map[uuid.UUID]*Component),
relationships: make(map[uuid.UUID][]Relationship),
vulnerabilities: make(map[uuid.UUID][]Vulnerability),
purls: make(map[string][]uuid.UUID),
opts: opts,
}
}
@@ -245,14 +272,18 @@ func (b *BOM) Root() *Component {
if !ok {
return nil
}
root.PkgID.BOMRef = b.bomRef(root)
if b.opts.GenerateBOMRef {
root.PkgID.BOMRef = b.bomRef(root)
}
return root
}
func (b *BOM) Components() map[uuid.UUID]*Component {
// Fill in BOMRefs for components
for id, c := range b.components {
b.components[id].PkgID.BOMRef = b.bomRef(c)
if b.opts.GenerateBOMRef {
for id, c := range b.components {
b.components[id].PkgID.BOMRef = b.bomRef(c)
}
}
return b.components
}

View File

@@ -48,7 +48,8 @@ func NewMarshaler(version string) Marshaler {
// MarshalReport converts the Trivy report to the CycloneDX format
func (m *Marshaler) MarshalReport(ctx context.Context, report types.Report) (*cdx.BOM, error) {
// Convert into an intermediate representation
bom, err := sbomio.NewEncoder().Encode(report)
opts := core.Options{GenerateBOMRef: true}
bom, err := sbomio.NewEncoder(opts).Encode(report)
if err != nil {
return nil, xerrors.Errorf("failed to marshal report: %w", err)
}
@@ -218,9 +219,9 @@ func (m *Marshaler) marshalVulnerabilities() *[]cdx.Vulnerability {
// componentType converts the Trivy component type to the CycloneDX component type
func (*Marshaler) componentType(t core.ComponentType) (cdx.ComponentType, error) {
switch t {
case core.TypeContainer:
case core.TypeContainerImage, core.TypeVM:
return cdx.ComponentTypeContainer, nil
case core.TypeApplication:
case core.TypeApplication, core.TypeFilesystem, core.TypeRepository:
return cdx.ComponentTypeApplication, nil
case core.TypeLibrary:
return cdx.ComponentTypeLibrary, nil
@@ -249,17 +250,17 @@ func (*Marshaler) Supplier(supplier string) *cdx.OrganizationalEntity {
}
func (*Marshaler) Hashes(files []core.File) *[]cdx.Hash {
hashes := lo.FilterMap(files, func(f core.File, index int) (digest.Digest, bool) {
return f.Hash, f.Hash != ""
digests := lo.FlatMap(files, func(file core.File, _ int) []digest.Digest {
return file.Digests
})
if len(hashes) == 0 {
if len(digests) == 0 {
return nil
}
var cdxHashes []cdx.Hash
for _, h := range hashes {
for _, d := range digests {
var alg cdx.HashAlgorithm
switch h.Algorithm() {
switch d.Algorithm() {
case digest.SHA1:
alg = cdx.HashAlgoSHA1
case digest.SHA256:
@@ -267,13 +268,13 @@ func (*Marshaler) Hashes(files []core.File) *[]cdx.Hash {
case digest.MD5:
alg = cdx.HashAlgoMD5
default:
log.Logger.Debugf("Unable to convert %q algorithm to CycloneDX format", h.Algorithm())
log.Logger.Debugf("Unable to convert %q algorithm to CycloneDX format", d.Algorithm())
continue
}
cdxHashes = append(cdxHashes, cdx.Hash{
Algorithm: alg,
Value: h.Encoded(),
Value: d.Encoded(),
})
}
return &cdxHashes

View File

@@ -24,7 +24,7 @@ import (
)
func TestMarshaler_MarshalReport(t *testing.T) {
testSBOM := core.NewBOM()
testSBOM := core.NewBOM(core.Options{GenerateBOMRef: true})
testSBOM.AddComponent(&core.Component{
Root: true,
Type: core.TypeApplication,
@@ -1022,7 +1022,7 @@ func TestMarshaler_MarshalReport(t *testing.T) {
BOMRef: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011&epoch=1",
Type: cdx.ComponentTypeLibrary,
Name: "acl",
Version: "2.2.53-1.el8",
Version: "1:2.2.53-1.el8",
Licenses: &cdx.Licenses{
cdx.LicenseChoice{
License: &cdx.License{

View File

@@ -37,7 +37,7 @@ func DecodeJSON(r io.Reader) (*cdx.BOM, error) {
func (b *BOM) UnmarshalJSON(data []byte) error {
log.Logger.Debug("Unmarshalling CycloneDX JSON...")
if b.BOM == nil {
b.BOM = core.NewBOM()
b.BOM = core.NewBOM(core.Options{GenerateBOMRef: true})
}
cdxBOM, err := DecodeJSON(bytes.NewReader(data))
@@ -143,9 +143,11 @@ func (b *BOM) parseComponent(c cdx.Component) (*core.Component, error) {
Group: c.Group,
Version: c.Version,
Licenses: b.unmarshalLicenses(c.Licenses),
Files: lo.Map(b.unmarshalHashes(c.Hashes), func(d digest.Digest, _ int) core.File {
return core.File{Hash: d} // CycloneDX doesn't have a file path for the hash
}),
Files: []core.File{
{
Digests: b.unmarshalHashes(c.Hashes),
},
},
PkgID: core.PkgID{
PURL: &purl,
BOMRef: c.BOMRef,
@@ -161,7 +163,7 @@ func (b *BOM) unmarshalType(t cdx.ComponentType) (core.ComponentType, error) {
var ctype core.ComponentType
switch t {
case cdx.ComponentTypeContainer:
ctype = core.TypeContainer
ctype = core.TypeContainerImage
case cdx.ComponentTypeApplication:
ctype = core.TypeApplication
case cdx.ComponentTypeLibrary:

View File

@@ -2,14 +2,18 @@ package io
import (
"errors"
"slices"
"sort"
"strconv"
debver "github.com/knqyf263/go-deb-version"
rpmver "github.com/knqyf263/go-rpm-version"
"github.com/package-url/packageurl-go"
"go.uber.org/zap"
"golang.org/x/exp/maps"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/dependency"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/purl"
@@ -125,7 +129,7 @@ func (m *Decoder) decodeComponents(sbom *types.SBOM) error {
// Third-party SBOMs may contain packages in types other than "Library"
if c.Type == core.TypeLibrary || c.PkgID.PURL != nil {
pkg, err := m.decodeLibrary(c)
if errors.Is(err, ErrUnsupportedType) {
if errors.Is(err, ErrUnsupportedType) || errors.Is(err, ErrPURLEmpty) {
continue
} else if err != nil {
return xerrors.Errorf("failed to decode library: %w", err)
@@ -156,15 +160,19 @@ func (m *Decoder) buildDependencyGraph() {
}
func (m *Decoder) decodeApplication(c *core.Component) *ftypes.Application {
app := &ftypes.Application{
FilePath: c.Name,
}
var app ftypes.Application
for _, prop := range c.Properties {
if prop.Name == core.PropertyType {
app.Type = ftypes.LangType(prop.Value)
}
}
return app
// Aggregation Types use the name of the language (e.g. `Java`, `Python`, etc.) as the component name.
// Other language files use the file path as their name.
if !slices.Contains(ftypes.AggregatingTypes, app.Type) {
app.FilePath = c.Name
}
return &app
}
func (m *Decoder) decodeLibrary(c *core.Component) (*ftypes.Package, error) {
@@ -182,6 +190,7 @@ func (m *Decoder) decodeLibrary(c *core.Component) (*ftypes.Package, error) {
return nil, ErrUnsupportedType
}
pkg.Name = m.pkgName(pkg, c)
pkg.ID = dependency.ID(p.LangType(), pkg.Name, p.Version) // Re-generate ID with the updated name
var err error
for _, prop := range c.Properties {
@@ -211,12 +220,19 @@ func (m *Decoder) decodeLibrary(c *core.Component) (*ftypes.Package, error) {
pkg.Identifier.BOMRef = c.PkgID.BOMRef
pkg.Licenses = c.Licenses
if len(c.Files) > 0 {
pkg.Digest = c.Files[0].Hash
for _, f := range c.Files {
if f.Path != "" && pkg.FilePath == "" {
pkg.FilePath = f.Path
}
// An empty path represents a package digest
if f.Path == "" && len(f.Digests) > 0 {
pkg.Digest = f.Digests[0]
}
}
if p.Class() == types.ClassOSPkg {
m.fillSrcPkg(pkg)
m.fillSrcPkg(c, pkg)
}
return pkg, nil
@@ -241,7 +257,12 @@ func (m *Decoder) pkgName(pkg *ftypes.Package, c *core.Component) string {
return c.Name
}
func (m *Decoder) fillSrcPkg(pkg *ftypes.Package) {
func (m *Decoder) fillSrcPkg(c *core.Component, pkg *ftypes.Package) {
if c.SrcName != "" && pkg.SrcName == "" {
pkg.SrcName = c.SrcName
}
m.parseSrcVersion(pkg, c.SrcVersion)
// Fill source package information for components in third-party SBOMs .
if pkg.SrcName == "" {
pkg.SrcName = pkg.Name
@@ -257,6 +278,29 @@ func (m *Decoder) fillSrcPkg(pkg *ftypes.Package) {
}
}
// parseSrcVersion parses the version of the source package.
func (m *Decoder) parseSrcVersion(pkg *ftypes.Package, ver string) {
if ver == "" {
return
}
switch pkg.Identifier.PURL.Type {
case packageurl.TypeRPM:
v := rpmver.NewVersion(ver)
pkg.SrcEpoch = v.Epoch()
pkg.SrcVersion = v.Version()
pkg.SrcRelease = v.Release()
case packageurl.TypeDebian:
v, err := debver.NewVersion(ver)
if err != nil {
log.Logger.Debugw("Failed to parse Debian version", zap.Error(err))
return
}
pkg.SrcEpoch = v.Epoch()
pkg.SrcVersion = v.Version()
pkg.SrcRelease = v.Revision()
}
}
// addOSPkgs traverses relationships and adds OS packages
func (m *Decoder) addOSPkgs(sbom *types.SBOM) {
var pkgs []ftypes.Package

View File

@@ -2,48 +2,52 @@ package io
import (
"fmt"
"slices"
"strconv"
"github.com/package-url/packageurl-go"
"github.com/samber/lo"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/digest"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/purl"
"github.com/aquasecurity/trivy/pkg/sbom/core"
"github.com/aquasecurity/trivy/pkg/scanner/utils"
"github.com/aquasecurity/trivy/pkg/types"
)
type Encoder struct {
bom *core.BOM
bom *core.BOM
opts core.Options
}
func NewEncoder() *Encoder {
return &Encoder{}
func NewEncoder(opts core.Options) *Encoder {
return &Encoder{opts: opts}
}
func (m *Encoder) Encode(report types.Report) (*core.BOM, error) {
func (e *Encoder) Encode(report types.Report) (*core.BOM, error) {
// Metadata component
root, err := m.rootComponent(report)
root, err := e.rootComponent(report)
if err != nil {
return nil, xerrors.Errorf("failed to create root component: %w", err)
}
m.bom = core.NewBOM()
m.bom.AddComponent(root)
e.bom = core.NewBOM(e.opts)
e.bom.AddComponent(root)
for _, result := range report.Results {
m.encodeResult(root, report.Metadata, result)
e.encodeResult(root, report.Metadata, result)
}
// Components that do not have their own dependencies MUST be declared as empty elements within the graph.
if _, ok := m.bom.Relationships()[root.ID()]; !ok {
m.bom.AddRelationship(root, nil, "")
if _, ok := e.bom.Relationships()[root.ID()]; !ok {
e.bom.AddRelationship(root, nil, "")
}
return m.bom, nil
return e.bom, nil
}
func (m *Encoder) rootComponent(r types.Report) (*core.Component, error) {
func (e *Encoder) rootComponent(r types.Report) (*core.Component, error) {
root := &core.Component{
Root: true,
Name: r.ArtifactName,
@@ -58,7 +62,7 @@ func (m *Encoder) rootComponent(r types.Report) (*core.Component, error) {
switch r.ArtifactType {
case ftypes.ArtifactContainerImage:
root.Type = core.TypeContainer
root.Type = core.TypeContainerImage
props = append(props, core.Property{
Name: core.PropertyImageID,
Value: r.Metadata.ImageID,
@@ -73,9 +77,11 @@ func (m *Encoder) rootComponent(r types.Report) (*core.Component, error) {
}
case ftypes.ArtifactVM:
root.Type = core.TypeContainer
case ftypes.ArtifactFilesystem, ftypes.ArtifactRepository:
root.Type = core.TypeApplication
root.Type = core.TypeVM
case ftypes.ArtifactFilesystem:
root.Type = core.TypeFilesystem
case ftypes.ArtifactRepository:
root.Type = core.TypeRepository
case ftypes.ArtifactCycloneDX:
return r.BOM.Root(), nil
}
@@ -113,9 +119,8 @@ func (m *Encoder) rootComponent(r types.Report) (*core.Component, error) {
return root, nil
}
func (m *Encoder) encodeResult(root *core.Component, metadata types.Metadata, result types.Result) {
if result.Type == ftypes.NodePkg || result.Type == ftypes.PythonPkg ||
result.Type == ftypes.GemSpec || result.Type == ftypes.Jar || result.Type == ftypes.CondaPkg {
func (e *Encoder) encodeResult(root *core.Component, metadata types.Metadata, result types.Result) {
if slices.Contains(ftypes.AggregatingTypes, result.Type) {
// If a package is language-specific package that isn't associated with a lock file,
// it will be a dependency of a component under "metadata".
// e.g.
@@ -126,7 +131,7 @@ func (m *Encoder) encodeResult(root *core.Component, metadata types.Metadata, re
// ref. https://cyclonedx.org/use-cases/#inventory
// Dependency graph from #1 to #2
m.encodePackages(root, result)
e.encodePackages(root, result)
} else if result.Class == types.ClassOSPkg || result.Class == types.ClassLangPkg {
// If a package is OS package, it will be a dependency of "Operating System" component.
// e.g.
@@ -146,21 +151,21 @@ func (m *Encoder) encodeResult(root *core.Component, metadata types.Metadata, re
// -> etc.
// #2
appComponent := m.resultComponent(root, result, metadata.OS)
appComponent := e.resultComponent(root, result, metadata.OS)
// #3
m.encodePackages(appComponent, result)
e.encodePackages(appComponent, result)
}
}
func (m *Encoder) encodePackages(parent *core.Component, result types.Result) {
func (e *Encoder) encodePackages(parent *core.Component, result types.Result) {
// Get dependency parents first
parents := ftypes.Packages(result.Packages).ParentDeps()
// Group vulnerabilities by package ID
vulns := make(map[string][]core.Vulnerability)
for _, vuln := range result.Vulnerabilities {
v := m.vulnerability(vuln)
v := e.vulnerability(vuln)
vulns[v.PkgID] = append(vulns[v.PkgID], v)
}
@@ -171,15 +176,15 @@ func (m *Encoder) encodePackages(parent *core.Component, result types.Result) {
result.Packages[i].ID = pkgID
// Convert packages to components
c := m.component(result.Type, pkg)
components[pkgID] = c
c := e.component(result, pkg)
components[pkgID+pkg.FilePath] = c
// Add a component
m.bom.AddComponent(c)
e.bom.AddComponent(c)
// Add vulnerabilities
if vv := vulns[pkgID]; vv != nil {
m.bom.AddVulnerabilities(c, vv)
e.bom.AddVulnerabilities(c, vv)
}
}
@@ -190,26 +195,26 @@ func (m *Encoder) encodePackages(parent *core.Component, result types.Result) {
continue
}
directPkg := components[pkg.ID]
m.bom.AddRelationship(parent, directPkg, core.RelationshipContains)
directPkg := components[pkg.ID+pkg.FilePath]
e.bom.AddRelationship(parent, directPkg, core.RelationshipContains)
for _, dep := range pkg.DependsOn {
indirectPkg, ok := components[dep]
if !ok {
continue
}
m.bom.AddRelationship(directPkg, indirectPkg, core.RelationshipDependsOn)
e.bom.AddRelationship(directPkg, indirectPkg, core.RelationshipDependsOn)
}
// Components that do not have their own dependencies MUST be declared as empty elements within the graph.
// TODO: Should check if the component has actually no dependencies or the dependency graph is not supported.
if len(pkg.DependsOn) == 0 {
m.bom.AddRelationship(directPkg, nil, "")
e.bom.AddRelationship(directPkg, nil, "")
}
}
}
func (m *Encoder) resultComponent(root *core.Component, r types.Result, osFound *ftypes.OS) *core.Component {
func (e *Encoder) resultComponent(root *core.Component, r types.Result, osFound *ftypes.OS) *core.Component {
component := &core.Component{
Name: r.Target,
Properties: []core.Property{
@@ -235,18 +240,24 @@ func (m *Encoder) resultComponent(root *core.Component, r types.Result, osFound
component.Type = core.TypeApplication
}
m.bom.AddRelationship(root, component, core.RelationshipContains)
e.bom.AddRelationship(root, component, core.RelationshipContains)
return component
}
func (*Encoder) component(pkgType ftypes.TargetType, pkg ftypes.Package) *core.Component {
func (*Encoder) component(result types.Result, pkg ftypes.Package) *core.Component {
name := pkg.Name
version := pkg.Version
version := utils.FormatVersion(pkg)
var group string
// there are cases when we can't build purl
// e.g. local Go packages
if pu := pkg.Identifier.PURL; pu != nil {
version = pu.Version
for _, q := range pu.Qualifiers {
if q.Key == "epoch" && q.Value != "0" {
version = fmt.Sprintf("%s:%s", q.Value, version)
}
}
// Use `group` field for GroupID and `name` for ArtifactID for java files
// https://github.com/aquasecurity/trivy/issues/4675
// Use `group` field for npm scopes
@@ -264,7 +275,7 @@ func (*Encoder) component(pkgType ftypes.TargetType, pkg ftypes.Package) *core.C
},
{
Name: core.PropertyPkgType,
Value: string(pkgType),
Value: string(result.Type),
},
{
Name: core.PropertyFilePath,
@@ -303,16 +314,25 @@ func (*Encoder) component(pkgType ftypes.TargetType, pkg ftypes.Package) *core.C
var files []core.File
if pkg.FilePath != "" || pkg.Digest != "" {
files = append(files, core.File{
Path: pkg.FilePath,
Hash: pkg.Digest,
Path: pkg.FilePath,
Digests: lo.Ternary(pkg.Digest != "", []digest.Digest{pkg.Digest}, nil),
})
}
// TODO(refactor): simplify the list of conditions
var srcFile string
if result.Class == types.ClassLangPkg && !slices.Contains(ftypes.AggregatingTypes, result.Type) {
srcFile = result.Target
}
return &core.Component{
Type: core.TypeLibrary,
Name: name,
Group: group,
Version: version,
Type: core.TypeLibrary,
Name: name,
Group: group,
Version: version,
SrcName: pkg.SrcName,
SrcVersion: utils.FormatSrcVersion(pkg),
SrcFile: srcFile,
PkgID: core.PkgID{
PURL: pkg.Identifier.PURL,
},

View File

@@ -113,7 +113,7 @@ func TestEncoder_Encode(t *testing.T) {
},
wantComponents: map[uuid.UUID]*core.Component{
uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): {
Type: core.TypeContainer,
Type: core.TypeContainerImage,
Name: "debian:12",
Root: true,
PkgID: core.PkgID{
@@ -320,7 +320,8 @@ func TestEncoder_Encode(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d")
got, err := sbomio.NewEncoder().Encode(tt.report)
opts := core.Options{GenerateBOMRef: true}
got, err := sbomio.NewEncoder(opts).Encode(tt.report)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return

View File

@@ -183,8 +183,7 @@ func decodeAttestCycloneDXJSONFormat(r io.ReadSeeker) (Format, bool) {
func Decode(f io.Reader, format Format) (types.SBOM, error) {
var (
v interface{}
bom = core.NewBOM()
sbom types.SBOM
bom = core.NewBOM(core.Options{})
decoder interface{ Decode(any) error }
)
@@ -212,10 +211,10 @@ func Decode(f io.Reader, format Format) (types.SBOM, error) {
}
decoder = json.NewDecoder(f)
case FormatSPDXJSON:
v = &spdx.SPDX{SBOM: &sbom}
v = &spdx.SPDX{BOM: bom}
decoder = json.NewDecoder(f)
case FormatSPDXTV:
v = &spdx.SPDX{SBOM: &sbom}
v = &spdx.SPDX{BOM: bom}
decoder = spdx.NewTVDecoder(f)
default:
return types.SBOM{}, xerrors.Errorf("%s scanning is not yet supported", format)
@@ -227,11 +226,7 @@ func Decode(f io.Reader, format Format) (types.SBOM, error) {
return types.SBOM{}, xerrors.Errorf("failed to decode: %w", err)
}
// TODO: use BOM in SPDX
if format == FormatSPDXJSON || format == FormatSPDXTV {
return sbom, nil
}
var sbom types.SBOM
if err := sbomio.NewDecoder(bom).Decode(&sbom); err != nil {
return types.SBOM{}, xerrors.Errorf("failed to decode: %w", err)
}

View File

@@ -4,26 +4,25 @@ import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/mitchellh/hashstructure/v2"
"github.com/package-url/packageurl-go"
"github.com/samber/lo"
"github.com/spdx/tools-golang/spdx"
"github.com/spdx/tools-golang/spdx/v2/common"
spdxutils "github.com/spdx/tools-golang/utils"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/clock"
"github.com/aquasecurity/trivy/pkg/digest"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/licensing"
"github.com/aquasecurity/trivy/pkg/licensing/expression"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/purl"
"github.com/aquasecurity/trivy/pkg/scanner/utils"
"github.com/aquasecurity/trivy/pkg/sbom/core"
sbomio "github.com/aquasecurity/trivy/pkg/sbom/io"
"github.com/aquasecurity/trivy/pkg/types"
"github.com/aquasecurity/trivy/pkg/uuid"
)
@@ -40,19 +39,6 @@ const (
CategoryPackageManager = "PACKAGE-MANAGER"
RefTypePurl = "purl"
PropertySchemaVersion = "SchemaVersion"
// Image properties
PropertySize = "Size"
PropertyImageID = "ImageID"
PropertyRepoDigest = "RepoDigest"
PropertyDiffID = "DiffID"
PropertyRepoTag = "RepoTag"
// Package properties
PropertyPkgID = "PkgID"
PropertyLayerDiffID = "LayerDiffID"
PropertyLayerDigest = "LayerDigest"
// Package Purpose fields
PackagePurposeOS = "OPERATING-SYSTEM"
PackagePurposeContainer = "CONTAINER"
@@ -75,8 +61,20 @@ const (
var (
SourcePackagePrefix = "built package from"
SourceFilePrefix = "package found in"
)
// duplicateProperties contains a list of properties contained in other fields.
var duplicateProperties = []string{
// `SourceInfo` contains SrcName and SrcVersion (it contains PropertySrcRelease and PropertySrcEpoch)
core.PropertySrcName,
core.PropertySrcRelease,
core.PropertySrcEpoch,
core.PropertySrcVersion,
// `File` contains filePath.
core.PropertyFilePath,
}
type Marshaler struct {
format spdx.Document
hasher Hash
@@ -107,75 +105,95 @@ func NewMarshaler(version string, opts ...marshalOption) *Marshaler {
return m
}
func (m *Marshaler) Marshal(ctx context.Context, r types.Report) (*spdx.Document, error) {
var relationShips []*spdx.Relationship
packages := make(map[spdx.ElementID]*spdx.Package)
pkgDownloadLocation := getPackageDownloadLocation(r.ArtifactType, r.ArtifactName)
func (m *Marshaler) MarshalReport(ctx context.Context, report types.Report) (*spdx.Document, error) {
// Convert into an intermediate representation
bom, err := sbomio.NewEncoder(core.Options{}).Encode(report)
if err != nil {
return nil, xerrors.Errorf("failed to marshal report: %w", err)
}
return m.Marshal(ctx, bom)
}
func (m *Marshaler) Marshal(ctx context.Context, bom *core.BOM) (*spdx.Document, error) {
var (
relationShips []*spdx.Relationship
packages []*spdx.Package
)
root := bom.Root()
pkgDownloadLocation := m.packageDownloadLocation(root)
// Component ID => SPDX ID
packageIDs := make(map[uuid.UUID]spdx.ElementID)
// Root package contains OS, OS packages, language-specific packages and so on.
rootPkg, err := m.rootPackage(r, pkgDownloadLocation)
rootPkg, err := m.rootSPDXPackage(root, pkgDownloadLocation)
if err != nil {
return nil, xerrors.Errorf("failed to generate a root package: %w", err)
}
packages[rootPkg.PackageSPDXIdentifier] = rootPkg
packages = append(packages, rootPkg)
relationShips = append(relationShips,
relationShip(DocumentSPDXIdentifier, rootPkg.PackageSPDXIdentifier, RelationShipDescribe),
m.spdxRelationShip(DocumentSPDXIdentifier, rootPkg.PackageSPDXIdentifier, RelationShipDescribe),
)
packageIDs[root.ID()] = rootPkg.PackageSPDXIdentifier
var spdxFiles []*spdx.File
for _, result := range r.Results {
if len(result.Packages) == 0 {
var files []*spdx.File
for _, c := range bom.Components() {
if c.Root {
continue
}
parentPackage, err := m.resultToSpdxPackage(result, r.Metadata.OS, pkgDownloadLocation)
spdxPackage, err := m.spdxPackage(c, pkgDownloadLocation)
if err != nil {
return nil, xerrors.Errorf("failed to parse result: %w", err)
return nil, xerrors.Errorf("spdx package error: %w", err)
}
packages[parentPackage.PackageSPDXIdentifier] = &parentPackage
relationShips = append(relationShips,
relationShip(rootPkg.PackageSPDXIdentifier, parentPackage.PackageSPDXIdentifier, RelationShipContains),
)
packages = append(packages, &spdxPackage)
packageIDs[c.ID()] = spdxPackage.PackageSPDXIdentifier
for _, pkg := range result.Packages {
spdxPackage, err := m.pkgToSpdxPackage(result.Type, pkgDownloadLocation, result.Class, r.Metadata, pkg)
if err != nil {
return nil, xerrors.Errorf("failed to parse package: %w", err)
}
packages[spdxPackage.PackageSPDXIdentifier] = &spdxPackage
spdxFiles, err := m.spdxFiles(c)
if err != nil {
return nil, xerrors.Errorf("spdx files error: %w", err)
} else if len(spdxFiles) == 0 {
continue
}
files = append(files, spdxFiles...)
for _, file := range spdxFiles {
relationShips = append(relationShips,
relationShip(parentPackage.PackageSPDXIdentifier, spdxPackage.PackageSPDXIdentifier, RelationShipContains),
m.spdxRelationShip(spdxPackage.PackageSPDXIdentifier, file.FileSPDXIdentifier, RelationShipContains),
)
files, err := m.pkgFiles(pkg)
if err != nil {
return nil, xerrors.Errorf("package file error: %w", err)
} else if files == nil {
}
verificationCode, err := spdxutils.GetVerificationCode(spdxFiles, "")
if err != nil {
return nil, xerrors.Errorf("package verification error: %w", err)
}
spdxPackage.FilesAnalyzed = true
spdxPackage.PackageVerificationCode = &verificationCode
}
for id, rels := range bom.Relationships() {
for _, rel := range rels {
refA, ok := packageIDs[id]
if !ok {
continue
}
spdxFiles = append(spdxFiles, files...)
for _, file := range files {
relationShips = append(relationShips,
relationShip(spdxPackage.PackageSPDXIdentifier, file.FileSPDXIdentifier, RelationShipContains),
)
refB, ok := packageIDs[rel.Dependency]
if !ok {
continue
}
verificationCode, err := spdxutils.GetVerificationCode(files, "")
if err != nil {
return nil, xerrors.Errorf("package verification error: %w", err)
}
spdxPackage.FilesAnalyzed = true
spdxPackage.PackageVerificationCode = &verificationCode
relationShips = append(relationShips, m.spdxRelationShip(refA, refB, m.spdxRelationshipType(rel.Type)))
}
}
sortPackages(packages)
sortRelationships(relationShips)
sortFiles(files)
return &spdx.Document{
SPDXVersion: spdx.Version,
DataLicense: spdx.DataLicense,
SPDXIdentifier: DocumentSPDXIdentifier,
DocumentName: r.ArtifactName,
DocumentNamespace: getDocumentNamespace(r, m),
DocumentName: root.Name,
DocumentNamespace: getDocumentNamespace(root),
CreationInfo: &spdx.CreationInfo{
Creators: []common.Creator{
{
@@ -189,214 +207,215 @@ func (m *Marshaler) Marshal(ctx context.Context, r types.Report) (*spdx.Document
},
Created: clock.Now(ctx).UTC().Format(time.RFC3339),
},
Packages: toPackages(packages),
Packages: packages,
Relationships: relationShips,
Files: spdxFiles,
Files: files,
}, nil
}
func toPackages(packages map[spdx.ElementID]*spdx.Package) []*spdx.Package {
ret := maps.Values(packages)
sort.Slice(ret, func(i, j int) bool {
if ret[i].PackageName != ret[j].PackageName {
return ret[i].PackageName < ret[j].PackageName
}
return ret[i].PackageSPDXIdentifier < ret[j].PackageSPDXIdentifier
})
return ret
func (m *Marshaler) packageDownloadLocation(root *core.Component) string {
location := noneField
// this field is used for git/mercurial/subversion/bazaar:
// https://spdx.github.io/spdx-spec/v2.2.2/package-information/#77-package-download-location-field
if root.Type == core.TypeRepository {
// Trivy currently only supports git repositories. Format examples:
// git+https://git.myproject.org/MyProject.git
// git+http://git.myproject.org/MyProject
location = fmt.Sprintf("git+%s", root.Name)
}
return location
}
func (m *Marshaler) resultToSpdxPackage(result types.Result, os *ftypes.OS, pkgDownloadLocation string) (spdx.Package, error) {
switch result.Class {
case types.ClassOSPkg:
osPkg, err := m.osPackage(os, pkgDownloadLocation)
if err != nil {
return spdx.Package{}, xerrors.Errorf("failed to parse operating system package: %w", err)
}
return osPkg, nil
case types.ClassLangPkg:
langPkg, err := m.langPackage(result.Target, pkgDownloadLocation, result.Type)
if err != nil {
return spdx.Package{}, xerrors.Errorf("failed to parse application package: %w", err)
}
return langPkg, nil
default:
// unsupported packages
return spdx.Package{}, nil
}
}
func (m *Marshaler) parseFile(filePath string, d digest.Digest) (spdx.File, error) {
pkgID, err := calcPkgID(m.hasher, filePath)
if err != nil {
return spdx.File{}, xerrors.Errorf("failed to get %s package ID: %w", filePath, err)
}
file := spdx.File{
FileSPDXIdentifier: spdx.ElementID(fmt.Sprintf("File-%s", pkgID)),
FileName: filePath,
Checksums: digestToSpdxFileChecksum(d),
}
return file, nil
}
func (m *Marshaler) rootPackage(r types.Report, pkgDownloadLocation string) (*spdx.Package, error) {
func (m *Marshaler) rootSPDXPackage(root *core.Component, pkgDownloadLocation string) (*spdx.Package, error) {
var externalReferences []*spdx.PackageExternalReference
attributionTexts := []string{attributionText(PropertySchemaVersion, strconv.Itoa(r.SchemaVersion))}
// When the target is a container image, add PURL to the external references of the root package.
if p, err := purl.New(purl.TypeOCI, r.Metadata, ftypes.Package{}); err != nil {
return nil, xerrors.Errorf("failed to new package url for oci: %w", err)
} else if p != nil {
externalReferences = append(externalReferences, purlExternalReference(p.String()))
if root.PkgID.PURL != nil {
externalReferences = append(externalReferences, m.purlExternalReference(root.PkgID.PURL.String()))
}
if r.Metadata.ImageID != "" {
attributionTexts = appendAttributionText(attributionTexts, PropertyImageID, r.Metadata.ImageID)
}
if r.Metadata.Size != 0 {
attributionTexts = appendAttributionText(attributionTexts, PropertySize, strconv.FormatInt(r.Metadata.Size, 10))
}
for _, d := range r.Metadata.RepoDigests {
attributionTexts = appendAttributionText(attributionTexts, PropertyRepoDigest, d)
}
for _, d := range r.Metadata.DiffIDs {
attributionTexts = appendAttributionText(attributionTexts, PropertyDiffID, d)
}
for _, t := range r.Metadata.RepoTags {
attributionTexts = appendAttributionText(attributionTexts, PropertyRepoTag, t)
}
pkgID, err := calcPkgID(m.hasher, fmt.Sprintf("%s-%s", r.ArtifactName, r.ArtifactType))
pkgID, err := calcPkgID(m.hasher, fmt.Sprintf("%s-%s", root.Name, root.Type))
if err != nil {
return nil, xerrors.Errorf("failed to get %s package ID: %w", pkgID, err)
}
pkgPurpose := PackagePurposeSource
if r.ArtifactType == ftypes.ArtifactContainerImage {
if root.Type == core.TypeContainerImage {
pkgPurpose = PackagePurposeContainer
}
return &spdx.Package{
PackageName: r.ArtifactName,
PackageSPDXIdentifier: elementID(camelCase(string(r.ArtifactType)), pkgID),
PackageName: root.Name,
PackageSPDXIdentifier: elementID(camelCase(string(root.Type)), pkgID),
PackageDownloadLocation: pkgDownloadLocation,
PackageAttributionTexts: attributionTexts,
PackageAttributionTexts: m.spdxAttributionTexts(root),
PackageExternalReferences: externalReferences,
PrimaryPackagePurpose: pkgPurpose,
}, nil
}
func (m *Marshaler) osPackage(osFound *ftypes.OS, pkgDownloadLocation string) (spdx.Package, error) {
if osFound == nil {
return spdx.Package{}, nil
func (m *Marshaler) appendAttributionText(attributionTexts []string, key, value string) []string {
if value == "" {
return attributionTexts
}
return append(attributionTexts, fmt.Sprintf("%s: %s", key, value))
}
pkgID, err := calcPkgID(m.hasher, osFound)
func (m *Marshaler) purlExternalReference(packageURL string) *spdx.PackageExternalReference {
return &spdx.PackageExternalReference{
Category: CategoryPackageManager,
RefType: RefTypePurl,
Locator: packageURL,
}
}
func (m *Marshaler) spdxPackage(c *core.Component, pkgDownloadLocation string) (spdx.Package, error) {
pkgID, err := calcPkgID(m.hasher, c)
if err != nil {
return spdx.Package{}, xerrors.Errorf("failed to get os metadata package ID: %w", err)
}
return spdx.Package{
PackageName: string(osFound.Family),
PackageVersion: osFound.Name,
PackageSPDXIdentifier: elementID(ElementOperatingSystem, pkgID),
PackageDownloadLocation: pkgDownloadLocation,
PrimaryPackagePurpose: PackagePurposeOS,
}, nil
}
var elementType, purpose, license, sourceInfo string
var supplier *spdx.Supplier
switch c.Type {
case core.TypeOS:
elementType = ElementOperatingSystem
purpose = PackagePurposeOS
case core.TypeApplication:
elementType = ElementApplication
purpose = PackagePurposeApplication
case core.TypeLibrary:
elementType = ElementPackage
purpose = PackagePurposeLibrary
license = m.spdxLicense(c)
func (m *Marshaler) langPackage(target, pkgDownloadLocation string, appType ftypes.LangType) (spdx.Package, error) {
pkgID, err := calcPkgID(m.hasher, fmt.Sprintf("%s-%s", target, appType))
if err != nil {
return spdx.Package{}, xerrors.Errorf("failed to get %s package ID: %w", target, err)
}
if c.SrcName != "" {
sourceInfo = fmt.Sprintf("%s: %s %s", SourcePackagePrefix, c.SrcName, c.SrcVersion)
} else if c.SrcFile != "" {
sourceInfo = fmt.Sprintf("%s: %s", SourceFilePrefix, c.SrcFile)
}
return spdx.Package{
PackageName: string(appType),
PackageSourceInfo: target, // TODO: Files seems better
PackageSPDXIdentifier: elementID(ElementApplication, pkgID),
PackageDownloadLocation: pkgDownloadLocation,
PrimaryPackagePurpose: PackagePurposeApplication,
}, nil
}
func (m *Marshaler) pkgToSpdxPackage(t ftypes.TargetType, pkgDownloadLocation string, class types.ResultClass, metadata types.Metadata, pkg ftypes.Package) (spdx.Package, error) {
license := GetLicense(pkg)
pkgID, err := calcPkgID(m.hasher, pkg)
if err != nil {
return spdx.Package{}, xerrors.Errorf("failed to get %s package ID: %w", pkg.Name, err)
}
var pkgSrcInfo string
if class == types.ClassOSPkg && pkg.SrcName != "" {
pkgSrcInfo = fmt.Sprintf("%s: %s %s", SourcePackagePrefix, pkg.SrcName, utils.FormatSrcVersion(pkg))
}
var pkgExtRefs []*spdx.PackageExternalReference
if pkg.Identifier.PURL != nil {
pkgExtRefs = []*spdx.PackageExternalReference{purlExternalReference(pkg.Identifier.PURL.String())}
}
var attrTexts []string
attrTexts = appendAttributionText(attrTexts, PropertyPkgID, pkg.ID)
attrTexts = appendAttributionText(attrTexts, PropertyLayerDigest, pkg.Layer.Digest)
attrTexts = appendAttributionText(attrTexts, PropertyLayerDiffID, pkg.Layer.DiffID)
supplier := &spdx.Supplier{Supplier: PackageSupplierNoAssertion}
if pkg.Maintainer != "" {
supplier = &spdx.Supplier{
SupplierType: PackageSupplierOrganization, // Always use "Organization" at the moment as it is difficult to distinguish between "Person" or "Organization".
Supplier: pkg.Maintainer,
supplier = &spdx.Supplier{Supplier: PackageSupplierNoAssertion}
if c.Supplier != "" {
supplier = &spdx.Supplier{
SupplierType: PackageSupplierOrganization, // Always use "Organization" at the moment as it is difficult to distinguish between "Person" or "Organization".
Supplier: c.Supplier,
}
}
}
var checksum []spdx.Checksum
if pkg.Digest != "" && class == types.ClassOSPkg {
checksum = digestToSpdxFileChecksum(pkg.Digest)
var pkgExtRefs []*spdx.PackageExternalReference
if c.PkgID.PURL != nil {
pkgExtRefs = []*spdx.PackageExternalReference{m.purlExternalReference(c.PkgID.PURL.String())}
}
var digests []digest.Digest
for _, f := range c.Files {
// The file digests are stored separately.
if f.Path != "" {
continue
}
digests = append(digests, f.Digests...)
}
return spdx.Package{
PackageName: pkg.Name,
PackageVersion: utils.FormatVersion(pkg),
PackageSPDXIdentifier: elementID(ElementPackage, pkgID),
PackageDownloadLocation: pkgDownloadLocation,
PackageSourceInfo: pkgSrcInfo,
PackageSPDXIdentifier: elementID(elementType, pkgID),
PackageName: spdxPkgName(c),
PackageVersion: c.Version,
PrimaryPackagePurpose: purpose,
PackageDownloadLocation: pkgDownloadLocation,
PackageExternalReferences: pkgExtRefs,
PackageAttributionTexts: m.spdxAttributionTexts(c),
PackageSourceInfo: sourceInfo,
PackageSupplier: supplier,
PackageChecksums: m.spdxChecksums(digests),
// The Declared License is what the authors of a project believe govern the package
PackageLicenseConcluded: license,
// The Concluded License field is the license the SPDX file creator believes governs the package
PackageLicenseDeclared: license,
PackageExternalReferences: pkgExtRefs,
PackageAttributionTexts: attrTexts,
PrimaryPackagePurpose: PackagePurposeLibrary,
PackageSupplier: supplier,
PackageChecksums: checksum,
}, nil
}
func (m *Marshaler) pkgFiles(pkg ftypes.Package) ([]*spdx.File, error) {
if pkg.FilePath == "" {
return nil, nil
func spdxPkgName(component *core.Component) string {
if p := component.PkgID.PURL; p != nil && component.Group != "" {
if p.Type == packageurl.TypeMaven || p.Type == packageurl.TypeGradle {
return component.Group + ":" + component.Name
}
return component.Group + "/" + component.Name
}
return component.Name
}
func (m *Marshaler) spdxAttributionTexts(c *core.Component) []string {
var texts []string
for _, p := range c.Properties {
// Add properties that are not in other fields.
if !slices.Contains(duplicateProperties, p.Name) {
texts = m.appendAttributionText(texts, p.Name, p.Value)
}
}
return texts
}
func (m *Marshaler) spdxLicense(c *core.Component) string {
if len(c.Licenses) == 0 {
return noneField
}
return NormalizeLicense(c.Licenses)
}
func (m *Marshaler) spdxChecksums(digests []digest.Digest) []common.Checksum {
var checksums []common.Checksum
for _, d := range digests {
var alg spdx.ChecksumAlgorithm
switch d.Algorithm() {
case digest.SHA1:
alg = spdx.SHA1
case digest.SHA256:
alg = spdx.SHA256
case digest.MD5:
alg = spdx.MD5
default:
return nil
}
checksums = append(checksums, spdx.Checksum{
Algorithm: alg,
Value: d.Encoded(),
})
}
file, err := m.parseFile(pkg.FilePath, pkg.Digest)
return checksums
}
func (m *Marshaler) spdxFiles(c *core.Component) ([]*spdx.File, error) {
var files []*spdx.File
for _, file := range c.Files {
if file.Path == "" || len(file.Digests) == 0 {
continue
}
spdxFile, err := m.spdxFile(file.Path, file.Digests)
if err != nil {
return nil, xerrors.Errorf("failed to parse file: %w", err)
}
files = append(files, spdxFile)
}
return files, nil
}
func (m *Marshaler) spdxFile(filePath string, digests []digest.Digest) (*spdx.File, error) {
pkgID, err := calcPkgID(m.hasher, filePath)
if err != nil {
return nil, xerrors.Errorf("failed to parse file: %w", err)
return nil, xerrors.Errorf("failed to get %s package ID: %w", filePath, err)
}
return []*spdx.File{
&file,
return &spdx.File{
FileSPDXIdentifier: spdx.ElementID(fmt.Sprintf("File-%s", pkgID)),
FileName: filePath,
Checksums: m.spdxChecksums(digests),
}, nil
}
func elementID(elementType, pkgID string) spdx.ElementID {
return spdx.ElementID(fmt.Sprintf("%s-%s", elementType, pkgID))
}
func relationShip(refA, refB spdx.ElementID, operator string) *spdx.Relationship {
func (m *Marshaler) spdxRelationShip(refA, refB spdx.ElementID, operator string) *spdx.Relationship {
ref := spdx.Relationship{
RefA: common.MakeDocElementID("", string(refA)),
RefB: common.MakeDocElementID("", string(refB)),
@@ -405,51 +424,65 @@ func relationShip(refA, refB spdx.ElementID, operator string) *spdx.Relationship
return &ref
}
func appendAttributionText(attributionTexts []string, key, value string) []string {
if value == "" {
return attributionTexts
}
return append(attributionTexts, attributionText(key, value))
}
func attributionText(key, value string) string {
return fmt.Sprintf("%s: %s", key, value)
}
func purlExternalReference(packageURL string) *spdx.PackageExternalReference {
return &spdx.PackageExternalReference{
Category: CategoryPackageManager,
RefType: RefTypePurl,
Locator: packageURL,
func (m *Marshaler) spdxRelationshipType(relType core.RelationshipType) string {
switch relType {
case core.RelationshipDependsOn:
return RelationShipDependsOn
case core.RelationshipContains:
return RelationShipContains
case core.RelationshipDescribes:
return RelationShipDescribe
default:
return RelationShipDependsOn
}
}
func GetLicense(p ftypes.Package) string {
if len(p.Licenses) == 0 {
return noneField
}
license := strings.Join(lo.Map(p.Licenses, func(license string, index int) string {
// e.g. GPL-3.0-with-autoconf-exception
license = strings.ReplaceAll(license, "-with-", " WITH ")
license = strings.ReplaceAll(license, "-WITH-", " WITH ")
return fmt.Sprintf("(%s)", license)
}), " AND ")
s, err := expression.Normalize(license, licensing.Normalize, expression.NormalizeForSPDX)
if err != nil {
// Not fail on the invalid license
log.Logger.Warnf("Unable to marshal SPDX licenses %q", license)
return ""
}
return s
func sortPackages(pkgs []*spdx.Package) {
sort.Slice(pkgs, func(i, j int) bool {
switch {
case pkgs[i].PrimaryPackagePurpose != pkgs[j].PrimaryPackagePurpose:
return pkgs[i].PrimaryPackagePurpose < pkgs[j].PrimaryPackagePurpose
case pkgs[i].PackageName != pkgs[j].PackageName:
return pkgs[i].PackageName < pkgs[j].PackageName
default:
return pkgs[i].PackageSPDXIdentifier < pkgs[j].PackageSPDXIdentifier
}
})
}
func getDocumentNamespace(r types.Report, m *Marshaler) string {
func sortRelationships(rels []*spdx.Relationship) {
sort.Slice(rels, func(i, j int) bool {
switch {
case rels[i].RefA.ElementRefID != rels[j].RefA.ElementRefID:
return rels[i].RefA.ElementRefID < rels[j].RefA.ElementRefID
case rels[i].RefB.ElementRefID != rels[j].RefB.ElementRefID:
return rels[i].RefB.ElementRefID < rels[j].RefB.ElementRefID
default:
return rels[i].Relationship < rels[j].Relationship
}
})
}
func sortFiles(files []*spdx.File) {
sort.Slice(files, func(i, j int) bool {
switch {
case files[i].FileName != files[j].FileName:
return files[i].FileName < files[j].FileName
default:
return files[i].FileSPDXIdentifier < files[j].FileSPDXIdentifier
}
})
}
func elementID(elementType, pkgID string) spdx.ElementID {
return spdx.ElementID(fmt.Sprintf("%s-%s", elementType, pkgID))
}
func getDocumentNamespace(root *core.Component) string {
return fmt.Sprintf("%s/%s/%s-%s",
DocumentNamespace,
string(r.ArtifactType),
strings.ReplaceAll(strings.ReplaceAll(r.ArtifactName, "https://", ""), "http://", ""), // remove http(s):// prefix when scanning repos
string(root.Type),
strings.ReplaceAll(strings.ReplaceAll(root.Name, "https://", ""), "http://", ""), // remove http(s):// prefix when scanning repos
uuid.New().String(),
)
}
@@ -487,40 +520,19 @@ func camelCase(inputUnderScoreStr string) (camelCase string) {
return
}
func getPackageDownloadLocation(t ftypes.ArtifactType, artifactName string) string {
location := noneField
// this field is used for git/mercurial/subversion/bazaar:
// https://spdx.github.io/spdx-spec/v2.2.2/package-information/#77-package-download-location-field
if t == ftypes.ArtifactRepository {
// Trivy currently only supports git repositories. Format examples:
// git+https://git.myproject.org/MyProject.git
// git+http://git.myproject.org/MyProject
location = fmt.Sprintf("git+%s", artifactName)
}
return location
}
func digestToSpdxFileChecksum(d digest.Digest) []common.Checksum {
if d == "" {
return nil
}
var alg spdx.ChecksumAlgorithm
switch d.Algorithm() {
case digest.SHA1:
alg = spdx.SHA1
case digest.SHA256:
alg = spdx.SHA256
case digest.MD5:
alg = spdx.MD5
default:
return nil
}
return []spdx.Checksum{
{
Algorithm: alg,
Value: d.Encoded(),
},
func NormalizeLicense(licenses []string) string {
license := strings.Join(lo.Map(licenses, func(license string, index int) string {
// e.g. GPL-3.0-with-autoconf-exception
license = strings.ReplaceAll(license, "-with-", " WITH ")
license = strings.ReplaceAll(license, "-WITH-", " WITH ")
return fmt.Sprintf("(%s)", license)
}), " AND ")
s, err := expression.Normalize(license, licensing.Normalize, expression.NormalizeForSPDX)
if err != nil {
// Not fail on the invalid license
log.Logger.Warnf("Unable to marshal SPDX licenses %q", license)
return ""
}
return s
}

View File

@@ -2,6 +2,7 @@ package spdx_test
import (
"context"
"github.com/aquasecurity/trivy/pkg/sbom/core"
"github.com/package-url/packageurl-go"
"hash/fnv"
"testing"
@@ -144,7 +145,7 @@ func TestMarshaler_Marshal(t *testing.T) {
DataLicense: spdx.DataLicense,
SPDXIdentifier: "DOCUMENT",
DocumentName: "rails:latest",
DocumentNamespace: "http://aquasecurity.github.io/trivy/container_image/rails:latest-3ff14136-e09f-4df9-80ea-000000000001",
DocumentNamespace: "http://aquasecurity.github.io/trivy/container_image/rails:latest-3ff14136-e09f-4df9-80ea-000000000009",
CreationInfo: &spdx.CreationInfo{
Creators: []common.Creator{
{
@@ -160,12 +161,56 @@ func TestMarshaler_Marshal(t *testing.T) {
},
Packages: []*spdx.Package{
{
PackageSPDXIdentifier: spdx.ElementID("Package-eb0263038c3b445b"),
PackageSPDXIdentifier: spdx.ElementID("Application-9f48cdd13858abaf"),
PackageDownloadLocation: "NONE",
PackageName: "app/Gemfile.lock",
PrimaryPackagePurpose: tspdx.PackagePurposeApplication,
PackageAttributionTexts: []string{
"Class: lang-pkgs",
"Type: bundler",
},
},
{
PackageSPDXIdentifier: spdx.ElementID("Application-692290f4b2235359"),
PackageDownloadLocation: "NONE",
PackageName: "app/subproject/Gemfile.lock",
PrimaryPackagePurpose: tspdx.PackagePurposeApplication,
PackageAttributionTexts: []string{
"Class: lang-pkgs",
"Type: bundler",
},
},
{
PackageSPDXIdentifier: spdx.ElementID("ContainerImage-9396d894cd0cb6cb"),
PackageDownloadLocation: "NONE",
PackageName: "rails:latest",
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: tspdx.CategoryPackageManager,
RefType: tspdx.RefTypePurl,
Locator: "pkg:oci/rails@sha256%3Aa27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177?arch=arm64&repository_url=index.docker.io%2Flibrary%2Frails",
},
},
PackageAttributionTexts: []string{
"DiffID: sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a",
"ImageID: sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6",
"RepoDigest: rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177",
"RepoTag: rails:latest",
"SchemaVersion: 2",
"Size: 1024",
},
PrimaryPackagePurpose: tspdx.PackagePurposeContainer,
},
{
PackageSPDXIdentifier: spdx.ElementID("Package-b8d4663e6d412e7"),
PackageDownloadLocation: "NONE",
PackageName: "actioncontroller",
PackageVersion: "7.0.1",
PackageLicenseConcluded: "NONE",
PackageLicenseDeclared: "NONE",
PackageAttributionTexts: []string{
"PkgType: bundler",
},
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: tspdx.CategoryPackageManager,
@@ -175,14 +220,18 @@ func TestMarshaler_Marshal(t *testing.T) {
},
PrimaryPackagePurpose: tspdx.PackagePurposeLibrary,
PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion},
PackageSourceInfo: "package found in: app/subproject/Gemfile.lock",
},
{
PackageSPDXIdentifier: spdx.ElementID("Package-826226d056ff30c0"),
PackageSPDXIdentifier: spdx.ElementID("Package-3b51e821f6796568"),
PackageDownloadLocation: "NONE",
PackageName: "actionpack",
PackageVersion: "7.0.1",
PackageLicenseConcluded: "NONE",
PackageLicenseDeclared: "NONE",
PackageAttributionTexts: []string{
"PkgType: bundler",
},
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: tspdx.CategoryPackageManager,
@@ -192,14 +241,39 @@ func TestMarshaler_Marshal(t *testing.T) {
},
PrimaryPackagePurpose: tspdx.PackagePurposeLibrary,
PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion},
PackageSourceInfo: "package found in: app/subproject/Gemfile.lock",
},
{
PackageSPDXIdentifier: spdx.ElementID("Package-fd0dc3cf913d5bc3"),
PackageSPDXIdentifier: spdx.ElementID("Package-fb5630bc7d55a21c"),
PackageDownloadLocation: "NONE",
PackageName: "actionpack",
PackageVersion: "7.0.1",
PackageLicenseConcluded: "NONE",
PackageLicenseDeclared: "NONE",
PackageAttributionTexts: []string{
"PkgType: bundler",
},
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: tspdx.CategoryPackageManager,
RefType: tspdx.RefTypePurl,
Locator: "pkg:gem/actionpack@7.0.1",
},
},
PrimaryPackagePurpose: tspdx.PackagePurposeLibrary,
PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion},
PackageSourceInfo: "package found in: app/Gemfile.lock",
},
{
PackageSPDXIdentifier: spdx.ElementID("Package-5d43902b18ed2e2c"),
PackageDownloadLocation: "NONE",
PackageName: "binutils",
PackageVersion: "2.30-93.el8",
PackageLicenseConcluded: "GPL-3.0-or-later",
PackageLicenseDeclared: "GPL-3.0-or-later",
PackageAttributionTexts: []string{
"PkgType: centos",
},
PackageSupplier: &spdx.Supplier{
SupplierType: tspdx.PackageSupplierOrganization,
Supplier: "CentOS",
@@ -221,87 +295,56 @@ func TestMarshaler_Marshal(t *testing.T) {
},
},
{
PackageSPDXIdentifier: spdx.ElementID("Application-73c871d73f3c8248"),
PackageDownloadLocation: "NONE",
PackageName: "bundler",
PackageSourceInfo: "app/subproject/Gemfile.lock",
PrimaryPackagePurpose: tspdx.PackagePurposeApplication,
},
{
PackageSPDXIdentifier: spdx.ElementID("Application-c3fac92c1ac0a9fa"),
PackageDownloadLocation: "NONE",
PackageName: "bundler",
PackageSourceInfo: "app/Gemfile.lock",
PrimaryPackagePurpose: tspdx.PackagePurposeApplication,
},
{
PackageSPDXIdentifier: spdx.ElementID("OperatingSystem-197f9a00ebcb51f0"),
PackageSPDXIdentifier: spdx.ElementID("OperatingSystem-20f7fa3049cc748c"),
PackageDownloadLocation: "NONE",
PackageName: "centos",
PackageVersion: "8.3.2011",
PrimaryPackagePurpose: tspdx.PackagePurposeOS,
},
{
PackageSPDXIdentifier: spdx.ElementID("ContainerImage-9396d894cd0cb6cb"),
PackageDownloadLocation: "NONE",
PackageName: "rails:latest",
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: tspdx.CategoryPackageManager,
RefType: tspdx.RefTypePurl,
Locator: "pkg:oci/rails@sha256%3Aa27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177?arch=arm64&repository_url=index.docker.io%2Flibrary%2Frails",
},
},
PackageAttributionTexts: []string{
"SchemaVersion: 2",
"ImageID: sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6",
"Size: 1024",
"RepoDigest: rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177",
"DiffID: sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a",
"RepoTag: rails:latest",
"Class: os-pkgs",
"Type: centos",
},
PrimaryPackagePurpose: tspdx.PackagePurposeContainer,
},
},
Relationships: []*spdx.Relationship{
{
RefA: spdx.DocElementID{ElementRefID: "Application-692290f4b2235359"},
RefB: spdx.DocElementID{ElementRefID: "Package-3b51e821f6796568"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Application-692290f4b2235359"},
RefB: spdx.DocElementID{ElementRefID: "Package-b8d4663e6d412e7"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Application-9f48cdd13858abaf"},
RefB: spdx.DocElementID{ElementRefID: "Package-fb5630bc7d55a21c"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"},
RefB: spdx.DocElementID{ElementRefID: "Application-692290f4b2235359"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"},
RefB: spdx.DocElementID{ElementRefID: "Application-9f48cdd13858abaf"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"},
RefB: spdx.DocElementID{ElementRefID: "OperatingSystem-20f7fa3049cc748c"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"},
RefB: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"},
Relationship: "DESCRIBES",
},
{
RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"},
RefB: spdx.DocElementID{ElementRefID: "OperatingSystem-197f9a00ebcb51f0"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "OperatingSystem-197f9a00ebcb51f0"},
RefB: spdx.DocElementID{ElementRefID: "Package-fd0dc3cf913d5bc3"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"},
RefB: spdx.DocElementID{ElementRefID: "Application-73c871d73f3c8248"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Application-73c871d73f3c8248"},
RefB: spdx.DocElementID{ElementRefID: "Package-826226d056ff30c0"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Application-73c871d73f3c8248"},
RefB: spdx.DocElementID{ElementRefID: "Package-eb0263038c3b445b"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"},
RefB: spdx.DocElementID{ElementRefID: "Application-c3fac92c1ac0a9fa"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Application-c3fac92c1ac0a9fa"},
RefB: spdx.DocElementID{ElementRefID: "Package-826226d056ff30c0"},
RefA: spdx.DocElementID{ElementRefID: "OperatingSystem-20f7fa3049cc748c"},
RefB: spdx.DocElementID{ElementRefID: "Package-5d43902b18ed2e2c"},
Relationship: "CONTAINS",
},
},
@@ -420,7 +463,7 @@ func TestMarshaler_Marshal(t *testing.T) {
DataLicense: spdx.DataLicense,
SPDXIdentifier: "DOCUMENT",
DocumentName: "centos:latest",
DocumentNamespace: "http://aquasecurity.github.io/trivy/container_image/centos:latest-3ff14136-e09f-4df9-80ea-000000000001",
DocumentNamespace: "http://aquasecurity.github.io/trivy/container_image/centos:latest-3ff14136-e09f-4df9-80ea-000000000006",
CreationInfo: &spdx.CreationInfo{
Creators: []common.Creator{
{
@@ -436,12 +479,27 @@ func TestMarshaler_Marshal(t *testing.T) {
},
Packages: []*spdx.Package{
{
PackageSPDXIdentifier: spdx.ElementID("Package-d8dccb186bafaf37"),
PackageName: "centos:latest",
PackageSPDXIdentifier: "ContainerImage-413bfede37ad01fc",
PackageDownloadLocation: "NONE",
PackageAttributionTexts: []string{
"ImageID: sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6",
"RepoTag: centos:latest",
"SchemaVersion: 2",
"Size: 1024",
},
PrimaryPackagePurpose: tspdx.PackagePurposeContainer,
},
{
PackageSPDXIdentifier: spdx.ElementID("Package-40c4059fe08523bf"),
PackageDownloadLocation: "NONE",
PackageName: "acl",
PackageVersion: "1:2.2.53-1.el8",
PackageLicenseConcluded: "GPL-2.0-or-later",
PackageLicenseDeclared: "GPL-2.0-or-later",
PackageAttributionTexts: []string{
"PkgType: centos",
},
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: tspdx.CategoryPackageManager,
@@ -460,7 +518,7 @@ func TestMarshaler_Marshal(t *testing.T) {
},
},
{
PackageSPDXIdentifier: spdx.ElementID("Package-13fe667a0805e6b7"),
PackageSPDXIdentifier: spdx.ElementID("Package-69f68dd639314edd"),
PackageDownloadLocation: "NONE",
PackageName: "actionpack",
PackageVersion: "7.0.1",
@@ -475,6 +533,7 @@ func TestMarshaler_Marshal(t *testing.T) {
},
PackageAttributionTexts: []string{
"LayerDiffID: sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488",
"PkgType: gemspec",
},
PrimaryPackagePurpose: tspdx.PackagePurposeLibrary,
PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion},
@@ -484,7 +543,7 @@ func TestMarshaler_Marshal(t *testing.T) {
},
},
{
PackageSPDXIdentifier: spdx.ElementID("Package-d5443dbcbba0dbd4"),
PackageSPDXIdentifier: spdx.ElementID("Package-da2cda24d2ecbfe6"),
PackageDownloadLocation: "NONE",
PackageName: "actionpack",
PackageVersion: "7.0.1",
@@ -499,6 +558,7 @@ func TestMarshaler_Marshal(t *testing.T) {
},
PackageAttributionTexts: []string{
"LayerDiffID: sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488",
"PkgType: gemspec",
},
PrimaryPackagePurpose: tspdx.PackagePurposeLibrary,
PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion},
@@ -508,43 +568,18 @@ func TestMarshaler_Marshal(t *testing.T) {
},
},
{
PackageSPDXIdentifier: spdx.ElementID("OperatingSystem-197f9a00ebcb51f0"),
PackageSPDXIdentifier: spdx.ElementID("OperatingSystem-20f7fa3049cc748c"),
PackageDownloadLocation: "NONE",
PackageName: "centos",
PackageVersion: "8.3.2011",
PrimaryPackagePurpose: tspdx.PackagePurposeOS,
},
{
PackageName: "centos:latest",
PackageSPDXIdentifier: "ContainerImage-413bfede37ad01fc",
PackageDownloadLocation: "NONE",
PackageAttributionTexts: []string{
"SchemaVersion: 2",
"ImageID: sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6",
"Size: 1024",
"RepoTag: centos:latest",
"Class: os-pkgs",
"Type: centos",
},
PrimaryPackagePurpose: tspdx.PackagePurposeContainer,
},
{
PackageSPDXIdentifier: spdx.ElementID("Application-441a648f2aeeee72"),
PackageDownloadLocation: "NONE",
PackageName: "gemspec",
PackageSourceInfo: "Ruby",
PrimaryPackagePurpose: tspdx.PackagePurposeApplication,
},
},
Files: []*spdx.File{
{
FileSPDXIdentifier: "File-6a540784b0dc6d55",
FileName: "tools/project-john/specifications/actionpack.gemspec",
Checksums: []spdx.Checksum{
{
Algorithm: spdx.SHA1,
Value: "d2f9f9aed5161f6e4116a3f9573f41cd832f137c",
},
},
},
{
FileSPDXIdentifier: "File-fa42187221d0d0a8",
FileName: "tools/project-doe/specifications/actionpack.gemspec",
@@ -555,48 +590,53 @@ func TestMarshaler_Marshal(t *testing.T) {
},
},
},
{
FileSPDXIdentifier: "File-6a540784b0dc6d55",
FileName: "tools/project-john/specifications/actionpack.gemspec",
Checksums: []spdx.Checksum{
{
Algorithm: spdx.SHA1,
Value: "d2f9f9aed5161f6e4116a3f9573f41cd832f137c",
},
},
},
},
Relationships: []*spdx.Relationship{
{
RefA: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"},
RefB: spdx.DocElementID{ElementRefID: "OperatingSystem-20f7fa3049cc748c"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"},
RefB: spdx.DocElementID{ElementRefID: "Package-69f68dd639314edd"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"},
RefB: spdx.DocElementID{ElementRefID: "Package-da2cda24d2ecbfe6"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"},
RefB: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"},
Relationship: "DESCRIBES",
},
{
RefA: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"},
RefB: spdx.DocElementID{ElementRefID: "OperatingSystem-197f9a00ebcb51f0"},
RefA: spdx.DocElementID{ElementRefID: "OperatingSystem-20f7fa3049cc748c"},
RefB: spdx.DocElementID{ElementRefID: "Package-40c4059fe08523bf"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "OperatingSystem-197f9a00ebcb51f0"},
RefB: spdx.DocElementID{ElementRefID: "Package-d8dccb186bafaf37"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"},
RefB: spdx.DocElementID{ElementRefID: "Application-441a648f2aeeee72"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Application-441a648f2aeeee72"},
RefB: spdx.DocElementID{ElementRefID: "Package-d5443dbcbba0dbd4"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Package-d5443dbcbba0dbd4"},
RefB: spdx.DocElementID{ElementRefID: "File-6a540784b0dc6d55"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Application-441a648f2aeeee72"},
RefB: spdx.DocElementID{ElementRefID: "Package-13fe667a0805e6b7"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Package-13fe667a0805e6b7"},
RefA: spdx.DocElementID{ElementRefID: "Package-69f68dd639314edd"},
RefB: spdx.DocElementID{ElementRefID: "File-fa42187221d0d0a8"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Package-da2cda24d2ecbfe6"},
RefB: spdx.DocElementID{ElementRefID: "File-6a540784b0dc6d55"},
Relationship: "CONTAINS",
},
},
OtherLicenses: nil,
@@ -629,6 +669,26 @@ func TestMarshaler_Marshal(t *testing.T) {
},
},
},
{
Target: "pom.xml",
Class: types.ClassLangPkg,
Type: ftypes.Pom,
Packages: []ftypes.Package{
{
ID: "com.example:example:1.0.0",
Name: "com.example:example",
Version: "1.0.0",
Identifier: ftypes.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeMaven,
Namespace: "com.example",
Name: "example",
Version: "1.0.0",
},
},
},
},
},
},
},
wantSBOM: &spdx.Document{
@@ -636,7 +696,7 @@ func TestMarshaler_Marshal(t *testing.T) {
DataLicense: spdx.DataLicense,
SPDXIdentifier: "DOCUMENT",
DocumentName: "masahiro331/CVE-2021-41098",
DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/masahiro331/CVE-2021-41098-3ff14136-e09f-4df9-80ea-000000000001",
DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/masahiro331/CVE-2021-41098-3ff14136-e09f-4df9-80ea-000000000006",
CreationInfo: &spdx.CreationInfo{
Creators: []common.Creator{
{
@@ -652,7 +712,27 @@ func TestMarshaler_Marshal(t *testing.T) {
},
Packages: []*spdx.Package{
{
PackageSPDXIdentifier: spdx.ElementID("Package-3da61e86d0530402"),
PackageSPDXIdentifier: spdx.ElementID("Application-ed046c4a6b4da30f"),
PackageDownloadLocation: "NONE",
PackageName: "Gemfile.lock",
PrimaryPackagePurpose: tspdx.PackagePurposeApplication,
PackageAttributionTexts: []string{
"Class: lang-pkgs",
"Type: bundler",
},
},
{
PackageSPDXIdentifier: spdx.ElementID("Application-800d9e6e0f88ab3a"),
PackageDownloadLocation: "NONE",
PackageName: "pom.xml",
PrimaryPackagePurpose: tspdx.PackagePurposeApplication,
PackageAttributionTexts: []string{
"Class: lang-pkgs",
"Type: pom",
},
},
{
PackageSPDXIdentifier: spdx.ElementID("Package-e78eaf94802a53dc"),
PackageDownloadLocation: "NONE",
PackageName: "actioncable",
PackageVersion: "6.1.4.1",
@@ -667,13 +747,32 @@ func TestMarshaler_Marshal(t *testing.T) {
},
PrimaryPackagePurpose: tspdx.PackagePurposeLibrary,
PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion},
PackageSourceInfo: "package found in: Gemfile.lock",
PackageAttributionTexts: []string{
"PkgType: bundler",
},
},
{
PackageSPDXIdentifier: spdx.ElementID("Application-9dd4a4ba7077cc5a"),
PackageSPDXIdentifier: spdx.ElementID("Package-69cd7625c68537c7"),
PackageDownloadLocation: "NONE",
PackageName: "bundler",
PackageSourceInfo: "Gemfile.lock",
PrimaryPackagePurpose: tspdx.PackagePurposeApplication,
PackageName: "com.example:example",
PackageVersion: "1.0.0",
PackageLicenseConcluded: "NONE",
PackageLicenseDeclared: "NONE",
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: tspdx.CategoryPackageManager,
RefType: tspdx.RefTypePurl,
Locator: "pkg:maven/com.example/example@1.0.0",
},
},
PrimaryPackagePurpose: tspdx.PackagePurposeLibrary,
PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion},
PackageSourceInfo: "package found in: pom.xml",
PackageAttributionTexts: []string{
"PkgID: com.example:example:1.0.0",
"PkgType: pom",
},
},
{
PackageSPDXIdentifier: spdx.ElementID("Filesystem-5af0f1f08c20909a"),
@@ -686,6 +785,16 @@ func TestMarshaler_Marshal(t *testing.T) {
},
},
Relationships: []*spdx.Relationship{
{
RefA: spdx.DocElementID{ElementRefID: "Application-800d9e6e0f88ab3a"},
RefB: spdx.DocElementID{ElementRefID: "Package-69cd7625c68537c7"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Application-ed046c4a6b4da30f"},
RefB: spdx.DocElementID{ElementRefID: "Package-e78eaf94802a53dc"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"},
RefB: spdx.DocElementID{ElementRefID: "Filesystem-5af0f1f08c20909a"},
@@ -693,12 +802,12 @@ func TestMarshaler_Marshal(t *testing.T) {
},
{
RefA: spdx.DocElementID{ElementRefID: "Filesystem-5af0f1f08c20909a"},
RefB: spdx.DocElementID{ElementRefID: "Application-9dd4a4ba7077cc5a"},
RefB: spdx.DocElementID{ElementRefID: "Application-800d9e6e0f88ab3a"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Application-9dd4a4ba7077cc5a"},
RefB: spdx.DocElementID{ElementRefID: "Package-3da61e86d0530402"},
RefA: spdx.DocElementID{ElementRefID: "Filesystem-5af0f1f08c20909a"},
RefB: spdx.DocElementID{ElementRefID: "Application-ed046c4a6b4da30f"},
Relationship: "CONTAINS",
},
},
@@ -730,6 +839,7 @@ func TestMarshaler_Marshal(t *testing.T) {
Layer: ftypes.Layer{
DiffID: "sha256:661c3fd3cc16b34c070f3620ca6b03b6adac150f9a7e5d0e3c707a159990f88e",
},
Digest: "sha256:a5efa82f08774597165e8c1a102d45d0406913b74c184883ac91f409ae26009d",
FilePath: "usr/local/lib/ruby/gems/3.1.0/gems/typeprof-0.21.1/vscode/package.json",
},
},
@@ -741,7 +851,7 @@ func TestMarshaler_Marshal(t *testing.T) {
DataLicense: spdx.DataLicense,
SPDXIdentifier: "DOCUMENT",
DocumentName: "http://test-aggregate",
DocumentNamespace: "http://aquasecurity.github.io/trivy/repository/test-aggregate-3ff14136-e09f-4df9-80ea-000000000001",
DocumentNamespace: "http://aquasecurity.github.io/trivy/repository/test-aggregate-3ff14136-e09f-4df9-80ea-000000000003",
CreationInfo: &spdx.CreationInfo{
Creators: []common.Creator{
{
@@ -757,23 +867,7 @@ func TestMarshaler_Marshal(t *testing.T) {
},
Packages: []*spdx.Package{
{
PackageName: "http://test-aggregate",
PackageSPDXIdentifier: "Repository-1a78857c1a6a759e",
PackageDownloadLocation: "git+http://test-aggregate",
PackageAttributionTexts: []string{
"SchemaVersion: 2",
},
PrimaryPackagePurpose: tspdx.PackagePurposeSource,
},
{
PackageSPDXIdentifier: "Application-24f8a80152e2c0fc",
PackageDownloadLocation: "git+http://test-aggregate",
PackageName: "node-pkg",
PackageSourceInfo: "Node.js",
PrimaryPackagePurpose: tspdx.PackagePurposeApplication,
},
{
PackageSPDXIdentifier: spdx.ElementID("Package-daedb173cfd43058"),
PackageSPDXIdentifier: spdx.ElementID("Package-52b8e939bac2d133"),
PackageDownloadLocation: "git+http://test-aggregate",
PackageName: "ruby-typeprof",
PackageVersion: "0.20.1",
@@ -788,6 +882,7 @@ func TestMarshaler_Marshal(t *testing.T) {
},
PackageAttributionTexts: []string{
"LayerDiffID: sha256:661c3fd3cc16b34c070f3620ca6b03b6adac150f9a7e5d0e3c707a159990f88e",
"PkgType: node-pkg",
},
PrimaryPackagePurpose: tspdx.PackagePurposeLibrary,
PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion},
@@ -796,11 +891,26 @@ func TestMarshaler_Marshal(t *testing.T) {
Value: "da39a3ee5e6b4b0d3255bfef95601890afd80709",
},
},
{
PackageSPDXIdentifier: "Repository-1a78857c1a6a759e",
PackageName: "http://test-aggregate",
PackageDownloadLocation: "git+http://test-aggregate",
PackageAttributionTexts: []string{
"SchemaVersion: 2",
},
PrimaryPackagePurpose: tspdx.PackagePurposeSource,
},
},
Files: []*spdx.File{
{
FileName: "usr/local/lib/ruby/gems/3.1.0/gems/typeprof-0.21.1/vscode/package.json",
FileSPDXIdentifier: "File-a52825a3e5bc6dfe",
Checksums: []common.Checksum{
{
Algorithm: common.SHA256,
Value: "a5efa82f08774597165e8c1a102d45d0406913b74c184883ac91f409ae26009d",
},
},
},
},
Relationships: []*spdx.Relationship{
@@ -810,20 +920,15 @@ func TestMarshaler_Marshal(t *testing.T) {
Relationship: "DESCRIBES",
},
{
RefA: spdx.DocElementID{ElementRefID: "Repository-1a78857c1a6a759e"},
RefB: spdx.DocElementID{ElementRefID: "Application-24f8a80152e2c0fc"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Application-24f8a80152e2c0fc"},
RefB: spdx.DocElementID{ElementRefID: "Package-daedb173cfd43058"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Package-daedb173cfd43058"},
RefA: spdx.DocElementID{ElementRefID: "Package-52b8e939bac2d133"},
RefB: spdx.DocElementID{ElementRefID: "File-a52825a3e5bc6dfe"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Repository-1a78857c1a6a759e"},
RefB: spdx.DocElementID{ElementRefID: "Package-52b8e939bac2d133"},
Relationship: "CONTAINS",
},
},
},
},
@@ -840,7 +945,7 @@ func TestMarshaler_Marshal(t *testing.T) {
DataLicense: spdx.DataLicense,
SPDXIdentifier: "DOCUMENT",
DocumentName: "empty/path",
DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/empty/path-3ff14136-e09f-4df9-80ea-000000000001",
DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/empty/path-3ff14136-e09f-4df9-80ea-000000000002",
CreationInfo: &spdx.CreationInfo{
Creators: []common.Creator{
@@ -903,8 +1008,7 @@ func TestMarshaler_Marshal(t *testing.T) {
DataLicense: spdx.DataLicense,
SPDXIdentifier: "DOCUMENT",
DocumentName: "secret",
DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/secret-3ff14136-e09f-4df9-80ea-000000000001",
DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/secret-3ff14136-e09f-4df9-80ea-000000000002",
CreationInfo: &spdx.CreationInfo{
Creators: []common.Creator{
{
@@ -946,7 +1050,7 @@ func TestMarshaler_Marshal(t *testing.T) {
ArtifactType: ftypes.ArtifactFilesystem,
Results: types.Results{
{
Target: "artifact",
Target: "/usr/local/bin/test",
Class: types.ClassLangPkg,
Type: ftypes.GoBinary,
Packages: []ftypes.Package{
@@ -975,7 +1079,7 @@ func TestMarshaler_Marshal(t *testing.T) {
DataLicense: spdx.DataLicense,
SPDXIdentifier: "DOCUMENT",
DocumentName: "go-artifact",
DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/go-artifact-3ff14136-e09f-4df9-80ea-000000000001",
DocumentNamespace: "http://aquasecurity.github.io/trivy/filesystem/go-artifact-3ff14136-e09f-4df9-80ea-000000000005",
CreationInfo: &spdx.CreationInfo{
Creators: []common.Creator{
{
@@ -991,7 +1095,17 @@ func TestMarshaler_Marshal(t *testing.T) {
},
Packages: []*spdx.Package{
{
PackageSPDXIdentifier: spdx.ElementID("Package-9164ae38c5cdf815"),
PackageSPDXIdentifier: spdx.ElementID("Application-aab0f4e8cf174c67"),
PackageDownloadLocation: "NONE",
PackageName: "/usr/local/bin/test",
PrimaryPackagePurpose: tspdx.PackagePurposeApplication,
PackageAttributionTexts: []string{
"Class: lang-pkgs",
"Type: gobinary",
},
},
{
PackageSPDXIdentifier: spdx.ElementID("Package-9a16e221e11f8a90"),
PackageDownloadLocation: "NONE",
PackageName: "./private_repos/cnrm.googlesource.com/cnrm/",
PackageVersion: "(devel)",
@@ -999,25 +1113,13 @@ func TestMarshaler_Marshal(t *testing.T) {
PackageLicenseDeclared: "NONE",
PrimaryPackagePurpose: tspdx.PackagePurposeLibrary,
PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion},
},
{
PackageName: "go-artifact",
PackageSPDXIdentifier: "Filesystem-e340f27468b382be",
PackageDownloadLocation: "NONE",
PackageSourceInfo: "package found in: /usr/local/bin/test",
PackageAttributionTexts: []string{
"SchemaVersion: 2",
"PkgType: gobinary",
},
PrimaryPackagePurpose: tspdx.PackagePurposeSource,
},
{
PackageSPDXIdentifier: spdx.ElementID("Application-6666b83a5d554671"),
PackageDownloadLocation: "NONE",
PackageName: "gobinary",
PackageSourceInfo: "artifact",
PrimaryPackagePurpose: tspdx.PackagePurposeApplication,
},
{
PackageSPDXIdentifier: spdx.ElementID("Package-8451f2bc8e1f45aa"),
PackageSPDXIdentifier: spdx.ElementID("Package-b9b7ae633941e083"),
PackageDownloadLocation: "NONE",
PackageName: "golang.org/x/crypto",
PackageVersion: "v0.0.1",
@@ -1032,9 +1134,32 @@ func TestMarshaler_Marshal(t *testing.T) {
},
PrimaryPackagePurpose: tspdx.PackagePurposeLibrary,
PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion},
PackageSourceInfo: "package found in: /usr/local/bin/test",
PackageAttributionTexts: []string{
"PkgType: gobinary",
},
},
{
PackageName: "go-artifact",
PackageSPDXIdentifier: "Filesystem-e340f27468b382be",
PackageDownloadLocation: "NONE",
PackageAttributionTexts: []string{
"SchemaVersion: 2",
},
PrimaryPackagePurpose: tspdx.PackagePurposeSource,
},
},
Relationships: []*spdx.Relationship{
{
RefA: spdx.DocElementID{ElementRefID: "Application-aab0f4e8cf174c67"},
RefB: spdx.DocElementID{ElementRefID: "Package-9a16e221e11f8a90"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Application-aab0f4e8cf174c67"},
RefB: spdx.DocElementID{ElementRefID: "Package-b9b7ae633941e083"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"},
RefB: spdx.DocElementID{ElementRefID: "Filesystem-e340f27468b382be"},
@@ -1042,17 +1167,7 @@ func TestMarshaler_Marshal(t *testing.T) {
},
{
RefA: spdx.DocElementID{ElementRefID: "Filesystem-e340f27468b382be"},
RefB: spdx.DocElementID{ElementRefID: "Application-6666b83a5d554671"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Application-6666b83a5d554671"},
RefB: spdx.DocElementID{ElementRefID: "Package-9164ae38c5cdf815"},
Relationship: "CONTAINS",
},
{
RefA: spdx.DocElementID{ElementRefID: "Application-6666b83a5d554671"},
RefB: spdx.DocElementID{ElementRefID: "Package-8451f2bc8e1f45aa"},
RefB: spdx.DocElementID{ElementRefID: "Application-aab0f4e8cf174c67"},
Relationship: "CONTAINS",
},
},
@@ -1064,17 +1179,18 @@ func TestMarshaler_Marshal(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
// Fake function calculating the hash value
h := fnv.New64()
hasher := func(v interface{}, format hashstructure.Format, opts *hashstructure.HashOptions) (uint64, error) {
hasher := func(v any, format hashstructure.Format, opts *hashstructure.HashOptions) (uint64, error) {
h.Reset()
var str string
switch v.(type) {
case ftypes.Package:
str = v.(ftypes.Package).Name + v.(ftypes.Package).FilePath
switch vv := v.(type) {
case *core.Component:
str = vv.Name + vv.Version + vv.SrcFile
for _, f := range vv.Files {
str += f.Path
}
case string:
str = v.(string)
case *ftypes.OS:
str = v.(*ftypes.OS).Name
str = vv
default:
require.Failf(t, "unknown type", "%T", v)
}
@@ -1090,7 +1206,7 @@ func TestMarshaler_Marshal(t *testing.T) {
uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d")
marshaler := tspdx.NewMarshaler("0.38.1", tspdx.WithHasher(hasher))
spdxDoc, err := marshaler.Marshal(ctx, tc.inputReport)
spdxDoc, err := marshaler.MarshalReport(ctx, tc.inputReport)
require.NoError(t, err)
assert.Equal(t, tc.wantSBOM, spdxDoc)
@@ -1101,62 +1217,52 @@ func TestMarshaler_Marshal(t *testing.T) {
func Test_GetLicense(t *testing.T) {
tests := []struct {
name string
input ftypes.Package
input []string
want string
}{
{
name: "happy path",
input: ftypes.Package{
Licenses: []string{
"GPLv2+",
},
input: []string{
"GPLv2+",
},
want: "GPL-2.0-or-later",
},
{
name: "happy path with multi license",
input: ftypes.Package{
Licenses: []string{
"GPLv2+",
"GPLv3+",
},
input: []string{
"GPLv2+",
"GPLv3+",
},
want: "GPL-2.0-or-later AND GPL-3.0-or-later",
},
{
name: "happy path with OR operator",
input: ftypes.Package{
Licenses: []string{
"GPLv2+",
"LGPL 2.0 or GNU LESSER",
},
input: []string{
"GPLv2+",
"LGPL 2.0 or GNU LESSER",
},
want: "GPL-2.0-or-later AND (LGPL-2.0-only OR LGPL-3.0-only)",
},
{
name: "happy path with AND operator",
input: ftypes.Package{
Licenses: []string{
"GPLv2+",
"LGPL 2.0 and GNU LESSER",
},
input: []string{
"GPLv2+",
"LGPL 2.0 and GNU LESSER",
},
want: "GPL-2.0-or-later AND LGPL-2.0-only AND LGPL-3.0-only",
},
{
name: "happy path with WITH operator",
input: ftypes.Package{
Licenses: []string{
"AFL 2.0",
"AFL 3.0 with distribution exception",
},
input: []string{
"AFL 2.0",
"AFL 3.0 with distribution exception",
},
want: "AFL-2.0 AND AFL-3.0 WITH distribution-exception",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, tspdx.GetLicense(tt.input), "getLicense(%v)", tt.input)
assert.Equal(t, tt.want, tspdx.NormalizeLicense(tt.input))
})
}
}

View File

@@ -27,13 +27,13 @@
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0",
"referenceLocator": "pkg:invalid",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"name": "musl",
"sourceInfo": "built package from: invalid",
"sourceInfo": "built package from: musl",
"versionInfo": "1.2.3-r0"
}
],

View File

@@ -2,13 +2,10 @@ package spdx
import (
"bytes"
"errors"
"fmt"
"io"
"sort"
"strings"
version "github.com/knqyf263/go-rpm-version"
"github.com/package-url/packageurl-go"
"github.com/samber/lo"
"github.com/spdx/tools-golang/json"
@@ -17,17 +14,14 @@ import (
"github.com/spdx/tools-golang/tagvalue"
"golang.org/x/xerrors"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/purl"
"github.com/aquasecurity/trivy/pkg/types"
)
var (
errUnknownPackageFormat = xerrors.New("unknown package format")
"github.com/aquasecurity/trivy/pkg/sbom/core"
)
type SPDX struct {
*types.SBOM
*core.BOM
trivySBOM bool
pkgFilePaths map[common.ElementID]string
}
func NewTVDecoder(r io.Reader) *TVDecoder {
@@ -48,8 +42,7 @@ func (tv *TVDecoder) Decode(v interface{}) error {
if !ok {
return xerrors.Errorf("invalid struct type tag-value decoder needed SPDX struct")
}
err = a.unmarshal(spdxDocument)
if err != nil {
if err = a.unmarshal(spdxDocument); err != nil {
return xerrors.Errorf("failed to unmarshal spdx: %w", err)
}
@@ -57,273 +50,57 @@ func (tv *TVDecoder) Decode(v interface{}) error {
}
func (s *SPDX) UnmarshalJSON(b []byte) error {
if s.BOM == nil {
s.BOM = core.NewBOM(core.Options{})
}
if s.pkgFilePaths == nil {
s.pkgFilePaths = make(map[common.ElementID]string)
}
spdxDocument, err := json.Read(bytes.NewReader(b))
if err != nil {
return xerrors.Errorf("failed to load spdx json: %w", err)
}
err = s.unmarshal(spdxDocument)
if err != nil {
if err = s.unmarshal(spdxDocument); err != nil {
return xerrors.Errorf("failed to unmarshal spdx: %w", err)
}
return nil
}
func (s *SPDX) unmarshal(spdxDocument *spdx.Document) error {
var osPkgs []ftypes.Package
apps := make(map[common.ElementID]*ftypes.Application)
packageSPDXIdentifierMap := createPackageSPDXIdentifierMap(spdxDocument.Packages)
packageFilePaths := getPackageFilePaths(spdxDocument)
s.trivySBOM = s.isTrivySBOM(spdxDocument)
// Hold packages that are not processed by relationships
orphanPkgs := createPackageSPDXIdentifierMap(spdxDocument.Packages)
// Parse files and find file paths for packages
s.parseFiles(spdxDocument)
relationships := lo.Filter(spdxDocument.Relationships, func(rel *spdx.Relationship, _ int) bool {
// Skip the DESCRIBES relationship.
return rel.Relationship != common.TypeRelationshipDescribe && rel.Relationship != "DESCRIBE"
})
// Package relationships would be as belows:
// - Root (container image, filesystem, etc.)
// - Operating System (debian 10)
// - OS package A
// - OS package B
// - Application 1 (package-lock.json)
// - Node.js package A
// - Node.js package B
// - Application 2 (Pipfile.lock)
// - Python package A
// - Python package B
for _, rel := range relationships {
pkgA := packageSPDXIdentifierMap[rel.RefA.ElementRefID]
pkgB := packageSPDXIdentifierMap[rel.RefB.ElementRefID]
if pkgA == nil || pkgB == nil {
// Skip the missing pkg relationship.
continue
}
switch {
// Relationship: root package => OS
case isOperatingSystem(pkgB.PackageSPDXIdentifier):
s.SBOM.Metadata.OS = parseOS(*pkgB)
delete(orphanPkgs, pkgB.PackageSPDXIdentifier)
// Relationship: OS => OS package
case isOperatingSystem(pkgA.PackageSPDXIdentifier):
pkg, _, err := parsePkg(*pkgB, packageFilePaths)
if errors.Is(err, errUnknownPackageFormat) {
continue
} else if err != nil {
return xerrors.Errorf("failed to parse os package: %w", err)
}
osPkgs = append(osPkgs, *pkg)
delete(orphanPkgs, pkgB.PackageSPDXIdentifier)
// Relationship: root package => application
case isApplication(pkgB.PackageSPDXIdentifier):
// pass
// Relationship: application => language-specific package
case isApplication(pkgA.PackageSPDXIdentifier):
app, ok := apps[pkgA.PackageSPDXIdentifier]
if !ok {
app = initApplication(*pkgA)
apps[pkgA.PackageSPDXIdentifier] = app
}
lib, _, err := parsePkg(*pkgB, packageFilePaths)
if errors.Is(err, errUnknownPackageFormat) {
continue
} else if err != nil {
return xerrors.Errorf("failed to parse language-specific package: %w", err)
}
app.Libraries = append(app.Libraries, *lib)
// They are no longer orphan packages
delete(orphanPkgs, pkgA.PackageSPDXIdentifier)
delete(orphanPkgs, pkgB.PackageSPDXIdentifier)
}
}
// Fill OS packages
if len(osPkgs) > 0 {
s.Packages = []ftypes.PackageInfo{{Packages: osPkgs}}
}
// Fill applications
for _, app := range apps {
s.SBOM.Applications = append(s.SBOM.Applications, *app)
}
// Fallback for when there are no effective relationships.
if err := s.parsePackages(orphanPkgs); err != nil {
return err
}
return nil
}
// parsePackages processes the packages and categorizes them into OS packages and application packages.
// Note that all language-specific packages are treated as a single application.
func (s *SPDX) parsePackages(pkgs map[common.ElementID]*spdx.Package) error {
var (
osPkgs []ftypes.Package
apps = make(map[ftypes.LangType]ftypes.Application)
)
for _, p := range pkgs {
pkg, pkgURL, err := parsePkg(*p, nil)
if errors.Is(err, errUnknownPackageFormat) {
continue
} else if err != nil {
return xerrors.Errorf("failed to parse package: %w", err)
}
switch pkgURL.Class() {
case types.ClassOSPkg:
osPkgs = append(osPkgs, *pkg)
case types.ClassLangPkg:
// Language-specific packages
pkgType := pkgURL.LangType()
app, ok := apps[pkgType]
if !ok {
app.Type = pkgType
}
app.Libraries = append(app.Libraries, *pkg)
apps[pkgType] = app
}
}
if len(osPkgs) > 0 {
s.Packages = []ftypes.PackageInfo{{Packages: osPkgs}}
}
for _, app := range apps {
sort.Sort(app.Libraries)
s.SBOM.Applications = append(s.SBOM.Applications, app)
}
return nil
}
func createPackageSPDXIdentifierMap(packages []*spdx.Package) map[common.ElementID]*spdx.Package {
return lo.SliceToMap(packages, func(pkg *spdx.Package) (common.ElementID, *spdx.Package) {
return pkg.PackageSPDXIdentifier, pkg
})
}
func createFileSPDXIdentifierMap(files []*spdx.File) map[string]*spdx.File {
ret := make(map[string]*spdx.File)
for _, file := range files {
ret[string(file.FileSPDXIdentifier)] = file
}
return ret
}
func isOperatingSystem(elementID spdx.ElementID) bool {
return strings.HasPrefix(string(elementID), ElementOperatingSystem)
}
func isApplication(elementID spdx.ElementID) bool {
return strings.HasPrefix(string(elementID), ElementApplication)
}
func isFile(elementID spdx.ElementID) bool {
return strings.HasPrefix(string(elementID), ElementFile)
}
func initApplication(pkg spdx.Package) *ftypes.Application {
app := &ftypes.Application{Type: ftypes.LangType(pkg.PackageName)}
switch app.Type {
case ftypes.NodePkg, ftypes.PythonPkg, ftypes.GemSpec, ftypes.Jar, ftypes.CondaPkg:
app.FilePath = ""
default:
app.FilePath = pkg.PackageSourceInfo
}
return app
}
func parseOS(pkg spdx.Package) *ftypes.OS {
return &ftypes.OS{
Family: ftypes.OSType(pkg.PackageName),
Name: pkg.PackageVersion,
}
}
func parsePkg(spdxPkg spdx.Package, packageFilePaths map[string]string) (*ftypes.Package, *purl.PackageURL, error) {
pkgURL, err := parseExternalReferences(spdxPkg.PackageExternalReferences)
// Convert all SPDX packages into Trivy components
components, err := s.parsePackages(spdxDocument)
if err != nil {
return nil, nil, xerrors.Errorf("external references error: %w", err)
return xerrors.Errorf("package parse error: %w", err)
}
pkg := pkgURL.Package()
if spdxPkg.PackageLicenseDeclared != "NONE" {
pkg.Licenses = strings.Split(spdxPkg.PackageLicenseDeclared, ",")
}
if strings.HasPrefix(spdxPkg.PackageSourceInfo, SourcePackagePrefix) {
srcPkgName := strings.TrimPrefix(spdxPkg.PackageSourceInfo, fmt.Sprintf("%s: ", SourcePackagePrefix))
pkg.SrcEpoch, pkg.SrcName, pkg.SrcVersion, pkg.SrcRelease, err = parseSourceInfo(pkgURL.Type, srcPkgName)
if err != nil {
return nil, nil, xerrors.Errorf("failed to parse source info: %w", err)
}
}
if path, ok := packageFilePaths[string(spdxPkg.PackageSPDXIdentifier)]; ok {
pkg.FilePath = path
} else if len(spdxPkg.Files) > 0 {
// Take the first file name
pkg.FilePath = spdxPkg.Files[0].FileName
}
pkg.ID = lookupAttributionTexts(spdxPkg.PackageAttributionTexts, PropertyPkgID)
pkg.Layer.Digest = lookupAttributionTexts(spdxPkg.PackageAttributionTexts, PropertyLayerDigest)
pkg.Layer.DiffID = lookupAttributionTexts(spdxPkg.PackageAttributionTexts, PropertyLayerDiffID)
return pkg, pkgURL, nil
}
func parseExternalReferences(refs []*spdx.PackageExternalReference) (*purl.PackageURL, error) {
for _, ref := range refs {
// Extract the package information from PURL
if ref.RefType != RefTypePurl || ref.Category != CategoryPackageManager {
// Parse relationships and build the dependency graph
for _, rel := range spdxDocument.Relationships {
// Skip the DESCRIBES relationship.
if rel.Relationship == common.TypeRelationshipDescribe || rel.Relationship == "DESCRIBE" {
continue
}
packageURL, err := purl.FromString(ref.Locator)
if err != nil {
return nil, xerrors.Errorf("failed to parse purl from string: %w", err)
}
return packageURL, nil
compA := components[rel.RefA.ElementRefID]
compB := components[rel.RefB.ElementRefID]
s.BOM.AddRelationship(compA, compB, s.parseRelationshipType(rel.Relationship))
}
return nil, errUnknownPackageFormat
return nil
}
func lookupAttributionTexts(attributionTexts []string, key string) string {
for _, text := range attributionTexts {
if strings.HasPrefix(text, key) {
return strings.TrimPrefix(text, fmt.Sprintf("%s: ", key))
}
}
return ""
}
// parseFiles parses Relationships and finds filepaths for packages
func (s *SPDX) parseFiles(spdxDocument *spdx.Document) {
fileSPDXIdentifierMap := lo.SliceToMap(spdxDocument.Files, func(file *spdx.File) (common.ElementID, *spdx.File) {
return file.FileSPDXIdentifier, file
})
func parseSourceInfo(pkgType, sourceInfo string) (epoch int, name, ver, rel string, err error) {
srcNameVersion := strings.TrimPrefix(sourceInfo, fmt.Sprintf("%s: ", SourcePackagePrefix))
ss := strings.Split(srcNameVersion, " ")
if len(ss) != 2 {
return 0, "", "", "", xerrors.Errorf("invalid source info (%s)", sourceInfo)
}
name = ss[0]
if pkgType == packageurl.TypeRPM {
v := version.NewVersion(ss[1])
epoch = v.Epoch()
ver = v.Version()
rel = v.Release()
} else {
ver = ss[1]
}
return epoch, name, ver, rel, nil
}
// getPackageFilePaths parses Relationships and finds filepaths for packages
func getPackageFilePaths(spdxDocument *spdx.Document) map[string]string {
packageFilePaths := make(map[string]string)
fileSPDXIdentifierMap := createFileSPDXIdentifierMap(spdxDocument.Files)
for _, rel := range spdxDocument.Relationships {
if rel.Relationship != common.TypeRelationshipContains && rel.Relationship != "CONTAIN" {
// Skip the DESCRIBES relationship.
@@ -335,14 +112,157 @@ func getPackageFilePaths(spdxDocument *spdx.Document) map[string]string {
// hasFiles values converted in Relationships
// https://github.com/spdx/tools-golang/pull/201
if isFile(rel.RefB.ElementRefID) {
file, ok := fileSPDXIdentifierMap[string(rel.RefB.ElementRefID)]
file, ok := fileSPDXIdentifierMap[rel.RefB.ElementRefID]
if ok {
// Save filePaths for packages
// Insert filepath will be later
packageFilePaths[string(rel.RefA.ElementRefID)] = file.FileName
s.pkgFilePaths[rel.RefA.ElementRefID] = file.FileName
}
continue
}
}
return packageFilePaths
}
func (s *SPDX) parsePackages(spdxDocument *spdx.Document) (map[common.ElementID]*core.Component, error) {
// Find a root package
var rootID common.ElementID
for _, rel := range spdxDocument.Relationships {
if rel.RefA.ElementRefID == DocumentSPDXIdentifier && rel.Relationship == RelationShipDescribe {
rootID = rel.RefB.ElementRefID
break
}
}
// Convert packages into components
components := make(map[common.ElementID]*core.Component)
for _, pkg := range spdxDocument.Packages {
component, err := s.parsePackage(*pkg)
if err != nil {
return nil, xerrors.Errorf("failed to parse package: %w", err)
}
components[pkg.PackageSPDXIdentifier] = component
if pkg.PackageSPDXIdentifier == rootID {
component.Root = true
}
s.BOM.AddComponent(component)
}
return components, nil
}
func (s *SPDX) parsePackage(spdxPkg spdx.Package) (*core.Component, error) {
var err error
component := &core.Component{
Type: s.parseType(spdxPkg),
Name: spdxPkg.PackageName,
Version: spdxPkg.PackageVersion,
}
// PURL
if component.PkgID.PURL, err = s.parseExternalReferences(spdxPkg.PackageExternalReferences); err != nil {
return nil, xerrors.Errorf("external references error: %w", err)
}
// License
if spdxPkg.PackageLicenseDeclared != "NONE" {
component.Licenses = strings.Split(spdxPkg.PackageLicenseDeclared, ",")
}
// Source package
if strings.HasPrefix(spdxPkg.PackageSourceInfo, SourcePackagePrefix) {
srcPkgName := strings.TrimPrefix(spdxPkg.PackageSourceInfo, fmt.Sprintf("%s: ", SourcePackagePrefix))
component.SrcName, component.SrcVersion, _ = strings.Cut(srcPkgName, " ")
}
// Files
// TODO: handle checksums as well
if path, ok := s.pkgFilePaths[spdxPkg.PackageSPDXIdentifier]; ok {
component.Files = []core.File{
{Path: path},
}
} else if len(spdxPkg.Files) > 0 {
component.Files = []core.File{
{Path: spdxPkg.Files[0].FileName}, // Take the first file name
}
}
// Attributions
for _, attr := range spdxPkg.PackageAttributionTexts {
k, v, ok := strings.Cut(attr, ": ")
if !ok {
continue
}
component.Properties = append(component.Properties, core.Property{
Name: k,
Value: v,
})
}
// For backward-compatibility
// Older Trivy versions put the file path in "sourceInfo" and the package type in "name".
if s.trivySBOM && component.Type == core.TypeApplication && spdxPkg.PackageSourceInfo != "" {
component.Name = spdxPkg.PackageSourceInfo
component.Properties = append(component.Properties, core.Property{
Name: core.PropertyType,
Value: spdxPkg.PackageName,
})
}
return component, nil
}
func (s *SPDX) parseType(pkg spdx.Package) core.ComponentType {
id := string(pkg.PackageSPDXIdentifier)
switch {
case strings.HasPrefix(id, ElementOperatingSystem):
return core.TypeOS
case strings.HasPrefix(id, ElementApplication):
return core.TypeApplication
case strings.HasPrefix(id, ElementPackage):
return core.TypeLibrary
default:
return core.TypeLibrary // unknown is handled as a library
}
}
func (s *SPDX) parseRelationshipType(rel string) core.RelationshipType {
switch rel {
case common.TypeRelationshipDescribe:
return core.RelationshipDescribes
case common.TypeRelationshipContains, "CONTAIN":
return core.RelationshipContains
case common.TypeRelationshipDependsOn:
return core.RelationshipDependsOn
default:
return core.RelationshipContains
}
}
func (s *SPDX) parseExternalReferences(refs []*spdx.PackageExternalReference) (*packageurl.PackageURL, error) {
for _, ref := range refs {
// Extract the package information from PURL
if ref.RefType != RefTypePurl || ref.Category != CategoryPackageManager {
continue
}
packageURL, err := packageurl.FromString(ref.Locator)
if err != nil {
return nil, xerrors.Errorf("failed to parse purl from string: %w", err)
}
return &packageURL, nil
}
return nil, nil
}
func (s *SPDX) isTrivySBOM(spdxDocument *spdx.Document) bool {
for _, c := range spdxDocument.CreationInfo.Creators {
if c.CreatorType == "Tool" && strings.HasPrefix(c.Creator, "trivy") {
return true
}
}
return false
}
func isFile(elementID spdx.ElementID) bool {
return strings.HasPrefix(string(elementID), ElementFile)
}

View File

@@ -2,6 +2,7 @@ package spdx_test
import (
"encoding/json"
sbomio "github.com/aquasecurity/trivy/pkg/sbom/io"
"github.com/package-url/packageurl-go"
"os"
"sort"
@@ -27,6 +28,15 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
inputFile: "testdata/happy/bom.json",
want: types.SBOM{
Metadata: types.Metadata{
ImageID: "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5",
DiffIDs: []string{
"sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
"sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3",
},
RepoTags: []string{
"maven-test-project:latest",
"tmp-test:latest",
},
OS: &ftypes.OS{
Family: "alpine",
Name: "3.16.0",
@@ -36,6 +46,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
{
Packages: ftypes.Packages{
{
ID: "musl@1.2.3-r0",
Name: "musl",
Version: "1.2.3-r0",
SrcName: "musl",
@@ -68,6 +79,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
FilePath: "app/composer/composer.lock",
Libraries: ftypes.Packages{
{
ID: "pear/log@1.13.1",
Name: "pear/log",
Version: "1.13.1",
Identifier: ftypes.PkgIdentifier{
@@ -83,7 +95,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
},
},
{
ID: "pear/pear_exception@v1.0.0",
Name: "pear/pear_exception",
Version: "v1.0.0",
Identifier: ftypes.PkgIdentifier{
@@ -105,6 +117,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
FilePath: "app/gobinary/gobinary",
Libraries: ftypes.Packages{
{
ID: "github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a",
Name: "github.com/package-url/packageurl-go",
Version: "v0.1.1-0.20220203205134-d70459300c8a",
Identifier: ftypes.PkgIdentifier{
@@ -125,6 +138,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
Type: "jar",
Libraries: ftypes.Packages{
{
ID: "org.codehaus.mojo:child-project:1.0",
Name: "org.codehaus.mojo:child-project",
Identifier: ftypes.PkgIdentifier{
PURL: &packageurl.PackageURL{
@@ -145,6 +159,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
Type: "node-pkg",
Libraries: ftypes.Packages{
{
ID: "bootstrap@5.0.2",
Name: "bootstrap",
Version: "5.0.2",
Identifier: ftypes.PkgIdentifier{
@@ -170,7 +185,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
want: types.SBOM{
Applications: []ftypes.Application{
{
Type: "node-pkg",
Type: ftypes.NodePkg,
Libraries: ftypes.Packages{
{
ID: "yargs-parser@21.1.1",
@@ -228,6 +243,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
FilePath: "app/composer/composer.lock",
Libraries: ftypes.Packages{
{
ID: "pear/log@1.13.1",
Name: "pear/log",
Version: "1.13.1",
Identifier: ftypes.PkgIdentifier{
@@ -240,7 +256,7 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
},
},
{
ID: "pear/pear_exception@v1.0.0",
Name: "pear/pear_exception",
Version: "v1.0.0",
Identifier: ftypes.PkgIdentifier{
@@ -266,9 +282,10 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
Type: ftypes.Jar,
Libraries: ftypes.Packages{
{
FilePath: "modules/apm/elastic-apm-agent-1.36.0.jar",
ID: "co.elastic.apm:apm-agent:1.36.0",
Name: "co.elastic.apm:apm-agent",
Version: "1.36.0",
FilePath: "modules/apm/elastic-apm-agent-1.36.0.jar",
Identifier: ftypes.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeMaven,
@@ -279,9 +296,10 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
},
},
{
FilePath: "modules/apm/elastic-apm-agent-1.36.0.jar",
ID: "co.elastic.apm:apm-agent-cached-lookup-key:1.36.0",
Name: "co.elastic.apm:apm-agent-cached-lookup-key",
Version: "1.36.0",
FilePath: "modules/apm/elastic-apm-agent-1.36.0.jar",
Identifier: ftypes.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeMaven,
@@ -315,8 +333,8 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
},
{
name: "sad path invalid purl",
inputFile: "testdata/sad/invalid-source-info.json",
wantErr: "failed to parse source info:",
inputFile: "testdata/sad/invalid-purl.json",
wantErr: "purl is missing type or name",
},
}
@@ -326,22 +344,24 @@ func TestUnmarshaler_Unmarshal(t *testing.T) {
require.NoError(t, err)
defer f.Close()
v := &spdx.SPDX{SBOM: &types.SBOM{}}
err = json.NewDecoder(f).Decode(v)
var v spdx.SPDX
err = json.NewDecoder(f).Decode(&v)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
assert.ErrorContains(t, err, tt.wantErr)
return
}
// Not compare the SPDX field
v.BOM = nil
sort.Slice(v.Applications, func(i, j int) bool {
return v.Applications[i].Type < v.Applications[j].Type
})
var got types.SBOM
err = sbomio.NewDecoder(v.BOM).Decode(&got)
require.NoError(t, err)
assert.Equal(t, tt.want, *v.SBOM)
// Not compare BOM
got.BOM = nil
sort.Slice(got.Applications, func(i, j int) bool {
return got.Applications[i].Type < got.Applications[j].Type
})
assert.Equal(t, tt.want, got)
})
}
}