mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 07:40:48 -08:00
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:
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
94
integration/testdata/fixtures/sbom/centos-7-spdx.json
vendored
Normal file
94
integration/testdata/fixtures/sbom/centos-7-spdx.json
vendored
Normal 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"
|
||||
}
|
||||
57
integration/testdata/fixtures/sbom/centos-7-spdx.txt
vendored
Normal file
57
integration/testdata/fixtures/sbom/centos-7-spdx.txt
vendored
Normal 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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ const (
|
||||
ArtifactFilesystem ArtifactType = "filesystem"
|
||||
ArtifactRemoteRepository ArtifactType = "repository"
|
||||
ArtifactCycloneDX ArtifactType = "cyclonedx"
|
||||
ArtifactSPDX ArtifactType = "spdx"
|
||||
ArtifactAWSAccount ArtifactType = "aws_account"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
230
pkg/sbom/spdx/testdata/happy/bom.json
vendored
Normal 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"
|
||||
}
|
||||
34
pkg/sbom/spdx/testdata/happy/empty-bom.json
vendored
Normal file
34
pkg/sbom/spdx/testdata/happy/empty-bom.json
vendored
Normal 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"
|
||||
}
|
||||
42
pkg/sbom/spdx/testdata/happy/os-only-bom.json
vendored
Normal file
42
pkg/sbom/spdx/testdata/happy/os-only-bom.json
vendored
Normal 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"
|
||||
}
|
||||
82
pkg/sbom/spdx/testdata/happy/unrelated-bom.json
vendored
Normal file
82
pkg/sbom/spdx/testdata/happy/unrelated-bom.json
vendored
Normal 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"
|
||||
}
|
||||
58
pkg/sbom/spdx/testdata/sad/invalid-source-info.json
vendored
Normal file
58
pkg/sbom/spdx/testdata/sad/invalid-source-info.json
vendored
Normal 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
228
pkg/sbom/spdx/unmarshal.go
Normal 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
|
||||
}
|
||||
184
pkg/sbom/spdx/unmarshal_test.go
Normal file
184
pkg/sbom/spdx/unmarshal_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user