mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-05 20:40:16 -08:00
283 lines
9.6 KiB
Go
283 lines
9.6 KiB
Go
package sbom_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/aquasecurity/trivy/pkg/sbom"
|
|
)
|
|
|
|
// Test data constants for SBOM format detection and decoding.
|
|
// Each constant contains base64-encoded in-toto statement in the payload field.
|
|
const (
|
|
// SPDX attestation (DSSE envelope)
|
|
// payload decoded: {"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://spdx.dev/Document","subject":[{"name":"test","digest":{"sha256":"abc123"}}],"predicate":{"SPDXID":"SPDXRef-DOCUMENT","spdxVersion":"SPDX-2.3","name":"test","dataLicense":"CC0-1.0","documentNamespace":"http://example.invalid/test","creationInfo":{"creators":["Tool: test"],"created":"2025-01-01T00:00:00Z"},"packages":[]}}
|
|
spdxAttestation = `{
|
|
"payloadType": "application/vnd.in-toto+json",
|
|
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3NwZHguZGV2L0RvY3VtZW50Iiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7IlNQRFhJRCI6IlNQRFhSZWYtRE9DVU1FTlQiLCJzcGR4VmVyc2lvbiI6IlNQRFgtMi4zIiwibmFtZSI6InRlc3QiLCJkYXRhTGljZW5zZSI6IkNDMC0xLjAiLCJkb2N1bWVudE5hbWVzcGFjZSI6Imh0dHA6Ly9leGFtcGxlLmludmFsaWQvdGVzdCIsImNyZWF0aW9uSW5mbyI6eyJjcmVhdG9ycyI6WyJUb29sOiB0ZXN0Il0sImNyZWF0ZWQiOiIyMDI1LTAxLTAxVDAwOjAwOjAwWiJ9LCJwYWNrYWdlcyI6W119fQ==",
|
|
"signatures": []
|
|
}`
|
|
|
|
// SPDX attestation with invalid SPDXID
|
|
// payload decoded: {"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://spdx.dev/Document","subject":[{"name":"test","digest":{"sha256":"abc123"}}],"predicate":{"SPDXID":"InvalidID","spdxVersion":"SPDX-2.3","name":"test"}}
|
|
spdxAttestationInvalidID = `{
|
|
"payloadType": "application/vnd.in-toto+json",
|
|
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3NwZHguZGV2L0RvY3VtZW50Iiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7IlNQRFhJRCI6IkludmFsaWRJRCIsInNwZHhWZXJzaW9uIjoiU1BEWC0yLjMiLCJuYW1lIjoidGVzdCJ9fQ==",
|
|
"signatures": []
|
|
}`
|
|
|
|
// SPDX attestation with invalid predicate (string instead of object)
|
|
// payload decoded: {"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://spdx.dev/Document","subject":[{"name":"test","digest":{"sha256":"abc123"}}],"predicate":"invalid"}
|
|
spdxAttestationInvalidPredicate = `{
|
|
"payloadType": "application/vnd.in-toto+json",
|
|
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3NwZHguZGV2L0RvY3VtZW50Iiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjoiaW52YWxpZCJ9",
|
|
"signatures": []
|
|
}`
|
|
|
|
// CycloneDX attestation (DSSE envelope)
|
|
// payload decoded: {"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://cyclonedx.org/bom","subject":[{"name":"test","digest":{"sha256":"abc123"}}],"predicate":{"bomFormat":"CycloneDX","specVersion":"1.4","version":1}}
|
|
cycloneDXAttestation = `{
|
|
"payloadType": "application/vnd.in-toto+json",
|
|
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvYm9tIiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7ImJvbUZvcm1hdCI6IkN5Y2xvbmVEWCIsInNwZWNWZXJzaW9uIjoiMS40IiwidmVyc2lvbiI6MX19",
|
|
"signatures": []
|
|
}`
|
|
|
|
// Sigstore bundle with CycloneDX SBOM
|
|
// dsseEnvelope.payload decoded: {"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://cyclonedx.org/bom","subject":[{"name":"test","digest":{"sha256":"abc123"}}],"predicate":{"bomFormat":"CycloneDX","specVersion":"1.4","version":1}}
|
|
sigstoreBundleCycloneDX = `{
|
|
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
|
|
"dsseEnvelope": {
|
|
"payloadType": "application/vnd.in-toto+json",
|
|
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvYm9tIiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7ImJvbUZvcm1hdCI6IkN5Y2xvbmVEWCIsInNwZWNWZXJzaW9uIjoiMS40IiwidmVyc2lvbiI6MX19",
|
|
"signatures": []
|
|
}
|
|
}`
|
|
|
|
// Sigstore bundle with SPDX SBOM
|
|
// dsseEnvelope.payload decoded: {"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://spdx.dev/Document","subject":[{"name":"test","digest":{"sha256":"abc123"}}],"predicate":{"SPDXID":"SPDXRef-DOCUMENT","spdxVersion":"SPDX-2.3","name":"test","dataLicense":"CC0-1.0","documentNamespace":"http://example.invalid/test","creationInfo":{"creators":["Tool: test"],"created":"2025-01-01T00:00:00Z"},"packages":[]}}
|
|
sigstoreBundleSPDX = `{
|
|
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
|
|
"dsseEnvelope": {
|
|
"payloadType": "application/vnd.in-toto+json",
|
|
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3NwZHguZGV2L0RvY3VtZW50Iiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7IlNQRFhJRCI6IlNQRFhSZWYtRE9DVU1FTlQiLCJzcGR4VmVyc2lvbiI6IlNQRFgtMi4zIiwibmFtZSI6InRlc3QiLCJkYXRhTGljZW5zZSI6IkNDMC0xLjAiLCJkb2N1bWVudE5hbWVzcGFjZSI6Imh0dHA6Ly9leGFtcGxlLmludmFsaWQvdGVzdCIsImNyZWF0aW9uSW5mbyI6eyJjcmVhdG9ycyI6WyJUb29sOiB0ZXN0Il0sImNyZWF0ZWQiOiIyMDI1LTAxLTAxVDAwOjAwOjAwWiJ9LCJwYWNrYWdlcyI6W119fQ==",
|
|
"signatures": []
|
|
}
|
|
}`
|
|
|
|
// Sigstore bundle with unsupported media type version
|
|
sigstoreBundleUnsupportedVersion = `{
|
|
"mediaType": "application/vnd.dev.sigstore.bundle.v0.4+json",
|
|
"dsseEnvelope": {
|
|
"payloadType": "application/vnd.in-toto+json",
|
|
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvYm9tIiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7ImJvbUZvcm1hdCI6IkN5Y2xvbmVEWCIsInNwZWNWZXJzaW9uIjoiMS40IiwidmVyc2lvbiI6MX19",
|
|
"signatures": []
|
|
}
|
|
}`
|
|
)
|
|
|
|
func TestDetectFormat(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want sbom.Format
|
|
}{
|
|
{
|
|
name: "SPDX attestation",
|
|
input: spdxAttestation,
|
|
want: sbom.FormatAttestSPDXJSON,
|
|
},
|
|
{
|
|
name: "SPDX attestation with invalid SPDXID",
|
|
input: spdxAttestationInvalidID,
|
|
want: sbom.FormatUnknown,
|
|
},
|
|
{
|
|
name: "CycloneDX attestation",
|
|
input: cycloneDXAttestation,
|
|
want: sbom.FormatAttestCycloneDXJSON,
|
|
},
|
|
{
|
|
name: "SPDX JSON",
|
|
input: `{
|
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
"spdxVersion": "SPDX-2.3",
|
|
"name": "test"
|
|
}`,
|
|
want: sbom.FormatSPDXJSON,
|
|
},
|
|
{
|
|
name: "CycloneDX JSON",
|
|
input: `{
|
|
"bomFormat": "CycloneDX",
|
|
"specVersion": "1.4"
|
|
}`,
|
|
want: sbom.FormatCycloneDXJSON,
|
|
},
|
|
{
|
|
name: "Unknown format",
|
|
input: `{
|
|
"unknown": "format"
|
|
}`,
|
|
want: sbom.FormatUnknown,
|
|
},
|
|
{
|
|
name: "Sigstore bundle with CycloneDX",
|
|
input: sigstoreBundleCycloneDX,
|
|
want: sbom.FormatSigstoreBundleCycloneDXJSON,
|
|
},
|
|
{
|
|
name: "Sigstore bundle with SPDX",
|
|
input: sigstoreBundleSPDX,
|
|
want: sbom.FormatSigstoreBundleSPDXJSON,
|
|
},
|
|
{
|
|
name: "Sigstore bundle with unsupported version",
|
|
input: sigstoreBundleUnsupportedVersion,
|
|
want: sbom.FormatUnknown,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := strings.NewReader(tt.input)
|
|
got, err := sbom.DetectFormat(r)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecode(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
format sbom.Format
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "SPDX attestation",
|
|
input: spdxAttestation,
|
|
format: sbom.FormatAttestSPDXJSON,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "SPDX attestation with invalid predicate",
|
|
input: spdxAttestationInvalidPredicate,
|
|
format: sbom.FormatAttestSPDXJSON,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "CycloneDX attestation",
|
|
input: cycloneDXAttestation,
|
|
format: sbom.FormatAttestCycloneDXJSON,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Sigstore bundle with CycloneDX",
|
|
input: sigstoreBundleCycloneDX,
|
|
format: sbom.FormatSigstoreBundleCycloneDXJSON,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Sigstore bundle with SPDX",
|
|
input: sigstoreBundleSPDX,
|
|
format: sbom.FormatSigstoreBundleSPDXJSON,
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := strings.NewReader(tt.input)
|
|
_, err := sbom.Decode(t.Context(), r, tt.format)
|
|
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsSPDXJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "Valid SPDX JSON",
|
|
input: `{
|
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
"spdxVersion": "SPDX-2.3"
|
|
}`,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "Invalid SPDXID",
|
|
input: `{
|
|
"SPDXID": "InvalidID",
|
|
"spdxVersion": "SPDX-2.3"
|
|
}`,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "Not SPDX",
|
|
input: `{
|
|
"bomFormat": "CycloneDX"
|
|
}`,
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := strings.NewReader(tt.input)
|
|
got, err := sbom.IsSPDXJSON(r)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsCycloneDXJSON(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "Valid CycloneDX JSON",
|
|
input: `{
|
|
"bomFormat": "CycloneDX",
|
|
"specVersion": "1.4"
|
|
}`,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "Not CycloneDX",
|
|
input: `{
|
|
"SPDXID": "SPDXRef-DOCUMENT"
|
|
}`,
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := strings.NewReader(tt.input)
|
|
got, err := sbom.IsCycloneDXJSON(r)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|