Skip to content

Commit 77486a5

Browse files
ralphbeanclaude
andcommitted
Support stdout output for attest-blob bundles
Enable `attest-blob --bundle=-` to write bundles to stdout with a trailing newline, allowing users to create JSONL files containing multiple attestations by redirecting and appending output. This change adds support for the convention of using "-" to represent stdout. When the bundle path is "-", the bundle is written to stdout instead of a file, and the signature output is suppressed to avoid conflicts. Changes: - Add stdout detection in attest/attest_blob.go and signcommon/common.go - Suppress signature output when bundle goes to stdout - Add comprehensive test coverage in attest_blob_test.go - Update flag description and add JSONL example to documentation Example usage: cosign attest-blob --key key.key --predicate pred1.json \ --type slsaprovenance --bundle=- blob.txt >> attestations.jsonl cosign attest-blob --key key.key --predicate pred2.json \ --type slsaprovenance --bundle=- blob.txt >> attestations.jsonl 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> Signed-off-by: Ralph Bean <[email protected]>
1 parent 0b4362b commit 77486a5

File tree

6 files changed

+85
-7
lines changed

6 files changed

+85
-7
lines changed

cmd/cosign/cli/attest/attest_blob.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,18 +186,24 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
186186
}
187187
}
188188

189-
if err := os.WriteFile(c.BundlePath, contents, 0600); err != nil {
190-
return fmt.Errorf("create bundle file: %w", err)
189+
// If BundlePath is "-", write to stdout with trailing newline
190+
if c.BundlePath == "-" {
191+
fmt.Fprintln(os.Stdout, string(contents))
192+
} else {
193+
if err := os.WriteFile(c.BundlePath, contents, 0600); err != nil {
194+
return fmt.Errorf("create bundle file: %w", err)
195+
}
196+
fmt.Fprintln(os.Stderr, "Bundle wrote in the file ", c.BundlePath)
191197
}
192-
fmt.Fprintln(os.Stderr, "Bundle wrote in the file ", c.BundlePath)
193198
}
194199

195200
if c.OutputSignature != "" {
196201
if err := os.WriteFile(c.OutputSignature, bundleComponents.SignedPayload, 0600); err != nil {
197202
return fmt.Errorf("create signature file: %w", err)
198203
}
199204
fmt.Fprintf(os.Stderr, "Signature written in %s\n", c.OutputSignature)
200-
} else {
205+
} else if c.BundlePath != "-" {
206+
// Only output signature to stdout if bundle is not going to stdout
201207
fmt.Fprintln(os.Stdout, string(bundleComponents.SignedPayload))
202208
}
203209

cmd/cosign/cli/attest/attest_blob_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,62 @@ func TestStatementPath(t *testing.T) {
341341
err := at.Exec(ctx, "")
342342
assert.NoError(t, err)
343343
}
344+
345+
// TestAttestBlobBundleStdout tests that the bundle is printed to stdout
346+
// with a trailing newline when --bundle is set to "-"
347+
func TestAttestBlobBundleStdout(t *testing.T) {
348+
ctx := context.Background()
349+
td := t.TempDir()
350+
351+
keys, _ := cosign.GenerateKeyPair(nil)
352+
keyRef := writeFile(t, td, string(keys.PrivateBytes), "key.pem")
353+
354+
blob := []byte("foo")
355+
blobPath := writeFile(t, td, string(blob), "foo.txt")
356+
predicatePath := makeSLSA02PredicateFile(t, td)
357+
358+
// Capture stdout
359+
oldStdout := os.Stdout
360+
r, w, _ := os.Pipe()
361+
os.Stdout = w
362+
363+
at := AttestBlobCommand{
364+
KeyOpts: options.KeyOpts{
365+
KeyRef: keyRef,
366+
NewBundleFormat: true,
367+
BundlePath: "-", // stdout
368+
},
369+
PredicatePath: predicatePath,
370+
PredicateType: "slsaprovenance",
371+
RekorEntryType: "dsse",
372+
}
373+
374+
err := at.Exec(ctx, blobPath)
375+
if err != nil {
376+
t.Fatal(err)
377+
}
378+
379+
// Restore stdout and read captured output
380+
w.Close()
381+
os.Stdout = oldStdout
382+
var buf bytes.Buffer
383+
_, _ = buf.ReadFrom(r)
384+
output := buf.String()
385+
386+
// Verify output has trailing newline
387+
if !strings.HasSuffix(output, "\n") {
388+
t.Fatal("expected bundle output to have trailing newline")
389+
}
390+
391+
// Verify output is valid JSON
392+
outputWithoutNewline := strings.TrimSuffix(output, "\n")
393+
var bundle interface{}
394+
if err := json.Unmarshal([]byte(outputWithoutNewline), &bundle); err != nil {
395+
t.Fatalf("bundle output is not valid JSON: %v", err)
396+
}
397+
398+
// Verify bundle is non-empty
399+
if len(outputWithoutNewline) == 0 {
400+
t.Fatal("expected non-empty bundle output")
401+
}
402+
}

cmd/cosign/cli/attest_blob.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ func AttestBlob() *cobra.Command {
5252
cosign attest-blob --predicate <FILE> --type <TYPE> --key hashivault://[KEY] <BLOB>
5353
5454
# supply attestation via stdin
55-
echo <PAYLOAD> | cosign attest-blob --predicate - --yes`,
55+
echo <PAYLOAD> | cosign attest-blob --predicate - --yes
56+
57+
# create a JSONL file with multiple attestations by outputting bundles to stdout
58+
cosign attest-blob --key cosign.key --predicate <FILE1> --type <TYPE> --bundle=- <BLOB> >> attestations.jsonl
59+
cosign attest-blob --key cosign.key --predicate <FILE2> --type <TYPE> --bundle=- <BLOB> >> attestations.jsonl`,
5660

5761
PersistentPreRun: options.BindViper,
5862
RunE: func(cmd *cobra.Command, args []string) error {

cmd/cosign/cli/options/attest_blob.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func (o *AttestBlobOptions) AddFlags(cmd *cobra.Command) {
9595
_ = cmd.MarkFlagFilename("key", certificateExts...)
9696

9797
cmd.Flags().StringVar(&o.BundlePath, "bundle", "",
98-
"write everything required to verify the blob to a FILE")
98+
"write everything required to verify the blob to a FILE (use \"-\" for stdout)")
9999
_ = cmd.MarkFlagFilename("bundle", bundleExts...)
100100

101101
cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", true,

cmd/cosign/cli/signcommon/common.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,11 @@ func WriteNewBundleWithSigningConfig(ctx context.Context, ko options.KeyOpts, ce
514514
}
515515

516516
if bundleOpts.BundlePath != "" {
517+
// If bundleOpts.BundlePath is "-", write to stdout with trailing newline
518+
if bundleOpts.BundlePath == "-" {
519+
fmt.Fprintln(os.Stdout, string(bundle))
520+
return nil
521+
}
517522
if err := os.WriteFile(bundleOpts.BundlePath, bundle, 0600); err != nil {
518523
return fmt.Errorf("creating bundle file: %w", err)
519524
}

doc/cosign_attest-blob.md

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)