feat(sbom): Add unmarshal for spdx (#2868)

Signed-off-by: knqyf263 <knqyf263@gmail.com>
Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
Masahiro331
2022-09-15 14:39:59 +09:00
committed by GitHub
parent db0aaf18e6
commit 9f6680a1fa
15 changed files with 1159 additions and 24 deletions

View File

@@ -12,10 +12,9 @@ import (
"testing"
"time"
"github.com/samber/lo"
cdx "github.com/CycloneDX/cyclonedx-go"
"github.com/docker/go-connections/nat"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
testcontainers "github.com/testcontainers/testcontainers-go"

View File

@@ -11,7 +11,7 @@ import (
"strings"
"testing"
"github.com/docker/docker/api/types"
api "github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -213,7 +213,7 @@ func TestDockerEngine(t *testing.T) {
require.NoError(t, err, tt.name)
// ensure image doesnt already exists
_, _ = cli.ImageRemove(ctx, tt.input, types.ImageRemoveOptions{
_, _ = cli.ImageRemove(ctx, tt.input, api.ImageRemoveOptions{
Force: true,
PruneChildren: true,
})
@@ -264,11 +264,11 @@ func TestDockerEngine(t *testing.T) {
compareReports(t, tt.golden, output)
// cleanup
_, err = cli.ImageRemove(ctx, tt.input, types.ImageRemoveOptions{
_, err = cli.ImageRemove(ctx, tt.input, api.ImageRemoveOptions{
Force: true,
PruneChildren: true,
})
_, err = cli.ImageRemove(ctx, tt.imageTag, types.ImageRemoveOptions{
_, err = cli.ImageRemove(ctx, tt.imageTag, api.ImageRemoveOptions{
Force: true,
PruneChildren: true,
})

View File

@@ -8,23 +8,28 @@ import (
"testing"
cdx "github.com/CycloneDX/cyclonedx-go"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/types"
)
func TestCycloneDX(t *testing.T) {
func TestSBOM(t *testing.T) {
type args struct {
input string
format string
artifactType string
}
tests := []struct {
name string
args args
golden string
name string
args args
golden string
override types.Report
}{
{
name: "centos7-bom by trivy",
name: "centos7 cyclonedx",
args: args{
input: "testdata/fixtures/sbom/centos-7-cyclonedx.json",
format: "cyclonedx",
@@ -33,7 +38,7 @@ func TestCycloneDX(t *testing.T) {
golden: "testdata/centos-7-cyclonedx.json.golden",
},
{
name: "fluentd-multiple-lockfiles-bom by trivy",
name: "fluentd-multiple-lockfiles cyclonedx",
args: args{
input: "testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json",
format: "cyclonedx",
@@ -42,7 +47,7 @@ func TestCycloneDX(t *testing.T) {
golden: "testdata/fluentd-multiple-lockfiles-cyclonedx.json.golden",
},
{
name: "centos7-bom in in-toto attestation",
name: "centos7 in in-toto attestation",
args: args{
input: "testdata/fixtures/sbom/centos-7-cyclonedx.intoto.jsonl",
format: "cyclonedx",
@@ -50,6 +55,52 @@ func TestCycloneDX(t *testing.T) {
},
golden: "testdata/centos-7-cyclonedx.json.golden",
},
{
name: "centos7 spdx tag-value",
args: args{
input: "testdata/fixtures/sbom/centos-7-spdx.txt",
format: "json",
artifactType: "spdx",
},
golden: "testdata/centos-7.json.golden",
override: types.Report{
ArtifactName: "testdata/fixtures/sbom/centos-7-spdx.txt",
ArtifactType: ftypes.ArtifactType("spdx"),
Results: types.Results{
{
Target: "testdata/fixtures/sbom/centos-7-spdx.txt (centos 7.6.1810)",
Vulnerabilities: []types.DetectedVulnerability{
{Ref: "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810"},
{Ref: "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64&distro=centos-7.6.1810"},
{Ref: "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64&distro=centos-7.6.1810"},
},
},
},
},
},
{
name: "centos7 spdx json",
args: args{
input: "testdata/fixtures/sbom/centos-7-spdx.json",
format: "json",
artifactType: "spdx",
},
golden: "testdata/centos-7.json.golden",
override: types.Report{
ArtifactName: "testdata/fixtures/sbom/centos-7-spdx.json",
ArtifactType: ftypes.ArtifactType("spdx"),
Results: types.Results{
{
Target: "testdata/fixtures/sbom/centos-7-spdx.json (centos 7.6.1810)",
Vulnerabilities: []types.DetectedVulnerability{
{Ref: "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810"},
{Ref: "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64&distro=centos-7.6.1810"},
{Ref: "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64&distro=centos-7.6.1810"},
},
},
},
},
},
}
// Set up testing DB
@@ -61,7 +112,7 @@ func TestCycloneDX(t *testing.T) {
"--cache-dir", cacheDir, "sbom", "-q", "--skip-db-update", "--format", tt.args.format,
}
// Setup the output file
// Set up the output file
outputFile := filepath.Join(t.TempDir(), "output.json")
if *update {
outputFile = tt.golden
@@ -75,13 +126,46 @@ func TestCycloneDX(t *testing.T) {
assert.NoError(t, err)
// Compare want and got
want := decodeCycloneDX(t, tt.golden)
got := decodeCycloneDX(t, outputFile)
assert.Equal(t, want, got)
switch tt.args.format {
case "cyclonedx":
want := decodeCycloneDX(t, tt.golden)
got := decodeCycloneDX(t, outputFile)
assert.Equal(t, want, got)
case "json":
compareSBOMReports(t, tt.golden, outputFile, tt.override)
default:
require.Fail(t, "invalid format", "format: %s", tt.args.format)
}
})
}
}
// TODO(teppei): merge into compareReports
func compareSBOMReports(t *testing.T, wantFile, gotFile string, overrideWant types.Report) {
want := readReport(t, wantFile)
want.ArtifactName = overrideWant.ArtifactName
want.ArtifactType = overrideWant.ArtifactType
want.Metadata.ImageID = ""
want.Metadata.ImageConfig = v1.ConfigFile{}
want.Metadata.DiffIDs = nil
for i, result := range want.Results {
for j := range result.Vulnerabilities {
want.Results[i].Vulnerabilities[j].Layer.DiffID = ""
}
}
for i, result := range overrideWant.Results {
want.Results[i].Target = result.Target
for j, vuln := range result.Vulnerabilities {
want.Results[i].Vulnerabilities[j].Ref = vuln.Ref
}
}
got := readReport(t, gotFile)
assert.Equal(t, want, got)
}
func decodeCycloneDX(t *testing.T, filePath string) *cdx.BOM {
f, err := os.Open(filePath)
require.NoError(t, err)

View File

@@ -0,0 +1,94 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"creationInfo": {
"created": "2022-09-13T13:27:55.874784Z",
"creators": [
"Tool: trivy",
"Organization: aquasecurity"
]
},
"dataLicense": "CC0-1.0",
"documentNamespace": "http://aquasecurity.github.io/trivy/container_image/integration/testdata/fixtures/images/centos-7.tar.gz-2906855d-5098-4a22-9a72-4f7099ea3d66",
"name": "integration/testdata/fixtures/images/centos-7.tar.gz",
"packages": [
{
"SPDXID": "SPDXRef-ContainerImage-dd5cad897c6263",
"attributionTexts": [
"SchemaVersion: 2",
"ImageID: sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427",
"DiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a"
],
"filesAnalyzed": false,
"name": "integration/testdata/fixtures/images/centos-7.tar.gz"
},
{
"SPDXID": "SPDXRef-OperatingSystem-2e91c856c499a371",
"filesAnalyzed": false,
"name": "centos",
"versionInfo": "7.6.1810"
},
{
"SPDXID": "SPDXRef-Package-5a18334f22149877",
"attributionTexts": [
"LayerDigest: sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b",
"LayerDiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a"
],
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64\u0026distro=centos-7.6.1810",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseConcluded": "GPLv3+",
"licenseDeclared": "GPLv3+",
"name": "bash",
"sourceInfo": "built package from: bash 4.2.46-31.el7",
"versionInfo": "4.2.46"
},
{
"SPDXID": "SPDXRef-Package-e16b1cbaa5186199",
"attributionTexts": [
"LayerDigest: sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b",
"LayerDiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a"
],
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64\u0026distro=centos-7.6.1810",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseConcluded": "OpenSSL",
"licenseDeclared": "OpenSSL",
"name": "openssl-libs",
"sourceInfo": "built package from: openssl-libs 1:1.0.2k-16.el7",
"versionInfo": "1.0.2k"
}
],
"relationships": [
{
"relatedSpdxElement": "SPDXRef-ContainerImage-dd5cad897c6263",
"relationshipType": "DESCRIBE",
"spdxElementId": "SPDXRef-DOCUMENT"
},
{
"relatedSpdxElement": "SPDXRef-OperatingSystem-2e91c856c499a371",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-ContainerImage-dd5cad897c6263"
},
{
"relatedSpdxElement": "SPDXRef-Package-5a18334f22149877",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-OperatingSystem-2e91c856c499a371"
},
{
"relatedSpdxElement": "SPDXRef-Package-e16b1cbaa5186199",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-OperatingSystem-2e91c856c499a371"
}
],
"spdxVersion": "SPDX-2.2"
}

View File

@@ -0,0 +1,57 @@
SPDXVersion: SPDX-2.2
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentName: integration/testdata/fixtures/images/centos-7.tar.gz
DocumentNamespace: http://aquasecurity.github.io/trivy/container_image/integration/testdata/fixtures/images/centos-7.tar.gz-6a2c050f-bc12-46dc-b2df-1f4e3e0b5e1d
Creator: Organization: aquasecurity
Creator: Tool: trivy
Created: 2022-09-13T13:24:58.796907Z
##### Package: integration/testdata/fixtures/images/centos-7.tar.gz
PackageName: integration/testdata/fixtures/images/centos-7.tar.gz
SPDXID: SPDXRef-ContainerImage-dd5cad897c6263
FilesAnalyzed: false
PackageAttributionText: SchemaVersion: 2
PackageAttributionText: ImageID: sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427
PackageAttributionText: DiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a
##### Package: centos
PackageName: centos
SPDXID: SPDXRef-OperatingSystem-2e91c856c499a371
PackageVersion: 7.6.1810
FilesAnalyzed: false
##### Package: bash
PackageName: bash
SPDXID: SPDXRef-Package-5a18334f22149877
PackageVersion: 4.2.46
FilesAnalyzed: false
PackageSourceInfo: built package from: bash 4.2.46-31.el7
PackageLicenseConcluded: GPLv3+
PackageLicenseDeclared: GPLv3+
ExternalRef: PACKAGE-MANAGER purl pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810
PackageAttributionText: LayerDigest: sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b
PackageAttributionText: LayerDiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a
##### Package: openssl-libs
PackageName: openssl-libs
SPDXID: SPDXRef-Package-e16b1cbaa5186199
PackageVersion: 1.0.2k
FilesAnalyzed: false
PackageSourceInfo: built package from: openssl-libs 1:1.0.2k-16.el7
PackageLicenseConcluded: OpenSSL
PackageLicenseDeclared: OpenSSL
ExternalRef: PACKAGE-MANAGER purl pkg:rpm/centos/openssl-libs@1:1.0.2k-16.el7?arch=x86_64&distro=centos-7.6.1810
PackageAttributionText: LayerDigest: sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b
PackageAttributionText: LayerDiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a
##### Relationships
Relationship: SPDXRef-DOCUMENT DESCRIBE SPDXRef-ContainerImage-dd5cad897c6263
Relationship: SPDXRef-ContainerImage-dd5cad897c6263 CONTAINS SPDXRef-OperatingSystem-2e91c856c499a371
Relationship: SPDXRef-OperatingSystem-2e91c856c499a371 DEPENDS_ON SPDXRef-Package-5a18334f22149877
Relationship: SPDXRef-OperatingSystem-2e91c856c499a371 DEPENDS_ON SPDXRef-Package-e16b1cbaa5186199

View File

@@ -20,6 +20,7 @@ import (
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/sbom"
"github.com/aquasecurity/trivy/pkg/sbom/cyclonedx"
"github.com/aquasecurity/trivy/pkg/sbom/spdx"
)
type Artifact struct {
@@ -83,6 +84,9 @@ func (a Artifact) Inspect(_ context.Context) (types.ArtifactReference, error) {
switch format {
case sbom.FormatCycloneDXJSON, sbom.FormatCycloneDXXML, sbom.FormatAttestCycloneDXJSON:
artifactType = types.ArtifactCycloneDX
case sbom.FormatSPDXTV, sbom.FormatSPDXJSON:
artifactType = types.ArtifactSPDX
}
return types.ArtifactReference{
@@ -117,6 +121,13 @@ func (a Artifact) Decode(f io.Reader, format sbom.Format) (sbom.SBOM, error) {
},
}
decoder = json.NewDecoder(f)
case sbom.FormatSPDXJSON:
v = &spdx.SPDX{SBOM: &bom}
decoder = json.NewDecoder(f)
case sbom.FormatSPDXTV:
v = &spdx.SPDX{SBOM: &bom}
decoder = spdx.NewTVDecoder(f)
default:
return sbom.SBOM{}, xerrors.Errorf("%s scanning is not yet supported", format)

View File

@@ -95,6 +95,7 @@ const (
ArtifactFilesystem ArtifactType = "filesystem"
ArtifactRemoteRepository ArtifactType = "repository"
ArtifactCycloneDX ArtifactType = "cyclonedx"
ArtifactSPDX ArtifactType = "spdx"
ArtifactAWSAccount ArtifactType = "aws_account"
)

View File

@@ -1,12 +1,14 @@
package sbom
import (
"bufio"
"encoding/json"
"encoding/xml"
"io"
"strings"
"github.com/in-toto/in-toto-golang/in_toto"
stypes "github.com/spdx/tools-golang/spdx"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/attestation"
@@ -19,6 +21,7 @@ type SBOM struct {
Applications []types.Application
CycloneDX *types.CycloneDX
SPDX *stypes.Document2_2
}
type Format string
@@ -27,19 +30,26 @@ const (
FormatCycloneDXJSON Format = "cyclonedx-json"
FormatCycloneDXXML Format = "cyclonedx-xml"
FormatSPDXJSON Format = "spdx-json"
FormatSPDXTV Format = "spdx-tv"
FormatSPDXXML Format = "spdx-xml"
FormatAttestCycloneDXJSON Format = "attest-cyclonedx-json"
FormatUnknown Format = "unknown"
)
func DetectFormat(r io.ReadSeeker) (Format, error) {
type cyclonedx struct {
// XML specific field
XMLNS string `json:"-" xml:"xmlns,attr"`
type (
cyclonedx struct {
// XML specific field
XMLNS string `json:"-" xml:"xmlns,attr"`
// JSON specific field
BOMFormat string `json:"bomFormat" xml:"-"`
}
// JSON specific field
BOMFormat string `json:"bomFormat" xml:"-"`
}
spdx struct {
SpdxID string `json:"SPDXID"`
}
)
// Try CycloneDX JSON
var cdxBom cyclonedx
@@ -64,7 +74,28 @@ func DetectFormat(r io.ReadSeeker) (Format, error) {
return FormatUnknown, xerrors.Errorf("seek error: %w", err)
}
// TODO: implement SPDX
// Try SPDX json
var spdxBom spdx
if err := json.NewDecoder(r).Decode(&spdxBom); err == nil {
if strings.HasPrefix(spdxBom.SpdxID, "SPDX") {
return FormatSPDXJSON, nil
}
}
if _, err := r.Seek(0, io.SeekStart); err != nil {
return FormatUnknown, xerrors.Errorf("seek error: %w", err)
}
// Try SPDX tag-value
if scanner := bufio.NewScanner(r); scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "SPDX") {
return FormatSPDXTV, nil
}
}
if _, err := r.Seek(0, io.SeekStart); err != nil {
return FormatUnknown, xerrors.Errorf("seek error: %w", err)
}
// Try in-toto attestation
var s attestation.Statement

230
pkg/sbom/spdx/testdata/happy/bom.json vendored Normal file
View File

@@ -0,0 +1,230 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"creationInfo": {
"created": "2022-09-12T17:02:46.826609Z",
"creators": [
"Tool: trivy",
"Organization: aquasecurity"
]
},
"dataLicense": "CC0-1.0",
"documentNamespace": "http://aquasecurity.github.io/trivy/container/meven-test-project-eb7a0384-b04a-4fc6-8afb-1662fe59ca79",
"name": "maven-test-projecct",
"packages": [
{
"SPDXID": "SPDXRef-Application-150e605f5f17224d",
"filesAnalyzed": false,
"name": "jar",
"sourceInfo": "Java"
},
{
"SPDXID": "SPDXRef-Application-24f8a80152e2c0fc",
"filesAnalyzed": false,
"name": "node-pkg",
"sourceInfo": "Node.js"
},
{
"SPDXID": "SPDXRef-Application-36324ee492e03f0a",
"filesAnalyzed": false,
"name": "gobinary",
"sourceInfo": "app/gobinary/gobinary"
},
{
"SPDXID": "SPDXRef-Application-4af197c15114fb0e",
"filesAnalyzed": false,
"name": "composer",
"sourceInfo": "app/composer/composer.lock"
},
{
"SPDXID": "SPDXRef-ContainerImage-b5d81cde5f95c8fc",
"attributionTexts": [
"SchemaVersion: 2",
"ImageID: sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5",
"DiffID: sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3",
"DiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
"RepoTag: maven-test-project:latest",
"RepoTag: tmp-test:latest"
],
"filesAnalyzed": false,
"name": "meven-test-project"
},
{
"SPDXID": "SPDXRef-OperatingSystem-bd17bf9010aa612c",
"filesAnalyzed": false,
"name": "alpine",
"versionInfo": "3.16.0"
},
{
"SPDXID": "SPDXRef-Package-2906575950df652b",
"attributionTexts": [
"LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1"
],
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:composer/pear/log@1.13.1",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseConcluded": "NONE",
"licenseDeclared": "NONE",
"name": "pear/log",
"versionInfo": "1.13.1"
},
{
"SPDXID": "SPDXRef-Package-2a53baa495b9ddaf",
"attributionTexts": [
"LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1"
],
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:maven/org.codehaus.mojo/child-project@1.0",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseConcluded": "NONE",
"licenseDeclared": "NONE",
"name": "org.codehaus.mojo:child-project",
"versionInfo": "1.0"
},
{
"SPDXID": "SPDXRef-Package-5e2e255ac76747ef",
"attributionTexts": [
"LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1"
],
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:composer/pear/pear_exception@v1.0.0",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseConcluded": "NONE",
"licenseDeclared": "NONE",
"name": "pear/pear_exception",
"versionInfo": "v1.0.0"
},
{
"SPDXID": "SPDXRef-Package-5f1dbaff8de5eb06",
"attributionTexts": [
"LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1"
],
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:npm/bootstrap@5.0.2",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"name": "bootstrap",
"versionInfo": "5.0.2"
},
{
"SPDXID": "SPDXRef-Package-84ebffe38343d949",
"attributionTexts": [
"LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1"
],
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseConcluded": "NONE",
"licenseDeclared": "NONE",
"name": "github.com/package-url/packageurl-go",
"versionInfo": "v0.1.1-0.20220203205134-d70459300c8a"
},
{
"SPDXID": "SPDXRef-Package-b7ebaf0233f1ef7b",
"attributionTexts": [
"LayerDiffID: sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3"
],
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"name": "musl",
"sourceInfo": "built package from: musl 1.2.3-r0",
"versionInfo": "1.2.3-r0"
}
],
"relationships": [
{
"relatedSpdxElement": "SPDXRef-ContainerImage-b5d81cde5f95c8fc",
"relationshipType": "DESCRIBE",
"spdxElementId": "SPDXRef-DOCUMENT"
},
{
"relatedSpdxElement": "SPDXRef-OperatingSystem-bd17bf9010aa612c",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc"
},
{
"relatedSpdxElement": "SPDXRef-Package-b7ebaf0233f1ef7b",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-OperatingSystem-bd17bf9010aa612c"
},
{
"relatedSpdxElement": "SPDXRef-Application-150e605f5f17224d",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc"
},
{
"relatedSpdxElement": "SPDXRef-Package-2a53baa495b9ddaf",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Application-150e605f5f17224d"
},
{
"relatedSpdxElement": "SPDXRef-Application-24f8a80152e2c0fc",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc"
},
{
"relatedSpdxElement": "SPDXRef-Package-5f1dbaff8de5eb06",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Application-24f8a80152e2c0fc"
},
{
"relatedSpdxElement": "SPDXRef-Application-4af197c15114fb0e",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc"
},
{
"relatedSpdxElement": "SPDXRef-Package-2906575950df652b",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Application-4af197c15114fb0e"
},
{
"relatedSpdxElement": "SPDXRef-Package-5e2e255ac76747ef",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Application-4af197c15114fb0e"
},
{
"relatedSpdxElement": "SPDXRef-Application-36324ee492e03f0a",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc"
},
{
"relatedSpdxElement": "SPDXRef-Package-84ebffe38343d949",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Application-36324ee492e03f0a"
}
],
"spdxVersion": "SPDX-2.2"
}

View File

@@ -0,0 +1,34 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"creationInfo": {
"created": "2022-09-12T17:03:35.840861Z",
"creators": [
"Tool: trivy",
"Organization: aquasecurity"
]
},
"dataLicense": "CC0-1.0",
"documentDescribes": [
"SPDXRef-ContainerImage-7f428bd075b13fe8"
],
"documentNamespace": "http://aquasecurity.github.io/trivy/container/maven-test-project-3e87878b-eac3-4baa-af11-bdf2c5eab8ea",
"name": "maven-test-project",
"packages": [
{
"SPDXID": "SPDXRef-ContainerImage-7f428bd075b13fe8",
"attributionTexts": [
"SchemaVersion: 2"
],
"filesAnalyzed": false,
"name": "maven-test-project"
}
],
"relationships": [
{
"relatedSpdxElement": "SPDXRef-ContainerImage-7f428bd075b13fe8",
"relationshipType": "DESCRIBE",
"spdxElementId": "SPDXRef-DOCUMENT"
}
],
"spdxVersion": "SPDX-2.2"
}

View File

@@ -0,0 +1,42 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"creationInfo": {
"created": "2022-09-12T17:04:09.262672Z",
"creators": [
"Tool: trivy",
"Organization: aquasecurity"
]
},
"dataLicense": "CC0-1.0",
"documentNamespace": "http://aquasecurity.github.io/trivy/container/maven-test-project-1a255568-e896-498f-93de-7975a5cb0212",
"name": "maven-test-project",
"packages": [
{
"SPDXID": "SPDXRef-ContainerImage-fbbee9b988766322",
"attributionTexts": [
"SchemaVersion: 2"
],
"filesAnalyzed": false,
"name": "maven-test-project"
},
{
"SPDXID": "SPDXRef-OperatingSystem-bd17bf9010aa612c",
"filesAnalyzed": false,
"name": "alpine",
"versionInfo": "3.16.0"
}
],
"relationships": [
{
"relatedSpdxElement": "SPDXRef-ContainerImage-fbbee9b988766322",
"relationshipType": "DESCRIBE",
"spdxElementId": "SPDXRef-DOCUMENT"
},
{
"relatedSpdxElement": "SPDXRef-OperatingSystem-bd17bf9010aa612c",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-ContainerImage-fbbee9b988766322"
}
],
"spdxVersion": "SPDX-2.2"
}

View File

@@ -0,0 +1,82 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"creationInfo": {
"created": "2022-09-12T17:04:28.43059Z",
"creators": [
"Tool: trivy",
"Organization: aquasecurity"
]
},
"dataLicense": "CC0-1.0",
"documentNamespace": "http://aquasecurity.github.io/trivy/application/maven-test-project-8aa3b6db-bae7-4755-b43e-00b4bcf46410",
"name": "maven-test-project",
"packages": [
{
"SPDXID": "SPDXRef-Application-193be12e25033404",
"filesAnalyzed": false,
"name": "composer",
"sourceInfo": "app/composer/composer.lock"
},
{
"SPDXID": "SPDXRef-Filesystem-12d960c003a8275b",
"attributionTexts": [
"SchemaVersion: 2"
],
"filesAnalyzed": false,
"name": "maven-test-project"
},
{
"SPDXID": "SPDXRef-Package-2906575950df652b",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:composer/pear/log@1.13.1",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseConcluded": "NONE",
"licenseDeclared": "NONE",
"name": "pear/log",
"versionInfo": "1.13.1"
},
{
"SPDXID": "SPDXRef-Package-5e2e255ac76747ef",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:composer/pear/pear_exception@v1.0.0",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseConcluded": "NONE",
"licenseDeclared": "NONE",
"name": "pear/pear_exception",
"versionInfo": "v1.0.0"
}
],
"relationships": [
{
"relatedSpdxElement": "SPDXRef-Filesystem-12d960c003a8275b",
"relationshipType": "DESCRIBE",
"spdxElementId": "SPDXRef-DOCUMENT"
},
{
"relatedSpdxElement": "SPDXRef-Application-193be12e25033404",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-Filesystem-12d960c003a8275b"
},
{
"relatedSpdxElement": "SPDXRef-Package-2906575950df652b",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Application-193be12e25033404"
},
{
"relatedSpdxElement": "SPDXRef-Package-5e2e255ac76747ef",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Application-193be12e25033404"
}
],
"spdxVersion": "SPDX-2.2"
}

View File

@@ -0,0 +1,58 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"creationInfo": {
"created": "2022-09-12T17:02:46.826609Z",
"creators": [
"Tool: trivy",
"Organization: aquasecurity"
]
},
"dataLicense": "CC0-1.0",
"documentNamespace": "http://aquasecurity.github.io/trivy/container/meven-test-project-eb7a0384-b04a-4fc6-8afb-1662fe59ca79",
"name": "maven-test-projecct",
"packages": [
{
"SPDXID": "SPDXRef-ContainerImage-b5d81cde5f95c8fc",
"filesAnalyzed": false,
"name": "meven-test-project"
},
{
"SPDXID": "SPDXRef-OperatingSystem-bd17bf9010aa612c",
"filesAnalyzed": false,
"name": "alpine",
"versionInfo": "3.16.0"
},
{
"SPDXID": "SPDXRef-Package-b7ebaf0233f1ef7b",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"name": "musl",
"sourceInfo": "built package from: invalid",
"versionInfo": "1.2.3-r0"
}
],
"relationships": [
{
"relatedSpdxElement": "SPDXRef-ContainerImage-b5d81cde5f95c8fc",
"relationshipType": "DESCRIBE",
"spdxElementId": "SPDXRef-DOCUMENT"
},
{
"relatedSpdxElement": "SPDXRef-OperatingSystem-bd17bf9010aa612c",
"relationshipType": "CONTAINS",
"spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc"
},
{
"relatedSpdxElement": "SPDXRef-Package-b7ebaf0233f1ef7b",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-OperatingSystem-bd17bf9010aa612c"
}
],
"spdxVersion": "SPDX-2.2"
}

228
pkg/sbom/spdx/unmarshal.go Normal file
View File

@@ -0,0 +1,228 @@
package spdx
import (
"bytes"
"fmt"
"io"
"strings"
version "github.com/knqyf263/go-rpm-version"
"github.com/package-url/packageurl-go"
"github.com/samber/lo"
"github.com/spdx/tools-golang/jsonloader"
"github.com/spdx/tools-golang/spdx"
"github.com/spdx/tools-golang/tvloader"
"golang.org/x/xerrors"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/purl"
"github.com/aquasecurity/trivy/pkg/sbom"
)
var (
errUnknownPackageFormat = xerrors.New("unknown package format")
)
type SPDX struct {
*sbom.SBOM
}
func NewTVDecoder(r io.Reader) *TVDecoder {
return &TVDecoder{r: r}
}
type TVDecoder struct {
r io.Reader
}
func (tv *TVDecoder) Decode(v interface{}) error {
spdxDocument, err := tvloader.Load2_2(tv.r)
if err != nil {
return xerrors.Errorf("failed to load tag-value spdx: %w", err)
}
a, ok := v.(*SPDX)
if !ok {
return xerrors.Errorf("invalid struct type tag-value decoder needed SPDX struct")
}
err = a.unmarshal(spdxDocument)
if err != nil {
return xerrors.Errorf("failed to unmarshal spdx: %w", err)
}
return nil
}
func (s *SPDX) UnmarshalJSON(b []byte) error {
spdxDocument, err := jsonloader.Load2_2(bytes.NewReader(b))
if err != nil {
return xerrors.Errorf("failed to load spdx json: %w", err)
}
err = s.unmarshal(spdxDocument)
if err != nil {
return xerrors.Errorf("failed to unmarshal spdx: %w", err)
}
return nil
}
func (s *SPDX) unmarshal(spdxDocument *spdx.Document2_2) error {
var osPkgs []ftypes.Package
apps := map[spdx.ElementID]*ftypes.Application{}
// 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 spdxDocument.Relationships {
pkgA := lo.FromPtr(spdxDocument.Packages[rel.RefA.ElementRefID])
pkgB := lo.FromPtr(spdxDocument.Packages[rel.RefB.ElementRefID])
switch {
// Relationship: root package => OS
case isOperatingSystem(pkgB.PackageSPDXIdentifier):
s.SBOM.OS = parseOS(pkgB)
// Relationship: OS => OS package
case isOperatingSystem(pkgA.PackageSPDXIdentifier):
pkg, err := parsePkg(pkgB)
if err != nil {
return xerrors.Errorf("failed to parse os package: %w", err)
}
osPkgs = append(osPkgs, *pkg)
// 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)
if err != nil {
return xerrors.Errorf("failed to parse language-specific package: %w", err)
}
app.Libraries = append(app.Libraries, *lib)
}
}
// 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)
}
// Keep the original document
s.SPDX = spdxDocument
return nil
}
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 initApplication(pkg spdx.Package2_2) *ftypes.Application {
app := &ftypes.Application{
Type: pkg.PackageName,
FilePath: pkg.PackageSourceInfo,
}
if pkg.PackageName == ftypes.NodePkg || pkg.PackageName == ftypes.PythonPkg ||
pkg.PackageName == ftypes.GemSpec || pkg.PackageName == ftypes.Jar {
app.FilePath = ""
}
return app
}
func parseOS(pkg spdx.Package2_2) *ftypes.OS {
return &ftypes.OS{
Family: pkg.PackageName,
Name: pkg.PackageVersion,
}
}
func parsePkg(spdxPkg spdx.Package2_2) (*ftypes.Package, error) {
pkg, pkgType, err := parseExternalReferences(spdxPkg.PackageExternalReferences)
if err != nil {
return nil, xerrors.Errorf("external references error: %w", err)
}
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(pkgType, srcPkgName)
if err != nil {
return nil, xerrors.Errorf("failed to parse source info: %w", err)
}
}
for _, f := range spdxPkg.Files {
pkg.FilePath = f.FileName
break // Take the first file name
}
pkg.Layer.Digest = lookupAttributionTexts(spdxPkg.PackageAttributionTexts, PropertyLayerDigest)
pkg.Layer.DiffID = lookupAttributionTexts(spdxPkg.PackageAttributionTexts, PropertyLayerDiffID)
return pkg, nil
}
func parseExternalReferences(refs []*spdx.PackageExternalReference2_2) (*ftypes.Package, string, error) {
for _, ref := range refs {
// Extract the package information from PURL
if ref.RefType == RefTypePurl && ref.Category == CategoryPackageManager {
packageURL, err := purl.FromString(ref.Locator)
if err != nil {
return nil, "", xerrors.Errorf("failed to parse purl from string: %w", err)
}
pkg := packageURL.Package()
pkg.Ref = ref.Locator
return pkg, packageURL.Type, nil
}
}
return nil, "", errUnknownPackageFormat
}
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 ""
}
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
}

View File

@@ -0,0 +1,184 @@
package spdx_test
import (
"encoding/json"
"os"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/sbom"
"github.com/aquasecurity/trivy/pkg/sbom/spdx"
)
func TestUnmarshaler_Unmarshal(t *testing.T) {
tests := []struct {
name string
inputFile string
want sbom.SBOM
wantErr string
}{
{
name: "happy path",
inputFile: "testdata/happy/bom.json",
want: sbom.SBOM{
OS: &ftypes.OS{
Family: "alpine",
Name: "3.16.0",
},
Packages: []ftypes.PackageInfo{
{
Packages: []ftypes.Package{
{
Name: "musl", Version: "1.2.3-r0", SrcName: "musl", SrcVersion: "1.2.3-r0", Licenses: []string{"MIT"},
Ref: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0",
Layer: ftypes.Layer{
DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3",
},
},
},
},
},
Applications: []ftypes.Application{
{
Type: "composer",
FilePath: "app/composer/composer.lock",
Libraries: []ftypes.Package{
{
Name: "pear/log",
Version: "1.13.1",
Ref: "pkg:composer/pear/log@1.13.1",
Layer: ftypes.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
},
{
Name: "pear/pear_exception",
Version: "v1.0.0",
Ref: "pkg:composer/pear/pear_exception@v1.0.0",
Layer: ftypes.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
},
},
},
{
Type: "gobinary",
FilePath: "app/gobinary/gobinary",
Libraries: []ftypes.Package{
{
Name: "github.com/package-url/packageurl-go",
Version: "v0.1.1-0.20220203205134-d70459300c8a",
Ref: "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a",
Layer: ftypes.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
},
},
},
{
Type: "jar",
Libraries: []ftypes.Package{
{
Name: "org.codehaus.mojo:child-project",
Ref: "pkg:maven/org.codehaus.mojo/child-project@1.0",
Version: "1.0",
Layer: ftypes.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
},
},
},
{
Type: "node-pkg",
Libraries: []ftypes.Package{
{
Name: "bootstrap",
Version: "5.0.2",
Ref: "pkg:npm/bootstrap@5.0.2",
Licenses: []string{"MIT"},
Layer: ftypes.Layer{
DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1",
},
},
},
},
},
},
},
{
name: "happy path for unrelated bom",
inputFile: "testdata/happy/unrelated-bom.json",
want: sbom.SBOM{
Applications: []ftypes.Application{
{
Type: "composer",
FilePath: "app/composer/composer.lock",
Libraries: []ftypes.Package{
{
Name: "pear/log",
Version: "1.13.1",
Ref: "pkg:composer/pear/log@1.13.1",
},
{
Name: "pear/pear_exception",
Version: "v1.0.0",
Ref: "pkg:composer/pear/pear_exception@v1.0.0",
},
},
},
},
},
},
{
name: "happy path only os component",
inputFile: "testdata/happy/os-only-bom.json",
want: sbom.SBOM{
OS: &ftypes.OS{
Family: "alpine",
Name: "3.16.0",
},
},
},
{
name: "happy path empty component",
inputFile: "testdata/happy/empty-bom.json",
want: sbom.SBOM{},
},
{
name: "sad path invalid purl",
inputFile: "testdata/sad/invalid-source-info.json",
wantErr: "failed to parse source info:",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := os.Open(tt.inputFile)
require.NoError(t, err)
defer f.Close()
v := &spdx.SPDX{SBOM: &sbom.SBOM{}}
err = json.NewDecoder(f).Decode(v)
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
// Not compare the CycloneDX field
v.SPDX = nil
sort.Slice(v.Applications, func(i, j int) bool {
return v.Applications[i].Type < v.Applications[j].Type
})
require.NoError(t, err)
assert.Equal(t, tt.want, *v.SBOM)
})
}
}