feat(image): add Sigstore bundle SBOM support (#9516)

Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
Thomas Grininger
2025-11-25 07:10:06 +01:00
committed by GitHub
parent 8876b46162
commit e1f3f28ae4
12 changed files with 507 additions and 255 deletions

View File

@@ -0,0 +1,81 @@
// Package registrytest provides utilities for testing with OCI registries.
package registrytest
import (
"fmt"
"net/http/httptest"
"testing"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/static"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/stretchr/testify/require"
)
// NewServer starts a test registry server with OCI 1.1 referrers support.
func NewServer(t *testing.T) *httptest.Server {
t.Helper()
return httptest.NewServer(registry.New(registry.WithReferrersSupport(true)))
}
// PushRandomImage pushes a random image to the registry and returns its reference and descriptor.
func PushRandomImage(t *testing.T, registryHost, repo, tag string) (name.Reference, v1.Descriptor) {
t.Helper()
ref, err := name.ParseReference(fmt.Sprintf("%s/%s:%s", registryHost, repo, tag))
require.NoError(t, err)
img, err := random.Image(10, 1)
require.NoError(t, err)
err = remote.Write(ref, img)
require.NoError(t, err)
d, err := img.Digest()
require.NoError(t, err)
sz, err := img.Size()
require.NoError(t, err)
mt, err := img.MediaType()
require.NoError(t, err)
return ref, v1.Descriptor{
Digest: d,
Size: sz,
MediaType: mt,
}
}
// PushReferrer pushes an artifact referrer to the registry attached to the subject image.
// The artifactType is used both as the config media type (for OCI 1.1 artifact type) and layer media type.
func PushReferrer(t *testing.T, registryHost, repo string, subjectDesc v1.Descriptor, artifactType string, content []byte) {
t.Helper()
// Create an OCI artifact with the content as a layer
layer := static.NewLayer(content, types.MediaType(artifactType))
// Start with an empty image and add the layer
img := mutate.MediaType(empty.Image, types.OCIManifestSchema1)
// Set the config media type to the artifact type - this is how OCI 1.1 identifies the artifact type
// The registry will use this as the artifactType in the referrers API response
img = mutate.ConfigMediaType(img, types.MediaType(artifactType))
img, err := mutate.AppendLayers(img, layer)
require.NoError(t, err)
// Set the subject to create the referrer relationship
img = mutate.Subject(img, subjectDesc).(v1.Image)
// Push the referrer
d, err := img.Digest()
require.NoError(t, err)
ref, err := name.ParseReference(fmt.Sprintf("%s/%s@%s", registryHost, repo, d.String()))
require.NoError(t, err)
err = remote.Write(ref, img)
require.NoError(t, err)
}

View File

@@ -17,6 +17,13 @@ type CosignPredicate struct {
Data any Data any
} }
// SigstoreBundle represents the structure of a Sigstore bundle containing a DSSE envelope.
// This format is used by Cosign v3+ with the new bundle format.
// cf. https://github.com/sigstore/cosign/blob/main/specs/BUNDLE_SPEC.md
type SigstoreBundle struct {
DSSEEnvelope Statement `json:"dsseEnvelope"`
}
// Statement holds in-toto statement headers and the predicate. // Statement holds in-toto statement headers and the predicate.
type Statement in_toto.Statement type Statement in_toto.Statement

View File

@@ -105,7 +105,9 @@ func (a Artifact) parseReferrer(ctx context.Context, repo string, desc v1.Descri
return artifact.Reference{}, xerrors.Errorf("SBOM download error: %w", err) return artifact.Reference{}, xerrors.Errorf("SBOM download error: %w", err)
} }
res, err := a.inspectSBOMFile(ctx, filepath.Join(tmpDir, fileName)) filePath := filepath.Join(tmpDir, fileName)
res, err := a.inspectSBOMFile(ctx, filePath)
if err != nil { if err != nil {
return res, xerrors.Errorf("SBOM error: %w", err) return res, xerrors.Errorf("SBOM error: %w", err)
} }

View File

