From 2fd95f1dba948e7c534bbfcc70d5647722a4f3f3 Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Tue, 6 Dec 2022 02:01:22 +0100 Subject: [PATCH] imagetools inspect: handle provenance and sbom Signed-off-by: CrazyMax --- docs/reference/buildx_imagetools_inspect.md | 164 ++++---- util/imagetools/loader.go | 415 ++++++++++++++++++++ util/imagetools/printers.go | 238 +++++------ 3 files changed, 584 insertions(+), 233 deletions(-) create mode 100644 util/imagetools/loader.go diff --git a/docs/reference/buildx_imagetools_inspect.md b/docs/reference/buildx_imagetools_inspect.md index 82555b4933db..cd51a5fbec91 100644 --- a/docs/reference/buildx_imagetools_inspect.md +++ b/docs/reference/buildx_imagetools_inspect.md @@ -72,7 +72,8 @@ unset. Following fields are available: * `.Name`: provides the reference of the image * `.Manifest`: provides the manifest or manifest list * `.Image`: provides the image config -* `.BuildInfo`: provides [build info from image config](https://github.com/moby/buildkit/blob/master/docs/build-repro.md#image-config) +* `.Provenance`: provides provenance or [build info from image config](https://github.com/moby/buildkit/blob/master/docs/build-repro.md#image-config) +* `.SBOM`: provides SBOM #### `.Name` @@ -122,18 +123,17 @@ Manifests: Platform: linux/riscv64 ``` -#### `.BuildInfo` +#### `.Provenance` ```console -$ docker buildx imagetools inspect crazymax/buildx:buildinfo --format "{{.BuildInfo}}" +$ docker buildx imagetools inspect crazymax/buildx:buildinfo --format "{{.Provenance}}" Name: docker.io/crazymax/buildx:buildinfo -Frontend: dockerfile.v0 -Attrs: - filename: Dockerfile - source: docker/dockerfile-upstream:master-labs - build-arg:bar: foo - build-arg:foo: bar -Sources: +BuildSource: +BuildDefinition: Dockerfile +BuildParameters: + bar: foo + foo: bar +Materials: Type: docker-image Ref: docker.io/docker/buildx-bin:0.6.1@sha256:a652ced4a4141977c7daaed0a074dcd9844a78d7d2615465b12f433ae6dd29f0 Pin: sha256:a652ced4a4141977c7daaed0a074dcd9844a78d7d2615465b12f433ae6dd29f0 @@ -178,12 +178,12 @@ $ docker buildx imagetools inspect moby/buildkit:master --format "{{json .Manife { "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", - "digest": "sha256:79d97f205e2799d99a3a8ae2a1ef17acb331e11784262c3faada847dc6972c52", + "digest": "sha256:eef5f92f1e942856995ae4714b85a58277b2a7fcc3bcb62ea2f0d38e0f5e88de", "size": 2010, "manifests": [ { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "digest": "sha256:bd1e78f06de26610fadf4eb9d04b1a45a545799d6342701726e952cc0c11c912", + "digest": "sha256:f9f41c85124686c2afe330a985066748a91d7a5d505777fe274df804ab5e077e", "size": 1158, "platform": { "architecture": "amd64", @@ -192,7 +192,7 @@ $ docker buildx imagetools inspect moby/buildkit:master --format "{{json .Manife }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "digest": "sha256:d37dcced63ec0965824fca644f0ac9efad8569434ec15b4c83adfcb3dcfc743b", + "digest": "sha256:82097c2be19c617aafb3c3e43c88548738d4b2bf3db5c36666283a918b390266", "size": 1158, "platform": { "architecture": "arm", @@ -202,7 +202,7 @@ $ docker buildx imagetools inspect moby/buildkit:master --format "{{json .Manife }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "digest": "sha256:ce142eb2255e6af46f2809e159fd03081697c7605a3de03b9cbe9a52ddb244bf", + "digest": "sha256:b6b91e6c823d7220ded7d3b688e571ba800b13d91bbc904c1d8053593e3ee42c", "size": 1158, "platform": { "architecture": "arm64", @@ -211,7 +211,7 @@ $ docker buildx imagetools inspect moby/buildkit:master --format "{{json .Manife }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "digest": "sha256:f59bfb5062fff76ce464bfa4e25ebaaaac887d6818238e119d68613c456d360c", + "digest": "sha256:797061bcc16778de048b96f769c018ec24da221088050bbe926ea3b8d51d77e8", "size": 1158, "platform": { "architecture": "s390x", @@ -220,7 +220,7 @@ $ docker buildx imagetools inspect moby/buildkit:master --format "{{json .Manife }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "digest": "sha256:cc96426e0c50a78105d5637d31356db5dd6ec594f21b24276e534a32da09645c", + "digest": "sha256:b93d3a84d18c4d0b8c279e77343d854d9b5177df7ea55cf468d461aa2523364e", "size": 1159, "platform": { "architecture": "ppc64le", @@ -229,7 +229,7 @@ $ docker buildx imagetools inspect moby/buildkit:master --format "{{json .Manife }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "digest": "sha256:39f9c1e2878e6c333acb23187d6b205ce82ed934c60da326cb2c698192631478", + "digest": "sha256:d5c950dd1b270d437c838187112a0cb44c9258248d7a3a8bcb42fae8f717dc01", "size": 1158, "platform": { "architecture": "riscv64", @@ -241,42 +241,20 @@ $ docker buildx imagetools inspect moby/buildkit:master --format "{{json .Manife ``` ```console -$ docker buildx imagetools inspect crazymax/buildx:buildinfo --format "{{json .BuildInfo}}" +$ docker buildx imagetools inspect crazymax/buildkit:attest --format "{{json .Provenance}}" ``` ```json { - "frontend": "dockerfile.v0", - "attrs": { - "build-arg:bar": "foo", - "build-arg:foo": "bar", - "filename": "Dockerfile", - "source": "crazymax/dockerfile:buildattrs" - }, - "sources": [ - { - "type": "docker-image", - "ref": "docker.io/docker/buildx-bin:0.6.1@sha256:a652ced4a4141977c7daaed0a074dcd9844a78d7d2615465b12f433ae6dd29f0", - "pin": "sha256:a652ced4a4141977c7daaed0a074dcd9844a78d7d2615465b12f433ae6dd29f0" - }, - { - "type": "docker-image", - "ref": "docker.io/library/alpine:3.13@sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c", - "pin": "sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c" - }, - { - "type": "docker-image", - "ref": "docker.io/moby/buildkit:v0.9.0@sha256:8dc668e7f66db1c044aadbed306020743516a94848793e0f81f94a087ee78cab", - "pin": "sha256:8dc668e7f66db1c044aadbed306020743516a94848793e0f81f94a087ee78cab" - }, + "Materials": [ { - "type": "docker-image", - "ref": "docker.io/tonistiigi/xx@sha256:21a61be4744f6531cb5f33b0e6f40ede41fa3a1b8c82d5946178f80cc84bfc04", - "pin": "sha256:21a61be4744f6531cb5f33b0e6f40ede41fa3a1b8c82d5946178f80cc84bfc04" + "Type": "docker-image", + "Ref": "docker.io/docker/buildkit-syft-scanner:stable-1", + "Pin": "sha256:b45f1d207e16c3a3a5a10b254ad8ad358d01f7ea090d382b95c6b2ee2b3ef765" }, { - "type": "http", - "ref": "https://raw.githubusercontent.com/moby/moby/master/README.md", - "pin": "sha256:419455202b0ef97e480d7f8199b26a721a417818bc0e2d106975f74323f25e6c" + "Type": "docker-image", + "Ref": "docker.io/library/alpine:latest", + "Pin": "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4" } ] } @@ -412,39 +390,37 @@ $ docker buildx imagetools inspect crazymax/buildx:buildinfo --format "{{json .} } ] }, - "buildinfo": { - "frontend": "dockerfile.v0", - "attrs": { - "build-arg:bar": "foo", - "build-arg:foo": "bar", - "filename": "Dockerfile", - "source": "docker/dockerfile-upstream:master-labs" + "provenance": { + "BuildDefinition": "Dockerfile", + "BuildParameters": { + "bar": "foo", + "foo": "bar" }, - "sources": [ + "Materials": [ { - "type": "docker-image", - "ref": "docker.io/docker/buildx-bin:0.6.1@sha256:a652ced4a4141977c7daaed0a074dcd9844a78d7d2615465b12f433ae6dd29f0", - "pin": "sha256:a652ced4a4141977c7daaed0a074dcd9844a78d7d2615465b12f433ae6dd29f0" + "Type": "docker-image", + "Ref": "docker.io/docker/buildx-bin:0.6.1@sha256:a652ced4a4141977c7daaed0a074dcd9844a78d7d2615465b12f433ae6dd29f0", + "Pin": "sha256:a652ced4a4141977c7daaed0a074dcd9844a78d7d2615465b12f433ae6dd29f0" }, { - "type": "docker-image", - "ref": "docker.io/library/alpine:3.13", - "pin": "sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c" + "Type": "docker-image", + "Ref": "docker.io/library/alpine:3.13", + "Pin": "sha256:026f721af4cf2843e07bba648e158fb35ecc876d822130633cc49f707f0fc88c" }, { - "type": "docker-image", - "ref": "docker.io/moby/buildkit:v0.9.0", - "pin": "sha256:8dc668e7f66db1c044aadbed306020743516a94848793e0f81f94a087ee78cab" + "Type": "docker-image", + "Ref": "docker.io/moby/buildkit:v0.9.0", + "Pin": "sha256:8dc668e7f66db1c044aadbed306020743516a94848793e0f81f94a087ee78cab" }, { - "type": "docker-image", - "ref": "docker.io/tonistiigi/xx@sha256:21a61be4744f6531cb5f33b0e6f40ede41fa3a1b8c82d5946178f80cc84bfc04", - "pin": "sha256:21a61be4744f6531cb5f33b0e6f40ede41fa3a1b8c82d5946178f80cc84bfc04" + "Type": "docker-image", + "Ref": "docker.io/tonistiigi/xx@sha256:21a61be4744f6531cb5f33b0e6f40ede41fa3a1b8c82d5946178f80cc84bfc04", + "Pin": "sha256:21a61be4744f6531cb5f33b0e6f40ede41fa3a1b8c82d5946178f80cc84bfc04" }, { - "type": "http", - "ref": "https://raw.githubusercontent.com/moby/moby/master/README.md", - "pin": "sha256:419455202b0ef97e480d7f8199b26a721a417818bc0e2d106975f74323f25e6c" + "Type": "http", + "Ref": "https://raw.githubusercontent.com/moby/moby/master/README.md", + "Pin": "sha256:419455202b0ef97e480d7f8199b26a721a417818bc0e2d106975f74323f25e6c" } ] } @@ -453,16 +429,16 @@ $ docker buildx imagetools inspect crazymax/buildx:buildinfo --format "{{json .} #### Multi-platform -Multi-platform images are supported for `.Image` and `.BuildInfo` fields. If -you want to pick up a specific platform, you can specify it using the `index` -go template function: +Multi-platform images are supported for `.Image`, `.Provenance` and `.SBOM` +fields. If you want to pick up a specific platform, you can specify it using +the `index` go template function: ```console $ docker buildx imagetools inspect --format '{{json (index .Image "linux/s390x")}}' moby/buildkit:master ``` ```json { - "created": "2022-02-25T17:13:27.89891722Z", + "created": "2022-11-30T17:42:26.414957336Z", "architecture": "s390x", "os": "linux", "config": { @@ -481,8 +457,8 @@ $ docker buildx imagetools inspect --format '{{json (index .Image "linux/s390x") "diff_ids": [ "sha256:41048e32d0684349141cf05f629c5fc3c5915d1f3426b66dbb8953a540e01e1e", "sha256:2651209b9208fff6c053bc3c17353cb07874e50f1a9bc96d6afd03aef63de76a", - "sha256:6741ed7e73039d853fa8902246a4c7e8bf9dd09652fd1b08251bc5f9e8876a7f", - "sha256:92ac046adeeb65c86ae3f0b458dee04ad4a462e417661c04d77642c66494f69b" + "sha256:88577322e65f094ce8ac27435880f1a8a9baadb569258026bb141770451bafcb", + "sha256:de8f9a790e4ed10ff1f1f8ea923c9da4f97246a7e200add2dc6650eba3f10a20" ] }, "history": [ @@ -501,23 +477,23 @@ $ docker buildx imagetools inspect --format '{{json (index .Image "linux/s390x") "comment": "buildkit.dockerfile.v0" }, { - "created": "2022-02-24T00:34:00.924540012Z", + "created": "2022-08-25T00:39:25.652811078Z", "created_by": "COPY examples/buildctl-daemonless/buildctl-daemonless.sh /usr/bin/ # buildkit", "comment": "buildkit.dockerfile.v0" }, { - "created": "2022-02-25T17:13:27.89891722Z", + "created": "2022-11-30T17:42:26.414957336Z", "created_by": "VOLUME [/var/lib/buildkit]", "comment": "buildkit.dockerfile.v0", "empty_layer": true }, { - "created": "2022-02-25T17:13:27.89891722Z", + "created": "2022-11-30T17:42:26.414957336Z", "created_by": "COPY / /usr/bin/ # buildkit", "comment": "buildkit.dockerfile.v0" }, { - "created": "2022-02-25T17:13:27.89891722Z", + "created": "2022-11-30T17:42:26.414957336Z", "created_by": "ENTRYPOINT [\"buildkitd\"]", "comment": "buildkit.dockerfile.v0", "empty_layer": true @@ -541,24 +517,24 @@ $ docker buildx imagetools inspect --raw crazymax/loop | jq "schemaVersion": 2, "config": { "mediaType": "application/vnd.docker.container.image.v1+json", - "digest": "sha256:7ace7d324e79b360b2db8b820d83081863d96d22e734cdf297a8e7fd83f6ceb3", - "size": 2298 + "digest": "sha256:a98999183d2c7a8845f6d56496e51099ce6e4359ee7255504174b05430c4b78b", + "size": 2762 }, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "digest": "sha256:5843afab387455b37944e709ee8c78d7520df80f8d01cf7f861aae63beeddb6b", - "size": 2811478 + "digest": "sha256:8663204ce13b2961da55026a2034abb9e5afaaccf6a9cfb44ad71406dcd07c7b", + "size": 2818370 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "digest": "sha256:726d3732a87e1c430d67e8969de6b222a889d45e045ebae1a008a37ba38f3b1f", - "size": 1776812 + "digest": "sha256:f0868a92f8e1e5018ed4e60eb845ed4ff0e2229897f4105e5a4735c1d6fd874f", + "size": 1821402 }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", - "digest": "sha256:5d7cf9b33148a8f220c84f27dd2cfae46aca019a3ea3fbf7274f6d6dbfae8f3b", - "size": 382855 + "digest": "sha256:d010066dbdfcf7c12fca30cd4b567aa7218eb6762ab53169d043655b7a8d7f2e", + "size": 404457 } ] } @@ -574,7 +550,7 @@ $ docker buildx imagetools inspect --raw moby/buildkit:master | jq "manifests": [ { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "digest": "sha256:667d28c9fb33820ce686887a717a148e89fa77f9097f9352996bbcce99d352b1", + "digest": "sha256:f9f41c85124686c2afe330a985066748a91d7a5d505777fe274df804ab5e077e", "size": 1158, "platform": { "architecture": "amd64", @@ -583,7 +559,7 @@ $ docker buildx imagetools inspect --raw moby/buildkit:master | jq }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "digest": "sha256:71789527b64ab3d7b3de01d364b449cd7f7a3da758218fbf73b9c9aae05a6775", + "digest": "sha256:82097c2be19c617aafb3c3e43c88548738d4b2bf3db5c36666283a918b390266", "size": 1158, "platform": { "architecture": "arm", @@ -593,7 +569,7 @@ $ docker buildx imagetools inspect --raw moby/buildkit:master | jq }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "digest": "sha256:fb64667e1ce6ab0d05478f3a8402af07b27737598dcf9a510fb1d792b13a66be", + "digest": "sha256:b6b91e6c823d7220ded7d3b688e571ba800b13d91bbc904c1d8053593e3ee42c", "size": 1158, "platform": { "architecture": "arm64", @@ -602,7 +578,7 @@ $ docker buildx imagetools inspect --raw moby/buildkit:master | jq }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "digest": "sha256:1c3ddf95a0788e23f72f25800c05abc4458946685e2b66788c3d978cde6da92b", + "digest": "sha256:797061bcc16778de048b96f769c018ec24da221088050bbe926ea3b8d51d77e8", "size": 1158, "platform": { "architecture": "s390x", @@ -611,7 +587,7 @@ $ docker buildx imagetools inspect --raw moby/buildkit:master | jq }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "digest": "sha256:05bcde6d460a284e5bc88026cd070277e8380355de3126cbc8fe8a452708c6b1", + "digest": "sha256:b93d3a84d18c4d0b8c279e77343d854d9b5177df7ea55cf468d461aa2523364e", "size": 1159, "platform": { "architecture": "ppc64le", @@ -620,7 +596,7 @@ $ docker buildx imagetools inspect --raw moby/buildkit:master | jq }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", - "digest": "sha256:c04c57765304ab84f4f9807fff3e11605c3a60e16435c734b02c723680f6bd6e", + "digest": "sha256:d5c950dd1b270d437c838187112a0cb44c9258248d7a3a8bcb42fae8f717dc01", "size": 1158, "platform": { "architecture": "riscv64", diff --git a/util/imagetools/loader.go b/util/imagetools/loader.go new file mode 100644 index 000000000000..05c0a4575335 --- /dev/null +++ b/util/imagetools/loader.go @@ -0,0 +1,415 @@ +package imagetools + +// TODO: replace with go-imageinspect library when public + +import ( + "context" + "encoding/base64" + "encoding/json" + "sort" + "strings" + "sync" + + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/platforms" + "github.com/containerd/containerd/remotes" + "github.com/docker/distribution/reference" + binfotypes "github.com/moby/buildkit/util/buildinfo/types" + "github.com/moby/buildkit/util/contentutil" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" +) + +const ( + annotationReference = "vnd.docker.reference.digest" +) + +type contentCache interface { + content.Provider + content.Ingester +} + +type loader struct { + resolver remotes.Resolver + cache contentCache +} + +type manifest struct { + desc ocispec.Descriptor + manifest ocispec.Manifest +} + +type index struct { + desc ocispec.Descriptor + index ocispec.Index +} + +type asset struct { + config *ocispec.Image + sbom json.RawMessage + provenance *provenance +} + +type result struct { + mu sync.Mutex + indexes map[digest.Digest]index + manifests map[digest.Digest]manifest + images map[string]digest.Digest + refs map[digest.Digest][]digest.Digest + + platforms []string + assets map[string]asset +} + +func newLoader(resolver remotes.Resolver) *loader { + return &loader{ + resolver: resolver, + cache: contentutil.NewBuffer(), + } +} + +func (l *loader) Load(ctx context.Context, ref string) (*result, error) { + named, err := parseReference(ref) + if err != nil { + return nil, err + } + + _, desc, err := l.resolver.Resolve(ctx, named.String()) + if err != nil { + return nil, err + } + + canonical, err := reference.WithDigest(named, desc.Digest) + if err != nil { + return nil, err + } + + fetcher, err := l.resolver.Fetcher(ctx, canonical.String()) + if err != nil { + return nil, err + } + + 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), + } + + if err := l.fetch(ctx, fetcher, desc, r); err != nil { + return nil, err + } + + for platform, dgst := range r.images { + r.platforms = append(r.platforms, platform) + + mfst, ok := r.manifests[dgst] + if !ok { + return nil, errors.Errorf("image %s not found", platform) + } + + var a asset + annotations := make(map[string]string, len(mfst.manifest.Annotations)+len(mfst.desc.Annotations)) + for k, v := range mfst.desc.Annotations { + annotations[k] = v + } + for k, v := range mfst.manifest.Annotations { + annotations[k] = v + } + + if err := l.scanConfig(ctx, fetcher, mfst.manifest.Config, &a); err != nil { + return nil, err + } + + refs, ok := r.refs[dgst] + if ok { + if err := l.scanSBOM(ctx, fetcher, r, refs, &a); err != nil { + return nil, err + } + } + + if err := l.scanProvenance(ctx, fetcher, mfst.manifest.Config, &a); err != nil { + return nil, err + } + + r.assets[platform] = a + } + + sort.Strings(r.platforms) + return r, nil +} + +func (l *loader) fetch(ctx context.Context, fetcher remotes.Fetcher, desc ocispec.Descriptor, r *result) error { + _, err := remotes.FetchHandler(l.cache, fetcher)(ctx, desc) + if err != nil { + return err + } + + switch desc.MediaType { + case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest: + var mfst ocispec.Manifest + dt, err := content.ReadBlob(ctx, l.cache, desc) + if err != nil { + return err + } + if err := json.Unmarshal(dt, &mfst); err != nil { + return err + } + r.mu.Lock() + r.manifests[desc.Digest] = manifest{ + desc: desc, + manifest: mfst, + } + r.mu.Unlock() + + ref, ok := desc.Annotations[annotationReference] + if ok { + refdgst, err := digest.Parse(ref) + if err != nil { + return err + } + r.mu.Lock() + r.refs[refdgst] = append(r.refs[refdgst], desc.Digest) + r.mu.Unlock() + } else { + p := desc.Platform + if p == nil { + p, err = l.readPlatformFromConfig(ctx, fetcher, mfst.Config) + if err != nil { + return err + } + } + r.mu.Lock() + r.images[platforms.Format(platforms.Normalize(*p))] = desc.Digest + r.mu.Unlock() + } + case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex: + var idx ocispec.Index + dt, err := content.ReadBlob(ctx, l.cache, desc) + if err != nil { + return err + } + + if err := json.Unmarshal(dt, &idx); err != nil { + return err + } + + r.mu.Lock() + r.indexes[desc.Digest] = index{ + desc: desc, + index: idx, + } + r.mu.Unlock() + + eg, ctx := errgroup.WithContext(ctx) + for _, d := range idx.Manifests { + d := d + eg.Go(func() error { + return l.fetch(ctx, fetcher, d, r) + }) + } + + if err := eg.Wait(); err != nil { + return err + } + default: + } + return nil +} + +func (l *loader) readPlatformFromConfig(ctx context.Context, fetcher remotes.Fetcher, desc ocispec.Descriptor) (*ocispec.Platform, error) { + _, err := remotes.FetchHandler(l.cache, fetcher)(ctx, desc) + if err != nil { + return nil, err + } + + dt, err := content.ReadBlob(ctx, l.cache, desc) + if err != nil { + return nil, err + } + + var config ocispec.Image + if err := json.Unmarshal(dt, &config); err != nil { + return nil, err + } + + return &ocispec.Platform{ + OS: config.OS, + Architecture: config.Architecture, + Variant: config.Variant, + }, nil +} + +func parseReference(ref string) (reference.Named, error) { + named, err := reference.ParseNormalizedNamed(ref) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse %q", ref) + } + return reference.TagNameOnly(named), nil +} + +func (l *loader) scanConfig(ctx context.Context, fetcher remotes.Fetcher, desc ocispec.Descriptor, as *asset) error { + _, err := remotes.FetchHandler(l.cache, fetcher)(ctx, desc) + if err != nil { + return err + } + dt, err := content.ReadBlob(ctx, l.cache, desc) + if err != nil { + return err + } + return json.Unmarshal(dt, &as.config) +} + +func (l *loader) scanSBOM(ctx context.Context, fetcher remotes.Fetcher, r *result, refs []digest.Digest, as *asset) error { + ctx = remotes.WithMediaTypeKeyPrefix(ctx, "application/vnd.in-toto+json", "intoto") + for _, dgst := range refs { + mfst, ok := r.manifests[dgst] + if !ok { + return errors.Errorf("referenced image %s not found", dgst) + } + for _, layer := range mfst.manifest.Layers { + if layer.MediaType == "application/vnd.in-toto+json" && layer.Annotations["in-toto.io/predicate-type"] == "https://spdx.dev/Document" { + var sbom json.RawMessage + _, err := remotes.FetchHandler(l.cache, fetcher)(ctx, layer) + if err != nil { + return err + } + dt, err := content.ReadBlob(ctx, l.cache, layer) + if err != nil { + return err + } + if err := json.Unmarshal(dt, &sbom); err != nil { + return err + } + as.sbom = sbom + } + } + } + return nil +} + +type provenance struct { // TODO: this is only a stub, to be refactored later + BuildSource string `json:",omitempty"` + BuildDefinition string `json:",omitempty"` + BuildParameters map[string]string `json:",omitempty"` + Materials []material +} + +type material struct { + Type string `json:",omitempty"` + Ref string `json:",omitempty"` + Alias string `json:",omitempty"` + Pin string `json:",omitempty"` +} + +func (l *loader) scanProvenance(ctx context.Context, fetcher remotes.Fetcher, desc ocispec.Descriptor, as *asset) error { + _, err := remotes.FetchHandler(l.cache, fetcher)(ctx, desc) + if err != nil { + return err + } + dt, err := content.ReadBlob(ctx, l.cache, desc) + if err != nil { + return err + } + + var cfg binfotypes.ImageConfig + if err := json.Unmarshal(dt, &cfg); err != nil { + return err + } + + if cfg.BuildInfo == "" { + return nil + } + + dt, err = base64.StdEncoding.DecodeString(cfg.BuildInfo) + if err != nil { + return errors.Wrapf(err, "failed to decode buildinfo base64") + } + + var bi binfotypes.BuildInfo + if err := json.Unmarshal(dt, &bi); err != nil { + return errors.Wrapf(err, "failed to decode buildinfo") + } + + p := &provenance{} + defer func() { + as.provenance = p + }() + if bs := bi.Attrs["context"]; bs != nil { + p.BuildSource = *bs + } + + if fn := bi.Attrs["filename"]; fn != nil { + p.BuildDefinition = *fn + } + + for key, val := range bi.Attrs { + if val == nil || !strings.HasPrefix(key, "build-arg:") { + continue + } + if p.BuildParameters == nil { + p.BuildParameters = make(map[string]string) + } + p.BuildParameters[strings.TrimPrefix(key, "build-arg:")] = *val + } + + p.Materials = make([]material, len(bi.Sources)) + + for i, src := range bi.Sources { + // TODO: mark base image + p.Materials[i] = material{ + Type: string(src.Type), + Ref: src.Ref, + Alias: src.Alias, + Pin: src.Pin, + } + } + + return nil +} + +func (r *result) Configs() map[string]*ocispec.Image { + if len(r.assets) == 0 { + return nil + } + res := make(map[string]*ocispec.Image) + for p, a := range r.assets { + if a.config == nil { + continue + } + res[p] = a.config + } + return res +} + +func (r *result) Provenances() map[string]*provenance { + if len(r.assets) == 0 { + return nil + } + res := make(map[string]*provenance) + for p, a := range r.assets { + if a.provenance == nil { + continue + } + res[p] = a.provenance + } + return res +} + +func (r *result) SBOMs() map[string]json.RawMessage { + if len(r.assets) == 0 { + return nil + } + res := make(map[string]json.RawMessage) + for p, a := range r.assets { + if a.sbom == nil { + continue + } + res[p] = a.sbom + } + return res +} diff --git a/util/imagetools/printers.go b/util/imagetools/printers.go index 371607010f8a..5851a0718eae 100644 --- a/util/imagetools/printers.go +++ b/util/imagetools/printers.go @@ -8,18 +8,14 @@ import ( "os" "sort" "strings" - "sync" "text/tabwriter" "text/template" "github.com/containerd/containerd/images" "github.com/containerd/containerd/platforms" "github.com/docker/distribution/reference" - binfotypes "github.com/moby/buildkit/util/buildinfo/types" - "github.com/moby/buildkit/util/imageutil" "github.com/opencontainers/go-digest" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" - "golang.org/x/sync/errgroup" ) const defaultPfx = " " @@ -31,53 +27,46 @@ type Printer struct { name string format string - raw []byte - ref reference.Named - manifest ocispecs.Descriptor - index ocispecs.Index - platforms []ocispecs.Platform + insres *result + raw []byte + ref reference.Named + manifest ocispecs.Descriptor + index ocispecs.Index } func NewPrinter(ctx context.Context, opt Opt, name string, format string) (*Printer, error) { resolver := New(opt) - ref, err := parseRef(name) + insres, err := newLoader(resolver.resolver()).Load(ctx, name) if err != nil { return nil, err } - dt, manifest, err := resolver.Get(ctx, name) + ref, err := parseRef(name) if err != nil { return nil, err } - var index ocispecs.Index - if err = json.Unmarshal(dt, &index); err != nil { + dt, mfst, err := resolver.Get(ctx, ref.String()) + if err != nil { return nil, err } - var pforms []ocispecs.Platform - switch manifest.MediaType { - case images.MediaTypeDockerSchema2ManifestList, ocispecs.MediaTypeImageIndex: - for _, m := range index.Manifests { - if m.Platform != nil { - pforms = append(pforms, *m.Platform) - } - } - default: - pforms = append(pforms, platforms.DefaultSpec()) + var idx ocispecs.Index + if err = json.Unmarshal(dt, &idx); err != nil { + return nil, err } return &Printer{ - ctx: ctx, - resolver: resolver, - name: name, - format: format, - raw: dt, - ref: ref, - manifest: manifest, - index: index, - platforms: pforms, + ctx: ctx, + resolver: resolver, + name: name, + format: format, + insres: insres, + raw: dt, + ref: ref, + manifest: mfst, + index: idx, }, nil } @@ -112,46 +101,17 @@ func (p *Printer) Print(raw bool, out io.Writer) error { return err } - imageconfigs := make(map[string]*ocispecs.Image) - imageconfigsMutex := sync.Mutex{} - buildinfos := make(map[string]*binfotypes.BuildInfo) - buildinfosMutex := sync.Mutex{} - - eg, _ := errgroup.WithContext(p.ctx) - for _, platform := range p.platforms { - func(platform ocispecs.Platform) { - eg.Go(func() error { - img, dtic, err := p.getImageConfig(&platform) - if err != nil { - return err - } else if img != nil { - imageconfigsMutex.Lock() - imageconfigs[platforms.Format(platform)] = img - imageconfigsMutex.Unlock() - } - if bi, err := imageutil.BuildInfo(dtic); err != nil { - return err - } else if bi != nil { - buildinfosMutex.Lock() - buildinfos[platforms.Format(platform)] = bi - buildinfosMutex.Unlock() - } - return nil - }) - }(platform) - } - if err := eg.Wait(); err != nil { - return err - } - + imageconfigs := p.insres.Configs() + provenances := p.insres.Provenances() + sboms := p.insres.SBOMs() format := tpl.Root.String() - var manifest interface{} + var mfst interface{} switch p.manifest.MediaType { case images.MediaTypeDockerSchema2Manifest, ocispecs.MediaTypeImageManifest: - manifest = p.manifest + mfst = p.manifest case images.MediaTypeDockerSchema2ManifestList, ocispecs.MediaTypeImageIndex: - manifest = struct { + mfst = struct { SchemaVersion int `json:"schemaVersion"` MediaType string `json:"mediaType,omitempty"` Digest digest.Digest `json:"digest"` @@ -170,10 +130,11 @@ func (p *Printer) Print(raw bool, out io.Writer) error { switch { // TODO: print formatted config - case strings.HasPrefix(format, "{{.Manifest"), strings.HasPrefix(format, "{{.BuildInfo"): + case strings.HasPrefix(format, "{{.Manifest"), strings.HasPrefix(format, "{{.BuildInfo"), strings.HasPrefix(format, "{{.Provenance"): w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0) _, _ = fmt.Fprintf(w, "Name:\t%s\n", p.ref.String()) - if strings.HasPrefix(format, "{{.Manifest") { + switch { + case strings.HasPrefix(format, "{{.Manifest"): _, _ = fmt.Fprintf(w, "MediaType:\t%s\n", p.manifest.MediaType) _, _ = fmt.Fprintf(w, "Digest:\t%s\n", p.manifest.Digest) _ = w.Flush() @@ -181,42 +142,50 @@ func (p *Printer) Print(raw bool, out io.Writer) error { case images.MediaTypeDockerSchema2ManifestList, ocispecs.MediaTypeImageIndex: _ = p.printManifestList(out) } - } else if strings.HasPrefix(format, "{{.BuildInfo") { + case strings.HasPrefix(format, "{{.BuildInfo"), strings.HasPrefix(format, "{{.Provenance"): _ = w.Flush() - _ = p.printBuildInfos(buildinfos, out) + _ = p.printProvenances(provenances, out) } default: - if len(p.platforms) > 1 { + if len(p.insres.platforms) > 1 { return tpl.Execute(out, struct { - Name string `json:"name,omitempty"` - Manifest interface{} `json:"manifest,omitempty"` - Image map[string]*ocispecs.Image `json:"image,omitempty"` - BuildInfo map[string]*binfotypes.BuildInfo `json:"buildinfo,omitempty"` + Name string `json:"name,omitempty"` + Manifest interface{} `json:"manifest,omitempty"` + Image map[string]*ocispecs.Image `json:"image,omitempty"` + Provenance map[string]*provenance `json:"provenance,omitempty"` + SBOM map[string]json.RawMessage `json:"sbom,omitempty"` }{ - Name: p.name, - Manifest: manifest, - Image: imageconfigs, - BuildInfo: buildinfos, + Name: p.name, + Manifest: mfst, + Image: imageconfigs, + Provenance: provenances, + SBOM: sboms, }) } var ic *ocispecs.Image for _, v := range imageconfigs { ic = v } - var bi *binfotypes.BuildInfo - for _, v := range buildinfos { - bi = v + var prv *provenance + for _, v := range provenances { + prv = v + } + var sbom json.RawMessage + for _, v := range sboms { + sbom = v } return tpl.Execute(out, struct { - Name string `json:"name,omitempty"` - Manifest interface{} `json:"manifest,omitempty"` - Image *ocispecs.Image `json:"image,omitempty"` - BuildInfo *binfotypes.BuildInfo `json:"buildinfo,omitempty"` + Name string `json:"name,omitempty"` + Manifest interface{} `json:"manifest,omitempty"` + Image *ocispecs.Image `json:"image,omitempty"` + Provenance *provenance `json:"provenance,omitempty"` + SBOM json.RawMessage `json:"sbom,omitempty"` }{ - Name: p.name, - Manifest: manifest, - Image: ic, - BuildInfo: bi, + Name: p.name, + Manifest: mfst, + Image: ic, + Provenance: prv, + SBOM: sbom, }) } @@ -265,47 +234,49 @@ func (p *Printer) printManifestList(out io.Writer) error { return w.Flush() } -func (p *Printer) printBuildInfos(bis map[string]*binfotypes.BuildInfo, out io.Writer) error { - if len(bis) == 0 { +func (p *Printer) printProvenances(provenances map[string]*provenance, out io.Writer) error { + if len(provenances) == 0 { return nil - } else if len(bis) == 1 { - for _, bi := range bis { - return p.printBuildInfo(bi, "", out) + } else if len(provenances) == 1 { + for _, pr := range provenances { + return p.printProvenance(pr, "", out) } } var pkeys []string - for _, pform := range p.platforms { - pkeys = append(pkeys, platforms.Format(pform)) + for _, pform := range p.insres.platforms { + pkeys = append(pkeys, pform) } sort.Strings(pkeys) for _, platform := range pkeys { - bi := bis[platform] - w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0) - _, _ = fmt.Fprintf(w, "\t\nPlatform:\t%s\t\n", platform) - _ = w.Flush() - if err := p.printBuildInfo(bi, "", out); err != nil { - return err + if pr, ok := provenances[platform]; ok { + w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0) + _, _ = fmt.Fprintf(w, "\t\nPlatform:\t%s\t\n", platform) + _ = w.Flush() + if err := p.printProvenance(pr, "", out); err != nil { + return err + } } } return nil } -func (p *Printer) printBuildInfo(bi *binfotypes.BuildInfo, pfx string, out io.Writer) error { +func (p *Printer) printProvenance(pr *provenance, pfx string, out io.Writer) error { w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0) - _, _ = fmt.Fprintf(w, "%sFrontend:\t%s\n", pfx, bi.Frontend) + _, _ = fmt.Fprintf(w, "%sBuildSource:\t%s\n", pfx, pr.BuildSource) + _, _ = fmt.Fprintf(w, "%sBuildDefinition:\t%s\n", pfx, pr.BuildDefinition) - if len(bi.Attrs) > 0 { - _, _ = fmt.Fprintf(w, "%sAttrs:\t\n", pfx) + if len(pr.BuildParameters) > 0 { + _, _ = fmt.Fprintf(w, "%sBuildParameters:\t\n", pfx) _ = w.Flush() - for k, v := range bi.Attrs { - _, _ = fmt.Fprintf(w, "%s%s:\t%s\n", pfx+defaultPfx, k, *v) + for k, v := range pr.BuildParameters { + _, _ = fmt.Fprintf(w, "%s%s:\t%s\n", pfx+defaultPfx, k, v) } } - if len(bi.Sources) > 0 { - _, _ = fmt.Fprintf(w, "%sSources:\t\n", pfx) + if len(pr.Materials) > 0 { + _, _ = fmt.Fprintf(w, "%sMaterials:\t\n", pfx) _ = w.Flush() - for i, v := range bi.Sources { + for i, v := range pr.Materials { if i != 0 { _, _ = fmt.Fprintf(w, "\t\n") } @@ -315,32 +286,21 @@ func (p *Printer) printBuildInfo(bi *binfotypes.BuildInfo, pfx string, out io.Wr } } - if len(bi.Deps) > 0 { - _, _ = fmt.Fprintf(w, "%sDeps:\t\n", pfx) - _ = w.Flush() - firstPass := true - for k, v := range bi.Deps { - if !firstPass { - _, _ = fmt.Fprintf(w, "\t\n") - } - _, _ = fmt.Fprintf(w, "%sName:\t%s\n", pfx+defaultPfx, k) - _ = w.Flush() - _ = p.printBuildInfo(&v, pfx+defaultPfx, out) - firstPass = false - } - } + // TODO: deps not yet implemented with provenance + //if len(pr.Deps) > 0 { + // _, _ = fmt.Fprintf(w, "%sDeps:\t\n", pfx) + // _ = w.Flush() + // firstPass := true + // for k, v := range pr.Deps { + // if !firstPass { + // _, _ = fmt.Fprintf(w, "\t\n") + // } + // _, _ = fmt.Fprintf(w, "%sName:\t%s\n", pfx+defaultPfx, k) + // _ = w.Flush() + // _ = p.printProvenance(&v, pfx+defaultPfx, out) + // firstPass = false + // } + //} return w.Flush() } - -func (p *Printer) getImageConfig(platform *ocispecs.Platform) (*ocispecs.Image, []byte, error) { - _, dtic, err := p.resolver.ImageConfig(p.ctx, p.name, platform) - if err != nil { - return nil, nil, err - } - var img *ocispecs.Image - if err = json.Unmarshal(dtic, &img); err != nil { - return nil, nil, err - } - return img, dtic, nil -}