Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --platform flag to cosign sbom download #1975

Merged
merged 9 commits into from
Jun 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(not blocking) It looks like the []string return value is only ever read in test/e2e_test.go, and never in the actual CLI command. Is that right?

Maybe we should just have this return error alone, and refactor tests to not rely on the []string return value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree. I think the command was meant to return more than one sbom but it is short-circuited to spit out the one sbom. We should discuss if it will be returning more than one SBOM and decide how to change this. I think the logic to download the SBOMs (and the rest) should me moved away from the CLI to a more general package.

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.
puerco marked this conversation as resolved.
Show resolved Hide resolved
// /~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 @@ -934,7 +934,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 @@ -944,7 +949,7 @@ func TestAttachSBOM(t *testing.T) {
// Upload it!
must(attach.SBOMCmd(ctx, options.RegistryOptions{}, "./testdata/bom-go-mod.spdx", "spdx", imgName), t)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should attach also take a platform? 🤔

Copy link
Member Author

@puerco puerco Jun 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'll add --platform to cosign attach sbom in a follow-up


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