Skip to content

Commit 0c515fc

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 pred.json \ --type slsaprovenance --bundle=- blob1.txt >> attestations.jsonl cosign attest-blob --key key.key --predicate pred.json \ --type slsaprovenance --bundle=- blob2.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 f9a9a0b commit 0c515fc

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
@@ -182,18 +182,24 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
182182
}
183183
}
184184

185-
if err := os.WriteFile(c.BundlePath, contents, 0600); err != nil {
186-
return fmt.Errorf("create bundle file: %w", err)
185+
// If BundlePath is "-", write to stdout with trailing newline
186+
if c.BundlePath == "-" {
187+
fmt.Fprintln(os.Stdout, string(contents))
188+
} else {
189+
if err := os.WriteFile(c.BundlePath, contents, 0600); err != nil {
190+
return fmt.Errorf("create bundle file: %w", err)
191+
}
192+
fmt.Fprintln(os.Stderr, "Bundle wrote in the file ", c.BundlePath)
187193
}
188-
fmt.Fprintln(os.Stderr, "Bundle wrote in the file ", c.BundlePath)
189194
}
190195

191196
if c.OutputSignature != "" {
192197
if err := os.WriteFile(c.OutputSignature, bundleComponents.SignedPayload, 0600); err != nil {
193198
return fmt.Errorf("create signature file: %w", err)
194199
}
195200
fmt.Fprintf(os.Stderr, "Signature written in %s\n", c.OutputSignature)
196-
} else {
201+
} else if c.BundlePath != "-" {
202+
// Only output signature to stdout if bundle is not going to stdout
197203
fmt.Fprintln(os.Stdout, string(bundleComponents.SignedPayload))
198204
}
199205

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 <FILE> --type <TYPE> --bundle=- <BLOB1> >> attestations.jsonl
59+
cosign attest-blob --key cosign.key --predicate <FILE> --type <TYPE> --bundle=- <BLOB2> >> 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
@@ -496,6 +496,11 @@ func WriteNewBundleWithSigningConfig(ctx context.Context, ko options.KeyOpts, ce
496496
}
497497

498498
if bundlePath != "" {
499+
// If bundlePath is "-", write to stdout with trailing newline
500+
if bundlePath == "-" {
501+
fmt.Fprintln(os.Stdout, string(bundle))
502+
return nil
503+
}
499504
if err := os.WriteFile(bundlePath, bundle, 0600); err != nil {
500505
return fmt.Errorf("creating bundle file: %w", err)
501506
}

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)