mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
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:
43
integration/testdata/conda-spdx.json.golden
vendored
43
integration/testdata/conda-spdx.json.golden
vendored
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
],
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user