@@ -1,25 +1,26 @@
package image_test package image_test
import ( import (
"net/http" "fmt"
"net/http/httptest"
"net/url" "net/url"
"os" "os"
"testing" "testing"
v1 "github.com/google/go-containerregistry/pkg/v1" v1 "github.com/google/go-containerregistry/pkg/v1"
fakei "github.com/google/go-containerregistry/pkg/v1/fake" fakei "github.com/google/go-containerregistry/pkg/v1/fake"
typesv1 "github.com/google/go-containerregistry/pkg/v1/types"
"github.com/package-url/packageurl-go" "github.com/package-url/packageurl-go"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/aquasecurity/trivy/internal/cachetest" "github.com/aquasecurity/trivy/internal/cachetest"
"github.com/aquasecurity/trivy/internal/registrytest"
"github.com/aquasecurity/trivy/pkg/fanal/artifact" "github.com/aquasecurity/trivy/pkg/fanal/artifact"
image2 "github.com/aquasecurity/trivy/pkg/fanal/artifact/image" image2 "github.com/aquasecurity/trivy/pkg/fanal/artifact/image"
"github.com/aquasecurity/trivy/pkg/fanal/types" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/oci"
"github.com/aquasecurity/trivy/pkg/rekortest" "github.com/aquasecurity/trivy/pkg/rekortest"
"github.com/aquasecurity/trivy/pkg/sbom"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@@ -31,7 +32,7 @@ type fakeImage struct {
name string name string
repoDigests []string repoDigests []string
v1.Image v1.Image
types.ImageExtension ftypes.ImageExtension
} }
func (f fakeImage) ID() (string, error) { func (f fakeImage) ID() (string, error) {
@@ -46,47 +47,42 @@ func (f fakeImage) RepoDigests() []string {
return f.repoDigests return f.repoDigests
} }
func (f fakeImage) RepoTags() []string {
return nil
}
func TestArtifact_InspectRekorAttestation(t *testing.T) { func TestArtifact_InspectRekorAttestation(t *testing.T) {
type fields struct {
imageName string
repoDigests []string
}
tests := []struct { tests := []struct {
name string name string
fields fields imageName string
artifactOpt artifact.Option repoDigests []string
wantBlobs []cachetest.WantBlob wantBlobs []cachetest.WantBlob
want artifact.Reference want artifact.Reference
wantErr string wantErr string
}{ }{
{ {
name: "happy path", name: "happy path",
fields: fields{ imageName: "test/image:10",
imageName: "test/image:10", repoDigests: []string{
repoDigests: []string{ "test/image@sha256:782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02",
"test/image@sha256:782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02",
},
},
artifactOpt: artifact.Option{
SBOMSources: []string{"rekor"},
}, },
wantBlobs: []cachetest.WantBlob{ wantBlobs: []cachetest.WantBlob{
{ {
ID: "sha256:066b9998617ffb7dfe0a3219ac5c3efc1008a6223606fcf474e7d5c965e4e8da", ID: "sha256:066b9998617ffb7dfe0a3219ac5c3efc1008a6223606fcf474e7d5c965e4e8da",
BlobInfo: types.BlobInfo{ BlobInfo: ftypes.BlobInfo{
SchemaVersion: types.BlobJSONSchemaVersion, SchemaVersion: ftypes.BlobJSONSchemaVersion,
OS: types.OS{ OS: ftypes.OS{
Family: "alpine", Family: "alpine",
Name: "3.16.2", Name: "3.16.2",
}, },
PackageInfos: []types.PackageInfo{ PackageInfos: []ftypes.PackageInfo{
{ {
Packages: types.Packages{ Packages: ftypes.Packages{
{ {
ID: "musl@1.2.3-r0", ID: "musl@1.2.3-r0",
Name: "musl", Name: "musl",
Version: "1.2.3-r0", Version: "1.2.3-r0",
Identifier: types.PkgIdentifier{ Identifier: ftypes.PkgIdentifier{
PURL: &packageurl.PackageURL{ PURL: &packageurl.PackageURL{
Type: packageurl.TypeApk, Type: packageurl.TypeApk,
Namespace: "alpine", Namespace: "alpine",
@@ -104,7 +100,7 @@ func TestArtifact_InspectRekorAttestation(t *testing.T) {
SrcName: "musl", SrcName: "musl",
SrcVersion: "1.2.3-r0", SrcVersion: "1.2.3-r0",
Licenses: []string{"MIT"}, Licenses: []string{"MIT"},
Layer: types.Layer{ Layer: ftypes.Layer{
DiffID: "sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7", DiffID: "sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7",
}, },
}, },
@@ -116,7 +112,7 @@ func TestArtifact_InspectRekorAttestation(t *testing.T) {
}, },
want: artifact.Reference{ want: artifact.Reference{
Name: "test/image:10", Name: "test/image:10",
Type: types.TypeCycloneDX, Type: ftypes.TypeCycloneDX,
ID: "sha256:066b9998617ffb7dfe0a3219ac5c3efc1008a6223606fcf474e7d5c965e4e8da", ID: "sha256:066b9998617ffb7dfe0a3219ac5c3efc1008a6223606fcf474e7d5c965e4e8da",
BlobIDs: []string{ BlobIDs: []string{
"sha256:066b9998617ffb7dfe0a3219ac5c3efc1008a6223606fcf474e7d5c965e4e8da", "sha256:066b9998617ffb7dfe0a3219ac5c3efc1008a6223606fcf474e7d5c965e4e8da",
@@ -136,40 +132,34 @@ func TestArtifact_InspectRekorAttestation(t *testing.T) {
}, },
}, },
{ {
name: "error", name: "attestation not found",
fields: fields{ imageName: "test/image:10",
imageName: "test/image:10", repoDigests: []string{
repoDigests: []string{ "test/image@sha256:123456e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02",
"test/image@sha256:123456e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02",
},
},
artifactOpt: artifact.Option{
SBOMSources: []string{"rekor"},
}, },
wantErr: "remote SBOM fetching error", wantErr: "remote SBOM fetching error",
}, },
} }
log.InitLogger(false, true)
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
ts := rekortest.NewServer(t) ts := rekortest.NewServer(t)
defer ts.Close() defer ts.Close()
// Set the testing URL
tt.artifactOpt.RekorURL = ts.URL()
c := cachetest.NewCache(t, nil) c := cachetest.NewCache(t, nil)
fi := &fakei.FakeImage{} fi := &fakei.FakeImage{}
fi.ConfigFileReturns(&v1.ConfigFile{}, nil) fi.ConfigFileReturns(&v1.ConfigFile{}, nil)
img := &fakeImage{ img := &fakeImage{
name: tt.fields.imageName, name: tt.imageName,
repoDigests: tt.fields.repoDigests, repoDigests: tt.repoDigests,
Image: fi, Image: fi,
} }
a, err := image2.NewArtifact(img, c, tt.artifactOpt) a, err := image2.NewArtifact(img, c, artifact.Option{
SBOMSources: []string{"rekor"},
RekorURL: ts.URL(),
})
require.NoError(t, err) require.NoError(t, err)
got, err := a.Inspect(t.Context()) got, err := a.Inspect(t.Context())
@@ -177,156 +167,155 @@ func TestArtifact_InspectRekorAttestation(t *testing.T) {
assert.ErrorContains(t, err, tt.wantErr) assert.ErrorContains(t, err, tt.wantErr)
return return
} }
require.NoError(t, err)
defer a.Clean(got) defer a.Clean(got)
require.NoError(t, err, tt.name)
got.BOM = nil got.BOM = nil
assert.Equal(t, tt.want, got) assert.Equal(t, tt.want, got)
cachetest.AssertBlobs(t, c, tt.wantBlobs) cachetest.AssertBlobs(t, c, tt.wantBlobs)
}) })
} }
} }
func TestArtifact_inspectOCIReferrerSBOM(t *testing.T) { // Common test data for CycloneDX SBOM (used by OCI referrer tests)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var wantBlobsCycloneDX = []cachetest.WantBlob{
switch r.URL.Path { {
case "/v2": ID: "sha256:2171d8ccf798e94d09aca9c6abf15d28abd3236def1caa4a394b6f0a69c4266d",
_, err := w.Write([]byte("ok")) BlobInfo: ftypes.BlobInfo{
assert.NoError(t, err) SchemaVersion: ftypes.BlobJSONSchemaVersion,
case "/v2/test/image/referrers/sha256:782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02": Applications: []ftypes.Application{
w.Header().Set("Content-Type", string(typesv1.OCIImageIndex))
http.ServeFile(w, r, "testdata/index.json")
case "/v2/test/image/manifests/sha256:37c89af4907fa0af078aeba12d6f18dc0c63937c010030baaaa88e958f0719a5":
http.ServeFile(w, r, "testdata/manifest.json")
case "/v2/test/image/blobs/sha256:9e05dda2a2dcdd526c9204be8645ae48742861c27f093bf496a6397834acecf2":
http.ServeFile(w, r, "testdata/cyclonedx.json")
}
}))
defer ts.Close()
u, err := url.Parse(ts.URL)
require.NoError(t, err)
registry := u.Host
type fields struct {
imageName string
repoDigests []string
}
tests := []struct {
name string
fields fields
artifactOpt artifact.Option
wantBlobs []cachetest.WantBlob
want artifact.Reference
wantErr string
}{
{
name: "happy path",
fields: fields{
imageName: registry + "/test/image:10",
repoDigests: []string{
registry + "/test/image@sha256:782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02",
},
},
artifactOpt: artifact.Option{
SBOMSources: []string{"oci"},
},
wantBlobs: []cachetest.WantBlob{
{ {
ID: "sha256:2171d8ccf798e94d09aca9c6abf15d28abd3236def1caa4a394b6f0a69c4266d", Type: ftypes.GoBinary,
BlobInfo: types.BlobInfo{ Packages: ftypes.Packages{
SchemaVersion: types.BlobJSONSchemaVersion, {
Applications: []types.Application{ ID: "github.com/opencontainers/go-digest@v1.0.0",
{ Name: "github.com/opencontainers/go-digest",
Type: types.GoBinary, Version: "v1.0.0",
Packages: types.Packages{ Identifier: ftypes.PkgIdentifier{
{ PURL: &packageurl.PackageURL{
ID: "github.com/opencontainers/go-digest@v1.0.0", Type: packageurl.TypeGolang,
Name: "github.com/opencontainers/go-digest", Namespace: "github.com/opencontainers",
Version: "v1.0.0", Name: "go-digest",
Identifier: types.PkgIdentifier{ Version: "v1.0.0",
PURL: &packageurl.PackageURL{
Type: packageurl.TypeGolang,
Namespace: "github.com/opencontainers",
Name: "go-digest",
Version: "v1.0.0",
},
BOMRef: "pkg:golang/github.com/opencontainers/go-digest@v1.0.0",
},
},
{
ID: "golang.org/x/sync@v0.1.0",
Name: "golang.org/x/sync",
Version: "v0.1.0",
Identifier: types.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeGolang,
Namespace: "golang.org/x",
Name: "sync",
Version: "v0.1.0",
},
BOMRef: "pkg:golang/golang.org/x/sync@v0.1.0",
},
},
}, },
BOMRef: "pkg:golang/github.com/opencontainers/go-digest@v1.0.0",
},
},
{
ID: "golang.org/x/sync@v0.1.0",
Name: "golang.org/x/sync",
Version: "v0.1.0",
Identifier: ftypes.PkgIdentifier{
PURL: &packageurl.PackageURL{
Type: packageurl.TypeGolang,
Namespace: "golang.org/x",
Name: "sync",
Version: "v0.1.0",
},
BOMRef: "pkg:golang/golang.org/x/sync@v0.1.0",
}, },
}, },
}, },
}, },
}, },
want: artifact.Reference{ },
Name: registry + "/test/image:10", },
Type: types.TypeCycloneDX, }
ID: "sha256:2171d8ccf798e94d09aca9c6abf15d28abd3236def1caa4a394b6f0a69c4266d",
BlobIDs: []string{ func TestArtifact_InspectOCIReferrerSBOM(t *testing.T) {
"sha256:2171d8ccf798e94d09aca9c6abf15d28abd3236def1caa4a394b6f0a69c4266d", // Start a test registry with referrers support
}, ts := registrytest.NewServer(t)
defer ts.Close()
u, err := url.Parse(ts.URL)
require.NoError(t, err)
registryHost := u.Host
tests := []struct {
name string
setup func(t *testing.T) (imageName string, repoDigests []string)
wantType ftypes.ArtifactType
wantID string
wantBlobs []cachetest.WantBlob
}{
{
name: "CycloneDX SBOM",
setup: func(t *testing.T) (string, []string) {
ref, subjectDesc := registrytest.PushRandomImage(t, registryHost, "test/cyclonedx", "latest")
sbomContent, err := os.ReadFile("testdata/cyclonedx.json")
require.NoError(t, err)
registrytest.PushReferrer(t, registryHost, "test/cyclonedx", subjectDesc, oci.CycloneDXArtifactType, sbomContent)
return ref.String(),
[]string{fmt.Sprintf("%s/test/cyclonedx@%s", registryHost, subjectDesc.Digest.String())}
}, },
wantType: ftypes.TypeCycloneDX,
wantID: "sha256:2171d8ccf798e94d09aca9c6abf15d28abd3236def1caa4a394b6f0a69c4266d",
wantBlobs: wantBlobsCycloneDX,
}, },
{ {
name: "404", name: "Sigstore bundle",
fields: fields{ setup: func(t *testing.T) (string, []string) {
imageName: registry + "/test/image:unknown", ref, subjectDesc := registrytest.PushRandomImage(t, registryHost, "test/sigstore", "latest")
repoDigests: []string{
registry + "/test/image@sha256:123456e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02", bundleContent, err := os.ReadFile("testdata/sigstore-bundle.json")
}, require.NoError(t, err)
registrytest.PushReferrer(t, registryHost, "test/sigstore", subjectDesc, sbom.SigstoreBundleMediaType, bundleContent)
return ref.String(),
[]string{fmt.Sprintf("%s/test/sigstore@%s", registryHost, subjectDesc.Digest.String())}
}, },
artifactOpt: artifact.Option{ wantType: ftypes.TypeCycloneDX,
SBOMSources: []string{"oci"}, wantID: "sha256:2171d8ccf798e94d09aca9c6abf15d28abd3236def1caa4a394b6f0a69c4266d",
wantBlobs: wantBlobsCycloneDX,
},
{
name: "no referrers",
setup: func(t *testing.T) (string, []string) {
// Push image without any referrers
ref, subjectDesc := registrytest.PushRandomImage(t, registryHost, "test/no-referrers", "latest")
return ref.String(),
[]string{fmt.Sprintf("%s/test/no-referrers@%s", registryHost, subjectDesc.Digest.String())}
}, },
wantErr: "unable to get manifest", // Falls back to normal image scanning when no referrers found
wantType: ftypes.TypeContainerImage,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
imageName, repoDigests := tt.setup(t)
c := cachetest.NewCache(t, nil) c := cachetest.NewCache(t, nil)
fi := &fakei.FakeImage{} fi := &fakei.FakeImage{}
fi.ConfigFileReturns(&v1.ConfigFile{}, nil) fi.ConfigFileReturns(&v1.ConfigFile{}, nil)
img := &fakeImage{ img := &fakeImage{
name: tt.fields.imageName, name: imageName,
repoDigests: tt.fields.repoDigests, repoDigests: repoDigests,
Image: fi, Image: fi,
} }
a, err := image2.NewArtifact(img, c, tt.artifactOpt) a, err := image2.NewArtifact(img, c, artifact.Option{
SBOMSources: []string{"oci"},
})
require.NoError(t, err) require.NoError(t, err)
got, err := a.Inspect(t.Context()) got, err := a.Inspect(t.Context())
if tt.wantErr != "" { require.NoError(t, err)
assert.ErrorContains(t, err, tt.wantErr)
return
}
defer a.Clean(got) defer a.Clean(got)
require.NoError(t, err, tt.name) assert.Equal(t, tt.wantType, got.Type)
got.BOM = nil if tt.wantID != "" {
assert.Equal(t, tt.want, got) assert.Equal(t, tt.wantID, got.ID)
}
cachetest.AssertBlobs(t, c, tt.wantBlobs) if tt.wantBlobs != nil {
cachetest.AssertBlobs(t, c, tt.wantBlobs)
}
}) })
} }
} }

View File

@@ -1,15 +0,0 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 621,
"digest": "sha256:37c89af4907fa0af078aeba12d6f18dc0c63937c010030baaaa88e958f0719a5",
"annotations": {
"org.opencontainers.artifact.description": "CycloneDX JSON SBOM"
},
"artifactType": "application/vnd.cyclonedx+json"
}
]
}

View File

@@ -1,24 +0,0 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.cyclonedx+json",
"size": 2,
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
},
"layers": [
{
"mediaType": "application/vnd.cyclonedx+json",
"size": 2215,
"digest": "sha256:9e05dda2a2dcdd526c9204be8645ae48742861c27f093bf496a6397834acecf2"
}
],
"annotations": {
"org.opencontainers.artifact.description": "CycloneDX JSON SBOM"
},
"subject": {
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 1024,
"digest": "sha256:782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02"
}
}

View File

@@ -0,0 +1,59 @@
{
"dsseEnvelope": {
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvYm9tIiwic3ViamVjdCI6W3sibmFtZSI6ImdoY3IuaW8vcmluZ29kZXYvZGVtby1zYm9tIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImVhOWM2NTdjYTQ0MzcxZmFkZDAyNzUxMGYyYTFhNjhiNzk5MDAzMjk0YmY2YTQ4Mjc4Mjc5NzFjYzQzOTBhZDQifX1dLCJwcmVkaWNhdGUiOnsiYm9tRm9ybWF0IjoiQ3ljbG9uZURYIiwiY29tcG9uZW50cyI6W3siYm9tLXJlZiI6InBrZzpnb2xhbmcvZ2l0aHViLmNvbS9vcGVuY29udGFpbmVycy9nby1kaWdlc3RAdjEuMC4wIiwibmFtZSI6ImdpdGh1Yi5jb20vb3BlbmNvbnRhaW5lcnMvZ28tZGlnZXN0IiwicHJvcGVydGllcyI6W3sibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpQa2dUeXBlIiwidmFsdWUiOiJnb2JpbmFyeSJ9XSwicHVybCI6InBrZzpnb2xhbmcvZ2l0aHViLmNvbS9vcGVuY29udGFpbmVycy9nby1kaWdlc3RAdjEuMC4wIiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoidjEuMC4wIn0seyJib20tcmVmIjoicGtnOmdvbGFuZy9nb2xhbmcub3JnL3gvc3luY0B2MC4xLjAiLCJuYW1lIjoiZ29sYW5nLm9yZy94L3N5bmMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OlBrZ1R5cGUiLCJ2YWx1ZSI6ImdvYmluYXJ5In1dLCJwdXJsIjoicGtnOmdvbGFuZy9nb2xhbmcub3JnL3gvc3luY0B2MC4xLjAiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiJ2MC4xLjAifV0sImRlcGVuZGVuY2llcyI6W3siZGVwZW5kc09uIjpbInBrZzpnb2xhbmcvZ2l0aHViLmNvbS9vcGVuY29udGFpbmVycy9nby1kaWdlc3RAdjEuMC4wIiwicGtnOmdvbGFuZy9nb2xhbmcub3JnL3gvc3luY0B2MC4xLjAiXSwicmVmIjoiYzYzMDlkNGEtZjZkYi00YWNjLThlNDQtZjBkMjcxNDkyYjY1In0seyJkZXBlbmRzT24iOlsiYzYzMDlkNGEtZjZkYi00YWNjLThlNDQtZjBkMjcxNDkyYjY1Il0sInJlZiI6InBrZzpvY2kvZGVtby1yZWZlcnJlcnMtMjAyM0BzaGEyNTY6ZTc2YTEzNDc1YzZiNGE3MTNhMGU0YTdhODU3NGNlNDUwMjc0ZDM0MDM1N2EyYzQwYjgyMjFjZmNmZWRmOGIxOT9yZXBvc2l0b3J5X3VybD1sb2NhbGhvc3Q6NTAwMSUyRmRlbW8tcmVmZXJyZXJzLTIwMjNcdTAwMjZhcmNoPWFybTY0In1dLCJtZXRhZGF0YSI6eyJjb21wb25lbnQiOnsiYm9tLXJlZiI6InBrZzpvY2kvZGVtby1yZWZlcnJlcnMtMjAyM0BzaGEyNTY6ZTc2YTEzNDc1YzZiNGE3MTNhMGU0YTdhODU3NGNlNDUwMjc0ZDM0MDM1N2EyYzQwYjgyMjFjZmNmZWRmOGIxOT9yZXBvc2l0b3J5X3VybD1sb2NhbGhvc3Q6NTAwMSUyRmRlbW8tcmVmZXJyZXJzLTIwMjNcdTAwMjZhcmNoPWFybTY0IiwibmFtZSI6ImxvY2FsaG9zdDo1MDAxL2RlbW8tcmVmZXJyZXJzLTIwMjM6YXBwIiwicHJvcGVydGllcyI6W3sibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpTY2hlbWFWZXJzaW9uIiwidmFsdWUiOiIyIn1dLCJwdXJsIjoicGtnOm9jaS9kZW1vLXJlZmVycmVycy0yMDIzQHNoYTI1NjplNzZhMTM0NzVjNmI0YTcxM2EwZTRhN2E4NTc0Y2U0NTAyNzRkMzQwMzU3YTJjNDBiODIyMWNmY2ZlZGY4YjE5P3JlcG9zaXRvcnlfdXJsPWxvY2FsaG9zdDo1MDAxJTJGZGVtby1yZWZlcnJlcnMtMjAyM1x1MDAyNmFyY2g9YXJtNjQiLCJ0eXBlIjoiY29udGFpbmVyIn0sInRpbWVzdGFtcCI6IjIwMjMtMDMtMjJUMTE6MzU6MzgrMDA6MDAiLCJ0b29scyI6W3sibmFtZSI6InRyaXZ5IiwidmVuZG9yIjoiYXF1YXNlY3VyaXR5IiwidmVyc2lvbiI6IjAuMzkuMCJ9XX0sInNlcmlhbE51bWJlciI6InVybjp1dWlkOjJlM2M2ODRjLTFmOTktNDY2Yy1hZDZiLWI3ODM5NDJlYjlkMSIsInNwZWNWZXJzaW9uIjoiMS40IiwidmVyc2lvbiI6MSwidnVsbmVyYWJpbGl0aWVzIjpbXX19",
"payloadType": "application/vnd.in-toto+json",
"signatures": [
{
"sig": "MEUCIQCixpYXidhSSGs+WhNVjF6eHiTj6eo0KxF8ihhHlyoHKgIgFv/p/hIhoS/yNTo4Xb/+Ac3fTfd1xNcGB+IJqTNyP/I="
}
]
},
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
"verificationMaterial": {
"certificate": {
"rawBytes": "MIIC2zCCAmGgAwIBAgIUC8iVf2cW24GNi2YlrpI5xMh/3TMwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUwOTIzMDUwMzQyWhcNMjUwOTIzMDUxMzQyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAED4Q9U5ucpN780p9nUCiupZSrMpTVQW04/S+AFZVEKkBi3vndyRRXmogyQBDmpFO+ST8NZDBEJCKhEvfg/ZWKZaOCAYAwggF8MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUtHnkcI9f9YhXO7sRWkqqnKWaMBYwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wKwYDVR0RAQH/BCEwH4EddGhvbWFzLmdyaW5pbmdlckByaW5nb2Rldi5jb20wLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMC4GCisGAQQBg78wAQgEIAweaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGZdPTFfQAABAMARjBEAiAZakMhKO+tfDvTmISGjfgGaO+Ehj6u1hQPSTBQGpoSPgIgAZeiL6xH76MTSWoYww3EUheSgOwoKIYTMaNh57PqmEAwCgYIKoZIzj0EAwMDaAAwZQIwQBdf+r3InPKcMDBwMjZkdMarevw4IfC0PYGKBf3u0/Y0RK4cyDAuCS4y81is2WoGAjEAnVymG/CTeF4Ff/jqxqIWaMCoKFiJ85NsNi4+NJ7m5qr/UT1LTeuly075bSGHH9zj"
},
"tlogEntries": [
{
"canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZjYxZDkzMjkzMWVjNDliMTNiNTBjODgzZGQ3OTEzMzM3ZmMzZDQ2ZTdiYmI0YTczNDZiYTM4YmJmZmY1MjA1ZiJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjI4OTBmZmEyYmVlODFjYTg4OWQxM2Y3ZjRjNDJiN2NkODc1YTJiYWM4ZDIyODcyYzcyZjJlMjNmZDJhZTkzNWEifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVVQ0lRQ2l4cFlYaWRoU1NHcytXaE5WakY2ZUhpVGo2ZW8wS3hGOGloaEhseW9IS2dJZ0Z2L3AvaElob1MveU5UbzRYYi8rQWMzZlRmZDF4TmNHQitJSnFUTnlQL0k9IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VNeWVrTkRRVzFIWjBGM1NVSkJaMGxWUXpocFZtWXlZMWN5TkVkT2FUSlpiSEp3U1RWNFRXZ3ZNMVJOZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwVmQwOVVTWHBOUkZWM1RYcFJlVmRvWTA1TmFsVjNUMVJKZWsxRVZYaE5lbEY1VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVkVORkU1VlRWMVkzQk9Oemd3Y0RsdVZVTnBkWEJhVTNKTmNGUldVVmN3TkM5VEswRUtSbHBXUlV0clFta3pkbTVrZVZKU1dHMXZaM2xSUWtSdGNFWlBLMU5VT0U1YVJFSkZTa05MYUVWMlptY3ZXbGRMV21GUFEwRlpRWGRuWjBZNFRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVjBTRzVyQ21OSk9XWTVXV2hZVHpkelVsZHJjWEZ1UzFkaFRVSlpkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMHQzV1VSV1VqQlNRVkZJTDBKRFJYZElORVZrWkVkb2RtSlhSbnBNYldSNVlWYzFjR0p0Wkd4amEwSjVZVmMxYm1JeVVteGthVFZxWWpJd2R3cE1RVmxMUzNkWlFrSkJSMFIyZWtGQ1FWRlJaV0ZJVWpCalNFMDJUSGs1Ym1GWVVtOWtWMGwxV1RJNWRFd3llSFphTW14MVRESTVhR1JZVW05TlF6UkhDa05wYzBkQlVWRkNaemM0ZDBGUlowVkpRWGRsWVVoU01HTklUVFpNZVRsdVlWaFNiMlJYU1hWWk1qbDBUREo0ZGxveWJIVk1NamxvWkZoU2IwMUpSMG9LUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpJYzBWbFVVSXpRVWhWUVROVU1IZGhjMkpJUlZSS2FrZFNOR050VjJNelFYRktTMWh5YW1WUVN6TXZhRFJ3ZVFwblF6aHdOMjgwUVVGQlIxcGtVRlJHWmxGQlFVSkJUVUZTYWtKRlFXbEJXbUZyVFdoTFR5dDBaa1IyVkcxSlUwZHFabWRIWVU4clJXaHFOblV4YUZGUUNsTlVRbEZIY0c5VFVHZEpaMEZhWldsTU5uaElOelpOVkZOWGIxbDNkek5GVldobFUyZFBkMjlMU1ZsVVRXRk9hRFUzVUhGdFJVRjNRMmRaU1V0dldra0tlbW93UlVGM1RVUmhRVUYzV2xGSmQxRkNaR1lyY2pOSmJsQkxZMDFFUW5kTmFscHJaRTFoY21WMmR6Ukpaa013VUZsSFMwSm1NM1V3TDFrd1VrczBZd3A1UkVGMVExTTBlVGd4YVhNeVYyOUhRV3BGUVc1V2VXMUhMME5VWlVZMFJtWXZhbkY0Y1VsWFlVMURiMHRHYVVvNE5VNXpUbWswSzA1S04yMDFjWEl2Q2xWVU1VeFVaWFZzZVRBM05XSlRSMGhJT1hwcUNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9XX19",
"inclusionPromise": {
"signedEntryTimestamp": "MEQCHwyPMnKpy/yUQ0ZwR9gK4a2uckTuTUdLxu4evTPuVd4CIQCzhKnq3MeJbnQ+UKne4pLB+zkvN+e/Feb5EPc8WnyueQ=="
},
"inclusionProof": {
"checkpoint": {
"envelope": "rekor.sigstore.dev - 1193050959916656506\n427690837\nsJPSNHZNtTPycokyg+lqN2e6YlRLlJ57qNvKbfgu4DA=\n\n— rekor.sigstore.dev wNI9ajBFAiEAsJNoQ9+KC5oVw5F8m6TJsMWRICYM/b5GtvdlEUzfKewCIEZ4yKxVTQnS8fOeS1wdxrENfB+ExH45cYvvUElK6lyZ\n"
},
"hashes": [
"LiSPCqC8Ls8vziACm+javgVtDbCunAnG231n36f+rmA=",
"SOInOiumiLsg+YEerz/uDUJxNsOKe+RoCBn66/8CAx0=",
"L7DlG4/e8uoLsciiN10/PQ9Dp11pI20zjdqLZkL+qes=",
"LF7kYuNdnJjUcOI8W65buH30EMIADhu/Kxl5u6ZEDxE=",
"jS89+x6mwua8j9KN5Dv6CO55M78GF8hi8oSIr0Bf2nw=",
"QnOvO+8sDYohFzr5WL8YeAUXNvpPvu8uJ94IvrDkCW0=",
"4ctDWxRuy9Mz4TnkTNS+M6IbMvBeMJ4nOhlt6OKpRjE=",
"CNxxnyEXbd2FPeCFwuWruKKVDmiib5NLbmERibRvCQ0=",
"hfVaV9xEkKbkj2E2/qTT82VHIma+u9NCAFqqsRiinIw=",
"2ZZMmiqKJ0oGmxZHgx7TcGakJLUPNXVLRdD2U48OC7g=",
"E7mN474KbNy0VYJOJx8PWikuCKMn+miLKMG6HAtUY6o=",
"fg+F12WPA6GgbJ0+XKaR8IkjWVPhNcjTcpgtCjk0a9A=",
"rO8wDSOjmY8VkspFqYaJS4TV5HxywICMlHM8gTxXkAA=",
"1mfy94KpcItqshH9+gwqV6jccupcaMpVsF28New8zDY=",
"vS7O4ozHIQZJWBiov+mkpI27GE8zAmVCEkRcP3NDyNE="
],
"logIndex": "427690836",
"rootHash": "sJPSNHZNtTPycokyg+lqN2e6YlRLlJ57qNvKbfgu4DA=",
"treeSize": "427690837"
},
"integratedTime": "1758603823",
"kindVersion": {
"kind": "dsse",
"version": "0.0.1"
},
"logId": {
"keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
},
"logIndex": "549595098"
}
]
}
}

View File

@@ -1,14 +0,0 @@
package testdata.kubernetes.id_100
__rego_metadata__ := {
"id": "ID-100",
"title": "Bad Deployment",
"version": "v1.0.0",
"severity": "HIGH",
"type": "Kubernetes Security Check",
}
deny[res] {
input.kind == "Deployment"
res := {"type": "Kubernetes Check", "id": "ID-100", "msg": "deny", "severity": "CRITICAL"}
}

View File

@@ -77,11 +77,11 @@ func (a Artifact) Inspect(ctx context.Context) (artifact.Reference, error) {
var artifactType types.ArtifactType var artifactType types.ArtifactType
switch format { switch format {
case sbom.FormatCycloneDXJSON, sbom.FormatCycloneDXXML, sbom.FormatAttestCycloneDXJSON, sbom.FormatLegacyCosignAttestCycloneDXJSON: case sbom.FormatCycloneDXJSON, sbom.FormatCycloneDXXML, sbom.FormatAttestCycloneDXJSON,
sbom.FormatLegacyCosignAttestCycloneDXJSON, sbom.FormatSigstoreBundleCycloneDXJSON:
artifactType = types.TypeCycloneDX artifactType = types.TypeCycloneDX
case sbom.FormatSPDXTV, sbom.FormatSPDXJSON, sbom.FormatAttestSPDXJSON: case sbom.FormatSPDXTV, sbom.FormatSPDXJSON, sbom.FormatAttestSPDXJSON, sbom.FormatSigstoreBundleSPDXJSON:
artifactType = types.TypeSPDX artifactType = types.TypeSPDX
} }
return artifact.Reference{ return artifact.Reference{

View File

@@ -30,6 +30,9 @@ const (
CycloneDXArtifactType = "application/vnd.cyclonedx+json" CycloneDXArtifactType = "application/vnd.cyclonedx+json"
SPDXArtifactType = "application/spdx+json" SPDXArtifactType = "application/spdx+json"
// Sigstore bundle artifact type for Cosign attestations in "new bundle format" > 2.5.0
SigstoreBundleArtifactType = "application/vnd.dev.sigstore.bundle.v0.3+json"
// Media types // Media types
OCIImageManifest = "application/vnd.oci.image.manifest.v1+json" OCIImageManifest = "application/vnd.oci.image.manifest.v1+json"
@@ -40,6 +43,7 @@ const (
var SupportedSBOMArtifactTypes = []string{ var SupportedSBOMArtifactTypes = []string{
CycloneDXArtifactType, CycloneDXArtifactType,
SPDXArtifactType, SPDXArtifactType,
SigstoreBundleArtifactType,
} }
// Option is a functional option // Option is a functional option

View File

@@ -31,6 +31,14 @@ const (
FormatAttestSPDXJSON Format = "attest-spdx-json" FormatAttestSPDXJSON Format = "attest-spdx-json"
FormatUnknown Format = "unknown" FormatUnknown Format = "unknown"
// FormatSigstoreBundleCycloneDXJSON is used for Sigstore bundle format containing CycloneDX SBOM attestation.
// This format is produced by Cosign v3+ with the new bundle format.
// ref. https://github.com/sigstore/cosign/blob/main/specs/BUNDLE_SPEC.md
FormatSigstoreBundleCycloneDXJSON Format = "sigstore-bundle-cyclonedx-json"
// FormatSigstoreBundleSPDXJSON is used for Sigstore bundle format containing SPDX SBOM attestation.
FormatSigstoreBundleSPDXJSON Format = "sigstore-bundle-spdx-json"
// FormatLegacyCosignAttestCycloneDXJSON is used to support the older format of CycloneDX JSON Attestation // FormatLegacyCosignAttestCycloneDXJSON is used to support the older format of CycloneDX JSON Attestation
// produced by the Cosign V1. // produced by the Cosign V1.
// ref. https://github.com/sigstore/cosign/pull/2718 // ref. https://github.com/sigstore/cosign/pull/2718
@@ -40,6 +48,10 @@ const (
// This is necessary for backward-compatible SBOM detection. // This is necessary for backward-compatible SBOM detection.
// ref. https://github.com/in-toto/in-toto-golang/pull/188 // ref. https://github.com/in-toto/in-toto-golang/pull/188
PredicateCycloneDXBeforeV05 = "https://cyclonedx.org/schema" PredicateCycloneDXBeforeV05 = "https://cyclonedx.org/schema"
// SigstoreBundleMediaType is the media type for Sigstore bundles v0.3
// ref. https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto
SigstoreBundleMediaType = "application/vnd.dev.sigstore.bundle.v0.3+json"
) )
var ErrUnknownFormat = xerrors.New("Unknown SBOM format") var ErrUnknownFormat = xerrors.New("Unknown SBOM format")
@@ -56,6 +68,13 @@ type spdxHeader struct {
SpdxID string `json:"SPDXID"` SpdxID string `json:"SPDXID"`
} }
// sigstoreBundle represents the structure of a Sigstore bundle
// ref. https://github.com/sigstore/cosign/blob/main/specs/BUNDLE_SPEC.md
type sigstoreBundle struct {
MediaType string `json:"mediaType"`
DSSEEnvelope json.RawMessage `json:"dsseEnvelope"`
}
func IsCycloneDXJSON(r io.ReadSeeker) (bool, error) { func IsCycloneDXJSON(r io.ReadSeeker) (bool, error) {
if _, err := r.Seek(0, io.SeekStart); err != nil { if _, err := r.Seek(0, io.SeekStart); err != nil {
return false, xerrors.Errorf("seek error: %w", err) return false, xerrors.Errorf("seek error: %w", err)
@@ -152,6 +171,16 @@ func DetectFormat(r io.ReadSeeker) (Format, error) {
return format, nil return format, nil
} }
if _, err := r.Seek(0, io.SeekStart); err != nil {
return FormatUnknown, xerrors.Errorf("seek error: %w", err)
}
// Try Sigstore bundle
format, ok = decodeSigstoreBundleFormat(r)
if ok {
return format, nil
}
return FormatUnknown, nil return FormatUnknown, nil
} }
@@ -189,6 +218,41 @@ func decodeAttestationFormat(r io.ReadSeeker) (Format, bool) {
return "", false return "", false
} }
func decodeSigstoreBundleFormat(r io.ReadSeeker) (Format, bool) {
var bundle sigstoreBundle
if err := json.NewDecoder(r).Decode(&bundle); err != nil {
return "", false
}
// Check if the media type indicates a Sigstore bundle
if bundle.MediaType != SigstoreBundleMediaType {
return "", false
}
if bundle.DSSEEnvelope == nil {
return "", false
}
// Parse the DSSE envelope to determine the SBOM format
var s attestation.Statement
if err := json.Unmarshal(bundle.DSSEEnvelope, &s); err != nil {
return "", false
}
if s.Predicate == nil {
return "", false
}
switch s.PredicateType {
case in_toto.PredicateCycloneDX, PredicateCycloneDXBeforeV05:
return FormatSigstoreBundleCycloneDXJSON, true
case in_toto.PredicateSPDX:
return FormatSigstoreBundleSPDXJSON, true
}
return "", false
}
func Decode(ctx context.Context, f io.Reader, format Format) (types.SBOM, error) { func Decode(ctx context.Context, f io.Reader, format Format) (types.SBOM, error) {
var ( var (
v any v any
@@ -231,6 +295,30 @@ func Decode(ctx context.Context, f io.Reader, format Format) (types.SBOM, error)
Predicate: &spdx.SPDX{BOM: bom}, Predicate: &spdx.SPDX{BOM: bom},
} }
decoder = json.NewDecoder(f) decoder = json.NewDecoder(f)
case FormatSigstoreBundleCycloneDXJSON:
// Sigstore bundle
// => dsse envelope
// => in-toto attestation
// => CycloneDX JSON
bom = core.NewBOM(core.Options{GenerateBOMRef: true})
v = &attestation.SigstoreBundle{
DSSEEnvelope: attestation.Statement{
Predicate: &cyclonedx.BOM{BOM: bom},
},
}
decoder = json.NewDecoder(f)
case FormatSigstoreBundleSPDXJSON:
// Sigstore bundle
// => dsse envelope
// => in-toto attestation
// => SPDX JSON
bom = core.NewBOM(core.Options{})
v = &attestation.SigstoreBundle{
DSSEEnvelope: attestation.Statement{
Predicate: &spdx.SPDX{BOM: bom},
},
}
decoder = json.NewDecoder(f)
case FormatSPDXJSON: case FormatSPDXJSON:
bom = core.NewBOM(core.Options{}) bom = core.NewBOM(core.Options{})
v = &spdx.SPDX{BOM: bom} v = &spdx.SPDX{BOM: bom}

View File

@@ -1,7 +1,6 @@
package sbom_test package sbom_test
import ( import (
"context"
"strings" "strings"
"testing" "testing"
@@ -11,6 +10,74 @@ import (
"github.com/aquasecurity/trivy/pkg/sbom" "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) { func TestDetectFormat(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -18,37 +85,22 @@ func TestDetectFormat(t *testing.T) {
want sbom.Format want sbom.Format
}{ }{
{ {
name: "SPDX attestation with valid predicate", name: "SPDX attestation",
// DSSE envelope with base64-encoded in-toto statement input: spdxAttestation,
input: `{ want: sbom.FormatAttestSPDXJSON,
"payloadType": "application/vnd.in-toto+json",
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3NwZHguZGV2L0RvY3VtZW50Iiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7IlNQRFhJRCI6IlNQRFhSZWYtRE9DVU1FTlQiLCJzcGR4VmVyc2lvbiI6IlNQRFgtMi4zIiwibmFtZSI6InRlc3QifX0=",
"signatures": []
}`,
want: sbom.FormatAttestSPDXJSON,
}, },
{ {
name: "SPDX attestation without SPDXID prefix", name: "SPDX attestation with invalid SPDXID",
// Base64-encoded: {"_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"}} input: spdxAttestationInvalidID,
input: `{ want: sbom.FormatUnknown,
"payloadType": "application/vnd.in-toto+json",
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3NwZHguZGV2L0RvY3VtZW50Iiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7IlNQRFhJRCI6IkludmFsaWRJRCIsInNwZHhWZXJzaW9uIjoiU1BEWC0yLjMiLCJuYW1lIjoidGVzdCJ9fQ==",
"signatures": []
}`,
want: sbom.FormatUnknown,
}, },
{ {
name: "CycloneDX attestation", name: "CycloneDX attestation",
// Base64-encoded: {"_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"}} input: cycloneDXAttestation,
input: `{ want: sbom.FormatAttestCycloneDXJSON,
"payloadType": "application/vnd.in-toto+json",
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvYm9tIiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7ImJvbUZvcm1hdCI6IkN5Y2xvbmVEWCIsInNwZWNWZXJzaW9uIjoiMS40In19",
"signatures": []
}`,
want: sbom.FormatAttestCycloneDXJSON,
}, },
{ {
name: "Regular SPDX JSON (not attestation)", name: "SPDX JSON",
input: `{ input: `{
"SPDXID": "SPDXRef-DOCUMENT", "SPDXID": "SPDXRef-DOCUMENT",
"spdxVersion": "SPDX-2.3", "spdxVersion": "SPDX-2.3",
@@ -57,7 +109,7 @@ func TestDetectFormat(t *testing.T) {
want: sbom.FormatSPDXJSON, want: sbom.FormatSPDXJSON,
}, },
{ {
name: "Regular CycloneDX JSON (not attestation)", name: "CycloneDX JSON",
input: `{ input: `{
"bomFormat": "CycloneDX", "bomFormat": "CycloneDX",
"specVersion": "1.4" "specVersion": "1.4"
@@ -71,6 +123,21 @@ func TestDetectFormat(t *testing.T) {
}`, }`,
want: sbom.FormatUnknown, 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 { for _, tt := range tests {
@@ -84,7 +151,7 @@ func TestDetectFormat(t *testing.T) {
} }
} }
func TestDecode_SPDXAttestation(t *testing.T) { func TestDecode(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input string input string
@@ -92,33 +159,41 @@ func TestDecode_SPDXAttestation(t *testing.T) {
wantErr bool wantErr bool
}{ }{
{ {
name: "SPDX attestation decode", name: "SPDX attestation",
// Base64-encoded: {"_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://trivy.dev/test","creationInfo":{"creators":["Tool: test"],"created":"2025-01-01T00:00:00Z"},"packages":[]}} input: spdxAttestation,
input: `{
"payloadType": "application/vnd.in-toto+json",
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3NwZHguZGV2L0RvY3VtZW50Iiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7IlNQRFhJRCI6IlNQRFhSZWYtRE9DVU1FTlQiLCJzcGR4VmVyc2lvbiI6IlNQRFgtMi4zIiwibmFtZSI6InRlc3QiLCJkYXRhTGljZW5zZSI6IkNDMC0xLjAiLCJkb2N1bWVudE5hbWVzcGFjZSI6Imh0dHA6Ly90cml2eS5kZXYvdGVzdCIsImNyZWF0aW9uSW5mbyI6eyJjcmVhdG9ycyI6WyJUb29sOiB0ZXN0Il0sImNyZWF0ZWQiOiIyMDI1LTAxLTAxVDAwOjAwOjAwWiJ9LCJwYWNrYWdlcyI6W119fQ==",
"signatures": []
}`,
format: sbom.FormatAttestSPDXJSON, format: sbom.FormatAttestSPDXJSON,
wantErr: false, wantErr: false,
}, },
{ {
name: "Invalid SPDX attestation", name: "SPDX attestation with invalid predicate",
// Base64-encoded: {"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://spdx.dev/Document","subject":[{"name":"test","digest":{"sha256":"abc123"}}],"predicate":"invalid"} input: spdxAttestationInvalidPredicate,
input: `{
"payloadType": "application/vnd.in-toto+json",
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3NwZHguZGV2L0RvY3VtZW50Iiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjoiaW52YWxpZCJ9",
"signatures": []
}`,
format: sbom.FormatAttestSPDXJSON, format: sbom.FormatAttestSPDXJSON,
wantErr: true, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := strings.NewReader(tt.input) r := strings.NewReader(tt.input)
_, err := sbom.Decode(context.Background(), r, tt.format) _, err := sbom.Decode(t.Context(), r, tt.format)
if tt.wantErr { if tt.wantErr {
require.Error(t, err) require.Error(t, err)