Skip to content

Commit

Permalink
Add --platform flag to cosign sbom download (#1975)
Browse files Browse the repository at this point in the history
* Add --platform option to download sbom

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Const not found error and humanize not found err

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Download specified sbom with --platform

This commit implements the --platform flag of `cosign sbom download` that allows the
user to fetch an sbom for a specific arch.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Update platform string handling

This commit modifies the --platform flag in cosign download to improve
the handling of platform strings and matching.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Spin out platform fn and improve help

This commit refactors the platforms logic to its own image and improves the
user help when there is no sbom at the index level. cosign will now
show the user which platforms are available to use with --platform.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Add sbom platform cases to e2e

Add single image with platform case and update args in calls to download functions

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Update cosign download doc

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Add platformList.String method

Refacor listing platform to a method of platformList

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>

* Drop manual walk of signed index

This commit drops the findSignedImage() function in favor of the
SignedImge() function of the OCI package.

Signed-off-by: Adolfo García Veytia (Puerco) <puerco@chainguard.dev>
  • Loading branch information
puerco authored Jun 11, 2022
1 parent 67b1fd1 commit ec590b7
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 7 deletions.
4 changes: 3 additions & 1 deletion cmd/cosign/cli/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func downloadSignature() *cobra.Command {

func downloadSBOM() *cobra.Command {
o := &options.RegistryOptions{}
do := &options.SBOMDownloadOptions{}

cmd := &cobra.Command{
Use: "sbom",
Expand All @@ -68,11 +69,12 @@ func downloadSBOM() *cobra.Command {
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Fprintln(os.Stderr, "WARNING: Downloading SBOMs this way does not ensure its authenticity. If you want to ensure a tamper-proof SBOM, download it using 'cosign download attestation <image uri>' or verify its signature.")
_, err := download.SBOMCmd(cmd.Context(), *o, args[0], cmd.OutOrStdout())
_, err := download.SBOMCmd(cmd.Context(), *o, *do, args[0], cmd.OutOrStdout())
return err
},
}

do.AddFlags(cmd)
o.AddFlags(cmd)

return cmd
Expand Down
133 changes: 130 additions & 3 deletions cmd/cosign/cli/download/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,36 @@ package download

import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"

"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/sigstore/cosign/pkg/oci"
ociremote "github.com/sigstore/cosign/pkg/oci/remote"
)

func SBOMCmd(ctx context.Context, regOpts options.RegistryOptions, imageRef string, out io.Writer) ([]string, error) {
type platformList []struct {
hash v1.Hash
platform *v1.Platform
}

func (pl *platformList) String() string {
r := []string{}
for _, p := range *pl {
r = append(r, p.platform.String())
}
return strings.Join(r, ", ")
}

func SBOMCmd(
ctx context.Context, regOpts options.RegistryOptions,
dnOpts options.SBOMDownloadOptions, imageRef string, out io.Writer,
) ([]string, error) {
ref, err := name.ParseReference(imageRef)
if err != nil {
return nil, err
Expand All @@ -42,9 +62,62 @@ func SBOMCmd(ctx context.Context, regOpts options.RegistryOptions, imageRef stri
return nil, err
}

idx, isIndex := se.(oci.SignedImageIndex)

// We only allow --platform on multiarch indexes
if dnOpts.Platform != "" && !isIndex {
return nil, fmt.Errorf("specified reference is not a multiarch image")
}

if dnOpts.Platform != "" && isIndex {
targetPlatform, err := v1.ParsePlatform(dnOpts.Platform)
if err != nil {
return nil, fmt.Errorf("parsing platform: %w", err)
}
platforms, err := getIndexPlatforms(idx)
if err != nil {
return nil, fmt.Errorf("getting available platforms: %w", err)
}

platforms = matchPlatform(targetPlatform, platforms)
if len(platforms) == 0 {
return nil, fmt.Errorf("unable to find an SBOM for %s", targetPlatform.String())
}
if len(platforms) > 1 {
return nil, fmt.Errorf(
"platform spec matches more than one image architecture: %s",
platforms.String(),
)
}

nse, err := idx.SignedImage(platforms[0].hash)
if err != nil {
return nil, fmt.Errorf("searching for %s image: %w", platforms[0].hash.String(), err)
}
if nse == nil {
return nil, fmt.Errorf("unable to find image %s", platforms[0].hash.String())
}
se = nse
}

file, err := se.Attachment("sbom")
if err != nil {
return nil, err
if errors.Is(err, ociremote.ErrImageNotFound) {
if !isIndex {
return nil, errors.New("no sbom attached to reference")
}
// Help the user with the available architectures
pl, err := getIndexPlatforms(idx)
if len(pl) > 0 && err == nil {
fmt.Fprintf(
os.Stderr,
"\nThis multiarch image does not have an SBOM attached at the index level.\n"+
"Try using --platform with one of the following architectures:\n%s\n\n",
pl.String(),
)
}
return nil, fmt.Errorf("no SBOM found attached to image index")
} else if err != nil {
return nil, fmt.Errorf("getting sbom attachment: %w", err)
}

// "attach sbom" attaches a single static.NewFile
Expand All @@ -66,3 +139,57 @@ func SBOMCmd(ctx context.Context, regOpts options.RegistryOptions, imageRef stri

return sboms, nil
}

func getIndexPlatforms(idx oci.SignedImageIndex) (platformList, error) {
im, err := idx.IndexManifest()
if err != nil {
return nil, fmt.Errorf("fetching index manifest: %w", err)
}

platforms := platformList{}
for _, m := range im.Manifests {
if m.Platform == nil {
continue
}
platforms = append(platforms, struct {
hash v1.Hash
platform *v1.Platform
}{m.Digest, m.Platform})
}
return platforms, nil
}

// matchPlatform filters a list of platforms returning only those matching
// a base. "Based" on ko's internal equivalent while it moves to GGCR.
// https://github.com/google/ko/blob/e6a7a37e26d82a8b2bb6df991c5a6cf6b2728794/pkg/build/gobuild.go#L1020
func matchPlatform(base *v1.Platform, list platformList) platformList {
ret := platformList{}
for _, p := range list {
if base.OS != "" && base.OS != p.platform.OS {
continue
}
if base.Architecture != "" && base.Architecture != p.platform.Architecture {
continue
}
if base.Variant != "" && base.Variant != p.platform.Variant {
continue
}

if base.OSVersion != "" && p.platform.OSVersion != base.OSVersion {
if base.OS != "windows" {
continue
} else {
if pcount, bcount := strings.Count(base.OSVersion, "."), strings.Count(p.platform.OSVersion, "."); pcount == 2 && bcount == 3 {
if base.OSVersion != p.platform.OSVersion[:strings.LastIndex(p.platform.OSVersion, ".")] {
continue
}
} else {
continue
}
}
}
ret = append(ret, p)
}

return ret
}
31 changes: 31 additions & 0 deletions cmd/cosign/cli/options/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Copyright 2022 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package options

import "github.com/spf13/cobra"

// DownloadOptions is the struct for control
type SBOMDownloadOptions struct {
Platform string // Platform to download sboms
}

var _ Interface = (*SBOMDownloadOptions)(nil)

// AddFlags implements Interface
func (o *SBOMDownloadOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&o.Platform, "platform", "",
"download SBOM for a specific platform image")
}
1 change: 1 addition & 0 deletions doc/cosign_download_sbom.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion pkg/oci/remote/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ import (
"github.com/sigstore/cosign/pkg/oci"
)

var ErrImageNotFound = errors.New("image not found in registry")

// SignedImage provides access to a remote image reference, and its signatures.
func SignedImage(ref name.Reference, options ...Option) (oci.SignedImage, error) {
o := makeOptions(ref.Context(), options...)
ri, err := remoteImage(ref, o.ROpt...)
var te *transport.Error
if errors.As(err, &te) && te.StatusCode == http.StatusNotFound {
return nil, errors.New("image not found in registry")
return nil, ErrImageNotFound
} else if err != nil {
return nil, err
}
Expand Down
9 changes: 7 additions & 2 deletions test/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -977,7 +977,12 @@ func TestAttachSBOM(t *testing.T) {
defer cleanup()

out := bytes.Buffer{}
_, err := download.SBOMCmd(ctx, options.RegistryOptions{}, img.Name(), &out)

_, errPl := download.SBOMCmd(ctx, options.RegistryOptions{}, options.SBOMDownloadOptions{Platform: "darwin/amd64"}, img.Name(), &out)
if errPl == nil {
t.Fatalf("Expected error when passing Platform to single arch image")
}
_, err := download.SBOMCmd(ctx, options.RegistryOptions{}, options.SBOMDownloadOptions{}, img.Name(), &out)
if err == nil {
t.Fatal("Expected error")
}
Expand All @@ -987,7 +992,7 @@ func TestAttachSBOM(t *testing.T) {
// Upload it!
must(attach.SBOMCmd(ctx, options.RegistryOptions{}, "./testdata/bom-go-mod.spdx", "spdx", imgName), t)

sboms, err := download.SBOMCmd(ctx, options.RegistryOptions{}, imgName, &out)
sboms, err := download.SBOMCmd(ctx, options.RegistryOptions{}, options.SBOMDownloadOptions{}, imgName, &out)
if err != nil {
t.Fatal(err)
}
Expand Down

0 comments on commit ec590b7

Please sign in to comment.