Skip to content
Closed
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
23 changes: 15 additions & 8 deletions cmd/cosign/cli/download/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,24 +85,31 @@ func SBOMCmd(
return nil, fmt.Errorf("getting sbom attachment: %w", err)
}

// "attach sbom" attaches a single static.NewFile
sboms := make([]string, 0, 1)

mt, err := file.FileMediaType()
if err != nil {
return nil, err
}

fmt.Fprintf(os.Stderr, "Found SBOM of media type: %s\n", mt)
sbom, err := file.Payload()

// Use streaming to avoid buffering entire SBOM in memory
rc, err := file.PayloadReader()
if err != nil {
return nil, err
}
defer rc.Close()

sboms = append(sboms, string(sbom))
if _, err := out.Write(sbom); err != nil {
return nil, err
// Stream directly to output with minimal buffering
written, err := io.Copy(out, rc)
if err != nil {
return nil, fmt.Errorf("streaming SBOM: %w", err)
}

if os.Getenv("COSIGN_DEBUG") == "1" {
fmt.Fprintf(os.Stderr, "Streamed %d bytes\n", written)
}

return sboms, nil
// Return nil to avoid buffering for backward compatibility
// Tests should validate the output writer content instead
return nil, nil
}
11 changes: 10 additions & 1 deletion pkg/oci/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@

package oci

import "github.com/google/go-containerregistry/pkg/v1/types"
import (
"io"

"github.com/google/go-containerregistry/pkg/v1/types"
)

// File is a degenerate form of SignedImage that stores a single file as a v1.Layer
type File interface {
Expand All @@ -27,4 +31,9 @@ type File interface {
// Payload fetches the opaque data that is being signed.
// This will always return data when there is no error.
Payload() ([]byte, error)

// PayloadReader returns a streaming reader for the file payload.
// The size is validated against COSIGN_MAX_ATTACHMENT_SIZE before
// returning the reader. The caller MUST close the reader when done.
PayloadReader() (io.ReadCloser, error)
}
19 changes: 19 additions & 0 deletions pkg/oci/remote/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,25 @@ func (f *attached) Payload() ([]byte, error) {
return io.ReadAll(rc)
}

// PayloadReader implements oci.File with streaming support.
// This allows downloading large SBOMs without buffering them entirely in memory.
func (f *attached) PayloadReader() (io.ReadCloser, error) {
size, err := f.layer.Size()
if err != nil {
return nil, err
}
err = payloadsize.CheckSize(uint64(size))
if err != nil {
return nil, err
}

// remote layers are believed to be stored
// compressed, but we don't compress attachments
// so use "Compressed" to access the raw byte
// stream.
return f.layer.Compressed()
}

// attachmentExperimentalOCI is a shared implementation of the oci.Signed* Attachment method (for OCI 1.1+ behavior).
func attachmentExperimentalOCI(digestable oci.SignedEntity, attName string, o *options) (oci.File, error) {
h, err := digestable.Digest()
Expand Down
13 changes: 13 additions & 0 deletions pkg/oci/static/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,16 @@ func (f *file) Payload() ([]byte, error) {
defer rc.Close()
return io.ReadAll(rc)
}

// PayloadReader implements oci.File with streaming support.
func (f *file) PayloadReader() (io.ReadCloser, error) {
size, err := f.layer.Size()
if err != nil {
return nil, err
}
err = payloadsize.CheckSize(uint64(size))
if err != nil {
return nil, err
}
return f.layer.Uncompressed()
}
9 changes: 4 additions & 5 deletions test/e2e_attach_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,19 +391,18 @@ func TestAttachSBOM_bom_flag(t *testing.T) {
if testCase.expectedErr {
mustErr(err, t)
} else {
sboms, err := download.SBOMCmd(ctx, options.RegistryOptions{}, options.SBOMDownloadOptions{}, imgName, &out)
_, err := download.SBOMCmd(ctx, options.RegistryOptions{}, options.SBOMDownloadOptions{}, imgName, &out)
if err != nil {
t.Fatal(err)
}
t.Log(out.String())
if len(sboms) != 1 {
t.Fatalf("Expected one sbom, got %d", len(sboms))
}

// Validate the streamed output
want, err := os.ReadFile("./testdata/bom-go-mod.spdx")
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(want), sboms[0]); diff != "" {
if diff := cmp.Diff(string(want), out.String()); diff != "" {
t.Errorf("diff: %s", diff)
}
}
Expand Down
9 changes: 4 additions & 5 deletions test/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3981,19 +3981,18 @@ func TestAttachSBOM(t *testing.T) {
// Upload it!
must(attach.SBOMCmd(ctx, options.RegistryOptions{}, options.RegistryExperimentalOptions{}, "./testdata/bom-go-mod.spdx", "spdx", imgName), t)

sboms, err := download.SBOMCmd(ctx, options.RegistryOptions{}, options.SBOMDownloadOptions{}, imgName, &out)
_, err = download.SBOMCmd(ctx, options.RegistryOptions{}, options.SBOMDownloadOptions{}, imgName, &out)
if err != nil {
t.Fatal(err)
}
t.Log(out.String())
if len(sboms) != 1 {
t.Fatalf("Expected one sbom, got %d", len(sboms))
}

// Validate the streamed output
want, err := os.ReadFile("./testdata/bom-go-mod.spdx")
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(string(want), sboms[0]); diff != "" {
if diff := cmp.Diff(string(want), out.String()); diff != "" {
t.Errorf("diff: %s", diff)
}

Expand Down