From 6c485a98be63a3883c935f7728be92916333f3f1 Mon Sep 17 00:00:00 2001 From: Laurent Goderre Date: Thu, 29 Feb 2024 14:38:24 -0500 Subject: [PATCH 1/3] Add tests for imagetools inspect Signed-off-by: Laurent Goderre --- util/imagetools/loader_test.go | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 util/imagetools/loader_test.go diff --git a/util/imagetools/loader_test.go b/util/imagetools/loader_test.go new file mode 100644 index 00000000000..5adbb287b98 --- /dev/null +++ b/util/imagetools/loader_test.go @@ -0,0 +1,63 @@ +package imagetools + +import ( + "encoding/base64" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_scanSBOM(t *testing.T) { + +} + +func Test_scanProvenance(t *testing.T) { + +} + +func Test_isInTotoDSSE(t *testing.T) { + tests := []struct { + mime string + expected bool + }{ + { + mime: "application/vnd.in-toto.spdx+dsse", + expected: true, + }, + { + mime: "application/vnd.in-toto.provenance+dsse", + expected: true, + }, + { + mime: "application/vnd.in-toto+json", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.mime, func(t *testing.T) { + assert.Equal(t, isInTotoDSSE(test.mime), test.expected) + }) + } +} + +func Test_decodeDSSE(t *testing.T) { + // Returns input when mime isn't a DSSE type + actual, err := decodeDSSE([]byte("foobar"), "application/vnd.in-toto+json") + assert.NoError(t, err) + assert.Equal(t, []byte("foobar"), actual) + + // Returns the base64 decoded payload if is a DSSE + payload := base64.StdEncoding.EncodeToString([]byte("hello world")) + envelope := fmt.Sprintf("{\"payload\":\"%s\"}", payload) + actual, err = decodeDSSE([]byte(envelope), "application/vnd.in-toto.spdx+dsse") + assert.NoError(t, err) + assert.Equal(t, "hello world", string(actual)) + + actual, err = decodeDSSE([]byte("not a json"), "application/vnd.in-toto.spdx+dsse") + assert.Error(t, err) + + actual, err = decodeDSSE([]byte("{\"payload\": \"not base64\"}"), "application/vnd.in-toto.spdx+dsse") + assert.Error(t, err) +} From 1d0b542b1b6b57618cce72bda3639115743b80d6 Mon Sep 17 00:00:00 2001 From: Laurent Goderre Date: Fri, 1 Mar 2024 15:13:08 -0500 Subject: [PATCH 2/3] Add unit test for SBOM and Provenance scanning Signed-off-by: Laurent Goderre --- go.mod | 2 +- util/imagetools/imagetools_helpers_test.go | 255 +++++++++++++++++++++ util/imagetools/loader.go | 3 +- util/imagetools/loader_test.go | 85 ++++++- 4 files changed, 339 insertions(+), 6 deletions(-) create mode 100644 util/imagetools/imagetools_helpers_test.go diff --git a/go.mod b/go.mod index 7cfc48bb51a..db04613f119 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/google/uuid v1.6.0 github.com/hashicorp/go-cty-funcs v0.0.0-20230405223818-a090f58aa992 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/sys/mountinfo v0.7.1 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/hashicorp/go-cleanhttp v0.5.2 // 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/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/util/imagetools/imagetools_helpers_test.go b/util/imagetools/imagetools_helpers_test.go new file mode 100644 index 00000000000..52a3a05dc2c --- /dev/null +++ b/util/imagetools/imagetools_helpers_test.go @@ -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": ""} + } + ] + } + } +} +` diff --git a/util/imagetools/loader.go b/util/imagetools/loader.go index 06df9c01ac7..b09f05d401a 100644 --- a/util/imagetools/loader.go +++ b/util/imagetools/loader.go @@ -16,6 +16,7 @@ import ( "github.com/containerd/containerd/platforms" "github.com/containerd/containerd/remotes" "github.com/distribution/reference" + intoto "github.com/in-toto/in-toto-golang/in_toto" "github.com/moby/buildkit/util/contentutil" "github.com/opencontainers/go-digest" 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 { 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) if err != nil { return nil, err diff --git a/util/imagetools/loader_test.go b/util/imagetools/loader_test.go index 5adbb287b98..acea6fd547b 100644 --- a/util/imagetools/loader_test.go +++ b/util/imagetools/loader_test.go @@ -1,19 +1,96 @@ package imagetools import ( + "context" "encoding/base64" "fmt" "testing" + "github.com/opencontainers/go-digest" + "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) { @@ -55,9 +132,9 @@ func Test_decodeDSSE(t *testing.T) { assert.NoError(t, err) 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) - 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) } From abff44456271a4b626c35fd15c690b37a8d96ed4 Mon Sep 17 00:00:00 2001 From: Laurent Goderre Date: Tue, 5 Mar 2024 13:10:13 -0500 Subject: [PATCH 3/3] Added test for imagetool inspect load Signed-off-by: Laurent Goderre --- util/imagetools/imagetools_helpers_test.go | 248 ++++++++++++++++----- util/imagetools/loader_test.go | 67 +++++- 2 files changed, 256 insertions(+), 59 deletions(-) diff --git a/util/imagetools/imagetools_helpers_test.go b/util/imagetools/imagetools_helpers_test.go index 52a3a05dc2c..77a3d74bd0e 100644 --- a/util/imagetools/imagetools_helpers_test.go +++ b/util/imagetools/imagetools_helpers_test.go @@ -3,6 +3,7 @@ package imagetools import ( "context" "encoding/base64" + "encoding/json" "fmt" "io" "strings" @@ -31,13 +32,27 @@ type mockResolver struct { pusher remotes.Pusher } +var manifests = make(map[digest.Digest]manifest) +var indexes = make(map[digest.Digest]index) + 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 + switch desc.MediaType { + case ocispec.MediaTypeImageIndex: + reader := io.NopCloser(strings.NewReader(indexes[desc.Digest].desc.Annotations["test_content"])) + return reader, nil + case ocispec.MediaTypeImageManifest: + reader := io.NopCloser(strings.NewReader(manifests[desc.Digest].desc.Annotations["test_content"])) + return reader, nil + default: + 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 + d := digest.Digest(strings.ReplaceAll(ref, "docker.io/library/test@", "")) + return string(d), indexes[d].desc, nil } func (r mockResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) { @@ -57,84 +72,213 @@ func getMockResolver() remotes.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), - } + return getImageFromManifests(getBaseManifests()) +} + +func getImageWithAttestation(t attestationType) *result { + manifestList := getBaseManifests() - r.images["linux/amd64"] = "sha256:linux/amd64" - r.images["linux/arm64"] = "sha256:linux/arm64" + objManifest := ocispec.Manifest{ + MediaType: v1.MediaTypeImageManifest, + Layers: getAttestationLayers(t), + Annotations: map[string]string{ + "platform": "linux/amd64", + }, + } + jsonContent, _ := json.Marshal(objManifest) + jsonString := string(jsonContent) + d := digest.FromString(jsonString) - r.manifests["sha256:linux/amd64-manifest"] = manifest{ + manifestList[d] = manifest{ desc: ocispec.Descriptor{ MediaType: v1.MediaTypeImageManifest, - Digest: "sha256:linux/amd64-manifest", + Digest: d, + Size: int64(len(jsonString)), + Annotations: map[string]string{ + "vnd.docker.reference.digest": string(getManifestDigestForArch(manifestList, "linux", "amd64")), + "vnd.docker.reference.type": "attestation-manifest", + "test_content": jsonString, + }, Platform: &v1.Platform{ - Architecture: "amd64", - OS: "linux", + Architecture: "unknown", + OS: "unknown", }, }, - manifest: ocispec.Manifest{ - MediaType: v1.MediaTypeImageManifest, - Layers: []v1.Descriptor{ - { - MediaType: v1.MediaTypeImageLayerGzip, - Digest: "sha256:linux/amd64-content", - Size: 1234, - }, - }, + manifest: objManifest, + } + + objManifest = ocispec.Manifest{ + MediaType: v1.MediaTypeImageManifest, + Layers: getAttestationLayers(t), + Annotations: map[string]string{ + "platform": "linux/arm64", }, } - r.manifests["sha256:linux/arm64-manifest"] = manifest{ + jsonContent, _ = json.Marshal(objManifest) + jsonString = string(jsonContent) + d = digest.FromString(jsonString) + manifestList[d] = manifest{ desc: ocispec.Descriptor{ MediaType: v1.MediaTypeImageManifest, - Digest: "sha256:linux/arm64-manifest", + Digest: d, + Size: int64(len(jsonString)), + Annotations: map[string]string{ + "vnd.docker.reference.digest": string(getManifestDigestForArch(manifestList, "linux", "arm64")), + "vnd.docker.reference.type": "attestation-manifest", + "test_content": jsonString, + }, Platform: &v1.Platform{ - Architecture: "arm64", - OS: "linux", + Architecture: "unknown", + OS: "unknown", }, }, - manifest: ocispec.Manifest{ - MediaType: v1.MediaTypeImageManifest, - Layers: []v1.Descriptor{ - { - MediaType: v1.MediaTypeImageLayerGzip, - Digest: "sha256:linux/arm64-content", - Size: 1234, + } + + return getImageFromManifests(manifestList) +} + +func getImageFromManifests(manifests map[digest.Digest]manifest) *result { + r := &result{ + indexes: make(map[digest.Digest]index), + manifests: manifests, + images: make(map[string]digest.Digest), + refs: make(map[digest.Digest][]digest.Digest), + assets: make(map[string]asset), + } + + r.images["linux/amd64"] = getManifestDigestForArch(manifests, "linux", "amd64") + r.images["linux/arm64"] = getManifestDigestForArch(manifests, "linux", "arm64") + + manifestsDesc := []v1.Descriptor{} + for _, val := range manifests { + manifestsDesc = append(manifestsDesc, val.desc) + } + + objIndex := v1.Index{ + MediaType: v1.MediaTypeImageIndex, + Manifests: manifestsDesc, + } + jsonContent, _ := json.Marshal(objIndex) + jsonString := string(jsonContent) + d := digest.FromString(jsonString) + + if _, ok := indexes[d]; !ok { + indexes[d] = index{ + desc: ocispec.Descriptor{ + MediaType: v1.MediaTypeImageIndex, + Digest: d, + Size: int64(len(jsonString)), + Annotations: map[string]string{ + "test_content": jsonString, }, }, - }, + index: objIndex, + } } + r.indexes[d] = indexes[d] return r } -func getImageWithAttestation(t attestationType) *result { - r := getImageNoAttestation() +func getManifestDigestForArch(manifests map[digest.Digest]manifest, os string, arch string) digest.Digest { + for d, m := range manifests { + if m.desc.Platform.OS == os && m.desc.Platform.Architecture == arch { + return d + } + } - r.manifests["sha256:linux/amd64-attestation"] = manifest{ - desc: ocispec.Descriptor{ + return digest.Digest("") +} + +func getBaseManifests() map[digest.Digest]manifest { + if len(manifests) == 0 { + config := getConfig() + content := "amd64-content" + objManifest := ocispec.Manifest{ 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", + Config: config, + Layers: []v1.Descriptor{ + { + MediaType: v1.MediaTypeImageLayerGzip, + Digest: digest.FromString(content), + Size: int64(len(content)), + }, }, - Platform: &v1.Platform{ - Architecture: "unknown", - OS: "unknown", + } + jsonContent, _ := json.Marshal(objManifest) + jsonString := string(jsonContent) + d := digest.FromString(jsonString) + + manifests[d] = manifest{ + desc: ocispec.Descriptor{ + MediaType: v1.MediaTypeImageManifest, + Digest: d, + Size: int64(len(jsonString)), + Platform: &v1.Platform{ + Architecture: "amd64", + OS: "linux", + }, + Annotations: map[string]string{ + "test_content": jsonString, + }, }, - }, - manifest: ocispec.Manifest{ + manifest: objManifest, + } + + content = "arm64-content" + objManifest = ocispec.Manifest{ MediaType: v1.MediaTypeImageManifest, - Layers: getAttestationLayers(t), + Config: config, + Layers: []v1.Descriptor{ + { + MediaType: v1.MediaTypeImageLayerGzip, + Digest: digest.FromString(content), + Size: int64(len(content)), + }, + }, + } + jsonContent, _ = json.Marshal(objManifest) + jsonString = string(jsonContent) + d = digest.FromString(jsonString) + + manifests[d] = manifest{ + desc: ocispec.Descriptor{ + MediaType: v1.MediaTypeImageManifest, + Digest: d, + Size: int64(len(jsonString)), + Platform: &v1.Platform{ + Architecture: "arm64", + OS: "linux", + }, + Annotations: map[string]string{ + "test_content": jsonString, + }, + }, + manifest: objManifest, + } + } + + return manifests +} + +func getConfig() v1.Descriptor { + config := v1.ImageConfig{ + Env: []string{ + "config", }, } + jsonContent, _ := json.Marshal(config) + jsonString := string(jsonContent) + d := digest.FromString(jsonString) - return r + return v1.Descriptor{ + MediaType: ocispec.MediaTypeImageConfig, + Digest: d, + Size: int64(len(jsonString)), + Annotations: map[string]string{ + "test_content": jsonString, + }, + } } func getAttestationLayers(t attestationType) []v1.Descriptor { diff --git a/util/imagetools/loader_test.go b/util/imagetools/loader_test.go index acea6fd547b..e4713742de0 100644 --- a/util/imagetools/loader_test.go +++ b/util/imagetools/loader_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "fmt" + "reflect" "testing" "github.com/opencontainers/go-digest" @@ -11,6 +12,44 @@ import ( "github.com/stretchr/testify/assert" ) +func TestLoad(t *testing.T) { + loader := newLoader(getMockResolver()) + ctx := context.Background() + + r := getImageNoAttestation() + indexDigest := reflect.ValueOf(r.indexes).MapKeys()[0].String() + result, err := loader.Load(ctx, fmt.Sprintf("test@%s", indexDigest)) + assert.NoError(t, err) + if err == nil { + assert.Equal(t, 1, len(result.indexes)) + assert.Equal(t, 2, len(result.images)) + assert.Equal(t, 2, len(result.platforms)) + assert.Equal(t, 2, len(result.manifests)) + assert.Equal(t, 2, len(result.assets)) + assert.Equal(t, 0, len(result.refs)) + } + + r = getImageWithAttestation(plainSpdx) + indexDigest = reflect.ValueOf(r.indexes).MapKeys()[0].String() + result, err = loader.Load(ctx, fmt.Sprintf("test@%s", indexDigest)) + assert.NoError(t, err) + if err == nil { + assert.Equal(t, 1, len(result.indexes)) + assert.Equal(t, 2, len(result.images)) + assert.Equal(t, 2, len(result.platforms)) + assert.Equal(t, 4, len(result.manifests)) + assert.Equal(t, 2, len(result.assets)) + assert.Equal(t, 2, len(result.refs)) + + for d1, m := range r.manifests { + if _, ok := m.desc.Annotations["vnd.docker.reference.digest"]; ok { + d2 := digest.Digest(m.desc.Annotations["vnd.docker.reference.digest"]) + assert.Equal(t, d1, result.refs[d2][0]) + } + } + } +} + func TestSBOM(t *testing.T) { tests := []struct { name string @@ -37,11 +76,19 @@ func TestSBOM(t *testing.T) { fetcher, _ := loader.resolver.Fetcher(ctx, "") r := getImageWithAttestation(test.contentType) - r.refs["sha256:linux/amd64"] = []digest.Digest{ - "sha256:linux/amd64-attestation", + imageDigest := r.images["linux/amd64"] + + // Manual mapping + for d, m := range r.manifests { + if m.desc.Annotations["vnd.docker.reference.digest"] == string(imageDigest) { + r.refs[imageDigest] = []digest.Digest{ + d, + } + } } + a := asset{} - loader.scanSBOM(ctx, fetcher, r, r.refs["sha256:linux/amd64"], &a) + loader.scanSBOM(ctx, fetcher, r, r.refs[imageDigest], &a) r.assets["linux/amd64"] = a actual, err := r.SBOM() @@ -77,13 +124,19 @@ func TestProvenance(t *testing.T) { fetcher, _ := loader.resolver.Fetcher(ctx, "") r := getImageWithAttestation(test.contentType) - - r.refs["sha256:linux/amd64"] = []digest.Digest{ - "sha256:linux/amd64-attestation", + imageDigest := r.images["linux/amd64"] + + // Manual mapping + for d, m := range r.manifests { + if m.desc.Annotations["vnd.docker.reference.digest"] == string(imageDigest) { + r.refs[imageDigest] = []digest.Digest{ + d, + } + } } a := asset{} - loader.scanProvenance(ctx, fetcher, r, r.refs["sha256:linux/amd64"], &a) + loader.scanProvenance(ctx, fetcher, r, r.refs[imageDigest], &a) r.assets["linux/amd64"] = a actual, err := r.Provenance()