mirror of https://github.com/docker/buildx.git
Add unit test for SBOM and Provenance scanning
Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>
This commit is contained in:
parent
6c485a98be
commit
1d0b542b1b
2
go.mod
2
go.mod
|
@ -25,6 +25,7 @@ require (
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/hashicorp/go-cty-funcs v0.0.0-20230405223818-a090f58aa992
|
github.com/hashicorp/go-cty-funcs v0.0.0-20230405223818-a090f58aa992
|
||||||
github.com/hashicorp/hcl/v2 v2.19.1
|
github.com/hashicorp/hcl/v2 v2.19.1
|
||||||
|
github.com/in-toto/in-toto-golang v0.5.0
|
||||||
github.com/moby/buildkit v0.13.0
|
github.com/moby/buildkit v0.13.0
|
||||||
github.com/moby/sys/mountinfo v0.7.1
|
github.com/moby/sys/mountinfo v0.7.1
|
||||||
github.com/moby/sys/signal v0.7.0
|
github.com/moby/sys/signal v0.7.0
|
||||||
|
@ -106,7 +107,6 @@ require (
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/imdario/mergo v0.3.16 // indirect
|
github.com/imdario/mergo v0.3.16 // indirect
|
||||||
github.com/in-toto/in-toto-golang v0.5.0 // indirect
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
|
|
@ -0,0 +1,255 @@
|
||||||
|
package imagetools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/remotes"
|
||||||
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
|
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type attestationType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
plainSpdx attestationType = 0
|
||||||
|
dsseEmbeded attestationType = 1
|
||||||
|
plainSpdxAndDSSEEmbed attestationType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockFetcher struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockResolver struct {
|
||||||
|
fetcher remotes.Fetcher
|
||||||
|
pusher remotes.Pusher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f mockFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
|
||||||
|
reader := io.NopCloser(strings.NewReader(desc.Annotations["test_content"]))
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r mockResolver) Resolve(ctx context.Context, ref string) (name string, desc ocispec.Descriptor, err error) {
|
||||||
|
return "", ocispec.Descriptor{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r mockResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) {
|
||||||
|
return r.fetcher, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r mockResolver) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) {
|
||||||
|
return r.pusher, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMockResolver() remotes.Resolver {
|
||||||
|
resolver := mockResolver{
|
||||||
|
fetcher: mockFetcher{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImageNoAttestation() *result {
|
||||||
|
r := &result{
|
||||||
|
indexes: make(map[digest.Digest]index),
|
||||||
|
manifests: make(map[digest.Digest]manifest),
|
||||||
|
images: make(map[string]digest.Digest),
|
||||||
|
refs: make(map[digest.Digest][]digest.Digest),
|
||||||
|
assets: make(map[string]asset),
|
||||||
|
}
|
||||||
|
|
||||||
|
r.images["linux/amd64"] = "sha256:linux/amd64"
|
||||||
|
r.images["linux/arm64"] = "sha256:linux/arm64"
|
||||||
|
|
||||||
|
r.manifests["sha256:linux/amd64-manifest"] = manifest{
|
||||||
|
desc: ocispec.Descriptor{
|
||||||
|
MediaType: v1.MediaTypeImageManifest,
|
||||||
|
Digest: "sha256:linux/amd64-manifest",
|
||||||
|
Platform: &v1.Platform{
|
||||||
|
Architecture: "amd64",
|
||||||
|
OS: "linux",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
manifest: ocispec.Manifest{
|
||||||
|
MediaType: v1.MediaTypeImageManifest,
|
||||||
|
Layers: []v1.Descriptor{
|
||||||
|
{
|
||||||
|
MediaType: v1.MediaTypeImageLayerGzip,
|
||||||
|
Digest: "sha256:linux/amd64-content",
|
||||||
|
Size: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r.manifests["sha256:linux/arm64-manifest"] = manifest{
|
||||||
|
desc: ocispec.Descriptor{
|
||||||
|
MediaType: v1.MediaTypeImageManifest,
|
||||||
|
Digest: "sha256:linux/arm64-manifest",
|
||||||
|
Platform: &v1.Platform{
|
||||||
|
Architecture: "arm64",
|
||||||
|
OS: "linux",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
manifest: ocispec.Manifest{
|
||||||
|
MediaType: v1.MediaTypeImageManifest,
|
||||||
|
Layers: []v1.Descriptor{
|
||||||
|
{
|
||||||
|
MediaType: v1.MediaTypeImageLayerGzip,
|
||||||
|
Digest: "sha256:linux/arm64-content",
|
||||||
|
Size: 1234,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImageWithAttestation(t attestationType) *result {
|
||||||
|
r := getImageNoAttestation()
|
||||||
|
|
||||||
|
r.manifests["sha256:linux/amd64-attestation"] = manifest{
|
||||||
|
desc: ocispec.Descriptor{
|
||||||
|
MediaType: v1.MediaTypeImageManifest,
|
||||||
|
Digest: "sha256:linux/amd64-attestation",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"vnd.docker.reference.digest": "sha256:linux/amd64",
|
||||||
|
"vnd.docker.reference.type": "attestation-manifest",
|
||||||
|
},
|
||||||
|
Platform: &v1.Platform{
|
||||||
|
Architecture: "unknown",
|
||||||
|
OS: "unknown",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
manifest: ocispec.Manifest{
|
||||||
|
MediaType: v1.MediaTypeImageManifest,
|
||||||
|
Layers: getAttestationLayers(t),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAttestationLayers(t attestationType) []v1.Descriptor {
|
||||||
|
layers := []v1.Descriptor{}
|
||||||
|
|
||||||
|
if t == plainSpdx || t == plainSpdxAndDSSEEmbed {
|
||||||
|
layers = append(layers, v1.Descriptor{
|
||||||
|
MediaType: inTotoGenericMime,
|
||||||
|
Digest: digest.FromString(attestationContent),
|
||||||
|
Size: int64(len(attestationContent)),
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"in-toto.io/predicate-type": intoto.PredicateSPDX,
|
||||||
|
"test_content": attestationContent,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
layers = append(layers, v1.Descriptor{
|
||||||
|
MediaType: inTotoGenericMime,
|
||||||
|
Digest: digest.FromString(provenanceContent),
|
||||||
|
Size: int64(len(provenanceContent)),
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"in-toto.io/predicate-type": slsa02.PredicateSLSAProvenance,
|
||||||
|
"test_content": provenanceContent,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if t == dsseEmbeded || t == plainSpdxAndDSSEEmbed {
|
||||||
|
dsseAttestation := fmt.Sprintf("{\"payload\":\"%s\"}", base64.StdEncoding.EncodeToString([]byte(attestationContent)))
|
||||||
|
dsseProvenance := fmt.Sprintf("{\"payload\":\"%s\"}", base64.StdEncoding.EncodeToString([]byte(provenanceContent)))
|
||||||
|
layers = append(layers, v1.Descriptor{
|
||||||
|
MediaType: inTotoSPDXDSSEMime,
|
||||||
|
Digest: digest.FromString(dsseAttestation),
|
||||||
|
Size: int64(len(dsseAttestation)),
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"in-toto.io/predicate-type": intoto.PredicateSPDX,
|
||||||
|
"test_content": dsseAttestation,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
layers = append(layers, v1.Descriptor{
|
||||||
|
MediaType: inTotoProvenanceDSSEMime,
|
||||||
|
Digest: digest.FromString(dsseProvenance),
|
||||||
|
Size: int64(len(dsseProvenance)),
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"in-toto.io/predicate-type": slsa02.PredicateSLSAProvenance,
|
||||||
|
"test_content": dsseProvenance,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return layers
|
||||||
|
}
|
||||||
|
|
||||||
|
const attestationContent = `
|
||||||
|
{
|
||||||
|
"_type": "https://in-toto.io/Statement/v0.1",
|
||||||
|
"predicateType": "https://spdx.dev/Document",
|
||||||
|
"predicate": {
|
||||||
|
"name": "sbom",
|
||||||
|
"spdxVersion": "SPDX-2.3",
|
||||||
|
"SPDXID": "SPDXRef-DOCUMENT",
|
||||||
|
"creationInfo": {
|
||||||
|
"created": "2024-01-31T16:09:05Z",
|
||||||
|
"creators": [
|
||||||
|
"Tool: buildkit-v0.11.0"
|
||||||
|
],
|
||||||
|
"licenseListVersion": "3.22"
|
||||||
|
},
|
||||||
|
"dataLicense": "CC0-1.0",
|
||||||
|
"documentNamespace": "https://example.com",
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "sbom",
|
||||||
|
"SPDXID": "SPDXRef-DocumentRoot-Directory-sbom",
|
||||||
|
"copyrightText": "",
|
||||||
|
"downloadLocation": "NOASSERTION",
|
||||||
|
"primaryPackagePurpose": "FILE",
|
||||||
|
"supplier": "NOASSERTION"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relationships": [
|
||||||
|
{
|
||||||
|
"relatedSpdxElement": "SPDXRef-DocumentRoot-Directory-sbom",
|
||||||
|
"relationshipType": "DESCRIBES",
|
||||||
|
"spdxElementId": "SPDXRef-DOCUMENT"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const provenanceContent = `
|
||||||
|
{
|
||||||
|
"_type": "https://in-toto.io/Statement/v0.1",
|
||||||
|
"predicateType": "https://slsa.dev/provenance/v0.2",
|
||||||
|
"predicate": {
|
||||||
|
"buildType": "https://example.com/Makefile",
|
||||||
|
"builder": {
|
||||||
|
"id": "mailto:person@example.com"
|
||||||
|
},
|
||||||
|
"invocation": {
|
||||||
|
"configSource": {
|
||||||
|
"uri": "https://example.com/example-1.2.3.tar.gz",
|
||||||
|
"digest": {"sha256": ""},
|
||||||
|
"entryPoint": "src:foo"
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"CFLAGS": "-O3"
|
||||||
|
},
|
||||||
|
"materials": [
|
||||||
|
{
|
||||||
|
"uri": "https://example.com/example-1.2.3.tar.gz",
|
||||||
|
"digest": {"sha256": ""}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/containerd/containerd/platforms"
|
"github.com/containerd/containerd/platforms"
|
||||||
"github.com/containerd/containerd/remotes"
|
"github.com/containerd/containerd/remotes"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
"github.com/moby/buildkit/util/contentutil"
|
"github.com/moby/buildkit/util/contentutil"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
@ -292,7 +293,7 @@ func (l *loader) scanSBOM(ctx context.Context, fetcher remotes.Fetcher, r *resul
|
||||||
}
|
}
|
||||||
for _, layer := range mfst.manifest.Layers {
|
for _, layer := range mfst.manifest.Layers {
|
||||||
if (layer.MediaType == inTotoGenericMime || isInTotoDSSE(layer.MediaType)) &&
|
if (layer.MediaType == inTotoGenericMime || isInTotoDSSE(layer.MediaType)) &&
|
||||||
layer.Annotations["in-toto.io/predicate-type"] == "https://spdx.dev/Document" {
|
layer.Annotations["in-toto.io/predicate-type"] == intoto.PredicateSPDX {
|
||||||
_, err := remotes.FetchHandler(l.cache, fetcher)(ctx, layer)
|
_, err := remotes.FetchHandler(l.cache, fetcher)(ctx, layer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -1,19 +1,96 @@
|
||||||
package imagetools
|
package imagetools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_scanSBOM(t *testing.T) {
|
func TestSBOM(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
contentType attestationType
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Plain SPDX",
|
||||||
|
contentType: plainSpdx,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SPDX in DSSE envelope",
|
||||||
|
contentType: dsseEmbeded,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Plain SPDX and SPDX in DSSE envelope",
|
||||||
|
contentType: plainSpdxAndDSSEEmbed,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
loader := newLoader(getMockResolver())
|
||||||
|
ctx := context.Background()
|
||||||
|
fetcher, _ := loader.resolver.Fetcher(ctx, "")
|
||||||
|
|
||||||
|
r := getImageWithAttestation(test.contentType)
|
||||||
|
r.refs["sha256:linux/amd64"] = []digest.Digest{
|
||||||
|
"sha256:linux/amd64-attestation",
|
||||||
|
}
|
||||||
|
a := asset{}
|
||||||
|
loader.scanSBOM(ctx, fetcher, r, r.refs["sha256:linux/amd64"], &a)
|
||||||
|
r.assets["linux/amd64"] = a
|
||||||
|
actual, err := r.SBOM()
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(actual))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_scanProvenance(t *testing.T) {
|
func TestProvenance(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
contentType attestationType
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Plain SPDX",
|
||||||
|
contentType: plainSpdx,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SPDX in DSSE envelope",
|
||||||
|
contentType: dsseEmbeded,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Plain SPDX and SPDX in DSSE envelope",
|
||||||
|
contentType: plainSpdxAndDSSEEmbed,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
loader := newLoader(getMockResolver())
|
||||||
|
ctx := context.Background()
|
||||||
|
fetcher, _ := loader.resolver.Fetcher(ctx, "")
|
||||||
|
|
||||||
|
r := getImageWithAttestation(test.contentType)
|
||||||
|
|
||||||
|
r.refs["sha256:linux/amd64"] = []digest.Digest{
|
||||||
|
"sha256:linux/amd64-attestation",
|
||||||
|
}
|
||||||
|
|
||||||
|
a := asset{}
|
||||||
|
loader.scanProvenance(ctx, fetcher, r, r.refs["sha256:linux/amd64"], &a)
|
||||||
|
r.assets["linux/amd64"] = a
|
||||||
|
actual, err := r.Provenance()
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(actual))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_isInTotoDSSE(t *testing.T) {
|
func Test_isInTotoDSSE(t *testing.T) {
|
||||||
|
@ -55,9 +132,9 @@ func Test_decodeDSSE(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "hello world", string(actual))
|
assert.Equal(t, "hello world", string(actual))
|
||||||
|
|
||||||
actual, err = decodeDSSE([]byte("not a json"), "application/vnd.in-toto.spdx+dsse")
|
_, err = decodeDSSE([]byte("not a json"), "application/vnd.in-toto.spdx+dsse")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
actual, err = decodeDSSE([]byte("{\"payload\": \"not base64\"}"), "application/vnd.in-toto.spdx+dsse")
|
_, err = decodeDSSE([]byte("{\"payload\": \"not base64\"}"), "application/vnd.in-toto.spdx+dsse")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue