mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-05 20:40:16 -08:00
feat(image): add Sigstore bundle SBOM support (#9516)
Co-authored-by: knqyf263 <knqyf263@gmail.com>
This commit is contained in:
81
internal/registrytest/registrytest.go
Normal file
81
internal/registrytest/registrytest.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
pkg/fanal/artifact/image/testdata/index.json
vendored
15
pkg/fanal/artifact/image/testdata/index.json
vendored
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
24
pkg/fanal/artifact/image/testdata/manifest.json
vendored
24
pkg/fanal/artifact/image/testdata/manifest.json
vendored
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
59
pkg/fanal/artifact/image/testdata/sigstore-bundle.json
vendored
Normal file
59
pkg/fanal/artifact/image/testdata/sigstore-bundle.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
14
pkg/fanal/artifact/image/testdata/valid.rego
vendored
14
pkg/fanal/artifact/image/testdata/valid.rego
vendored
@@ -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"}
|
|
||||||
}
|
|
||||||
@@ -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{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user