-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcosign-extract.sh
More file actions
executable file
·636 lines (556 loc) · 21.4 KB
/
cosign-extract.sh
File metadata and controls
executable file
·636 lines (556 loc) · 21.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
#!/usr/bin/env bash
set -euo pipefail
# Check shell compatibility
if [[ -z "${BASH_VERSION:-}" ]]; then
echo "Warning: This script was designed for bash but is running in: ${0##*/}" >&2
echo "Some features may not work correctly in zsh or other shells." >&2
echo "For best results, run with: bash $0 $*" >&2
echo "" >&2
fi
show_help() {
cat <<EOF
Usage:
$0 --type TYPE --image IMAGE[:TAG] [--choice index|all] [--output FILE] [--verify] [--no-extraction|--predicate-only]
$0 --image IMAGE[:TAG] --choice all --output DIR # extract ALL types
$0 --image IMAGE[:TAG] --list [--show-null]
$0 --image IMAGE[:TAG] --inspect-null
$0 --type TYPE --image IMAGE[:TAG] --verify --no-extraction # verify only, no extraction
$0 --type TYPE --image IMAGE[:TAG] --output FILE --predicate-only # extract only predicate content
Options:
--type TYPE Attestation type (slsa|cyclonedx|spdx|vuln|license|triage|custom)
--image IMAGE Fully qualified image reference (required)
--choice Which attestation to fetch: index, all
--last Automatically select the most recent attestation if multiple exist
--output PATH Output file (single type) or directory (all types)
--list List available predicateTypes and counts
--show-null Show entries missing predicateType in --list
--inspect-null Inspect referrers missing predicateType
--verify Verify attestations using cosign before extraction
--no-extraction Skip extraction and content output (useful with --verify for verification-only)
--predicate-only Extract only the predicate content, not the full attestation envelope (mutually exclusive with --no-extraction)
--certificate-oidc-issuer ISSUER OIDC issuer for verification (default: https://token.actions.githubusercontent.com)
--certificate-identity-regexp REGEX Identity regexp for verification (default: Aleph Alpha workflows)
-h, --help Show this help
Verification:
When --verify is used, attestations are verified using cosign verify-attestation before extraction.
Default verification uses GitHub Actions OIDC issuer and Aleph Alpha workflow identity patterns.
EOF
}
TYPE=""
IMAGE=""
CHOICE=""
OUTPUT_FILE=""
LIST_ONLY=false
SHOW_NULL=false
INSPECT_NULL=false
VERIFY=false
NO_EXTRACTION=false
PREDICATE_ONLY=false
USE_LAST=false
CERTIFICATE_OIDC_ISSUER="https://token.actions.githubusercontent.com"
CERTIFICATE_IDENTITY_REGEXP="https://github.com/Aleph-Alpha/shared-workflows/.github/workflows/(build-and-push|scan-and-reattest).yaml@.*"
while [[ $# -gt 0 ]]; do
case "$1" in
--type) TYPE="$2"; shift 2 ;;
--image) IMAGE="$2"; shift 2 ;;
--choice) CHOICE="$2"; shift 2 ;;
--last) USE_LAST=true; shift ;;
--output) OUTPUT_FILE="$2"; shift 2 ;;
--list) LIST_ONLY=true; shift ;;
--show-null) SHOW_NULL=true; shift ;;
--inspect-null) INSPECT_NULL=true; shift ;;
--verify) VERIFY=true; shift ;;
--no-extraction) NO_EXTRACTION=true; shift ;;
--predicate-only) PREDICATE_ONLY=true; shift ;;
--certificate-oidc-issuer) CERTIFICATE_OIDC_ISSUER="$2"; shift 2 ;;
--certificate-identity-regexp) CERTIFICATE_IDENTITY_REGEXP="$2"; shift 2 ;;
-h|--help) show_help; exit 0 ;;
*) echo "❌ Unknown option: $1"; show_help; exit 1 ;;
esac
done
# Validate mutually exclusive options
if $PREDICATE_ONLY && $NO_EXTRACTION; then
echo "❌ Conflicting options: --predicate-only and --no-extraction cannot be used together"
echo " --predicate-only extracts only the predicate content"
echo " --no-extraction skips all content extraction"
exit 1
fi
if [[ -z "$IMAGE" ]]; then
echo "❌ Missing required --image"
show_help
exit 1
fi
# Validate option combinations
if $NO_EXTRACTION && [ -n "$OUTPUT_FILE" ]; then
echo "❌ --no-extraction and --output cannot be used together"
exit 1
fi
if $NO_EXTRACTION && "$LIST_ONLY"; then
echo "❌ --no-extraction and --list cannot be used together"
exit 1
fi
if $NO_EXTRACTION && "$INSPECT_NULL"; then
echo "❌ --no-extraction and --inspect-null cannot be used together"
exit 1
fi
# Map type → predicateType
case "$TYPE" in
slsa) PRED_TYPE="https://slsa.dev/provenance/v1" ;;
cyclonedx) PRED_TYPE="https://cyclonedx.org/bom" ;;
spdx) PRED_TYPE="https://spdx.dev/Document" ;;
vuln) PRED_TYPE="https://cosign.sigstore.dev/attestation/vuln/v1" ;;
license) PRED_TYPE="https://aleph-alpha.com/attestations/license/v1" ;;
triage) PRED_TYPE="https://aleph-alpha.com/attestations/triage/v1" ;;
custom) PRED_TYPE="https://cosign.sigstore.dev/attestation/v1" ;;
"") PRED_TYPE="" ;; # allowed in --list/--inspect-null/all-types
*) echo "❌ Unknown type: $TYPE"; exit 1 ;;
esac
# If no type, list, or inspect-null is specified, default to --list
if [[ -z "$TYPE" && "$LIST_ONLY" == "false" && "$INSPECT_NULL" == "false" ]]; then
LIST_ONLY=true
fi
# Spinner function for loading indicator
spinner() {
local pid=$1
local spinstr='|/-\'
local first=true
while kill -0 "$pid" 2>/dev/null; do
local temp=${spinstr#?}
if $first; then
printf "%c" "$spinstr"
first=false
else
printf "\b%c" "$spinstr"
fi
spinstr=$temp${spinstr%"$temp"}
sleep 0.1
done
printf "\b \b" # Clear spinner character (backspace, space, backspace)
}
# Map predicateType → nice filename
pretty_name() {
case "$1" in
"https://slsa.dev/provenance/v1") echo "slsa" ;;
"https://cyclonedx.org/bom") echo "cyclonedx" ;;
"https://spdx.dev/Document") echo "spdx" ;;
"https://cosign.sigstore.dev/attestation/vuln/v1") echo "vuln" ;;
"https://aleph-alpha.com/attestations/license/v1") echo "license" ;;
"https://aleph-alpha.com/attestations/triage/v1") echo "triage" ;;
*) echo "$(echo "$1" | sed 's|https\?://||; s|[^A-Za-z0-9._-]|_|g')" ;;
esac
}
# Verify attestation using cosign
verify_attestation() {
local pred_type="$1"
local image="$2"
if ! $VERIFY; then
return 0
fi
echo "🔐 Verifying attestation with cosign..."
echo " ↳ Type: $pred_type"
echo " ↳ OIDC Issuer: $CERTIFICATE_OIDC_ISSUER"
echo " ↳ Identity Regexp: $CERTIFICATE_IDENTITY_REGEXP"
# Check if cosign is available
if ! command -v cosign >/dev/null 2>&1; then
echo "❌ cosign command not found. Please install cosign to use --verify option."
echo " Installation: https://docs.sigstore.dev/cosign/installation/"
exit 1
fi
# Perform verification
local temp_output
temp_output=$(mktemp 2>/dev/null || mktemp -t cosign-extract)
if cosign verify-attestation \
--type "$pred_type" \
--new-bundle-format \
--certificate-oidc-issuer="$CERTIFICATE_OIDC_ISSUER" \
--certificate-identity-regexp="$CERTIFICATE_IDENTITY_REGEXP" \
"$image" > "$temp_output" 2>&1; then
echo "✅ Attestation verification successful"
rm -f "$temp_output"
return 0
else
echo "❌ Attestation verification failed:"
cat "$temp_output"
rm -f "$temp_output"
return 1
fi
}
# Show helpful info about authentication
echo "ℹ️ Note: If you encounter authentication errors, ensure you're logged in to the registry:"
echo " podman login <registry>"
echo ""
# Resolve tag -> digest
if command -v crane >/dev/null 2>&1; then
if DIGEST=$(crane digest "$IMAGE" 2>/dev/null); then
echo "ℹ️ Using image digest: $DIGEST"
else
echo "❌ Failed to resolve image digest with crane"
exit 1
fi
else
echo "❌ crane command not found. Please install crane to resolve image digests."
echo " Installation: https://github.com/google/go-containerregistry/blob/main/cmd/crane/README.md"
exit 1
fi
# --list mode
if $LIST_ONLY; then
printf "🔎 Available predicateTypes for this image: "
# Create temp file for results before background process
TEMP_RESULT=$(mktemp 2>/dev/null || mktemp -t cosign-extract-result)
# Run the work in background and show spinner
(
# Get all bundle referrers
REFERRERS=$(oras discover "$IMAGE@$DIGEST" --format json \
| jq -r '.referrers[] | select(.artifactType=="application/vnd.dev.sigstore.bundle.v0.3+json") | .digest')
# Extract predicateTypes from bundle payloads
PRED_TYPES=()
for d in $REFERRERS; do
layer_digest=$(oras manifest fetch "$IMAGE@$d" 2>/dev/null | jq -r '.layers[0].digest')
if [ -z "$layer_digest" ] || [ "$layer_digest" == "null" ]; then
continue
fi
bundle=$(mktemp 2>/dev/null || mktemp -t cosign-extract)
if ! oras blob fetch "$IMAGE@$layer_digest" --output "$bundle" >/dev/null 2>&1; then
rm -f "$bundle"
continue
fi
# Try to extract predicateType from bundle payload
if jq -e '.dsseEnvelope.payload' "$bundle" >/dev/null 2>&1; then
raw=$(jq -r '.dsseEnvelope.payload' "$bundle" | base64 -d 2>/dev/null || jq -r '.dsseEnvelope.payload' "$bundle" | base64 -D 2>/dev/null)
if [ -n "$raw" ]; then
ptype=$(echo "$raw" | jq -r '.predicateType // empty' 2>/dev/null)
if [ -n "$ptype" ] && [ "$ptype" != "null" ]; then
PRED_TYPES+=("$ptype")
fi
fi
fi
rm -f "$bundle"
done
# Store results
if [ ${#PRED_TYPES[@]} -eq 0 ]; then
echo "FALLBACK" > "$TEMP_RESULT"
if $SHOW_NULL; then
oras discover "$IMAGE@$DIGEST" --format json \
| jq -r '.referrers[].annotations["dev.sigstore.bundle.predicateType"]' \
| sort | uniq -c | sed 's/^/ /' >> "$TEMP_RESULT"
else
oras discover "$IMAGE@$DIGEST" --format json \
| jq -r '.referrers[].annotations["dev.sigstore.bundle.predicateType"] // empty' \
| sed '/^$/d' \
| sort | uniq -c | sed 's/^/ /' >> "$TEMP_RESULT"
fi
else
printf '%s\n' "${PRED_TYPES[@]}" | sort | uniq -c | sed 's/^/ /' > "$TEMP_RESULT"
fi
) &
WORK_PID=$!
# Show spinner while work is running
spinner "$WORK_PID"
# Wait for work to complete
wait "$WORK_PID"
# Display results
echo "" # New line after spinner
if [ -f "$TEMP_RESULT" ]; then
if head -1 "$TEMP_RESULT" | grep -q "^FALLBACK$"; then
echo " (No predicateTypes found in bundles, checking annotations as fallback...)"
tail -n +2 "$TEMP_RESULT"
else
cat "$TEMP_RESULT"
fi
rm -f "$TEMP_RESULT"
fi
exit 0
fi
# --inspect-null mode
if $INSPECT_NULL; then
echo "🔎 Inspecting referrers with missing predicateType..."
NULL_REFS=$(oras discover "$IMAGE@$DIGEST" --format json \
| jq -r '.referrers[] | select(.annotations["dev.sigstore.bundle.predicateType"]==null) | .digest')
if [ -z "$NULL_REFS" ]; then
echo "✅ No null predicateType referrers found."
exit 0
fi
for d in $NULL_REFS; do
echo "----- Referrer $d -----"
layer_digest=$(oras manifest fetch "$IMAGE@$d" | jq -r '.layers[0].digest')
bundle=$(mktemp 2>/dev/null || mktemp -t cosign-extract)
oras blob fetch "$IMAGE@$layer_digest" --output "$bundle" >/dev/null
if jq -e '.dsseEnvelope.payload' "$bundle" >/dev/null 2>&1; then
raw=$(jq -r '.dsseEnvelope.payload' "$bundle" | base64 -d 2>/dev/null || jq -r '.dsseEnvelope.payload' "$bundle" | base64 -D)
echo "Inner predicateType: $(echo "$raw" | jq -r '.predicateType')"
echo "Predicate (truncated):"
echo "$raw" | jq '.predicate' | head -20
else
echo "⚠️ Not an attestation bundle (likely signature/raw blob)"
jq . "$bundle" | head -20
fi
rm -f "$bundle"
done
exit 0
fi
# Special case: extract ALL types
if [[ -z "$TYPE" && "$CHOICE" == "all" ]]; then
if [ -z "$OUTPUT_FILE" ]; then
echo "❌ When extracting all types, you must provide --output <directory>"
exit 1
fi
if [[ "$OUTPUT_FILE" =~ \.json$ ]]; then
echo "❌ --output must be a directory in all-types mode (got something ending with .json: $OUTPUT_FILE)"
exit 1
fi
if [ -e "$OUTPUT_FILE" ] && [ ! -d "$OUTPUT_FILE" ]; then
echo "❌ --output must be a directory (got a file: $OUTPUT_FILE)"
exit 1
fi
mkdir -p "$OUTPUT_FILE"
echo "🔎 Extracting all attestations for $IMAGE@$DIGEST ..."
REFERRERS=$(oras discover "$IMAGE@$DIGEST" --format json \
| jq -r '.referrers[] | select(.artifactType=="application/vnd.dev.sigstore.bundle.v0.3+json") | .digest')
# If verification is requested, collect all predicate types first
if $VERIFY; then
echo "🔐 Collecting predicate types for verification..."
PRED_TYPES=()
for d in $REFERRERS; do
layer_digest=$(oras manifest fetch "$IMAGE@$d" | jq -r '.layers[0].digest')
bundle=$(mktemp 2>/dev/null || mktemp -t cosign-extract)
oras blob fetch "$IMAGE@$layer_digest" --output "$bundle" >/dev/null
raw=$(jq -r '.dsseEnvelope.payload' "$bundle" | base64 -d 2>/dev/null || jq -r '.dsseEnvelope.payload' "$bundle" | base64 -D)
ptype=$(echo "$raw" | jq -r '.predicateType')
rm -f "$bundle"
# Add to array if not already present
if [[ ! " ${PRED_TYPES[@]} " =~ " ${ptype} " ]]; then
PRED_TYPES+=("$ptype")
fi
done
# Verify each unique predicate type
for ptype in "${PRED_TYPES[@]}"; do
verify_attestation "$ptype" "$IMAGE@$DIGEST"
if [ $? -ne 0 ]; then
echo "❌ Verification failed for type $ptype, aborting extraction"
exit 1
fi
done
# If we only need verification and no extraction, we're done
if $NO_EXTRACTION; then
echo "✅ Verification complete. All ${#PRED_TYPES[@]} attestation types are valid."
exit 0
fi
fi
idx=1
for d in $REFERRERS; do
layer_digest=$(oras manifest fetch "$IMAGE@$d" | jq -r '.layers[0].digest')
bundle=$(mktemp 2>/dev/null || mktemp -t cosign-extract)
oras blob fetch "$IMAGE@$layer_digest" --output "$bundle" >/dev/null
raw=$(jq -r '.dsseEnvelope.payload' "$bundle" | base64 -d 2>/dev/null || jq -r '.dsseEnvelope.payload' "$bundle" | base64 -D)
ptype=$(echo "$raw" | jq -r '.predicateType')
base=$(pretty_name "$ptype")
if $NO_EXTRACTION; then
echo "✅ Attestation $idx ($ptype) found and verified (content extraction skipped)"
else
file="$OUTPUT_FILE/${base}-${idx}.json"
echo "$raw" | jq . > "$file"
echo "💾 Attestation $idx ($ptype) written to $file"
fi
rm -f "$bundle"
idx=$((idx+1))
done
exit 0
fi
# Otherwise: extract a specific type
# First verify the attestation if requested
if $VERIFY; then
verify_attestation "$PRED_TYPE" "$IMAGE@$DIGEST"
if [ $? -ne 0 ]; then
echo "❌ Verification failed, aborting extraction"
exit 1
fi
# If we only need verification and no extraction, we're done
if $NO_EXTRACTION; then
echo "✅ Verification complete. Attestation exists and is valid."
exit 0
fi
fi
DIGESTS=()
# Collect referrers - always check inner predicateType since annotations may be unreliable
# Get all bundle referrers (don't filter by annotation as it may be incorrect in new cosign)
ALL_REFERRERS=$(oras discover "$IMAGE@$DIGEST" --format json \
| jq -r '.referrers[]
| select(.artifactType=="application/vnd.dev.sigstore.bundle.v0.3+json")
| .digest')
# If using --last, reverse the order to get most recent first
# Use portable method to reverse (works on both Linux and macOS)
if $USE_LAST; then
ALL_REFERRERS=$(printf '%s\n' $ALL_REFERRERS | awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--]}')
fi
# Check inner predicateType for all candidates (annotations may be incorrect)
# If no --choice is specified, we can stop after finding the first match
# If --choice is a number or "all", we need to check all to find the right one(s)
STOP_EARLY=false
if [ -z "$CHOICE" ]; then
# Stop after first match if no choice specified (user wants the first/only one)
STOP_EARLY=true
fi
for d in $ALL_REFERRERS; do
if $USE_LAST; then
# When using --last, stop at first match (most recent)
if [ ${#DIGESTS[@]} -gt 0 ]; then
break
fi
elif ! $STOP_EARLY || [ ${#DIGESTS[@]} -eq 0 ]; then
echo "🔎 Checking candidate referrer digest=$d"
fi
layer_digest=$(oras manifest fetch "$IMAGE@$d" 2>/dev/null | jq -r '.layers[0].digest')
if [ -z "$layer_digest" ] || [ "$layer_digest" == "null" ]; then
continue
fi
bundle=$(mktemp 2>/dev/null || mktemp -t cosign-extract)
if ! oras blob fetch "$IMAGE@$layer_digest" --output "$bundle" >/dev/null 2>&1; then
rm -f "$bundle"
continue
fi
raw=$(jq -r '.dsseEnvelope.payload' "$bundle" | base64 -d 2>/dev/null || jq -r '.dsseEnvelope.payload' "$bundle" | base64 -D 2>/dev/null)
if [ -z "$raw" ]; then
rm -f "$bundle"
continue
fi
inner=$(echo "$raw" | jq -r '.predicateType // empty' 2>/dev/null)
if [ -z "$inner" ] || [ "$inner" == "null" ]; then
rm -f "$bundle"
continue
fi
if ! $USE_LAST && ! $STOP_EARLY; then
echo " ↳ inner predicateType=$inner"
fi
rm -f "$bundle"
if [ "$inner" = "$PRED_TYPE" ]; then
DIGESTS+=("$d")
# When using --last or stop early, we found a match, so we're done
if $USE_LAST || ($STOP_EARLY && [ ${#DIGESTS[@]} -eq 1 ]); then
break
fi
fi
done
if [ ${#DIGESTS[@]} -eq 0 ]; then
echo "❌ No attestations found for type=$TYPE (predicateType=$PRED_TYPE)"
echo "ℹ️ Available predicateTypes for this image:"
# Get all bundle referrers and extract predicateTypes from payloads
ALL_REFERRERS=$(oras discover "$IMAGE@$DIGEST" --format json \
| jq -r '.referrers[] | select(.artifactType=="application/vnd.dev.sigstore.bundle.v0.3+json") | .digest')
AVAIL_TYPES=()
for d in $ALL_REFERRERS; do
layer_digest=$(oras manifest fetch "$IMAGE@$d" 2>/dev/null | jq -r '.layers[0].digest')
if [ -z "$layer_digest" ] || [ "$layer_digest" == "null" ]; then
continue
fi
bundle=$(mktemp 2>/dev/null || mktemp -t cosign-extract)
if ! oras blob fetch "$IMAGE@$layer_digest" --output "$bundle" >/dev/null 2>&1; then
rm -f "$bundle"
continue
fi
if jq -e '.dsseEnvelope.payload' "$bundle" >/dev/null 2>&1; then
raw=$(jq -r '.dsseEnvelope.payload' "$bundle" | base64 -d 2>/dev/null || jq -r '.dsseEnvelope.payload' "$bundle" | base64 -D 2>/dev/null)
if [ -n "$raw" ]; then
ptype=$(echo "$raw" | jq -r '.predicateType // empty' 2>/dev/null)
if [ -n "$ptype" ] && [ "$ptype" != "null" ]; then
AVAIL_TYPES+=("$ptype")
fi
fi
fi
rm -f "$bundle"
done
if [ ${#AVAIL_TYPES[@]} -eq 0 ]; then
# Fallback to annotations
oras discover "$IMAGE@$DIGEST" --format json \
| jq -r '.referrers[].annotations["dev.sigstore.bundle.predicateType"] // empty' \
| sed '/^$/d' \
| sort | uniq -c | sed 's/^/ /'
else
printf '%s\n' "${AVAIL_TYPES[@]}" | sort | uniq -c | sed 's/^/ /'
fi
exit 1
fi
# Skip listing when --last is used or when we stopped early (only one match)
if ! $USE_LAST && ! $STOP_EARLY; then
echo "🔎 Found ${#DIGESTS[@]} attestations for type=$TYPE:"
i=1
for d in "${DIGESTS[@]}"; do
echo " [$i] $d"
i=$((i+1))
done
fi
# Function to fetch + decode one attestation
fetch_attestation() {
local ref_digest="$1"
local index="${2:-}"
local bundle
bundle=$(mktemp 2>/dev/null || mktemp -t cosign-extract)
# fetch manifest → get blob digest
layer_digest=$(oras manifest fetch "$IMAGE@$ref_digest" | jq -r '.layers[0].digest')
oras blob fetch "$IMAGE@$layer_digest" --output "$bundle" >/dev/null
raw=$(jq -r '.dsseEnvelope.payload' "$bundle" | base64 -d 2>/dev/null || jq -r '.dsseEnvelope.payload' "$bundle" | base64 -D)
# Skip extraction if --no-extraction is used
if $NO_EXTRACTION; then
echo "✅ Attestation found and verified (content extraction skipped)"
rm -f "$bundle"
return 0
fi
local output=""
# If --predicate-only is set, extract only the predicate content
if $PREDICATE_ONLY; then
output=$(echo "$raw" | jq '.predicate')
# Otherwise, handle special cases and return full attestation
elif echo "$raw" | jq -e '.predicate.Data' >/dev/null 2>&1; then
data=$(echo "$raw" | jq -r '.predicate.Data')
if echo "$data" | jq empty >/dev/null 2>&1; then
output=$(echo "$raw" | jq --argjson parsed "$data" '.predicate.Data=$parsed')
else
output=$(echo "$raw" | jq .)
fi
else
output=$(echo "$raw" | jq .)
fi
if [ -n "$OUTPUT_FILE" ]; then
if [ -n "$index" ]; then
file="${OUTPUT_FILE%.json}-$index.json"
echo "$output" > "$file"
echo "💾 Attestation $index written to $file"
else
echo "$output" > "$OUTPUT_FILE"
echo "💾 Attestation written to $OUTPUT_FILE"
fi
else
echo "$output"
fi
rm -f "$bundle"
}
# Decide what to extract
if [ "$CHOICE" == "all" ]; then
idx=1
for d in "${DIGESTS[@]}"; do
echo "----- Attestation $d -----"
fetch_attestation "$d" "$idx"
echo ""
idx=$((idx+1))
done
else
if [ ${#DIGESTS[@]} -eq 1 ]; then
fetch_attestation "${DIGESTS[0]}"
else
if [ -z "$CHOICE" ]; then
if $USE_LAST; then
# Automatically select the last (most recent) attestation
CHOICE=${#DIGESTS[@]}
echo "ℹ️ Automatically selecting most recent attestation [$CHOICE/${#DIGESTS[@]}]"
else
echo -n "Select attestation [1-${#DIGESTS[@]}]: "
read -r CHOICE
fi
fi
INDEX=$((CHOICE-1))
if [ $INDEX -lt 0 ] || [ $INDEX -ge ${#DIGESTS[@]} ]; then
echo "❌ Invalid choice. Pick 1..${#DIGESTS[@]} or 'all'."
exit 1
fi
fetch_attestation "${DIGESTS[$INDEX]}"
fi
fi