gh-attestation cmd integration (#8698)

* add attestation cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add codeowners

Signed-off-by: Meredith Lancaster <malancas@github.com>

* update args passed to the attestation cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* rename file

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use gh-attestation branch for passing iostreams from the root

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add package security team entry to codeowners

Signed-off-by: Meredith Lancaster <malancas@github.com>

* start moving over verify cmd and general verification code

Signed-off-by: Meredith Lancaster <malancas@github.com>

* clean up common and verify specific policy code

Signed-off-by: Meredith Lancaster <malancas@github.com>

* move artifact package over

Signed-off-by: Meredith Lancaster <malancas@github.com>

* start pulling in the github api client wrapper

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix imports

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add logger and test packages

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add additional packages to support verify command

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix mock api client

Signed-off-by: Meredith Lancaster <malancas@github.com>

* clean up mock api client

Signed-off-by: Meredith Lancaster <malancas@github.com>

* include missing fields

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use correct owner

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add more mock api client options

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add download cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add inspect cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* pass factory object to inspect cmd, add inspect sub cmd to attestation cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add verify-tuf-root cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* pass iostream struct from command

Signed-off-by: Meredith Lancaster <malancas@github.com>

* rename logger pkg to logger

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix path in codeowners

Signed-off-by: Meredith Lancaster <malancas@github.com>

* formatter

Signed-off-by: Meredith Lancaster <malancas@github.com>

* go mod tidy

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix printf linter issue

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix printf linter issue

Signed-off-by: Meredith Lancaster <malancas@github.com>

* check user's GH host for compatibility

Signed-off-by: Meredith Lancaster <malancas@github.com>

* pass oci client to commands directly

Signed-off-by: Meredith Lancaster <malancas@github.com>

* rename command

Signed-off-by: Meredith Lancaster <malancas@github.com>

* mark tuf-root-verify cmd hidden

Signed-off-by: Meredith Lancaster <malancas@github.com>

* move client initialization back to subcommands

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add more verbose options and logging

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add missing logger

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add testing around OCI and API client

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add integration test

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix file path

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix command

Signed-off-by: Meredith Lancaster <malancas@github.com>

* build executable before integration test

Signed-off-by: Meredith Lancaster <malancas@github.com>

* split integration tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* remove integration test steps

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix flag value

Signed-off-by: Meredith Lancaster <malancas@github.com>

* run integration tests on ubuntu for now

Signed-off-by: Meredith Lancaster <malancas@github.com>

* pull over doc updates

Signed-off-by: Meredith Lancaster <malancas@github.com>

* delete unused test data

Signed-off-by: Meredith Lancaster <malancas@github.com>

* remove Go patch version

Signed-off-by: Meredith Lancaster <malancas@github.com>

* switch assert to require

Signed-off-by: Meredith Lancaster <malancas@github.com>

* rename file

Signed-off-by: Meredith Lancaster <malancas@github.com>

* move integration tests to prexisting test workflow

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use platform matrix for integration tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* simplify build step

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use StringEnumFlag handling

Signed-off-by: Meredith Lancaster <malancas@github.com>

* typo

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use the iostreams.Test helper func

Signed-off-by: Meredith Lancaster <malancas@github.com>

* create interface for oci client

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add tests for oci client

Signed-off-by: Meredith Lancaster <malancas@github.com>

* rename files

Signed-off-by: Meredith Lancaster <malancas@github.com>

* format file

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix shellcheck issues

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use testing TempDir method

Signed-off-by: Meredith Lancaster <malancas@github.com>

* cleanup unused tempdir handling

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use table driven tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* check correct cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* support repo option in download sub cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* switch over to using RunE

Signed-off-by: Meredith Lancaster <malancas@github.com>

* unexport top level subcommand funcs

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add comment around keychain option

Signed-off-by: Meredith Lancaster <malancas@github.com>

* update comments

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix inconsistent naming

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add tests for CLI commands

Signed-off-by: Meredith Lancaster <malancas@github.com>

* check for noattestationsfound err

Signed-off-by: Meredith Lancaster <malancas@github.com>

* try out metadata abstraction instead

Signed-off-by: Meredith Lancaster <malancas@github.com>

* switch to using MetadataStore abstraction

Signed-off-by: Meredith Lancaster <malancas@github.com>

* include test case with failing metadata store

Signed-off-by: Meredith Lancaster <malancas@github.com>

* look for err specific to file write

Signed-off-by: Meredith Lancaster <malancas@github.com>

* unexport fields

Signed-off-by: Meredith Lancaster <malancas@github.com>

* return err when an unsupported hash alg is provided

Signed-off-by: Meredith Lancaster <malancas@github.com>

* PrintTableToStdOut returns err when rendering fails

Signed-off-by: Meredith Lancaster <malancas@github.com>

* start adding sigstore verifier unit tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add more sigstore verifier specific tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use cli table printer

Signed-off-by: Meredith Lancaster <malancas@github.com>

* return JSON results in slice instead of table

Signed-off-by: Meredith Lancaster <malancas@github.com>

* move mock client to test file

Signed-off-by: Meredith Lancaster <malancas@github.com>

* remove unneeded table printer method

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add initial tests for tufrootverify cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* formatting

Signed-off-by: Meredith Lancaster <malancas@github.com>

* cleanup method

Signed-off-by: Meredith Lancaster <malancas@github.com>

* close file in error handling branch

Signed-off-by: Meredith Lancaster <malancas@github.com>

* normalize artifact path

Signed-off-by: Meredith Lancaster <malancas@github.com>

* remove unneeded embedded file system

Signed-off-by: Meredith Lancaster <malancas@github.com>

* include image name reference err

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use GH_DEBUG value for io handling

Signed-off-by: Meredith Lancaster <malancas@github.com>

* remove quiet and verbose flags

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add more tufrootveriify tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* GitHubTUFOptions no longer needs to return error

Signed-off-by: Meredith Lancaster <malancas@github.com>

* remove unneeded slice

Signed-off-by: Meredith Lancaster <malancas@github.com>

* normalize all relative paths

Signed-off-by: Meredith Lancaster <malancas@github.com>

* clean up nil client checks

Signed-off-by: Meredith Lancaster <malancas@github.com>

* set api server based on host

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add comment about http client

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use format flag to handle json output in verify cmd

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use format flag to handle json output

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use normalized path for cli test arg

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add tests for json output

Signed-off-by: Meredith Lancaster <malancas@github.com>

* cleanup error wrapping

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use test fixtures correctly by normalizing path

Signed-off-by: Meredith Lancaster <malancas@github.com>

* dont clean

Signed-off-by: Meredith Lancaster <malancas@github.com>

* escape backwards slash for windows files with replace

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use strings.Split func

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use strings.Replace for all command tests

Signed-off-by: Meredith Lancaster <malancas@github.com>

* use CLI cache dir to store tuf metadata

Signed-off-by: Meredith Lancaster <malancas@github.com>

* Tweaked docstrings for gh attestation download

* Tweaked docstrings for gh attestation verify

* Fix for bug in gh attestation where the wrong hostname was being passed to the API client.

* lets hide tuf-root-verify eh?

* Forgot verify's short str.

* add remote verification test

Signed-off-by: Meredith Lancaster <malancas@github.com>

* Revert "add remote verification test"

This reverts commit c0ceb99ca8.

* update json result handling

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add json tags to struct returned by command

Signed-off-by: Meredith Lancaster <malancas@github.com>

* fix how json results are handled

Signed-off-by: Meredith Lancaster <malancas@github.com>

* add test to ensure JSON output is valid

Signed-off-by: Meredith Lancaster <malancas@github.com>

---------

Signed-off-by: Meredith Lancaster <malancas@github.com>
Co-authored-by: Phill MV <phillmv@github.com>
This commit is contained in:
Meredith Lancaster 2024-04-01 11:13:47 -06:00 committed by GitHub
parent ec812a16f7
commit 90b7bf97c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 4893 additions and 53 deletions

3
.github/CODEOWNERS vendored
View file

@ -2,3 +2,6 @@
pkg/cmd/codespace/ @cli/codespaces
internal/codespaces/ @cli/codespaces
# Limit Package Security team ownership to the attestation command package
pkg/cmd/attestation/ @cli/package-security

View file

@ -1,4 +1,4 @@
name: Tests
name: Unit and Integration Tests
on: [push, pull_request]
permissions:
@ -37,3 +37,27 @@ jobs:
- name: Build
run: go build -v ./cmd/gh
integration-tests:
env:
GH_TOKEN: ${{ github.token }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Set up Go 1.21
uses: actions/setup-go@v5
with:
go-version: 1.21
- name: Check out code
uses: actions/checkout@v4
- name: Build executable
run: make
- name: Run attestation command integration Tests
run: ./test/integration/attestation-cmd/download-and-verify-package-attestation.sh

111
go.mod
View file

@ -14,14 +14,17 @@ require (
github.com/cli/safeexec v1.0.1
github.com/cpuguy83/go-md2man/v2 v2.0.4
github.com/creack/pty v1.1.21
github.com/distribution/reference v0.5.0
github.com/gabriel-vasile/mimetype v1.4.3
github.com/gdamore/tcell/v2 v2.5.4
github.com/google/go-cmp v0.5.9
github.com/google/go-cmp v0.6.0
github.com/google/go-containerregistry v0.19.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gorilla/websocket v1.4.2
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.3.0
github.com/henvic/httpretty v0.1.3
github.com/in-toto/in-toto-golang v0.9.0
github.com/joho/godotenv v1.5.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-colorable v0.1.13
@ -29,18 +32,20 @@ require (
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/microsoft/dev-tunnels v0.0.25
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38
github.com/opentracing/opentracing-go v1.1.0
github.com/opentracing/opentracing-go v1.2.0
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278
github.com/spf13/cobra v1.6.1
github.com/sigstore/protobuf-specs v0.3.0
github.com/sigstore/sigstore-go v0.2.1-0.20240222221148-8bd2a8139edc
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
github.com/zalando/go-keyring v0.2.4
golang.org/x/crypto v0.17.0
golang.org/x/sync v0.1.0
golang.org/x/term v0.15.0
golang.org/x/crypto v0.19.0
golang.org/x/sync v0.6.0
golang.org/x/term v0.17.0
golang.org/x/text v0.14.0
google.golang.org/grpc v1.56.3
google.golang.org/grpc v1.61.0
google.golang.org/protobuf v1.33.0
gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v3 v3.0.1
@ -49,41 +54,113 @@ require (
require (
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/alessio/shellescape v1.4.2 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cli/browser v1.3.0 // indirect
github.com/cli/shurcooL-graphql v0.0.4 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 // indirect
github.com/danieljoos/wincred v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/docker/cli v24.0.0+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v24.0.7+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/fatih/color v1.14.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.22.0 // indirect
github.com/go-openapi/errors v0.21.0 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/loads v0.21.5 // indirect
github.com/go-openapi/runtime v0.27.1 // indirect
github.com/go-openapi/spec v0.20.14 // indirect
github.com/go-openapi/strfmt v0.22.0 // indirect
github.com/go-openapi/swag v0.22.9 // indirect
github.com/go-openapi/validate v0.22.6 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/certificate-transparency-go v1.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/itchyny/gojq v0.12.13 // indirect
github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.13.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rodaine/table v1.0.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sassoftware/relic v7.2.1+incompatible // indirect
github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
github.com/sigstore/rekor v1.3.5 // indirect
github.com/sigstore/sigstore v1.8.1 // indirect
github.com/sigstore/timestamp-authority v1.2.2 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/viper v1.18.2 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/theupdateframework/go-tuf v0.7.0 // indirect
github.com/theupdateframework/go-tuf/v2 v2.0.0-20240222081530-454b12158917 // indirect
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/transparency-dev/merkle v0.0.2 // indirect
github.com/vbatts/tar-split v0.11.3 // indirect
github.com/yuin/goldmark v1.5.2 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
golang.org/x/net v0.17.0 // indirect
go.mongodb.org/mongo-driver v1.13.1 // indirect
go.opentelemetry.io/otel v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.15.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.18.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect
gopkg.in/go-jose/go-jose.v2 v2.6.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
k8s.io/klog/v2 v2.120.0 // indirect
)

418
go.sum
View file

@ -1,5 +1,31 @@
cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
cloud.google.com/go/kms v1.15.5 h1:pj1sRfut2eRbD9pFRjNnPNg/CzJPuQAzUujMIM1vVeM=
cloud.google.com/go/kms v1.15.5/go.mod h1:cU2H5jnp6G2TDpUGZyqTCoy1n16fbubHZjmVXSMtwDI=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg=
github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
@ -8,14 +34,54 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.49.21 h1:Rl8KW6HqkwzhATwvXhyr7vD4JFUMi7oXGAw9SrxxIFY=
github.com/aws/aws-sdk-go v1.49.21/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o=
github.com/aws/aws-sdk-go-v2/config v1.26.6/go.mod h1:uKU6cnDmYCvJ+pxO9S4cWDb2yWWIH5hra+32hVh1MI4=
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
github.com/aws/aws-sdk-go-v2/service/kms v1.27.9 h1:W9PbZAZAEcelhhjb7KuwUtf+Lbc+i7ByYJRuWLlnxyQ=
github.com/aws/aws-sdk-go-v2/service/kms v1.27.9/go.mod h1:2tFmR7fQnOdQlM2ZCEPpFnBIQD1U8wmXmduBgZbOag0=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY=
github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
@ -32,70 +98,199 @@ github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY=
github.com/cli/shurcooL-graphql v0.0.4/go.mod h1:3waN4u02FiZivIV+p1y4d0Jo1jc6BViMA73C+sZo2fk=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 h1:vU+EP9ZuFUCYE0NYLwTSob+3LNEJATzNfP/DC7SWGWI=
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc=
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE=
github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc=
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I=
github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM=
github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.22.0 h1:wQ/d07nf78HNj4u+KiSY0sT234IAyePPbMgpUjUJQR0=
github.com/go-openapi/analysis v0.22.0/go.mod h1:acDnkkCI2QxIo8sSIPgmp1wUlRohV7vfGtAIVae73b0=
github.com/go-openapi/errors v0.21.0 h1:FhChC/duCnfoLj1gZ0BgaBmzhJC2SL/sJr8a2vAobSY=
github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho=
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
github.com/go-openapi/loads v0.21.5 h1:jDzF4dSoHw6ZFADCGltDb2lE4F6De7aWSpe+IcsRzT0=
github.com/go-openapi/loads v0.21.5/go.mod h1:PxTsnFBoBe+z89riT+wYt3prmSBP6GDAQh2l9H1Flz8=
github.com/go-openapi/runtime v0.27.1 h1:ae53yaOoh+fx/X5Eaq8cRmavHgDma65XPZuvBqvJYto=
github.com/go-openapi/runtime v0.27.1/go.mod h1:fijeJEiEclyS8BRurYE1DE5TLb9/KZl6eAdbzjsrlLU=
github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do=
github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
github.com/go-openapi/strfmt v0.22.0 h1:Ew9PnEYc246TwrEspvBdDHS4BVKXy/AOVsfqGDgAcaI=
github.com/go-openapi/strfmt v0.22.0/go.mod h1:HzJ9kokGIju3/K6ap8jL+OlGAbjpSv27135Yr9OivU4=
github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE=
github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE=
github.com/go-openapi/validate v0.22.6 h1:+NhuwcEYpWdO5Nm4bmvhGLW0rt1Fcc532Mu3wpypXfo=
github.com/go-openapi/validate v0.22.6/go.mod h1:eaddXSqKeTg5XpSmj1dYyFTK/95n/XHwcOY+BMxKMyM=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/certificate-transparency-go v1.1.7 h1:IASD+NtgSTJLPdzkthwvAG1ZVbF2WtFg4IvoA68XGSw=
github.com/google/certificate-transparency-go v1.1.7/go.mod h1:FSSBo8fyMVgqptbfF6j5p/XNdgQftAhSmXcIxV9iphE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.19.0 h1:uIsMRBV7m/HDkDxE/nXMnv1q+lOOSPlQ/ywc5JbB8Ic=
github.com/google/go-containerregistry v0.19.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w=
github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM=
github.com/google/trillian v1.6.0 h1:jMBeDBIkINFvS2n6oV5maDqfRlxREAc6CW9QYWQ0qT4=
github.com/google/trillian v1.6.0/go.mod h1:Yu3nIMITzNhhMJEHjAtp6xKiu+H/iHu2Oq5FjV2mCWI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p2tOJQ=
github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8=
github.com/henvic/httpretty v0.1.3 h1:4A6vigjz6Q/+yAfTD4wqipCv+Px69C7Th/NhT0ApuU8=
github.com/henvic/httpretty v0.1.3/go.mod h1:UUEv7c2kHZ5SPQ51uS3wBpzPDibg2U3Y+IaXyHy5GBg=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU=
github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4=
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7PDk6RgkP+A/SFfUD0ZR/AgG6SpRNEDKZy8=
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b/go.mod h1:hQmNrgofl+IY/8L+n20H6E6PWBBTokdsv+q49j0QhsU=
github.com/jellydator/ttlcache/v3 v3.1.1 h1:RCgYJqo3jgvhl+fEWvjNW8thxGWsgxi+TPhRir1Y9y8=
github.com/jellydator/ttlcache/v3 v3.1.1/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e h1:RLTpX495BXToqxpM90Ws4hXEo4Wfh81jr9DX1n/4WOo=
github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e/go.mod h1:EAuqr9VFWxBi9nD5jc/EA2MT1RFty9288TF6zdtYoCU=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@ -110,6 +305,9 @@ github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
@ -118,6 +316,11 @@ github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3r
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/microsoft/dev-tunnels v0.0.25 h1:UlMKUI+2O8cSu4RlB52ioSyn1LthYSVkJA+CSTsdKoA=
github.com/microsoft/dev-tunnels v0.0.25/go.mod h1:frU++12T/oqxckXkDpTuYa427ncguEOodSPZcGCCrzQ=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
@ -128,12 +331,33 @@ github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0Fr
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d h1:jKIUJdMcIVGOSHi6LSqJqw9RqblyblE2ZrHvFbWR3S0=
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d/go.mod h1:YX2wUZOcJGOIycErz2s9KvDaP0jnWwRCirQMPLPpQ+Y=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@ -142,28 +366,95 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rodaine/table v1.0.1 h1:U/VwCnUxlVYxw8+NJiLIuCxA/xa6jL38MY3FYysVWWQ=
github.com/rodaine/table v1.0.1/go.mod h1:UVEtfBsflpeEcD56nF4F5AocNFta0ZuolpSVdPtlmP4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A=
github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk=
github.com/sassoftware/relic/v7 v7.6.1 h1:O5s8ewCgq5QYNpv45dK4u6IpBmDM9RIcsbf/G1uXepQ=
github.com/sassoftware/relic/v7 v7.6.1/go.mod h1:NxwtWxWxlUa9as2qZi635Ye6bBT/tGnMALLq7dSfOOU=
github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA=
github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278 h1:kdEGVAV4sO46DPtb8k793jiecUEhaX9ixoIBt41HEGU=
github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/sigstore/protobuf-specs v0.3.0 h1:E49qS++llp4psM+3NNVEb+C4AD422bT9VkOQIPrNLpA=
github.com/sigstore/protobuf-specs v0.3.0/go.mod h1:ynKzXpqr3dUj2Xk9O/5ZUhjnpi0F53DNi5AdH6pS3jc=
github.com/sigstore/rekor v1.3.5 h1:QoVXcS7NppKY+rpbEFVHr4evGDZBBSh65X0g8PXoUkQ=
github.com/sigstore/rekor v1.3.5/go.mod h1:CWqOk/fmnPwORQmm7SyDgB54GTJizqobbZ7yOP1lvw8=
github.com/sigstore/sigstore v1.8.1 h1:mAVposMb14oplk2h/bayPmIVdzbq2IhCgy4g6R0ZSjo=
github.com/sigstore/sigstore v1.8.1/go.mod h1:02SL1158BSj15bZyOFz7m+/nJzLZfFd9A8ab3Kz7w/E=
github.com/sigstore/sigstore-go v0.2.1-0.20240222221148-8bd2a8139edc h1:S8mCkKxbnn38DQz41jnPyaSKZYviGT7wMLM+7iHOp3I=
github.com/sigstore/sigstore-go v0.2.1-0.20240222221148-8bd2a8139edc/go.mod h1:yODm8pZ33BSpmezTcvvwzgHZgDxbqB6hBAg1izfG+EQ=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.1 h1:rEDdUefulkIQaMJyzLwtgPDLNXBIltBABiFYfb0YmgQ=
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.1/go.mod h1:RCdYCc1IxCYWzh2IdzdA6Yf7JIY0cMRqH08fpQYechw=
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.1 h1:DvRWG99QGWZC5mp42SEde2Xke/Q384Idnj2da7yB+Mk=
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.1/go.mod h1:s13mo3a0UCQS3+PAUUZfvKe48sMDMsHk2GE1b2YfPcU=
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.1 h1:lwdRsJv1UbBemuk7w5YfXAQilQxMoFevrzamdPbG0wY=
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.1/go.mod h1:2OaSQ80EcdyVRSQ3T4d1lsc6Scopblsiq8U2AEk5K1A=
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.1 h1:9Ki0qudKpc1FQdef7xHO2bkLyTuw+qNUpWRzjBEmF4c=
github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.1/go.mod h1:nhIgyu4YwwNgalIwTGsoAzam16jjAn3ADRSWKbWPwGI=
github.com/sigstore/timestamp-authority v1.2.2 h1:X4qyutnCQqJ0apMewFyx+3t7Tws00JQ/JonBiu3QvLE=
github.com/sigstore/timestamp-authority v1.2.2/go.mod h1:nEah4Eq4wpliDjlY342rXclGSO7Kb9hoRrl9tqLW13A=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug=
github.com/theupdateframework/go-tuf/v2 v2.0.0-20240222081530-454b12158917 h1:Ov8+IAeR7pivNDC0Cd25MyyaCR3WPlGBED4wNxIFQ8s=
github.com/theupdateframework/go-tuf/v2 v2.0.0-20240222081530-454b12158917/go.mod h1:+gWwqe1pk4nvGeOKosGJqPgD+N/kbD9M0QVLL9TGIYU=
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8=
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4=
github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=
github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU=
@ -172,47 +463,85 @@ github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18W
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68=
github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.step.sm/crypto v0.43.0 h1:siTS/iiqaX4qBUeTxVyag5I2rijuKOMDkXSnrKcei7s=
go.step.sm/crypto v0.43.0/go.mod h1:iKrtuRbFlqimEG/+fWSu7kcZzl4Bd/+w5xkuqA5OSic=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@ -220,21 +549,40 @@ golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/api v0.160.0 h1:SEspjXHVqE1m5a1fRy8JFB+5jSu+V0GEDKDghF3ttO4=
google.golang.org/api v0.160.0/go.mod h1:0mu0TpK33qnydLvWqbImq2b1eQ5FHRSDCBzAxX9ZHyw=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg=
google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8=
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/go-jose/go-jose.v2 v2.6.1 h1:qEzJlIDmG9q5VO0M/o8tGS65QMHMS1w01TQJB1VPJ4U=
gopkg.in/go-jose/go-jose.v2 v2.6.1/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
k8s.io/klog/v2 v2.120.0 h1:z+q5mfovBj1fKFxiRzsa2DsJLPIVMk/KFL81LMOfK+8=
k8s.io/klog/v2 v2.120.0/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE=
software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ=

View file

@ -0,0 +1,33 @@
package api
import (
"fmt"
"github.com/sigstore/sigstore-go/pkg/bundle"
)
const (
GetAttestationByRepoAndSubjectDigestPath = "repos/%s/attestations/%s"
GetAttestationByOwnerAndSubjectDigestPath = "orgs/%s/attestations/%s"
)
type ErrNoAttestations struct {
name string
digest string
}
func (e ErrNoAttestations) Error() string {
return fmt.Sprintf("no attestations found for digest %s in %s", e.name, e.digest)
}
func newErrNoAttestations(name, digest string) ErrNoAttestations {
return ErrNoAttestations{name, digest}
}
type Attestation struct {
Bundle *bundle.ProtobufBundle `json:"bundle"`
}
type AttestationsResponse struct {
Attestations []*Attestation `json:"attestations"`
}

View file

@ -0,0 +1,104 @@
package api
import (
"fmt"
"io"
"net/http"
"strings"
"github.com/cli/cli/v2/api"
ioconfig "github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/go-gh/v2/pkg/auth"
)
const (
DefaultLimit = 30
maxLimitForFlag = 1000
maxLimitForFetch = 100
)
type apiClient interface {
RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error)
}
type Client interface {
GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error)
GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error)
}
type LiveClient struct {
api apiClient
host string
logger *ioconfig.Handler
}
func NewLiveClient(hc *http.Client, l *ioconfig.Handler) *LiveClient {
host, _ := auth.DefaultHost()
return &LiveClient{
api: api.NewClientFromHTTP(hc),
host: strings.TrimSuffix(host, "/"),
logger: l,
}
}
func (c *LiveClient) BuildRepoAndDigestURL(repo, digest string) string {
repo = strings.Trim(repo, "/")
return fmt.Sprintf(GetAttestationByRepoAndSubjectDigestPath, repo, digest)
}
// GetByRepoAndDigest fetches the attestation by repo and digest
func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) {
url := c.BuildRepoAndDigestURL(repo, digest)
return c.getAttestations(url, repo, digest, limit)
}
func (c *LiveClient) BuildOwnerAndDigestURL(owner, digest string) string {
owner = strings.Trim(owner, "/")
return fmt.Sprintf(GetAttestationByOwnerAndSubjectDigestPath, owner, digest)
}
// GetByOwnerAndDigest fetches attestation by owner and digest
func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) {
url := c.BuildOwnerAndDigestURL(owner, digest)
return c.getAttestations(url, owner, digest, limit)
}
func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*Attestation, error) {
c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest)
perPage := limit
if perPage <= 0 || perPage > maxLimitForFlag {
return nil, fmt.Errorf("limit must be greater than 0 and less than or equal to %d", maxLimitForFlag)
}
if perPage > maxLimitForFetch {
perPage = maxLimitForFetch
}
// ref: https://github.com/cli/go-gh/blob/d32c104a9a25c9de3d7c7b07a43ae0091441c858/example_gh_test.go#L96
url = fmt.Sprintf("%s?per_page=%d", url, perPage)
var attestations []*Attestation
var resp AttestationsResponse
var err error
// if no attestation or less than limit, then keep fetching
for url != "" && len(attestations) < limit {
url, err = c.api.RESTWithNext(c.host, http.MethodGet, url, nil, &resp)
if err != nil {
return nil, err
}
attestations = append(attestations, resp.Attestations...)
}
if len(attestations) == 0 {
return nil, newErrNoAttestations(name, digest)
}
if len(attestations) > limit {
return attestations[:limit], nil
}
return attestations, nil
}

View file

@ -0,0 +1,174 @@
package api
import (
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/stretchr/testify/require"
)
const (
testRepo = "github/example"
testOwner = "github"
testDigest = "sha256:12313213"
)
func NewClientWithMockGHClient(hasNextPage bool) Client {
fetcher := mockDataGenerator{
NumAttestations: 5,
}
l := io.NewTestHandler()
if hasNextPage {
return &LiveClient{
api: mockAPIClient{
OnRESTWithNext: fetcher.OnRESTSuccessWithNextPage,
},
logger: l,
}
}
return &LiveClient{
api: mockAPIClient{
OnRESTWithNext: fetcher.OnRESTSuccess,
},
logger: l,
}
}
func TestGetURL(t *testing.T) {
c := LiveClient{}
testData := []struct {
repo string
digest string
expected string
}{
{repo: "/github/example/", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"},
{repo: "/github/example", digest: "sha256:12313213", expected: "repos/github/example/attestations/sha256:12313213"},
}
for _, data := range testData {
s := c.BuildRepoAndDigestURL(data.repo, data.digest)
require.Equal(t, data.expected, s)
}
}
func TestGetByDigest(t *testing.T) {
c := NewClientWithMockGHClient(false)
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
require.NoError(t, err)
require.Equal(t, 5, len(attestations))
bundle := (attestations)[0].Bundle
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1")
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit)
require.NoError(t, err)
require.Equal(t, 5, len(attestations))
bundle = (attestations)[0].Bundle
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1")
}
func TestGetByDigestGreaterThanLimit(t *testing.T) {
c := NewClientWithMockGHClient(false)
limit := 3
// The method should return five results when the limit is not set
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, limit)
require.NoError(t, err)
require.Equal(t, 3, len(attestations))
bundle := (attestations)[0].Bundle
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1")
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, limit)
require.NoError(t, err)
require.Equal(t, len(attestations), limit)
bundle = (attestations)[0].Bundle
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1")
}
func TestGetByDigestWithNextPage(t *testing.T) {
c := NewClientWithMockGHClient(true)
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
require.NoError(t, err)
require.Equal(t, len(attestations), 10)
bundle := (attestations)[0].Bundle
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1")
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit)
require.NoError(t, err)
require.Equal(t, len(attestations), 10)
bundle = (attestations)[0].Bundle
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1")
}
func TestGetByDigestGreaterThanLimitWithNextPage(t *testing.T) {
c := NewClientWithMockGHClient(true)
limit := 7
// The method should return five results when the limit is not set
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, limit)
require.NoError(t, err)
require.Equal(t, len(attestations), limit)
bundle := (attestations)[0].Bundle
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1")
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, limit)
require.NoError(t, err)
require.Equal(t, len(attestations), limit)
bundle = (attestations)[0].Bundle
require.Equal(t, bundle.GetMediaType(), "application/vnd.dev.sigstore.bundle+json;version=0.1")
}
func TestGetByDigest_NoAttestationsFound(t *testing.T) {
fetcher := mockDataGenerator{
NumAttestations: 5,
}
c := LiveClient{
api: mockAPIClient{
OnRESTWithNext: fetcher.OnRESTWithNextNoAttestations,
},
logger: io.NewTestHandler(),
}
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
require.Error(t, err)
require.IsType(t, ErrNoAttestations{}, err)
require.Nil(t, attestations)
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit)
require.Error(t, err)
require.IsType(t, ErrNoAttestations{}, err)
require.Nil(t, attestations)
}
func TestGetByDigest_Error(t *testing.T) {
fetcher := mockDataGenerator{
NumAttestations: 5,
}
c := LiveClient{
api: mockAPIClient{
OnRESTWithNext: fetcher.OnRESTWithNextError,
},
logger: io.NewTestHandler(),
}
attestations, err := c.GetByRepoAndDigest(testRepo, testDigest, DefaultLimit)
require.Error(t, err)
require.Nil(t, attestations)
attestations, err = c.GetByOwnerAndDigest(testOwner, testDigest, DefaultLimit)
require.Error(t, err)
require.Nil(t, attestations)
}

View file

@ -0,0 +1,89 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
)
type mockAPIClient struct {
OnRESTWithNext func(hostname, method, p string, body io.Reader, data interface{}) (string, error)
}
func (m mockAPIClient) RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
return m.OnRESTWithNext(hostname, method, p, body, data)
}
type mockDataGenerator struct {
NumAttestations int
}
func (m mockDataGenerator) OnRESTSuccess(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
return m.OnRESTWithNextSuccessHelper(hostname, method, p, body, data, false)
}
func (m mockDataGenerator) OnRESTSuccessWithNextPage(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
// if path doesn't contain after, it means first time hitting the mock server
// so return the first page and return the link header in the response
if !strings.Contains(p, "after") {
return m.OnRESTWithNextSuccessHelper(hostname, method, p, body, data, true)
}
// if path contain after, it means second time hitting the mock server and will not return the link header
return m.OnRESTWithNextSuccessHelper(hostname, method, p, body, data, false)
}
func (m mockDataGenerator) OnRESTWithNextSuccessHelper(hostname, method, p string, body io.Reader, data interface{}, hasNext bool) (string, error) {
atts := make([]*Attestation, m.NumAttestations)
for j := 0; j < m.NumAttestations; j++ {
att := makeTestAttestation()
atts[j] = &att
}
resp := AttestationsResponse{
Attestations: atts,
}
// // Convert the attestations to JSON
b, err := json.Marshal(resp)
if err != nil {
return "", err
}
err = json.Unmarshal(b, &data)
if err != nil {
return "", err
}
if hasNext {
// return a link header with the next page
return fmt.Sprintf("<%s&after=2>; rel=\"next\"", p), nil
}
return "", nil
}
func (m mockDataGenerator) OnRESTWithNextNoAttestations(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
resp := AttestationsResponse{
Attestations: make([]*Attestation, 0),
}
// // Convert the attestations to JSON
b, err := json.Marshal(resp)
if err != nil {
return "", err
}
err = json.Unmarshal(b, &data)
if err != nil {
return "", err
}
return "", nil
}
func (m mockDataGenerator) OnRESTWithNextError(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
return "", errors.New("failed to get attestations")
}

View file

@ -0,0 +1,71 @@
package api
import (
"encoding/json"
"fmt"
"os"
"github.com/sigstore/sigstore-go/pkg/bundle"
)
type MockClient struct {
OnGetByRepoAndDigest func(repo, digest string, limit int) ([]*Attestation, error)
OnGetByOwnerAndDigest func(owner, digest string, limit int) ([]*Attestation, error)
}
func (m MockClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) {
return m.OnGetByRepoAndDigest(repo, digest, limit)
}
func (m MockClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) {
return m.OnGetByOwnerAndDigest(owner, digest, limit)
}
func makeTestAttestation() Attestation {
bundleBytes, err := os.ReadFile("../test/data/sigstore-js-2.1.0-bundle.json")
if err != nil {
panic(err)
}
var b *bundle.ProtobufBundle
err = json.Unmarshal(bundleBytes, &b)
if err != nil {
panic(err)
}
return Attestation{Bundle: b}
}
func OnGetByRepoAndDigestSuccess(repo, digest string, limit int) ([]*Attestation, error) {
att1 := makeTestAttestation()
att2 := makeTestAttestation()
return []*Attestation{&att1, &att2}, nil
}
func OnGetByRepoAndDigestFailure(repo, digest string, limit int) ([]*Attestation, error) {
return nil, fmt.Errorf("failed to fetch by repo and digest")
}
func OnGetByOwnerAndDigestSuccess(owner, digest string, limit int) ([]*Attestation, error) {
att1 := makeTestAttestation()
att2 := makeTestAttestation()
return []*Attestation{&att1, &att2}, nil
}
func OnGetByOwnerAndDigestFailure(owner, digest string, limit int) ([]*Attestation, error) {
return nil, fmt.Errorf("failed to fetch by owner and digest")
}
func NewTestClient() *MockClient {
return &MockClient{
OnGetByRepoAndDigest: OnGetByRepoAndDigestSuccess,
OnGetByOwnerAndDigest: OnGetByOwnerAndDigestSuccess,
}
}
func NewFailTestClient() *MockClient {
return &MockClient{
OnGetByRepoAndDigest: OnGetByRepoAndDigestFailure,
OnGetByOwnerAndDigest: OnGetByOwnerAndDigestFailure,
}
}

View file

@ -0,0 +1,79 @@
package artifact
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
)
type artifactType int
const (
ociArtifactType artifactType = iota
fileArtifactType
)
// DigestedArtifact abstracts the software artifact being verified
type DigestedArtifact struct {
URL string
digest string
digestAlg string
}
func normalizeReference(reference string, pathSeparator rune) (normalized string, artifactType artifactType, err error) {
switch {
case strings.HasPrefix(reference, "oci://"):
return reference[6:], ociArtifactType, nil
case strings.HasPrefix(reference, "file://"):
uri, err := url.ParseRequestURI(reference)
if err != nil {
return "", 0, fmt.Errorf("failed to parse reference URI: %v", err)
}
var path string
if pathSeparator == '/' {
// Unix paths use forward slashes like URIs, so no need to modify
path = uri.Path
} else {
// Windows paths should be normalized to use backslashes
path = strings.ReplaceAll(uri.Path, "/", string(pathSeparator))
// Remove leading slash from Windows paths if present
if strings.HasPrefix(path, string(pathSeparator)) {
path = path[1:]
}
}
return filepath.Clean(path), fileArtifactType, nil
}
// Treat any other reference as a local file path
return filepath.Clean(reference), fileArtifactType, nil
}
func NewDigestedArtifact(client oci.Client, reference, digestAlg string) (artifact *DigestedArtifact, err error) {
normalized, artifactType, err := normalizeReference(reference, os.PathSeparator)
if err != nil {
return nil, err
}
if artifactType == ociArtifactType {
// TODO: should we allow custom digestAlg for OCI artifacts?
return digestContainerImageArtifact(normalized, client)
}
return digestLocalFileArtifact(normalized, digestAlg)
}
// Digest returns the artifact's digest
func (a *DigestedArtifact) Digest() string {
return a.digest
}
// Algorithm returns the artifact's algorithm
func (a *DigestedArtifact) Algorithm() string {
return a.digestAlg
}
// DigestWithAlg returns the digest:algorithm of the artifact
func (a *DigestedArtifact) DigestWithAlg() string {
return fmt.Sprintf("%s:%s", a.digestAlg, a.digest)
}

View file

@ -0,0 +1,99 @@
//go:build !windows
// +build !windows
package artifact
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestNormalizeReference(t *testing.T) {
testCases := []struct {
name string
reference string
pathSeparator rune
expectedResult string
expectedType artifactType
expectedError bool
}{
{
name: "file reference without scheme",
reference: "/path/to/file",
pathSeparator: '/',
expectedResult: "/path/to/file",
expectedType: fileArtifactType,
expectedError: false,
},
{
name: "file scheme uri with %20",
reference: "file:///path/to/file%20with%20spaces",
pathSeparator: '/',
expectedResult: "/path/to/file with spaces",
expectedType: fileArtifactType,
expectedError: false,
},
{
name: "windows file reference without scheme",
reference: `c:\path\to\file`,
pathSeparator: '\\',
expectedResult: `c:\path\to\file`,
expectedType: fileArtifactType,
expectedError: false,
},
{
name: "file reference with scheme",
reference: "file:///path/to/file",
pathSeparator: '/',
expectedResult: "/path/to/file",
expectedType: fileArtifactType,
expectedError: false,
},
{
name: "windows path",
reference: "file:///C:/path/to/file",
pathSeparator: '\\',
expectedResult: `C:\path\to\file`,
expectedType: fileArtifactType,
expectedError: false,
},
{
name: "windows path with backslashes",
reference: "file:///C:\\path\\to\\file",
pathSeparator: '\\',
expectedResult: `C:\path\to\file`,
expectedType: fileArtifactType,
expectedError: false,
},
{
name: "oci reference",
reference: "oci://example.com/repo:tag",
pathSeparator: '/',
expectedResult: "example.com/repo:tag",
expectedType: ociArtifactType,
expectedError: false,
},
{
name: "oci reference with digest",
reference: "oci://example.com/repo@sha256:abcdef1234567890",
pathSeparator: '/',
expectedResult: "example.com/repo@sha256:abcdef1234567890",
expectedType: ociArtifactType,
expectedError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, artifactType, err := normalizeReference(tc.reference, tc.pathSeparator)
if tc.expectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tc.expectedResult, result)
require.Equal(t, tc.expectedType, artifactType)
}
})
}
}

View file

@ -0,0 +1,59 @@
//go:build windows
// +build windows
package artifact
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestNormalizeReference(t *testing.T) {
testCases := []struct {
name string
reference string
pathSeparator rune
expectedResult string
expectedType artifactType
expectedError bool
}{
{
name: "windows file reference without scheme",
reference: `c:\path\to\file`,
pathSeparator: '\\',
expectedResult: `c:\path\to\file`,
expectedType: fileArtifactType,
expectedError: false,
},
{
name: "windows path",
reference: "file:///C:/path/to/file",
pathSeparator: '\\',
expectedResult: `C:\path\to\file`,
expectedType: fileArtifactType,
expectedError: false,
},
{
name: "windows path with backslashes",
reference: "file:///C:\\path\\to\\file",
pathSeparator: '\\',
expectedResult: `C:\path\to\file`,
expectedType: fileArtifactType,
expectedError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, artifactType, err := normalizeReference(tc.reference, tc.pathSeparator)
if tc.expectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tc.expectedResult, result)
require.Equal(t, tc.expectedType, artifactType)
}
})
}
}

View file

@ -0,0 +1,53 @@
package digest
import (
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"fmt"
"hash"
"io"
)
const (
SHA256DigestAlgorithm = "sha256"
SHA512DigestAlgorithm = "sha512"
)
var (
errUnsupportedAlgorithm = fmt.Errorf("unsupported digest algorithm")
validDigestAlgorithms = [...]string{SHA256DigestAlgorithm, SHA512DigestAlgorithm}
)
// IsValidDigestAlgorithm returns true if the provided algorithm is supported
func IsValidDigestAlgorithm(alg string) bool {
for _, a := range validDigestAlgorithms {
if a == alg {
return true
}
}
return false
}
// ValidDigestAlgorithms returns a list of supported digest algorithms
func ValidDigestAlgorithms() []string {
return validDigestAlgorithms[:]
}
func CalculateDigestWithAlgorithm(r io.Reader, alg string) (string, error) {
var h hash.Hash
switch alg {
case SHA256DigestAlgorithm:
h = sha256.New()
case SHA512DigestAlgorithm:
h = sha512.New()
default:
return "", errUnsupportedAlgorithm
}
if _, err := io.Copy(h, r); err != nil {
return "", fmt.Errorf("failed to calculate digest: %v", err)
}
digest := h.Sum(nil)
return hex.EncodeToString(digest), nil
}

View file

@ -0,0 +1,46 @@
package digest
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestArtifactDigestWithAlgorithm(t *testing.T) {
testString := "deadbeef"
sha512TestDigest := "113a3bc783d851fc0373214b19ea7be9fa3de541ecb9fe026d52c603e8ea19c174cc0e9705f8b90d312212c0c3a6d8453ddfb3e3141409cf4bedc8ef033590b4"
sha256TestDigest := "2baf1f40105d9501fe319a8ec463fdf4325a2a5df445adf3f572f626253678c9"
t.Run("sha256", func(t *testing.T) {
reader := strings.NewReader(testString)
digest, err := CalculateDigestWithAlgorithm(reader, "sha256")
assert.Nil(t, err)
assert.Equal(t, sha256TestDigest, digest)
})
t.Run("sha512", func(t *testing.T) {
reader := strings.NewReader(testString)
digest, err := CalculateDigestWithAlgorithm(reader, "sha512")
assert.Nil(t, err)
assert.Equal(t, sha512TestDigest, digest)
})
t.Run("fail with sha384", func(t *testing.T) {
reader := strings.NewReader(testString)
_, err := CalculateDigestWithAlgorithm(reader, "sha384")
require.Error(t, err)
require.ErrorAs(t, err, &errUnsupportedAlgorithm)
})
}
func TestValidDigestAlgorithms(t *testing.T) {
t.Run("includes sha256", func(t *testing.T) {
assert.Contains(t, ValidDigestAlgorithms(), "sha256")
})
t.Run("includes sha512", func(t *testing.T) {
assert.Contains(t, ValidDigestAlgorithms(), "sha512")
})
}

View file

@ -0,0 +1,25 @@
package artifact
import (
"fmt"
"os"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/digest"
)
func digestLocalFileArtifact(filename, digestAlg string) (*DigestedArtifact, error) {
data, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("failed to get open local artifact: %v", err)
}
defer data.Close()
digest, err := digest.CalculateDigestWithAlgorithm(data, digestAlg)
if err != nil {
return nil, fmt.Errorf("failed to calculate local artifact digest: %v", err)
}
return &DigestedArtifact{
URL: fmt.Sprintf("file://%s", filename),
digest: digest,
digestAlg: digestAlg,
}, nil
}

View file

@ -0,0 +1,28 @@
package artifact
import (
"fmt"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/distribution/reference"
)
func digestContainerImageArtifact(url string, client oci.Client) (*DigestedArtifact, error) {
// try to parse the url as a valid registry reference
named, err := reference.Parse(url)
if err != nil {
// cannot be parsed as a registry reference
return nil, fmt.Errorf("artifact %s is not a valid registry reference: %v", url, err)
}
digest, err := client.GetImageDigest(named.String())
if err != nil {
return nil, err
}
return &DigestedArtifact{
URL: fmt.Sprintf("oci://%s", named.String()),
digest: digest.Hex,
digestAlg: digest.Algorithm,
}, nil
}

View file

@ -0,0 +1,52 @@
package artifact
import (
"fmt"
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/stretchr/testify/require"
)
func TestDigestContainerImageArtifact(t *testing.T) {
expectedDigest := "1234567890abcdef"
client := oci.MockClient{}
url := "example.com/repo:tag"
digestedArtifact, err := digestContainerImageArtifact(url, client)
require.NoError(t, err)
require.Equal(t, fmt.Sprintf("oci://%s", url), digestedArtifact.URL)
require.Equal(t, expectedDigest, digestedArtifact.digest)
require.Equal(t, "sha256", digestedArtifact.digestAlg)
}
func TestParseImageRefFailure(t *testing.T) {
client := oci.ReferenceFailClient{}
url := "example.com/repo:tag"
_, err := digestContainerImageArtifact(url, client)
require.Error(t, err)
}
func TestFetchImageFailure(t *testing.T) {
testcase := []struct {
name string
client oci.Client
expectedErr error
}{
{
name: "Fail to authorize with registry",
client: oci.AuthFailClient{},
expectedErr: oci.ErrRegistryAuthz,
},
{
name: "Fail to fetch image due to denial",
client: oci.DeniedClient{},
expectedErr: oci.ErrDenied,
},
}
for _, tc := range testcase {
url := "example.com/repo:tag"
_, err := digestContainerImageArtifact(url, tc.client)
require.ErrorIs(t, err, tc.expectedErr)
}
}

View file

@ -0,0 +1,70 @@
package oci
import (
"errors"
"fmt"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
)
var ErrDenied = errors.New("the provided token was denied access to the requested resource, please check the token's expiration and repository access")
var ErrRegistryAuthz = errors.New("remote registry authorization failed, please authenticate with the registry and try again")
type Client interface {
GetImageDigest(imgName string) (*v1.Hash, error)
}
func checkForUnauthorizedOrDeniedErr(err transport.Error) error {
for _, diagnostic := range err.Errors {
switch diagnostic.Code {
case transport.UnauthorizedErrorCode:
return ErrRegistryAuthz
case transport.DeniedErrorCode:
return ErrDenied
}
}
return nil
}
type LiveClient struct {
parseReference func(string, ...name.Option) (name.Reference, error)
get func(name.Reference, ...remote.Option) (*remote.Descriptor, error)
}
// where name is formed like ghcr.io/github/my-image-repo
func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, error) {
name, err := c.parseReference(imgName)
if err != nil {
return nil, fmt.Errorf("failed to create image tag: %v", err)
}
// The user must already be authenticated with the container registry
// The authn.DefaultKeychain argument indicates that Get should checks the
// user's configuration for the registry credentials
desc, err := c.get(name, remote.WithAuthFromKeychain(authn.DefaultKeychain))
if err != nil {
var transportErr *transport.Error
if errors.As(err, &transportErr) {
if accessErr := checkForUnauthorizedOrDeniedErr(*transportErr); accessErr != nil {
return nil, accessErr
}
}
return nil, fmt.Errorf("failed to fetch remote image: %v", err)
}
return &desc.Digest, nil
}
// Unlike other parts of this command set, we cannot pass a custom HTTP client
// to the go-containerregistry library. This means we have limited visibility
// into the HTTP requests being made to container registries.
func NewLiveClient() *LiveClient {
return &LiveClient{
parseReference: name.ParseReference,
get: remote.Get,
}
}

View file

@ -0,0 +1,83 @@
package oci
import (
"fmt"
"testing"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"github.com/stretchr/testify/require"
)
func TestGetImageDigest_Success(t *testing.T) {
expectedDigest := v1.Hash{
Hex: "1234567890abcdef",
Algorithm: "sha256",
}
c := LiveClient{
parseReference: func(string, ...name.Option) (name.Reference, error) {
return name.Tag{}, nil
},
get: func(name.Reference, ...remote.Option) (*remote.Descriptor, error) {
d := remote.Descriptor{}
d.Digest = expectedDigest
return &d, nil
},
}
digest, err := c.GetImageDigest("test")
require.NoError(t, err)
require.Equal(t, &expectedDigest, digest)
}
func TestGetImageDigest_ReferenceFail(t *testing.T) {
c := LiveClient{
parseReference: func(string, ...name.Option) (name.Reference, error) {
return nil, fmt.Errorf("failed to parse reference")
},
get: func(name.Reference, ...remote.Option) (*remote.Descriptor, error) {
return nil, nil
},
}
digest, err := c.GetImageDigest("test")
require.Error(t, err)
require.Nil(t, digest)
}
func TestGetImageDigest_AuthFail(t *testing.T) {
c := LiveClient{
parseReference: func(string, ...name.Option) (name.Reference, error) {
return name.Tag{}, nil
},
get: func(name.Reference, ...remote.Option) (*remote.Descriptor, error) {
return nil, &transport.Error{Errors: []transport.Diagnostic{{Code: transport.UnauthorizedErrorCode}}}
},
}
digest, err := c.GetImageDigest("test")
require.Error(t, err)
require.ErrorIs(t, err, ErrRegistryAuthz)
require.Nil(t, digest)
}
func TestGetImageDigest_Denied(t *testing.T) {
c := LiveClient{
parseReference: func(string, ...name.Option) (name.Reference, error) {
return name.Tag{}, nil
},
get: func(name.Reference, ...remote.Option) (*remote.Descriptor, error) {
return nil, &transport.Error{Errors: []transport.Diagnostic{{Code: transport.DeniedErrorCode}}}
},
}
digest, err := c.GetImageDigest("test")
require.Error(t, err)
require.ErrorIs(t, err, ErrDenied)
require.Nil(t, digest)
}

View file

@ -0,0 +1,34 @@
package oci
import (
"fmt"
"github.com/google/go-containerregistry/pkg/v1"
)
type MockClient struct{}
func (c MockClient) GetImageDigest(imgName string) (*v1.Hash, error) {
return &v1.Hash{
Hex: "1234567890abcdef",
Algorithm: "sha256",
}, nil
}
type ReferenceFailClient struct{}
func (c ReferenceFailClient) GetImageDigest(imgName string) (*v1.Hash, error) {
return nil, fmt.Errorf("failed to parse reference")
}
type AuthFailClient struct{}
func (c AuthFailClient) GetImageDigest(imgName string) (*v1.Hash, error) {
return nil, ErrRegistryAuthz
}
type DeniedClient struct{}
func (c DeniedClient) GetImageDigest(imgName string) (*v1.Hash, error) {
return nil, ErrDenied
}

View file

@ -0,0 +1,28 @@
package attestation
import (
"github.com/cli/cli/v2/pkg/cmd/attestation/download"
"github.com/cli/cli/v2/pkg/cmd/attestation/inspect"
"github.com/cli/cli/v2/pkg/cmd/attestation/tufrootverify"
"github.com/cli/cli/v2/pkg/cmd/attestation/verify"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/spf13/cobra"
)
func NewCmdAttestation(f *cmdutil.Factory) *cobra.Command {
root := &cobra.Command{
Use: "attestation [subcommand]",
Short: "Work with artifact attestations",
Aliases: []string{"at"},
Hidden: true,
Long: "Download and verify artifact attestations.",
}
root.AddCommand(download.NewDownloadCmd(f, nil))
root.AddCommand(inspect.NewInspectCmd(f, nil))
root.AddCommand(verify.NewVerifyCmd(f, nil))
root.AddCommand(tufrootverify.NewTUFRootVerifyCmd(f, nil))
return root
}

View file

@ -0,0 +1,17 @@
package auth
import (
"errors"
"github.com/cli/go-gh/v2/pkg/auth"
)
var ErrUnsupportedHost = errors.New("The GH_HOST environment variable is set to a custom GitHub host. gh attestation does not currently support custom GitHub Enterprise hosts")
func IsHostSupported() error {
host, _ := auth.DefaultHost()
if host != "github.com" {
return ErrUnsupportedHost
}
return nil
}

View file

@ -0,0 +1,148 @@
package download
import (
"errors"
"fmt"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/cli/cli/v2/pkg/cmd/attestation/auth"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
)
func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command {
opts := &Options{}
downloadCmd := &cobra.Command{
Use: "download [<file-path> | oci://<image-uri>] [--owner | --repo]",
Args: cobra.ExactArgs(1),
Short: "Download an artifact's Sigstore bundle(s) for offline use",
Long: heredoc.Docf(`
Download an artifact's attestations, aka Sigstore bundle(s), for offline use.
The command requires either:
* a file path to an artifact, or
* a container image URI (e.g. %[1]soci://<image-uri>%[1]s)
(Note that if you provide an OCI URL, you must already be authenticated with
its container registry.)
In addition, the command requires either:
* the %[1]s--owner%[1]s flag (e.g. --owner github), or
* the %[1]s--repo%[1]s flag (e.g. --repo github/example).
The %[1]s--owner%[1]s flag value must match the name of the GitHub organization
that the artifact is associated with.
The %[1]s--repo%[1]s flag value must match the name of the GitHub repository
that the artifact is associated with.
Any associated Sigstore bundle(s) will be written to a file in the
current directory named after the artifact's digest. For example, if the
digest is "sha256:1234", the file will be named "sha256:1234.jsonl".
`, "`"),
Example: heredoc.Doc(`
# Download Sigstore bundle(s) for a local artifact associated with a GitHub organization
$ gh attestation download example.bin -o github
# Download Sigstore bundle(s) for a local artifact associated with a GitHub repository
$ gh attestation download example.bin -R github/example
# Download Sigstore bundle(s) for an OCI image associated with a GitHub organization
$ gh attestation download oci://example.com/foo/bar:latest -o github
`),
// PreRunE is used to validate flags before the command is run
// If an error is returned, its message will be printed to the terminal
// along with information about how use the command
PreRunE: func(cmd *cobra.Command, args []string) error {
// Create a logger for use throughout the download command
opts.Logger = io.NewHandler(f.IOStreams)
// set the artifact path
opts.ArtifactPath = args[0]
// check that the provided flags are valid
if err := opts.AreFlagsValid(); err != nil {
return err
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
hc, err := f.HttpClient()
if err != nil {
return err
}
opts.APIClient = api.NewLiveClient(hc, opts.Logger)
opts.OCIClient = oci.NewLiveClient()
opts.Store = NewLiveStore("")
if err := auth.IsHostSupported(); err != nil {
return err
}
if runF != nil {
return runF(opts)
}
if err := runDownload(opts); err != nil {
return fmt.Errorf("Failed to download the artifact's bundle(s): %v", err)
}
return nil
},
}
downloadCmd.Flags().StringVarP(&opts.Owner, "owner", "o", "", "a GitHub organization to scope attestation lookup by")
downloadCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format <owner>/<repo>")
downloadCmd.MarkFlagsMutuallyExclusive("owner", "repo")
downloadCmd.MarkFlagsOneRequired("owner", "repo")
cmdutil.StringEnumFlag(downloadCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact")
downloadCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch")
return downloadCmd
}
func runDownload(opts *Options) error {
artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm)
if err != nil {
return fmt.Errorf("failed to digest artifact: %v", err)
}
opts.Logger.VerbosePrintf("Downloading trusted metadata for artifact %s\n\n", opts.ArtifactPath)
c := verification.FetchAttestationsConfig{
APIClient: opts.APIClient,
Digest: artifact.DigestWithAlg(),
Limit: opts.Limit,
Owner: opts.Owner,
Repo: opts.Repo,
}
attestations, err := verification.GetRemoteAttestations(c)
if err != nil {
if errors.Is(err, api.ErrNoAttestations{}) {
fmt.Fprintf(opts.Logger.IO.Out, "No attestations found for %s\n", opts.ArtifactPath)
return nil
}
return fmt.Errorf("failed to fetch attestations: %v", err)
}
metadataFilePath, err := opts.Store.createMetadataFile(artifact.DigestWithAlg(), attestations)
if err != nil {
return fmt.Errorf("failed to write attestation: %v", err)
}
fmt.Fprintf(opts.Logger.IO.Out, "Wrote attestations to file %s.\nAny previous content has been overwritten\n\n", metadataFilePath)
fmt.Fprint(opts.Logger.IO.Out,
opts.Logger.ColorScheme.Greenf(
"The trusted metadata is now available at %s\n", metadataFilePath,
),
)
return nil
}

View file

@ -0,0 +1,314 @@
package download
import (
"bytes"
"fmt"
"net/http"
"strings"
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var artifactPath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz")
func TestNewDownloadCmd(t *testing.T) {
testIO, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: testIO,
HttpClient: func() (*http.Client, error) {
reg := &httpmock.Registry{}
client := &http.Client{}
httpmock.ReplaceTripper(client, reg)
return client, nil
},
}
store := &LiveStore{
outputPath: t.TempDir(),
}
testcases := []struct {
name string
cli string
wants Options
wantsErr bool
}{
{
name: "Invalid digest-alg flag",
cli: fmt.Sprintf("%s --owner sigstore --digest-alg sha384", artifactPath),
wants: Options{
ArtifactPath: artifactPath,
APIClient: api.NewTestClient(),
OCIClient: oci.MockClient{},
DigestAlgorithm: "sha384",
Owner: "sigstore",
Store: store,
Limit: 30,
},
wantsErr: true,
},
{
name: "Missing digest-alg flag",
cli: fmt.Sprintf("%s --owner sigstore", artifactPath),
wants: Options{
ArtifactPath: artifactPath,
APIClient: api.NewTestClient(),
OCIClient: oci.MockClient{},
DigestAlgorithm: "sha256",
Owner: "sigstore",
Store: store,
Limit: 30,
},
wantsErr: false,
},
{
name: "Missing owner and repo flags",
cli: artifactPath,
wants: Options{
ArtifactPath: artifactPath,
APIClient: api.NewTestClient(),
OCIClient: oci.MockClient{},
DigestAlgorithm: "sha256",
Owner: "sigstore",
Store: store,
Limit: 30,
},
wantsErr: true,
},
{
name: "Has both owner and repo flags",
cli: fmt.Sprintf("%s --owner sigstore --repo sigstore/sigstore-js", artifactPath),
wants: Options{
ArtifactPath: artifactPath,
APIClient: api.NewTestClient(),
OCIClient: oci.MockClient{},
DigestAlgorithm: "sha256",
Owner: "sigstore",
Store: store,
Repo: "sigstore/sigstore-js",
Limit: 30,
},
wantsErr: true,
},
{
name: "Uses default limit flag",
cli: fmt.Sprintf("%s --owner sigstore", artifactPath),
wants: Options{
ArtifactPath: artifactPath,
APIClient: api.NewTestClient(),
OCIClient: oci.MockClient{},
DigestAlgorithm: "sha256",
Owner: "sigstore",
Store: store,
Limit: 30,
},
wantsErr: false,
},
{
name: "Uses custom limit flag",
cli: fmt.Sprintf("%s --owner sigstore --limit 101", artifactPath),
wants: Options{
ArtifactPath: artifactPath,
APIClient: api.NewTestClient(),
OCIClient: oci.MockClient{},
DigestAlgorithm: "sha256",
Owner: "sigstore",
Store: store,
Limit: 101,
},
wantsErr: false,
},
{
name: "Uses invalid limit flag",
cli: fmt.Sprintf("%s --owner sigstore --limit 0", artifactPath),
wants: Options{
ArtifactPath: artifactPath,
APIClient: api.NewTestClient(),
OCIClient: oci.MockClient{},
DigestAlgorithm: "sha256",
Owner: "sigstore",
Store: store,
Limit: 0,
},
wantsErr: true,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
var opts *Options
cmd := NewDownloadCmd(f, func(o *Options) error {
opts = o
return nil
})
argv := strings.Split(tc.cli, " ")
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err := cmd.ExecuteC()
if tc.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wants.DigestAlgorithm, opts.DigestAlgorithm)
assert.Equal(t, tc.wants.Limit, opts.Limit)
assert.Equal(t, tc.wants.Owner, opts.Owner)
assert.Equal(t, tc.wants.Repo, opts.Repo)
assert.NotNil(t, opts.APIClient)
assert.NotNil(t, opts.OCIClient)
assert.NotNil(t, opts.Logger)
assert.NotNil(t, opts.Store)
})
}
}
func TestRunDownload(t *testing.T) {
tempDir := t.TempDir()
store := &LiveStore{
outputPath: tempDir,
}
baseOpts := Options{
ArtifactPath: artifactPath,
APIClient: api.NewTestClient(),
OCIClient: oci.MockClient{},
DigestAlgorithm: "sha512",
Owner: "sigstore",
Store: store,
Limit: 30,
Logger: io.NewTestHandler(),
}
t.Run("fetch and store attestations successfully with owner", func(t *testing.T) {
err := runDownload(&baseOpts)
require.NoError(t, err)
artifact, err := artifact.NewDigestedArtifact(baseOpts.OCIClient, baseOpts.ArtifactPath, baseOpts.DigestAlgorithm)
require.NoError(t, err)
require.FileExists(t, fmt.Sprintf("%s/%s.jsonl", tempDir, artifact.DigestWithAlg()))
actualLineCount, err := countLines(fmt.Sprintf("%s/%s.jsonl", tempDir, artifact.DigestWithAlg()))
require.NoError(t, err)
expectedLineCount := 2
require.Equal(t, expectedLineCount, actualLineCount)
})
t.Run("fetch and store attestations successfully with repo", func(t *testing.T) {
opts := baseOpts
opts.Owner = ""
opts.Repo = "sigstore/sigstore-js"
err := runDownload(&opts)
require.NoError(t, err)
artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm)
require.NoError(t, err)
require.FileExists(t, fmt.Sprintf("%s/%s.jsonl", tempDir, artifact.DigestWithAlg()))
actualLineCount, err := countLines(fmt.Sprintf("%s/%s.jsonl", tempDir, artifact.DigestWithAlg()))
require.NoError(t, err)
expectedLineCount := 2
require.Equal(t, expectedLineCount, actualLineCount)
})
t.Run("download OCI image attestations successfully", func(t *testing.T) {
opts := baseOpts
opts.ArtifactPath = "oci://ghcr.io/github/test"
err := runDownload(&opts)
require.NoError(t, err)
artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm)
require.NoError(t, err)
require.FileExists(t, fmt.Sprintf("%s/%s.jsonl", tempDir, artifact.DigestWithAlg()))
actualLineCount, err := countLines(fmt.Sprintf("%s/%s.jsonl", tempDir, artifact.DigestWithAlg()))
require.NoError(t, err)
expectedLineCount := 2
require.Equal(t, expectedLineCount, actualLineCount)
})
t.Run("cannot find artifact", func(t *testing.T) {
opts := baseOpts
opts.ArtifactPath = "../test/data/not-real.zip"
err := runDownload(&opts)
require.Error(t, err)
})
t.Run("no attestations found", func(t *testing.T) {
opts := baseOpts
opts.APIClient = api.MockClient{
OnGetByOwnerAndDigest: func(repo, digest string, limit int) ([]*api.Attestation, error) {
return nil, api.ErrNoAttestations{}
},
}
err := runDownload(&opts)
require.NoError(t, err)
artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm)
require.NoError(t, err)
require.NoFileExists(t, artifact.DigestWithAlg())
})
t.Run("failed to fetch attestations", func(t *testing.T) {
opts := baseOpts
opts.APIClient = api.MockClient{
OnGetByOwnerAndDigest: func(repo, digest string, limit int) ([]*api.Attestation, error) {
return nil, fmt.Errorf("failed to fetch attestations")
},
}
err := runDownload(&opts)
require.Error(t, err)
})
t.Run("cannot download OCI artifact", func(t *testing.T) {
opts := baseOpts
opts.ArtifactPath = "oci://ghcr.io/github/test"
opts.OCIClient = oci.ReferenceFailClient{}
err := runDownload(&opts)
require.Error(t, err)
require.ErrorContains(t, err, "failed to digest artifact")
})
t.Run("with missing API client", func(t *testing.T) {
customOpts := baseOpts
customOpts.APIClient = nil
require.Error(t, runDownload(&customOpts))
})
t.Run("fail to write attestations to metadata file", func(t *testing.T) {
opts := baseOpts
opts.Store = &MockStore{
OnCreateMetadataFile: OnCreateMetadataFileFailure,
}
err := runDownload(&opts)
require.Error(t, err)
require.ErrorAs(t, err, &ErrAttestationFileCreation)
})
}

View file

@ -0,0 +1,70 @@
package download
import (
"encoding/json"
"errors"
"fmt"
"os"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
)
var ErrAttestationFileCreation = fmt.Errorf("failed to write attestations to file")
type MetadataStore interface {
createMetadataFile(artifactDigest string, attestationsResp []*api.Attestation) (string, error)
}
type LiveStore struct {
outputPath string
}
func (s *LiveStore) createJSONLinesFilePath(artifact string) string {
path := fmt.Sprintf("%s.jsonl", artifact)
if s.outputPath != "" {
return fmt.Sprintf("%s/%s", s.outputPath, path)
}
return path
}
func (s *LiveStore) createMetadataFile(artifactDigest string, attestationsResp []*api.Attestation) (string, error) {
metadataFilePath := s.createJSONLinesFilePath(artifactDigest)
f, err := os.Create(metadataFilePath)
if err != nil {
return "", errors.Join(ErrAttestationFileCreation, fmt.Errorf("failed to create file: %v", err))
}
for _, resp := range attestationsResp {
bundle := resp.Bundle
attBytes, err := json.Marshal(bundle)
if err != nil {
if err = f.Close(); err != nil {
return "", errors.Join(ErrAttestationFileCreation, fmt.Errorf("failed to close file while marshalling JSON: %v", err))
}
return "", errors.Join(ErrAttestationFileCreation, fmt.Errorf("failed to marshall attestation to JSON while writing to file: %v", err))
}
withNewline := fmt.Sprintf("%s\n", attBytes)
_, err = f.Write([]byte(withNewline))
if err != nil {
if err = f.Close(); err != nil {
return "", errors.Join(ErrAttestationFileCreation, fmt.Errorf("failed to close file while handling write error: %v", err))
}
return "", errors.Join(ErrAttestationFileCreation, fmt.Errorf("failed to write attestations: %v", err))
}
}
if err = f.Close(); err != nil {
return "", errors.Join(ErrAttestationFileCreation, fmt.Errorf("failed to close file after writing attestations: %v", err))
}
return metadataFilePath, nil
}
func NewLiveStore(outputPath string) *LiveStore {
return &LiveStore{
outputPath: outputPath,
}
}

View file

@ -0,0 +1,87 @@
package download
import (
"bufio"
"fmt"
"os"
"path"
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/stretchr/testify/require"
)
type MockStore struct {
OnCreateMetadataFile func(artifactDigest string, attestationsResp []*api.Attestation) (string, error)
}
func (s *MockStore) createMetadataFile(artifact string, attestationsResp []*api.Attestation) (string, error) {
return s.OnCreateMetadataFile(artifact, attestationsResp)
}
func OnCreateMetadataFileFailure(artifactDigest string, attestationsResp []*api.Attestation) (string, error) {
return "", fmt.Errorf("failed to create trusted metadata file")
}
func TestCreateJSONLinesFilePath(t *testing.T) {
tempDir := t.TempDir()
artifact, err := artifact.NewDigestedArtifact(oci.MockClient{}, "../test/data/sigstore-js-2.1.0.tgz", "sha512")
require.NoError(t, err)
outputFileName := fmt.Sprintf("%s.jsonl", artifact.DigestWithAlg())
testCases := []struct {
name string
outputPath string
expected string
}{
{
name: "with output path",
outputPath: tempDir,
expected: path.Join(tempDir, outputFileName),
},
{
name: "with nested output path",
outputPath: path.Join(tempDir, "subdir"),
expected: path.Join(tempDir, "subdir", outputFileName),
},
{
name: "with output path with beginning slash",
outputPath: path.Join("/", tempDir, "subdir"),
expected: path.Join("/", tempDir, "subdir", outputFileName),
},
{
name: "without output path",
outputPath: "",
expected: outputFileName,
},
}
for _, tc := range testCases {
store := LiveStore{
tc.outputPath,
}
actualPath := store.createJSONLinesFilePath(artifact.DigestWithAlg())
require.Equal(t, tc.expected, actualPath)
}
}
func countLines(path string) (int, error) {
f, err := os.Open(path)
if err != nil {
return 0, err
}
defer f.Close()
counter := 0
scanner := bufio.NewScanner(f)
for scanner.Scan() {
counter += 1
}
return counter, nil
}

View file

@ -0,0 +1,35 @@
package download
import (
"fmt"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
)
const (
minLimit = 1
maxLimit = 1000
)
type Options struct {
APIClient api.Client
ArtifactPath string
DigestAlgorithm string
Logger *io.Handler
Limit int
Store MetadataStore
OCIClient oci.Client
Owner string
Repo string
}
func (opts *Options) AreFlagsValid() error {
// Check that limit is between 1 and 1000
if opts.Limit < minLimit || opts.Limit > maxLimit {
return fmt.Errorf("limit %d not allowed, must be between %d and %d", opts.Limit, minLimit, maxLimit)
}
return nil
}

View file

@ -0,0 +1,34 @@
package download
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestAreFlagsValid(t *testing.T) {
tests := []struct {
name string
limit int
}{
{
name: "Limit is too low",
limit: 0,
},
{
name: "Limit is too high",
limit: 1001,
},
}
for _, tc := range tests {
opts := Options{
Limit: tc.limit,
}
err := opts.AreFlagsValid()
require.Error(t, err)
expectedErrMsg := fmt.Sprintf("limit %d not allowed, must be between 1 and 1000", tc.limit)
require.ErrorContains(t, err, expectedErrMsg)
}
}

View file

@ -0,0 +1,128 @@
package inspect
import (
"encoding/json"
"fmt"
"strings"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
)
type workflow struct {
Repository string `json:"repository"`
}
type externalParameters struct {
Workflow workflow `json:"workflow"`
}
type githubInfo struct {
RepositoryID string `json:"repository_id"`
RepositoryOwnerId string `json:"repository_owner_id"`
}
type internalParameters struct {
GitHub githubInfo `json:"github"`
}
type buildDefinition struct {
ExternalParameters externalParameters `json:"externalParameters"`
InternalParameters internalParameters `json:"internalParameters"`
}
type metadata struct {
InvocationID string `json:"invocationId"`
}
type runDetails struct {
Metadata metadata `json:"metadata"`
}
// Predicate captures the predicate of a given attestation
type Predicate struct {
BuildDefinition buildDefinition `json:"buildDefinition"`
RunDetails runDetails `json:"runDetails"`
}
// AttestationDetail captures attestation source details
// that will be returned by the inspect command
type AttestationDetail struct {
OrgName string `json:"orgName"`
OrgID string `json:"orgId"`
RepositoryName string `json:"repositoryName"`
RepositoryID string `json:"repositoryId"`
WorkflowID string `json:"workflowId"`
}
func getOrgAndRepo(repoURL string) (string, string, error) {
after, found := strings.CutPrefix(repoURL, "https://github.com/")
if !found {
return "", "", fmt.Errorf("failed to get org and repo from %s", repoURL)
}
parts := strings.Split(after, "/")
return parts[0], parts[1], nil
}
func getAttestationDetail(attr api.Attestation) (AttestationDetail, error) {
envelope, err := attr.Bundle.Envelope()
if err != nil {
return AttestationDetail{}, fmt.Errorf("failed to get envelope from bundle: %v", err)
}
statement, err := envelope.EnvelopeContent().Statement()
if err != nil {
return AttestationDetail{}, fmt.Errorf("failed to get statement from envelope: %v", err)
}
var predicate Predicate
predicateJson, err := json.Marshal(statement.Predicate)
if err != nil {
return AttestationDetail{}, fmt.Errorf("failed to marshal predicate: %v", err)
}
err = json.Unmarshal(predicateJson, &predicate)
if err != nil {
return AttestationDetail{}, fmt.Errorf("failed to unmarshal predicate: %v", err)
}
org, repo, err := getOrgAndRepo(predicate.BuildDefinition.ExternalParameters.Workflow.Repository)
if err != nil {
return AttestationDetail{}, fmt.Errorf("failed to parse attestation content: %v", err)
}
return AttestationDetail{
OrgName: org,
OrgID: predicate.BuildDefinition.InternalParameters.GitHub.RepositoryOwnerId,
RepositoryName: repo,
RepositoryID: predicate.BuildDefinition.InternalParameters.GitHub.RepositoryID,
WorkflowID: predicate.RunDetails.Metadata.InvocationID,
}, nil
}
func getDetailsAsSlice(results []*verification.AttestationProcessingResult) ([][]string, error) {
details := make([][]string, len(results))
for i, result := range results {
detail, err := getAttestationDetail(*result.Attestation)
if err != nil {
return nil, fmt.Errorf("failed to get attestation detail: %v", err)
}
details[i] = []string{detail.RepositoryName, detail.RepositoryID, detail.OrgName, detail.OrgID, detail.WorkflowID}
}
return details, nil
}
func getAttestationDetails(results []*verification.AttestationProcessingResult) ([]AttestationDetail, error) {
details := make([]AttestationDetail, len(results))
for i, result := range results {
detail, err := getAttestationDetail(*result.Attestation)
if err != nil {
return nil, fmt.Errorf("failed to get attestation detail: %v", err)
}
details[i] = detail
}
return details, nil
}

View file

@ -0,0 +1,46 @@
package inspect
import (
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/stretchr/testify/require"
)
func TestGetOrgAndRepo(t *testing.T) {
t.Run("with valid source URL", func(t *testing.T) {
sourceURL := "https://github.com/github/gh-attestation"
org, repo, err := getOrgAndRepo(sourceURL)
require.Nil(t, err)
require.Equal(t, "github", org)
require.Equal(t, "gh-attestation", repo)
})
t.Run("with invalid source URL", func(t *testing.T) {
sourceURL := "hub.com/github/gh-attestation"
org, repo, err := getOrgAndRepo(sourceURL)
require.Error(t, err)
require.Zero(t, org)
require.Zero(t, repo)
})
}
func TestGetAttestationDetail(t *testing.T) {
bundlePath := test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json")
attestations, err := verification.GetLocalAttestations(bundlePath)
require.Len(t, attestations, 1)
require.NoError(t, err)
attestation := attestations[0]
detail, err := getAttestationDetail(*attestation)
require.NoError(t, err)
require.Equal(t, "sigstore", detail.OrgName)
require.Equal(t, "71096353", detail.OrgID)
require.Equal(t, "sigstore-js", detail.RepositoryName)
require.Equal(t, "495574555", detail.RepositoryID)
require.Equal(t, "https://github.com/sigstore/sigstore-js/actions/runs/6014488666/attempts/1", detail.WorkflowID)
}

View file

@ -0,0 +1,162 @@
package inspect
import (
"fmt"
"github.com/cli/cli/v2/internal/tableprinter"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/cli/cli/v2/pkg/cmd/attestation/auth"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
)
func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command {
opts := &Options{}
inspectCmd := &cobra.Command{
Use: "inspect [<file path> | oci://<OCI image URI>] --bundle <path-to-bundle>",
Args: cobra.ExactArgs(1),
Hidden: true,
Short: "Inspect a sigstore bundle",
Long: heredoc.Docf(`
Inspect a downloaded Sigstore bundle for a given artifact.
The command requires either:
* a relative path to a local artifact, or
* a container image URI (e.g. %[1]soci://<my-OCI-image-URI>%[1]s)
Note that if you provide an OCI URI for the artifact you must already
be authenticated with a container registry.
The command also requires the %[1]s--bundle%[1]s flag, which provides a file
path to a previously downloaded Sigstore bundle. (See also the %[1]sdownload%[1]s
command).
By default, the command will print information about the bundle in a table format.
If the %[1]s--json-result%[1]s flag is provided, the command will print the
information in JSON format.
`, "`"),
Example: heredoc.Doc(`
# Inspect a Sigstore bundle and print the results in table format
$ gh attestation inspect <my-artifact> --bundle <path-to-bundle>
# Inspect a Sigstore bundle and print the results in JSON format
$ gh attestation inspect <my-artifact> --bundle <path-to-bundle> --json-result
# Inspect a Sigsore bundle for an OCI artifact, and print the results in table format
$ gh attestation inspect oci://<my-OCI-image> --bundle <path-to-bundle>
`),
PreRunE: func(cmd *cobra.Command, args []string) error {
// Create a logger for use throughout the inspect command
opts.Logger = io.NewHandler(f.IOStreams)
// set the artifact path
opts.ArtifactPath = args[0]
// Clean file path options
// opts.Clean()
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
opts.OCIClient = oci.NewLiveClient()
if err := auth.IsHostSupported(); err != nil {
return err
}
if runF != nil {
return runF(opts)
}
if err := runInspect(opts); err != nil {
return fmt.Errorf("Failed to inspect the artifact and bundle: %w", err)
}
return nil
},
}
inspectCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles")
inspectCmd.MarkFlagRequired("bundle") //nolint:errcheck
cmdutil.StringEnumFlag(inspectCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact")
cmdutil.AddFormatFlags(inspectCmd, &opts.exporter)
return inspectCmd
}
func runInspect(opts *Options) error {
artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm)
if err != nil {
return fmt.Errorf("failed to digest artifact: %s", err)
}
opts.Logger.Printf("Verifying attestations for the artifact found at %s\n\n", artifact.URL)
attestations, err := verification.GetLocalAttestations(opts.BundlePath)
if err != nil {
return fmt.Errorf("failed to read attestations for subject: %s", artifact.DigestWithAlg())
}
config := verification.SigstoreConfig{
Logger: opts.Logger,
}
policy, err := buildPolicy(*artifact)
if err != nil {
return fmt.Errorf("failed to build policy: %v", err)
}
sigstore, err := verification.NewSigstoreVerifier(config, policy)
if err != nil {
return err
}
res := sigstore.Verify(attestations)
if res.Error != nil {
return fmt.Errorf("at least one attestation failed to verify against Sigstore: %v", res.Error)
}
opts.Logger.VerbosePrint(opts.Logger.ColorScheme.Green(
"Successfully verified all attestations against Sigstore!\n\n",
))
// If the user provides the --format=json flag, print the results in JSON format
if opts.exporter != nil {
details, err := getAttestationDetails(res.VerifyResults)
if err != nil {
return fmt.Errorf("failed to get attestation detail: %v", err)
}
// print the results to the terminal as an array of JSON objects
if err = opts.exporter.Write(opts.Logger.IO, details); err != nil {
return fmt.Errorf("failed to write JSON output")
}
return nil
}
// otherwise, print results in a table
details, err := getDetailsAsSlice(res.VerifyResults)
if err != nil {
return fmt.Errorf("failed to parse attestation details: %v", err)
}
headers := []string{"Repo Name", "Repo ID", "Org Name", "Org ID", "Workflow ID"}
t := tableprinter.New(opts.Logger.IO, tableprinter.WithHeader(headers...))
for _, row := range details {
for _, field := range row {
t.AddField(field, tableprinter.WithTruncate(nil))
}
t.EndRow()
}
if err = t.Render(); err != nil {
return fmt.Errorf("failed to print output: %v", err)
}
return nil
}

View file

@ -0,0 +1,179 @@
package inspect
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
SigstoreSanValue = "https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main"
SigstoreSanRegex = "^https://github.com/sigstore/sigstore-js/"
)
var (
artifactPath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz")
bundlePath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json")
)
func TestNewInspectCmd(t *testing.T) {
testIO, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: testIO,
HttpClient: func() (*http.Client, error) {
reg := &httpmock.Registry{}
client := &http.Client{}
httpmock.ReplaceTripper(client, reg)
return client, nil
},
}
testcases := []struct {
name string
cli string
wants Options
wantsErr bool
wantsExporter bool
}{
{
name: "Invalid digest-alg flag",
cli: fmt.Sprintf("%s --bundle %s --digest-alg sha384", artifactPath, bundlePath),
wants: Options{
ArtifactPath: artifactPath,
BundlePath: bundlePath,
DigestAlgorithm: "sha384",
OCIClient: oci.MockClient{},
},
wantsErr: true,
},
{
name: "Use default digest-alg value",
cli: fmt.Sprintf("%s --bundle %s", artifactPath, bundlePath),
wants: Options{
ArtifactPath: artifactPath,
BundlePath: bundlePath,
DigestAlgorithm: "sha256",
OCIClient: oci.MockClient{},
},
wantsErr: false,
},
{
name: "Use custom digest-alg value",
cli: fmt.Sprintf("%s --bundle %s --digest-alg sha512", artifactPath, bundlePath),
wants: Options{
ArtifactPath: artifactPath,
BundlePath: bundlePath,
DigestAlgorithm: "sha512",
OCIClient: oci.MockClient{},
},
wantsErr: false,
},
{
name: "Missing bundle flag",
cli: artifactPath,
wants: Options{
ArtifactPath: artifactPath,
DigestAlgorithm: "sha256",
OCIClient: oci.MockClient{},
},
wantsErr: true,
},
{
name: "Prints output in JSON format",
cli: fmt.Sprintf("%s --bundle %s --format json", artifactPath, bundlePath),
wants: Options{
ArtifactPath: artifactPath,
BundlePath: bundlePath,
DigestAlgorithm: "sha256",
OCIClient: oci.MockClient{},
},
wantsExporter: true,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
var opts *Options
cmd := NewInspectCmd(f, func(o *Options) error {
opts = o
return nil
})
argv := strings.Split(tc.cli, " ")
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err := cmd.ExecuteC()
if tc.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wants.ArtifactPath, opts.ArtifactPath)
assert.Equal(t, tc.wants.BundlePath, opts.BundlePath)
assert.Equal(t, tc.wants.DigestAlgorithm, opts.DigestAlgorithm)
assert.NotNil(t, opts.OCIClient)
assert.NotNil(t, opts.Logger)
assert.Equal(t, tc.wantsExporter, opts.exporter != nil)
})
}
}
func TestRunInspect(t *testing.T) {
opts := Options{
ArtifactPath: artifactPath,
BundlePath: bundlePath,
DigestAlgorithm: "sha512",
Logger: io.NewTestHandler(),
OCIClient: oci.MockClient{},
}
t.Run("with valid artifact and bundle", func(t *testing.T) {
require.Nil(t, runInspect(&opts))
})
t.Run("with missing artifact path", func(t *testing.T) {
customOpts := opts
customOpts.ArtifactPath = test.NormalizeRelativePath("../test/data/non-existent-artifact.zip")
require.Error(t, runInspect(&customOpts))
})
t.Run("with missing bundle path", func(t *testing.T) {
customOpts := opts
customOpts.BundlePath = test.NormalizeRelativePath("../test/data/non-existent-sigstoreBundle.json")
require.Error(t, runInspect(&customOpts))
})
}
func TestJSONOutput(t *testing.T) {
testIO, _, out, _ := iostreams.Test()
opts := Options{
ArtifactPath: artifactPath,
BundlePath: bundlePath,
DigestAlgorithm: "sha512",
Logger: io.NewHandler(testIO),
OCIClient: oci.MockClient{},
exporter: cmdutil.NewJSONExporter(),
}
require.Nil(t, runInspect(&opts))
var target []AttestationDetail
err := json.Unmarshal(out.Bytes(), &target)
require.NoError(t, err)
}

View file

@ -0,0 +1,24 @@
package inspect
import (
"path/filepath"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmdutil"
)
// Options captures the options for the inspect command
type Options struct {
ArtifactPath string
BundlePath string
DigestAlgorithm string
Logger *io.Handler
OCIClient oci.Client
exporter cmdutil.Exporter
}
// Clean cleans the file path option values
func (opts *Options) Clean() {
opts.BundlePath = filepath.Clean(opts.BundlePath)
}

View file

@ -0,0 +1,18 @@
package inspect
import (
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
sigstoreVerify "github.com/sigstore/sigstore-go/pkg/verify"
)
func buildPolicy(a artifact.DigestedArtifact) (sigstoreVerify.PolicyBuilder, error) {
artifactDigestPolicyOption, err := verification.BuildDigestPolicyOption(a)
if err != nil {
return sigstoreVerify.PolicyBuilder{}, err
}
policy := sigstoreVerify.NewPolicy(artifactDigestPolicyOption, sigstoreVerify.WithoutIdentitiesUnsafe())
return policy, nil
}

View file

@ -0,0 +1,61 @@
package io
import (
"fmt"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/utils"
)
type Handler struct {
ColorScheme *iostreams.ColorScheme
IO *iostreams.IOStreams
debugEnabled bool
}
func NewHandler(io *iostreams.IOStreams) *Handler {
enabled, _ := utils.IsDebugEnabled()
return &Handler{
ColorScheme: io.ColorScheme(),
IO: io,
debugEnabled: enabled,
}
}
func NewTestHandler() *Handler {
testIO, _, _, _ := iostreams.Test()
return NewHandler(testIO)
}
// Printf writes the formatted arguments to the stderr writer.
func (h *Handler) Printf(f string, v ...interface{}) (int, error) {
if !h.IO.IsStdoutTTY() {
return 0, nil
}
return fmt.Fprintf(h.IO.ErrOut, f, v...)
}
// Println writes the arguments to the stderr writer with a newline at the end.
func (h *Handler) Println(v ...interface{}) (int, error) {
if !h.IO.IsStdoutTTY() {
return 0, nil
}
return fmt.Fprintln(h.IO.ErrOut, v...)
}
func (h *Handler) VerbosePrint(msg string) (int, error) {
if !h.debugEnabled || !h.IO.IsStdoutTTY() {
return 0, nil
}
return fmt.Fprintln(h.IO.ErrOut, msg)
}
func (h *Handler) VerbosePrintf(f string, v ...interface{}) (int, error) {
if !h.debugEnabled || !h.IO.IsStdoutTTY() {
return 0, nil
}
return fmt.Fprintf(h.IO.ErrOut, f, v...)
}

View file

@ -0,0 +1,61 @@
{
"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1",
"verificationMaterial": {
"x509CertificateChain": {
"certificates": [
{
"rawBytes": "MIIGtDCCBjugAwIBAgIUCJLipSt09KLFc0JYfuDrSan//LswCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwODI5MTU0MDIzWhcNMjMwODI5MTU1MDIzWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPfm6LPXQeJTC89UOqiNWmnZYGmX4T3iLZGi0EV4bfOoM86Hza94XqyuwxAoWpCPecFCEbAe8l2dg/er3O9LEFqOCBVowggVWMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUeqpCXHr3pcUaL3EFKR+KsmuKQqowHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wYwYDVR0RAQH/BFkwV4ZVaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzLy5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMBIGCisGAQQBg78wAQIEBHB1c2gwNgYKKwYBBAGDvzABAwQoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAVBgorBgEEAYO/MAEEBAdSZWxlYXNlMCIGCisGAQQBg78wAQUEFHNpZ3N0b3JlL3NpZ3N0b3JlLWpzMB0GCisGAQQBg78wAQYED3JlZnMvaGVhZHMvbWFpbjA7BgorBgEEAYO/MAEIBC0MK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wZQYKKwYBBAGDvzABCQRXDFVodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wAQoEKgwoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAdBgorBgEEAYO/MAELBA8MDWdpdGh1Yi1ob3N0ZWQwNwYKKwYBBAGDvzABDAQpDCdodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMwOAYKKwYBBAGDvzABDQQqDCgyNmQxNjUxMzM4NmZmYWE3OTBiMWMzMmY5Mjc1NDRmMTMyMmU0MTk0MB8GCisGAQQBg78wAQ4EEQwPcmVmcy9oZWFkcy9tYWluMBkGCisGAQQBg78wAQ8ECwwJNDk1NTc0NTU1MCsGCisGAQQBg78wARAEHQwbaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlMBgGCisGAQQBg78wAREECgwINzEwOTYzNTMwZQYKKwYBBAGDvzABEgRXDFVodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoMjZkMTY1MTMzODZmZmFhNzkwYjFjMzJmOTI3NTQ0ZjEzMjJlNDE5NDAUBgorBgEEAYO/MAEUBAYMBHB1c2gwWgYKKwYBBAGDvzABFQRMDEpodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvYWN0aW9ucy9ydW5zLzYwMTQ0ODg2NjYvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzCBigYKKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABikHz+vwAAAQDAEcwRQIgZEo8c0eCZHEh4uzzJzFz9T+EfSTNTtB2FIH18vXpkOsCIQDE1MTti9RoDnRO3SET1Zkad6FoTx/k6ztQcwIDPmnRxTAKBggqhkjOPQQDAwNnADBkAjBB06fmNXx6ToaClFg2kOxnfLGgrvoR3F5GjDtvDBB8m9SWQNzL211jYmS/g+YbbyUCMC+ad6jIK+efe4XOIlhLcWxeZbBtMjKSrPxmm4jR3BFQQOBR+8r27CioyhxoSqvLuw=="
}
]
},
"tlogEntries": [
{
"logIndex": "33351527",
"logId": {
"keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
},
"kindVersion": {
"kind": "intoto",
"version": "0.0.2"
},
"integratedTime": "1693323623",
"inclusionPromise": {
"signedEntryTimestamp": "MEYCIQDhWvNSLvnq5ZS3qTIgC7K2uQFeA0g8FEEjNo1UQxeubAIhALDrD1uIiUkk3tQNp/4gKT/9j8zEyyxi9Ti+qaD8q2vI"
},
"inclusionProof": {
"logIndex": "29188096",
"rootHash": "fbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=",
"treeSize": "29188099",
"hashes": [
"z7VKeAC2d2x18Vtxt7n40GS3gtc1xfRwjjxxzpy/Trw=",
"/Kd8ZuCLZ7MukSwpjaSgxKFl5X2vdHWk6/qpNrkiUJo=",
"vvkKs7IShUI15nVb9c5olPgBnL9r4uP7+d5KzV0hSjs=",
"Fg4p0WJZw8Lghrpxkx1SXgzfroTeIIrEgEbfgr9IdXY=",
"bH8NahqE+hQ58Qxhg0V5fYusFQ1xsaQ/rK6slWVRT5k=",
"HplNgZ3/afEHq52zcfL2s1AKisOYDdMCWK1jOu1tGyw=",
"uOPuC2YDmwZe989ZN3Lgh5CKMXh9HETrSNgf0jV0WkM=",
"eVsvWKnEZ1+Xo3Ba15DfEiIhhmlrEaIeb+VfYmx8KhY=",
"uLuBRins5nkqq2rqd17R27pQTUF+xetttC6MsmlUzd0=",
"jRUq4D8O+FI47Wbw96s7yHCu4qzWUxpIVfxQEeprDmc=",
"rXEsmEJN4PEoTU8US4qVtdIsGB1MCiRlGOepoiC99kM="
],
"checkpoint": {
"envelope": "rekor.sigstore.dev - 2605736670972794746\n29188099\nfbEijQTFeiQCldZqez/u/WcrNPk4nxXI5ihofHYjFUA=\nTimestamp: 1693323623528968756\n\n— rekor.sigstore.dev wNI9ajBEAiAK0YTbxRlhOZeeP+844Y+W7iz+hsFIF8x2NYsmuaxifAIgYUFSurlKwN7j5jCwpqSVrkbouQoYIYlyWU9Om16svmI=\n"
}
},
"canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoiYXBwbGljYXRpb24vdm5kLmluLXRvdG8ranNvbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVWQwUkVORFFtcDFaMEYzU1VKQlowbFZRMHBNYVhCVGREQTVTMHhHWXpCS1dXWjFSSEpUWVc0dkwweHpkME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BOZDA5RVNUVk5WRlV3VFVSSmVsZG9ZMDVOYWsxM1QwUkpOVTFVVlRGTlJFbDZWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVWUVptMDJURkJZVVdWS1ZFTTRPVlZQY1dsT1YyMXVXbGxIYlZnMFZETnBURnBIYVRBS1JWWTBZbVpQYjAwNE5raDZZVGswV0hGNWRYZDRRVzlYY0VOUVpXTkdRMFZpUVdVNGJESmtaeTlsY2pOUE9VeEZSbkZQUTBKV2IzZG5aMVpYVFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZWbGNYQkRDbGhJY2pOd1kxVmhURE5GUmt0U0swdHpiWFZMVVhGdmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQxbDNXVVJXVWpCU1FWRklMMEpHYTNkV05GcFdZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRETk9jRm96VGpCaU0wcHNURE5PY0FwYU0wNHdZak5LYkV4WGNIcE1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU0wcHNZa2RXYUdNeVZYVmxWekZ6VVVoS2JGcHVUWFpoUjFab0NscElUWFppVjBad1ltcEJOVUpuYjNKQ1owVkZRVmxQTDAxQlJVSkNRM1J2WkVoU2QyTjZiM1pNTTFKMllUSldkVXh0Um1wa1IyeDJZbTVOZFZveWJEQUtZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVSkpSME5wYzBkQlVWRkNaemM0ZDBGUlNVVkNTRUl4WXpKbmQwNW5XVXRMZDFsQ1FrRkhSQXAyZWtGQ1FYZFJiMDFxV210TlZGa3hUVlJOZWs5RVdtMWFiVVpvVG5wcmQxbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZXQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVVZDUVdSVFdsZDRiRmxZVG14TlEwbEhRMmx6UjBGUlVVSm5OemgzUVZGVlJVWklUbkJhTTA0d1lqTktiRXd6VG5BS1dqTk9NR0l6U214TVYzQjZUVUl3UjBOcGMwZEJVVkZDWnpjNGQwRlJXVVZFTTBwc1dtNU5kbUZIVm1oYVNFMTJZbGRHY0dKcVFUZENaMjl5UW1kRlJRcEJXVTh2VFVGRlNVSkRNRTFMTW1nd1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveFl6SldlVmt5T1hWa1IxWjFDbVJETldwaU1qQjNXbEZaUzB0M1dVSkNRVWRFZG5wQlFrTlJVbGhFUmxadlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5XcGlNakIyWXpKc2JtTXpVbllLWTIxVmRtTXliRzVqTTFKMlkyMVZkR0Z1VFhaTWJXUndaRWRvTVZscE9UTmlNMHB5V20xNGRtUXpUWFpqYlZaeldsZEdlbHBUTlRWaVYzaEJZMjFXYlFwamVUbHZXbGRHYTJONU9YUlpWMngxVFVSblIwTnBjMGRCVVZGQ1p6YzRkMEZSYjBWTFozZHZUV3BhYTAxVVdURk5WRTE2VDBSYWJWcHRSbWhPZW10M0NsbHFSbXBOZWtwdFQxUkpNMDVVVVRCYWFrVjZUV3BLYkU1RVJUVk9SRUZrUW1kdmNrSm5SVVZCV1U4dlRVRkZURUpCT0UxRVYyUndaRWRvTVZscE1XOEtZak5PTUZwWFVYZE9kMWxMUzNkWlFrSkJSMFIyZWtGQ1JFRlJjRVJEWkc5a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFpqTW14dVl6TlNkZ3BqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZDA5QldVdExkMWxDUWtGSFJIWjZRVUpFVVZGeFJFTm5lVTV0VVhoT2FsVjRUWHBOTkU1dFdtMVpWMFV6Q2s5VVFtbE5WMDE2VFcxWk5VMXFZekZPUkZKdFRWUk5lVTF0VlRCTlZHc3dUVUk0UjBOcGMwZEJVVkZDWnpjNGQwRlJORVZGVVhkUVkyMVdiV041T1c4S1dsZEdhMk41T1hSWlYyeDFUVUpyUjBOcGMwZEJVVkZDWnpjNGQwRlJPRVZEZDNkS1RrUnJNVTVVWXpCT1ZGVXhUVU56UjBOcGMwZEJVVkZDWnpjNGR3cEJVa0ZGU0ZGM1ltRklVakJqU0UwMlRIazVibUZZVW05a1YwbDFXVEk1ZEV3elRuQmFNMDR3WWpOS2JFMUNaMGREYVhOSFFWRlJRbWMzT0hkQlVrVkZDa05uZDBsT2VrVjNUMVJaZWs1VVRYZGFVVmxMUzNkWlFrSkJSMFIyZWtGQ1JXZFNXRVJHVm05a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFlLWXpKc2JtTXpVblpqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZGt4dFpIQmtSMmd4V1drNU0ySXpTbkphYlhoMlpETk5kbU50Vm5OYVYwWjZXbE0xTlFwaVYzaEJZMjFXYldONU9XOWFWMFpyWTNrNWRGbFhiSFZOUkdkSFEybHpSMEZSVVVKbk56aDNRVkpOUlV0bmQyOU5hbHByVFZSWk1VMVVUWHBQUkZwdENscHRSbWhPZW10M1dXcEdhazE2U20xUFZFa3pUbFJSTUZwcVJYcE5ha3BzVGtSRk5VNUVRVlZDWjI5eVFtZEZSVUZaVHk5TlFVVlZRa0ZaVFVKSVFqRUtZekpuZDFkbldVdExkMWxDUWtGSFJIWjZRVUpHVVZKTlJFVndiMlJJVW5kamVtOTJUREprY0dSSGFERlphVFZxWWpJd2RtTXliRzVqTTFKMlkyMVZkZ3BqTW14dVl6TlNkbU50VlhSaGJrMTJXVmRPTUdGWE9YVmplVGw1WkZjMWVreDZXWGROVkZFd1QwUm5NazVxV1haWldGSXdXbGN4ZDJSSVRYWk5WRUZYQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVmRDUVdkTlFtNUNNVmx0ZUhCWmVrTkNhV2RaUzB0M1dVSkNRVWhYWlZGSlJVRm5VamhDU0c5QlpVRkNNa0ZPTURrS1RVZHlSM2g0UlhsWmVHdGxTRXBzYms1M1MybFRiRFkwTTJwNWRDODBaVXRqYjBGMlMyVTJUMEZCUVVKcGEwaDZLM1ozUVVGQlVVUkJSV04zVWxGSlp3cGFSVzg0WXpCbFExcElSV2cwZFhwNlNucEdlamxVSzBWbVUxUk9WSFJDTWtaSlNERTRkbGh3YTA5elEwbFJSRVV4VFZSMGFUbFNiMFJ1VWs4elUwVlVDakZhYTJGa05rWnZWSGd2YXpaNmRGRmpkMGxFVUcxdVVuaFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXVRVVJDYTBGcVFrSXdObVp0VGxoNE5sUnZZVU1LYkVabk1tdFBlRzVtVEVkbmNuWnZVak5HTlVkcVJIUjJSRUpDT0cwNVUxZFJUbnBNTWpFeGFsbHRVeTluSzFsaVlubFZRMDFESzJGa05tcEpTeXRsWmdwbE5GaFBTV3hvVEdOWGVHVmFZa0owVFdwTFUzSlFlRzF0TkdwU00wSkdVVkZQUWxJck9ISXlOME5wYjNsb2VHOVRjWFpNZFhjOVBRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdCIsInNpZyI6IlRVVlJRMGxEWVhSb2JsUkljMlJtV25GSWNUTnBXRXhxVFdVd1ZGVTViRXhaYmxJeGVtazNNM0JSZFZobU5VdElRV2xDTWpOS2FDdG5VMll4VVVWRGJrRlRSMlZ3TW1VeVRVZHVVRmhwYjFWTVZqUjJRMGxUU2tKdWEwcGFaejA5In1dfSwiaGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjFiZDg4ZTA1NGY2OGE3ZDE3MzkzNjcyYjAzZmExOGZkNjRmMGRjZDVmY2M1NDAzNWMxOWZlNjQwMWNmMmNmNWUifSwicGF5bG9hZEhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4Y2ViNGFiODEyNzczMTQ3M2E5ZWM4MTE0MGNiNjg0OWNmOGU5NzBjZGEzMmJhZWYwOTlkZjQ4YmEzMjY0NDQyIn19fX0="
}
],
"timestampVerificationData": null
},
"dsseEnvelope": {
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicGtnOm5wbS9zaWdzdG9yZUAyLjEuMCIsImRpZ2VzdCI6eyJzaGE1MTIiOiI5MGYyMjNmOTkyZTRjODhkZDA2OGNkMmE1ZmM1N2Y5ZDJiMzA3OTgzNDNkZDZlMzhmMjljMjQwZTA0YmEwOTBlZjgzMWY4NDQ5MDg0N2M0ZTgyYjkyMzJjNzhlOGEyNTg0NjNiMWU1NWMwZjc0NjlmNzMwMjY1MDA4ZmE2NjMzZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vc2xzYS1mcmFtZXdvcmsuZ2l0aHViLmlvL2dpdGh1Yi1hY3Rpb25zLWJ1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcyIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnltbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoicHVzaCIsInJlcG9zaXRvcnlfaWQiOiI0OTU1NzQ1NTUiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiNzEwOTYzNTMifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzQHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiIyNmQxNjUxMzM4NmZmYWE3OTBiMWMzMmY5Mjc1NDRmMTMyMmU0MTk0In19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcy9hY3Rpb25zL3J1bnMvNjAxNDQ4ODY2Ni9hdHRlbXB0cy8xIn19fX0=",
"payloadType": "application/vnd.in-toto+json",
"signatures": [
{
"sig": "MEQCICathnTHsdfZqHq3iXLjMe0TU9lLYnR1zi73pQuXf5KHAiB23Jh+gSf1QECnASGep2e2MGnPXioULV4vCISJBnkJZg==",
"keyid": ""
}
]
}
}

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,48 @@
{
"mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1",
"verificationMaterial": {
"tlogEntries": [
{
"logIndex": "6751924",
"logId": {
"keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
},
"kindVersion": {
"kind": "intoto",
"version": "0.0.2"
},
"integratedTime": "1667948287",
"inclusionPromise": {
"signedEntryTimestamp": "MEQCIEzguFRaGzOpMw9JJGUfqSJQ11qlzpcyVCkZfZYPwpLCAiBzdU4LnjtVKYCfyoTImFh3OLFWeOKygtS47Z8fp1GYHg=="
},
"canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoidGV4dC9wbGFpbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVU51ZWtORFFXbFhaMEYzU1VKQlowbFZWa2gzWldoUGRFZHVORXRUUkRGSU9GSkpOVGd4VFdaaWVXVjNkME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BKZUUxVVFUUk5ha2t4VDBSQk1sZG9ZMDVOYWtsNFRWUkJORTFxVFhkUFJFRXlWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVWSFp6WklhbmgwTWxWT2FVb3hhM2QzY1RWWVVVbEpkMDFhYmtwbVZsRXpZa1l3TVhVS1drdDBaVTFrWTFZdk0zRm9RMjFYVDJWamIzaFNjWGR5WWxsVWMyaEhaemxPZVZoalFtSjJaVFo2UzNkYVZsUk1aWEZQUTBGVlVYZG5aMFpCVFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZVM1YzQlNDall3YzBOd1oyWjFNRFIzWTNOcWRrTkdlSFF3WmsxcmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQwaDNXVVJXVWpCU1FWRklMMEpDVlhkRk5FVlNXVzVLY0ZsWE5VRmFSMVp2V1ZjeGJHTnBOV3BpTWpCM1RFRlpTMHQzV1VKQ1FVZEVkbnBCUWdwQlVWRmxZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRESjRkbG95YkhWTU1qbG9aRmhTYjAxSlIwcENaMjl5UW1kRlJVRmtXalZCWjFGRENrSkljMFZsVVVJelFVaFZRVE5VTUhkaGMySklSVlJLYWtkU05HTnRWMk16UVhGS1MxaHlhbVZRU3pNdmFEUndlV2RET0hBM2J6UkJRVUZIUlZkWVkxSUtPRUZCUVVKQlRVRlNha0pGUVdsQ1VsUnlSMFUxV1RGRmJsbHVhV0ZLUWl0dWMzWTRPVlpoV1hnelVWcHFiMk5GYVc0emNqa3hkMlpyUVVsblRYTnpLd3BtYzNOMU5WTk1VV3R1TjFkRVZFdFlaMjkzTjFONFlraFpVMXBxTTNscmVFRnlWbTUxZWtWM1EyZFpTVXR2V2tsNmFqQkZRWGROUkdGQlFYZGFVVWw0Q2tGUWFsTkhaR1JNU1haNVZVMUhTV3RhSzNVMlNtaEZPWEF4VG1wME0yUkZkSGRaYTAxNFptNUZWakpyTjAxSU1VSldiWGhuT1ZCelNtcHhlV05tYVNzS1pVRkpkMFJoUzI0eVEyUlBlRXR6ZUdObldVNXBORWgyYVVWdVduRjRiV1ZFZVc4eVdVWkpkSHB3U0daSlRWRnRZMUpUYkRreFZXVlBVME00SzFCMVJ3cG5kMDFMQ2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIiwic2lnIjoiVFVWVlEwbERWV2hCVm1WM1puZExiR3MxWmxaNmNGSkVWVkJvUlhjNVR6aEpNbkI0UXpWdVZHNVFabGxFUW5OUFFXbEZRVEJhUm5Gek9UbFJaMUk1YlVGMFJrMVhkRmR5VDJwdFZVTTBOM3BuWVc5dmJFdEpiMHhJTDA5M1pFMDkifV19LCJoYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZGNiNDkyNTljODY2MDdjMzQ2MzVkYWJiNDQzMWYwNjVlOWE3YTczNDcwNGNiNzNmMGFhMGY2YWFhMzg5NmEwNCJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjY4ZTY1NmIyNTFlNjdlODM1OGJlZjg0ODNhYjBkNTFjNjYxOWYzZTdhMWE5ZjBlNzU4MzhkNDFmZjM2OGY3MjgifX19fQ=="
}
],
"timestampVerificationData": {
"rfc3161Timestamps": []
},
"x509CertificateChain": {
"certificates": [
{
"rawBytes": "MIICnzCCAiWgAwIBAgIUVHwehOtGn4KSD1H8RI581MfbyewwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjIxMTA4MjI1ODA2WhcNMjIxMTA4MjMwODA2WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGg6Hjxt2UNiJ1kwwq5XQIIwMZnJfVQ3bF01uZKteMdcV/3qhCmWOecoxRqwrbYTshGg9NyXcBbve6zKwZVTLeqOCAUQwggFAMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU7WpR60sCpgfu04wcsjvCFxt0fMkwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0RAQH/BBUwE4ERYnJpYW5AZGVoYW1lci5jb20wLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGEWXcR8AAABAMARjBEAiBRTrGE5Y1EnYniaJB+nsv89VaYx3QZjocEin3r91wfkAIgMss+fssu5SLQkn7WDTKXgow7SxbHYSZj3ykxArVnuzEwCgYIKoZIzj0EAwMDaAAwZQIxAPjSGddLIvyUMGIkZ+u6JhE9p1Njt3dEtwYkMxfnEV2k7MH1BVmxg9PsJjqycfi+eAIwDaKn2CdOxKsxcgYNi4HviEnZqxmeDyo2YFItzpHfIMQmcRSl91UeOSC8+PuGgwMK"
},
{
"rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="
},
{
"rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"
}
]
}
},
"dsseEnvelope": {
"payload": "aGVsbG8sIHdvcmxkIQ==",
"payloadType": "text/plain",
"signatures": [
{
"sig": "NEUCICUhAVewfwKlk5fVzpRDUPhEw9O8I2pxC5nTnPfYDBsOAiEA0ZFqs99QgR9mAtFMWtWrOjmUC47zgaoolKIoLH/OwdM=",
"keyid": ""
}
]
}
}

View file

@ -0,0 +1,13 @@
package test
import (
"runtime"
"strings"
)
func NormalizeRelativePath(posixPath string) string {
if runtime.GOOS == "windows" {
return strings.ReplaceAll(posixPath, "/", "\\")
}
return posixPath
}

View file

@ -0,0 +1,83 @@
package tufrootverify
import (
"fmt"
"os"
"github.com/cli/cli/v2/pkg/cmd/attestation/auth"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/MakeNowJust/heredoc"
"github.com/sigstore/sigstore-go/pkg/tuf"
"github.com/spf13/cobra"
)
func NewTUFRootVerifyCmd(f *cmdutil.Factory, runF func() error) *cobra.Command {
var mirror string
var root string
var cmd = cobra.Command{
Use: "tuf-root-verify --mirror <mirror-url> --root <root.json>",
Args: cobra.ExactArgs(0),
Short: "Verify the TUF repository from a provided TUF root",
Hidden: true,
Long: heredoc.Docf(`
Verify a TUF repository with a local TUF root.
The command requires you provide the %[1]s--mirror%[1]s flag, which should be the URL
of the TUF repository mirror.
The command also requires you provide the %[1]s--root%[1]s flag, which should be the
path to the TUF root file.
GitHub relies on TUF to securely deliver the trust root for our signing authority.
For more information on TUF, see the official documentation: <https://theupdateframework.github.io/>.
`, "`"),
Example: heredoc.Doc(`
# Verify the TUF repository from a provided TUF root
gh attestation tuf-root-verify --mirror https://tuf-repo.github.com --root /path/to/1.root.json
`),
RunE: func(cmd *cobra.Command, args []string) error {
if err := auth.IsHostSupported(); err != nil {
return err
}
if runF != nil {
return runF()
}
if err := tufRootVerify(mirror, root); err != nil {
return fmt.Errorf("Failed to verify the TUF repository: %w", err)
}
io := f.IOStreams
fmt.Sprintln(io.Out, io.ColorScheme().Green("Successfully verified the TUF repository"))
return nil
},
}
cmd.Flags().StringVarP(&mirror, "mirror", "m", "", "URL to the TUF repository mirror")
cmd.MarkFlagRequired("mirror") //nolint:errcheck
cmd.Flags().StringVarP(&root, "root", "r", "", "Path to the TUF root file on disk")
cmd.MarkFlagRequired("root") //nolint:errcheck
return &cmd
}
func tufRootVerify(mirror, root string) error {
rb, err := os.ReadFile(root)
if err != nil {
return fmt.Errorf("failed to read root file %s: %v", root, err)
}
opts := verification.GitHubTUFOptions()
opts.Root = rb
opts.RepositoryBaseURL = mirror
// The purpose is the verify the TUF root and repository, make
// sure there is no caching enabled
opts.CacheValidity = 0
if _, err = tuf.New(opts); err != nil {
return fmt.Errorf("failed to create TUF client: %v", err)
}
return nil
}

View file

@ -0,0 +1,80 @@
package tufrootverify
import (
"bytes"
"strings"
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewTUFRootVerifyCmd(t *testing.T) {
testIO, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: testIO,
}
testcases := []struct {
name string
cli string
wantsErr bool
}{
{
name: "Missing mirror flag",
cli: "--root ../verification/embed/tuf-repo.github.com/root.json",
wantsErr: true,
},
{
name: "Missing root flag",
cli: "--mirror https://tuf-repo.github.com",
wantsErr: true,
},
{
name: "Has all required flags",
cli: "--mirror https://tuf-repo.github.com --root ../verification/embed/tuf-repo.github.com/root.json",
wantsErr: false,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
cmd := NewTUFRootVerifyCmd(f, func() error {
return nil
})
argv := strings.Split(tc.cli, " ")
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err := cmd.ExecuteC()
if tc.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}
func TestTUFRootVerify(t *testing.T) {
mirror := "https://tuf-repo.github.com"
root := test.NormalizeRelativePath("../verification/embed/tuf-repo.github.com/root.json")
t.Run("successfully verifies TUF root", func(t *testing.T) {
err := tufRootVerify(mirror, root)
require.NoError(t, err)
})
t.Run("fails because the root cannot be found", func(t *testing.T) {
notFoundRoot := test.NormalizeRelativePath("./does/not/exist/root.json")
err := tufRootVerify(mirror, notFoundRoot)
require.Error(t, err)
require.ErrorContains(t, err, "failed to read root file")
})
}

View file

@ -0,0 +1,115 @@
package verification
import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
"github.com/sigstore/sigstore-go/pkg/bundle"
)
var ErrLocalAttestations = errors.New("failed to load local attestations")
type FetchAttestationsConfig struct {
APIClient api.Client
BundlePath string
Digest string
Limit int
Owner string
Repo string
}
func (c *FetchAttestationsConfig) IsBundleProvided() bool {
return c.BundlePath != ""
}
func GetAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) {
if c.IsBundleProvided() {
return GetLocalAttestations(c.BundlePath)
}
return GetRemoteAttestations(c)
}
// GetLocalAttestations returns a slice of attestations read from a local bundle file.
func GetLocalAttestations(path string) ([]*api.Attestation, error) {
fileExt := filepath.Ext(path)
switch fileExt {
case ".json":
attestations, err := loadBundleFromJSONFile(path)
if err != nil {
return nil, fmt.Errorf("bundle could not be loaded from JSON file: %v", err)
}
return attestations, nil
case ".jsonl":
attestations, err := loadBundlesFromJSONLinesFile(path)
if err != nil {
return nil, fmt.Errorf("bundles could not be loaded from JSON lines file: %v", err)
}
return attestations, nil
}
return nil, ErrLocalAttestations
}
func loadBundleFromJSONFile(path string) ([]*api.Attestation, error) {
localAttestation, err := bundle.LoadJSONFromPath(path)
if err != nil {
return nil, err
}
return []*api.Attestation{{Bundle: localAttestation}}, nil
}
func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("could not open file: %v", err)
}
defer file.Close()
attestations := []*api.Attestation{}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
b := scanner.Bytes()
var bundle bundle.ProtobufBundle
bundle.Bundle = new(protobundle.Bundle)
err = bundle.UnmarshalJSON(b)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal bundle from JSON: %v", err)
}
a := api.Attestation{Bundle: &bundle}
attestations = append(attestations, &a)
}
if err := scanner.Err(); err != nil {
return nil, err
}
return attestations, nil
}
func GetRemoteAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) {
if c.APIClient == nil {
return nil, fmt.Errorf("api client must be provided")
}
// check if Repo is set first because if Repo has been set, Owner will be set using the value of Repo.
// If Repo is not set, the field will remain empty. It will not be populated using the value of Owner.
if c.Repo != "" {
attestations, err := c.APIClient.GetByRepoAndDigest(c.Repo, c.Digest, c.Limit)
if err != nil {
return nil, fmt.Errorf("failed to fetch attestations from %s: %w", c.Repo, err)
}
return attestations, nil
} else if c.Owner != "" {
attestations, err := c.APIClient.GetByOwnerAndDigest(c.Owner, c.Digest, c.Limit)
if err != nil {
return nil, fmt.Errorf("failed to fetch attestations from %s: %w", c.Owner, err)
}
return attestations, nil
}
return nil, fmt.Errorf("owner or repo must be provided")
}

View file

@ -0,0 +1,49 @@
package verification
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestLoadBundlesFromJSONLinesFile(t *testing.T) {
path := "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl"
attestations, err := loadBundlesFromJSONLinesFile(path)
require.NoError(t, err)
require.Len(t, attestations, 2)
}
func TestLoadBundleFromJSONFile(t *testing.T) {
path := "../test/data/sigstore-js-2.1.0-bundle.json"
attestations, err := loadBundleFromJSONFile(path)
require.NoError(t, err)
require.Len(t, attestations, 1)
}
func TestGetLocalAttestations(t *testing.T) {
t.Run("with JSON file containing one bundle", func(t *testing.T) {
path := "../test/data/sigstore-js-2.1.0-bundle.json"
attestations, err := GetLocalAttestations(path)
require.NoError(t, err)
require.Len(t, attestations, 1)
})
t.Run("with JSON lines file containing multiple bundles", func(t *testing.T) {
path := "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl"
attestations, err := GetLocalAttestations(path)
require.NoError(t, err)
require.Len(t, attestations, 2)
})
t.Run("with file with unrecognized extension", func(t *testing.T) {
path := "../test/data/sigstore-js-2.1.0-bundles.tgz"
attestations, err := GetLocalAttestations(path)
require.ErrorIs(t, err, ErrLocalAttestations)
require.Nil(t, attestations)
})
}

View file

@ -0,0 +1,163 @@
{
"signatures": [
{
"keyid": "a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf",
"sig": "304402203c8f5f7443f7052923e82f9ca0b1bb61a33498444076a2f43e1285a47f6e562d022014de99a7e5413440896b6804944e3c49390cfe6e617211b8dc42a8e67675bc13"
},
{
"keyid": "d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c",
"sig": "3044022009a20307f974af7e05cc9564eea497f45062e3b21272d1062713b3d22c868298022059d032ad973a28bdbd03959cf96b21398b6b6e2ca618c17ce6c13712246343a2"
},
{
"keyid": "539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a",
"sig": "3045022100edd270d36d0c8468b9a1f2ef1c81a270c72ffd50c65bca0ed1ebd424df09f64b022002b27ffafd7bc5bdfc25281b5b9b597cf2d67d4eeb4af2ff45eb3e666b860c21"
},
{
"keyid": "88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3",
"sig": "30460221008d7d95434e576d5876b2db30fd645505ca546618bbc7a8e4b39f64e6a36df9ad022100c00a5294e4ddd02d48d28918b87a06bdfdeccd0618ecdcec29bb2597a05fe474"
},
{
"keyid": "5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800",
"sig": "30450220215fb3d19d94560a3a2a6067a71c92daf867d13700c9500c4c32d8009a48a634022100df9fb6cee786313bf6c363daac4de39b3dd531f211f81d2391c41bd2d0f91a80"
},
{
"keyid": "4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228",
"sig": "304502204091ac5e61b6462d262ecc8442781dd09843bed39942a95a4884c8c6a2c212ef022100dcee86392748f48950d04d539ac1a6643ed1f0b4bd6856f8aeb5a135826c846f"
},
{
"keyid": "eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62",
"sig": "30460221009188548601a43b501223caeefca4876ae892e18a85c885004b4c8aeeb05a4421022100abdcc72d94597f8297d6297897ff96f285168dbe6b3d57f846dbc7a2948d2935"
},
{
"keyid": "8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12",
"sig": "3046022100b440561545d48759dc4140cda9f8af7c9405a101d6136dd0a26edd6562b7064f022100cafa917ed90350494e47d226b64a8ec63ef5ceebb8ba4d2dec2ce018e4ad402a"
}
],
"signed": {
"_type": "root",
"consistent_snapshot": true,
"expires": "2024-06-23T08:29:18Z",
"keys": {
"4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228": {
"keytype": "ecdsa",
"keyval": {
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENki7aZVips5SgRzCd/Om0CGzQKY/\nnv84giqVDmdwb2ys82Z6soFLasvYYEEQcwqaC170n9gr93wHUgPc796uJA==\n-----END PUBLIC KEY-----\n"
},
"scheme": "ecdsa-sha2-nistp256",
"x-tuf-on-ci-keyowner": "@ashtom"
},
"539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a": {
"keytype": "ecdsa",
"keyval": {
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElD0o2sOZN9n3RKQ7PtMLAoXj+2Ai\nn4PKT/pfnzDlNLrD3VTQwCc4sR4t+OLu4KQ+qk+kXkR9YuBsu3bdJZ1OWw==\n-----END PUBLIC KEY-----\n"
},
"scheme": "ecdsa-sha2-nistp256",
"x-tuf-on-ci-keyowner": "@nerdneha"
},
"5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800": {
"keytype": "ecdsa",
"keyval": {
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC9RNAsuDCNO6T7qA7Y5F8orw2tIW\nr7rUr4ffxvzTMrbkVtjR/trtE0q0+T0zQ8TWLyI6EYMwb947ej2ItfkOyA==\n-----END PUBLIC KEY-----\n"
},
"scheme": "ecdsa-sha2-nistp256",
"x-tuf-on-ci-keyowner": "@jacobdepriest"
},
"88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3": {
"keytype": "ecdsa",
"keyval": {
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBagkskNOpOTbetTX5CdnvMy+LiWn\nonRrNrqAHL4WgiebH7Uig7GLhC3bkeA/qgb926/vr9qhOPG9Buj2HatrPw==\n-----END PUBLIC KEY-----\n"
},
"scheme": "ecdsa-sha2-nistp256",
"x-tuf-on-ci-keyowner": "@gregose"
},
"8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12": {
"keytype": "ecdsa",
"keyval": {
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7IEoVNwrprchXGhT5sAhSax7SOd3\n8duuISghCzfmHdKJWSbV2wJRamRiUVRtmA83K/qm5cT20WXMCT5QeM/D3A==\n-----END PUBLIC KEY-----\n"
},
"scheme": "ecdsa-sha2-nistp256",
"x-tuf-on-ci-keyowner": "@trevrosen"
},
"a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf": {
"keytype": "ecdsa",
"keyval": {
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC2wJ3xscyXxBLybJ9FVjwkyQMe53\nRHUz77AjMO8MzVaT8xw6ZvJqdNZiytYtigWULlINxw6frNsWJKb/f7lC8A==\n-----END PUBLIC KEY-----\n"
},
"scheme": "ecdsa-sha2-nistp256",
"x-tuf-on-ci-keyowner": "@kommendorkapten"
},
"d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c": {
"keytype": "ecdsa",
"keyval": {
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDdORwcruW3gqAgaLjH/nNdGMB4kQ\nAvA+wD6DyO4P/wR8ee2ce83NZHq1ZADKhve0rlYKaKy3CqyQ5SmlZ36Zhw==\n-----END PUBLIC KEY-----\n"
},
"scheme": "ecdsa-sha2-nistp256",
"x-tuf-on-ci-keyowner": "@krukow"
},
"eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62": {
"keytype": "ecdsa",
"keyval": {
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENynVdQnM9h7xU71G7PiJpQaDemub\nkbjsjYwLlPJTQVuxQO8WeIpJf8MEh5rf01t2dDIuCsZ5gRx+QvDv0UzfsA==\n-----END PUBLIC KEY-----\n"
},
"scheme": "ecdsa-sha2-nistp256",
"x-tuf-on-ci-keyowner": "@mph4"
},
"eb9799b483affac9da87ef4c9ea467928415c961349e607e5e6e485679b07f8f": {
"keytype": "ecdsa",
"keyval": {
"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENKNcNcX+d73lS1TRFb9Vnp8JvOoh\nzYQ+in43iGenbG8RGo9L/6FJ2hoRbVU6xskvyuErcdPbCdI4GxrQ5i8hkw==\n-----END PUBLIC KEY-----\n"
},
"scheme": "ecdsa-sha2-nistp256",
"x-tuf-on-ci-online-uri": "azurekms://production-tuf-root.vault.azure.net/keys/Online-Key/aaf375fd8ed24acb949a5cc173700b05"
}
},
"roles": {
"root": {
"keyids": [
"a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf",
"4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228",
"88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3",
"5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800",
"d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c",
"eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62",
"8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12",
"539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a"
],
"threshold": 3
},
"snapshot": {
"keyids": [
"eb9799b483affac9da87ef4c9ea467928415c961349e607e5e6e485679b07f8f"
],
"threshold": 1,
"x-tuf-on-ci-expiry-period": 21,
"x-tuf-on-ci-signing-period": 7
},
"targets": {
"keyids": [
"a10513a5ab61acd0c6b6fbe0504856ead18f3b17c4fabbe3fa848c79a5a187cf",
"4f4d1dd75f2d7f3860e3a068d7bed90dec5f0faafcbe1ace7fb7d95d29e07228",
"88737ccdac7b49cc237e9aaead81be2a40278b886a693d8149a19cf543f093d3",
"5e01c9a0b2641a8965a4a74e7df0bc7b2d8278a2c3ca0cf7a3f2f783d3c69800",
"d6a89e23fb22801a0d1186bf1bdd007e228f65a8aa9964d24d06cb5fbb0ce91c",
"eb8eff37f93af2faaba519f341decec3cecd3eeafcace32966db9723842c8a62",
"8b498a80a1b7af188c10c9abdf6aade81d14faaffcde2abcd6063baa673ebd12",
"539dde44014c850fe6eeb8b299eb7dae2e1f4bf83454b949e98aa73542cdc65a"
],
"threshold": 3
},
"timestamp": {
"keyids": [
"eb9799b483affac9da87ef4c9ea467928415c961349e607e5e6e485679b07f8f"
],
"threshold": 1,
"x-tuf-on-ci-expiry-period": 7,
"x-tuf-on-ci-signing-period": 6
}
},
"spec_version": "1.0.31",
"version": 1,
"x-tuf-on-ci-expiry-period": 240,
"x-tuf-on-ci-signing-period": 60
}
}

View file

@ -0,0 +1,20 @@
package verification
import (
"encoding/hex"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
"github.com/sigstore/sigstore-go/pkg/verify"
)
// BuildDigestPolicyOption builds a verify.ArtifactPolicyOption
// from the given artifact digest and digest algorithm
func BuildDigestPolicyOption(a artifact.DigestedArtifact) (verify.ArtifactPolicyOption, error) {
// sigstore-go expects the artifact digest to be decoded from hex
decoded, err := hex.DecodeString(a.Digest())
if err != nil {
return nil, err
}
return verify.WithArtifactDigest(a.Algorithm(), decoded), nil
}

View file

@ -0,0 +1,208 @@
package verification
import (
"fmt"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/sigstore/sigstore-go/pkg/bundle"
"github.com/sigstore/sigstore-go/pkg/root"
"github.com/sigstore/sigstore-go/pkg/tuf"
"github.com/sigstore/sigstore-go/pkg/verify"
)
const (
PublicGoodIssuerOrg = "sigstore.dev"
GitHubIssuerOrg = "GitHub, Inc."
)
// AttestationProcessingResult captures processing a given attestation's signature verification and policy evaluation
type AttestationProcessingResult struct {
Attestation *api.Attestation `json:"attestation"`
VerificationResult *verify.VerificationResult `json:"verificationResult"`
}
type SigstoreResults struct {
VerifyResults []*AttestationProcessingResult
Error error
}
type SigstoreConfig struct {
CustomTrustedRoot string
Logger *io.Handler
NoPublicGood bool
}
type SigstoreVerifier struct {
ghVerifier *verify.SignedEntityVerifier
publicGoodVerifier *verify.SignedEntityVerifier
customVerifier *verify.SignedEntityVerifier
policy verify.PolicyBuilder
onlyVerifyWithGithub bool
Logger *io.Handler
}
// NewSigstoreVerifier creates a new SigstoreVerifier struct
// that is used to verify artifacts and attestations against the
// Public Good, GitHub, or a custom trusted root.
func NewSigstoreVerifier(config SigstoreConfig, policy verify.PolicyBuilder) (*SigstoreVerifier, error) {
customVerifier, err := newCustomVerifier(config.CustomTrustedRoot)
if err != nil {
return nil, fmt.Errorf("failed to create custom verifier: %v", err)
}
publicGoodVerifier, err := newPublicGoodVerifier()
if err != nil {
return nil, fmt.Errorf("failed to create Public Good Sigstore verifier: %v", err)
}
ghVerifier, err := newGitHubVerifier()
if err != nil {
return nil, fmt.Errorf("failed to create GitHub Sigstore verifier: %v", err)
}
return &SigstoreVerifier{
ghVerifier: ghVerifier,
publicGoodVerifier: publicGoodVerifier,
customVerifier: customVerifier,
Logger: config.Logger,
policy: policy,
onlyVerifyWithGithub: config.NoPublicGood,
}, nil
}
func (v *SigstoreVerifier) chooseVerifier(b *bundle.ProtobufBundle) (*verify.SignedEntityVerifier, string, error) {
verifyContent, err := b.VerificationContent()
if err != nil {
return nil, "", fmt.Errorf("failed to get bundle verification content: %v", err)
}
leafCert, ok := verifyContent.HasCertificate()
if !ok {
return nil, "", fmt.Errorf("leaf cert not found")
}
if len(leafCert.Issuer.Organization) != 1 {
return nil, "", fmt.Errorf("expected the leaf certificate issuer to only have one organization")
}
issuer := leafCert.Issuer.Organization[0]
// if user provided a custom trusted root file path, use the custom verifier
if v.customVerifier != nil {
return v.customVerifier, issuer, nil
}
if v.onlyVerifyWithGithub {
return v.ghVerifier, issuer, nil
}
if leafCert.Issuer.Organization[0] == PublicGoodIssuerOrg {
return v.publicGoodVerifier, issuer, nil
} else if leafCert.Issuer.Organization[0] == GitHubIssuerOrg {
return v.ghVerifier, issuer, nil
}
return nil, "", fmt.Errorf("leaf certificate issuer is not recognized")
}
func (v *SigstoreVerifier) Verify(attestations []*api.Attestation) *SigstoreResults {
// initialize the processing results before attempting to verify
// with multiple verifiers
results := make([]*AttestationProcessingResult, len(attestations))
for i, att := range attestations {
apr := &AttestationProcessingResult{
Attestation: att,
}
results[i] = apr
}
totalAttestations := len(attestations)
for i, apr := range results {
v.Logger.VerbosePrintf("Verifying attestation %d/%d against the configured Sigstore trust roots\n", i+1, totalAttestations)
// determine which verifier should attempt verification against the bundle
verifier, issuer, err := v.chooseVerifier(apr.Attestation.Bundle)
if err != nil {
return &SigstoreResults{
Error: fmt.Errorf("failed to find recognized issuer from bundle content: %v", err),
}
}
v.Logger.VerbosePrintf("Attempting verification against issuer \"%s\"\n", issuer)
// attempt to verify the attestation
result, err := verifier.Verify(apr.Attestation.Bundle, v.policy)
// if verification fails, create the error and exit verification early
if err != nil {
v.Logger.VerbosePrint(v.Logger.ColorScheme.Redf(
"Failed to verify against issuer \"%s\" \n\n", issuer,
))
return &SigstoreResults{
Error: fmt.Errorf("verifying with issuer \"%s\": %v", issuer, err),
}
}
// if verification is successful, add the result
// to the AttestationProcessingResult entry
v.Logger.VerbosePrint(v.Logger.ColorScheme.Greenf(
"SUCCESS - attestation signature verified with \"%s\"\n", issuer,
))
apr.VerificationResult = result
}
return &SigstoreResults{
VerifyResults: results,
}
}
func newCustomVerifier(trustedRootFilePath string) (*verify.SignedEntityVerifier, error) {
if trustedRootFilePath == "" {
return nil, nil
}
trustedRoot, err := root.NewTrustedRootFromPath(trustedRootFilePath)
if err != nil {
return nil, fmt.Errorf("failed to create trusted root from file %s: %v", trustedRootFilePath, err)
}
gv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedTimestamps(1))
if err != nil {
return nil, fmt.Errorf("failed to create custom verifier: %v", err)
}
return gv, nil
}
func newGitHubVerifier() (*verify.SignedEntityVerifier, error) {
opts := GitHubTUFOptions()
client, err := tuf.New(opts)
if err != nil {
return nil, fmt.Errorf("failed to create TUF client: %v", err)
}
trustedRoot, err := root.GetTrustedRoot(client)
if err != nil {
return nil, err
}
gv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedTimestamps(1))
if err != nil {
return nil, fmt.Errorf("failed to create GitHub verifier: %v", err)
}
return gv, nil
}
func newPublicGoodVerifier() (*verify.SignedEntityVerifier, error) {
client, err := tuf.DefaultClient()
if err != nil {
return nil, fmt.Errorf("failed to create TUF client: %v", err)
}
trustedRoot, err := root.GetTrustedRoot(client)
if err != nil {
return nil, fmt.Errorf("failed to get trusted root: %v", err)
}
sv, err := verify.NewSignedEntityVerifier(trustedRoot, verify.WithSignedCertificateTimestamps(1), verify.WithTransparencyLog(1), verify.WithObserverTimestamps(1))
if err != nil {
return nil, fmt.Errorf("failed to create Public Good verifier: %v", err)
}
return sv, nil
}

View file

@ -0,0 +1,60 @@
package verification
import (
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
"github.com/sigstore/sigstore-go/pkg/verify"
"github.com/stretchr/testify/require"
)
func buildPolicy(a artifact.DigestedArtifact) (verify.PolicyBuilder, error) {
artifactDigestPolicyOption, err := BuildDigestPolicyOption(a)
if err != nil {
return verify.PolicyBuilder{}, err
}
policy := verify.NewPolicy(artifactDigestPolicyOption, verify.WithoutIdentitiesUnsafe())
return policy, nil
}
func TestNewSigstoreVerifier(t *testing.T) {
artifactPath := test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz")
artifact, err := artifact.NewDigestedArtifact(nil, artifactPath, "sha512")
require.NoError(t, err)
policy, err := buildPolicy(*artifact)
require.NoError(t, err)
c := SigstoreConfig{
Logger: io.NewTestHandler(),
}
verifier, err := NewSigstoreVerifier(c, policy)
require.NoError(t, err)
t.Run("with invalid signature", func(t *testing.T) {
bundlePath := test.NormalizeRelativePath("../test/data/sigstoreBundle-invalid-signature.json")
attestations, err := GetLocalAttestations(bundlePath)
require.NotNil(t, attestations)
require.NoError(t, err)
res := verifier.Verify(attestations)
require.Error(t, res.Error)
require.ErrorContains(t, res.Error, "verifying with issuer \"sigstore.dev\"")
require.Nil(t, res.VerifyResults)
})
t.Run("with valid artifact and JSON lines file containing multiple Sigstore bundles", func(t *testing.T) {
bundlePath := test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl")
attestations, err := GetLocalAttestations(bundlePath)
require.Len(t, attestations, 2)
require.NoError(t, err)
res := verifier.Verify(attestations)
require.Len(t, res.VerifyResults, 2)
require.NoError(t, res.Error)
})
}

View file

@ -0,0 +1,41 @@
package verification
import (
_ "embed"
"os"
"path/filepath"
"github.com/cli/go-gh/v2/pkg/config"
"github.com/sigstore/sigstore-go/pkg/tuf"
)
//go:embed embed/tuf-repo.github.com/root.json
var githubRoot []byte
const GitHubTUFMirror = "https://tuf-repo.github.com"
func DefaultOptionsWithCacheSetting() *tuf.Options {
opts := tuf.DefaultOptions()
// The CODESPACES environment variable will be set to true in a Codespaces workspace
if os.Getenv("CODESPACES") == "true" {
// if the tool is being used in a Codespace, disable the local cache
// because there is a permissions issue preventing the tuf library
// from writing the Sigstore cache to the home directory
opts.DisableLocalCache = true
}
// Set the cache path to a directory owned by the CLI
opts.CachePath = filepath.Join(config.CacheDir(), ".sigstore", "root")
return opts
}
func GitHubTUFOptions() *tuf.Options {
opts := DefaultOptionsWithCacheSetting()
opts.Root = githubRoot
opts.RepositoryBaseURL = GitHubTUFMirror
return opts
}

View file

@ -0,0 +1,20 @@
package verification
import (
"os"
"path/filepath"
"testing"
"github.com/cli/go-gh/v2/pkg/config"
"github.com/stretchr/testify/require"
)
func TestGitHubTUFOptions(t *testing.T) {
os.Setenv("CODESPACES", "true")
opts := GitHubTUFOptions()
require.Equal(t, GitHubTUFMirror, opts.RepositoryBaseURL)
require.NotNil(t, opts.Root)
require.True(t, opts.DisableLocalCache)
require.Equal(t, filepath.Join(config.CacheDir(), ".sigstore", "root"), opts.CachePath)
}

View file

@ -0,0 +1,83 @@
package verify
import (
"fmt"
"path/filepath"
"strings"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmdutil"
)
// Options captures the options for the verify command
type Options struct {
ArtifactPath string
BundlePath string
CustomTrustedRoot string
DenySelfHostedRunner bool
DigestAlgorithm string
NoPublicGood bool
OIDCIssuer string
Owner string
Repo string
SAN string
SANRegex string
APIClient api.Client
Logger *io.Handler
Limit int
OCIClient oci.Client
exporter cmdutil.Exporter
}
// Clean cleans the file path option values
func (opts *Options) Clean() {
if opts.BundlePath != "" {
opts.BundlePath = filepath.Clean(opts.BundlePath)
}
}
func (opts *Options) SetPolicyFlags() {
// check that Repo is in the expected format if provided
if opts.Repo != "" {
// we expect the repo argument to be in the format <OWNER>/<REPO>
splitRepo := strings.Split(opts.Repo, "/")
// if Repo is provided but owner is not, set the OWNER portion of the Repo value
// to Owner
opts.Owner = splitRepo[0]
if opts.SAN == "" && opts.SANRegex == "" {
opts.SANRegex = expandToGitHubURL(opts.Repo)
}
return
}
if opts.SAN == "" && opts.SANRegex == "" {
opts.SANRegex = expandToGitHubURL(opts.Owner)
}
}
// AreFlagsValid checks that the provided flag combination is valid
// and returns an error otherwise
func (opts *Options) AreFlagsValid() error {
// check that Repo is in the expected format if provided
if opts.Repo != "" {
// we expect the repo argument to be in the format <OWNER>/<REPO>
splitRepo := strings.Split(opts.Repo, "/")
if len(splitRepo) != 2 {
return fmt.Errorf("invalid value provided for repo: %s", opts.Repo)
}
}
// Check that limit is between 1 and 1000
if opts.Limit < 1 || opts.Limit > 1000 {
return fmt.Errorf("limit %d not allowed, must be between 1 and 1000", opts.Limit)
}
return nil
}
func expandToGitHubURL(ownerOrRepo string) string {
return fmt.Sprintf("^https://github.com/%s/", ownerOrRepo)
}

View file

@ -0,0 +1,119 @@
package verify
import (
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
"github.com/stretchr/testify/require"
)
var (
publicGoodArtifactPath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz")
publicGoodBundlePath = test.NormalizeRelativePath("../test/data/psigstore-js-2.1.0-bundle.json")
)
func TestAreFlagsValid(t *testing.T) {
t.Run("has invalid Repo value", func(t *testing.T) {
opts := Options{
ArtifactPath: publicGoodArtifactPath,
DigestAlgorithm: "sha512",
OIDCIssuer: "some issuer",
Repo: "sigstoresigstore-js",
}
err := opts.AreFlagsValid()
require.Error(t, err)
require.ErrorContains(t, err, "invalid value provided for repo")
})
t.Run("invalid limit < 0", func(t *testing.T) {
opts := Options{
ArtifactPath: publicGoodArtifactPath,
BundlePath: publicGoodBundlePath,
DigestAlgorithm: "sha512",
Owner: "sigstore",
OIDCIssuer: "some issuer",
Limit: 0,
}
err := opts.AreFlagsValid()
require.Error(t, err)
require.ErrorContains(t, err, "limit 0 not allowed, must be between 1 and 1000")
})
t.Run("invalid limit > 1000", func(t *testing.T) {
opts := Options{
ArtifactPath: publicGoodArtifactPath,
BundlePath: publicGoodBundlePath,
DigestAlgorithm: "sha512",
Owner: "sigstore",
OIDCIssuer: "some issuer",
Limit: 1001,
}
err := opts.AreFlagsValid()
require.Error(t, err)
require.ErrorContains(t, err, "limit 1001 not allowed, must be between 1 and 1000")
})
}
func TestSetPolicyFlags(t *testing.T) {
t.Run("sets Owner and SANRegex when Repo is provided", func(t *testing.T) {
opts := Options{
ArtifactPath: publicGoodArtifactPath,
DigestAlgorithm: "sha512",
OIDCIssuer: "some issuer",
Repo: "sigstore/sigstore-js",
}
opts.SetPolicyFlags()
require.Equal(t, "sigstore", opts.Owner)
require.Equal(t, "sigstore/sigstore-js", opts.Repo)
require.Equal(t, "^https://github.com/sigstore/sigstore-js/", opts.SANRegex)
})
t.Run("does not set SANRegex when SANRegex and Repo are provided", func(t *testing.T) {
opts := Options{
ArtifactPath: publicGoodArtifactPath,
DigestAlgorithm: "sha512",
OIDCIssuer: "some issuer",
Repo: "sigstore/sigstore-js",
SANRegex: "^https://github/foo",
}
opts.SetPolicyFlags()
require.Equal(t, "sigstore", opts.Owner)
require.Equal(t, "sigstore/sigstore-js", opts.Repo)
require.Equal(t, "^https://github/foo", opts.SANRegex)
})
t.Run("sets SANRegex when Owner is provided", func(t *testing.T) {
opts := Options{
ArtifactPath: publicGoodArtifactPath,
BundlePath: publicGoodBundlePath,
DigestAlgorithm: "sha512",
OIDCIssuer: "some issuer",
Owner: "sigstore",
}
opts.SetPolicyFlags()
require.Equal(t, "sigstore", opts.Owner)
require.Equal(t, "^https://github.com/sigstore/", opts.SANRegex)
})
t.Run("does not set SANRegex when SANRegex and Owner are provided", func(t *testing.T) {
opts := Options{
ArtifactPath: publicGoodArtifactPath,
BundlePath: publicGoodBundlePath,
DigestAlgorithm: "sha512",
OIDCIssuer: "some issuer",
Owner: "sigstore",
SANRegex: "^https://github/foo",
}
opts.SetPolicyFlags()
require.Equal(t, "sigstore", opts.Owner)
require.Equal(t, "^https://github/foo", opts.SANRegex)
})
}

View file

@ -0,0 +1,85 @@
package verify
import (
"fmt"
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
"github.com/sigstore/sigstore-go/pkg/verify"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
)
const (
GitHubOIDCIssuer = "https://token.actions.githubusercontent.com"
SLSAPredicateType = "https://slsa.dev/provenance/v1"
// represents the GitHub hosted runner in the certificate RunnerEnvironment extension
GitHubRunner = "github-hosted"
)
func buildSANMatcher(san, sanRegex string) (verify.SubjectAlternativeNameMatcher, error) {
if san == "" && sanRegex == "" {
return verify.SubjectAlternativeNameMatcher{}, nil
}
sanMatcher, err := verify.NewSANMatcher(san, "", sanRegex)
if err != nil {
return verify.SubjectAlternativeNameMatcher{}, err
}
return sanMatcher, nil
}
func buildCertificateIdentityOption(opts *Options, runnerEnv string) (verify.PolicyOption, error) {
sanMatcher, err := buildSANMatcher(opts.SAN, opts.SANRegex)
if err != nil {
return nil, err
}
extensions := certificate.Extensions{
Issuer: opts.OIDCIssuer,
SourceRepositoryOwnerURI: fmt.Sprintf("https://github.com/%s", opts.Owner),
RunnerEnvironment: runnerEnv,
}
certId, err := verify.NewCertificateIdentity(sanMatcher, extensions)
if err != nil {
return nil, err
}
return verify.WithCertificateIdentity(certId), nil
}
func buildVerifyCertIdOption(opts *Options) (verify.PolicyOption, error) {
if opts.DenySelfHostedRunner {
withGHRunner, err := buildCertificateIdentityOption(opts, GitHubRunner)
if err != nil {
return nil, err
}
return withGHRunner, nil
}
// if Extensions.RunnerEnvironment value is set to the empty string
// through the second function argument,
// no certificate matching will happen on the RunnerEnvironment field
withAnyRunner, err := buildCertificateIdentityOption(opts, "")
if err != nil {
return nil, err
}
return withAnyRunner, nil
}
func buildVerifyPolicy(opts *Options, a artifact.DigestedArtifact) (verify.PolicyBuilder, error) {
artifactDigestPolicyOption, err := verification.BuildDigestPolicyOption(a)
if err != nil {
return verify.PolicyBuilder{}, err
}
certIdOption, err := buildVerifyCertIdOption(opts)
if err != nil {
return verify.PolicyBuilder{}, err
}
policy := verify.NewPolicy(artifactDigestPolicyOption, certIdOption)
return policy, nil
}

View file

@ -0,0 +1,31 @@
package verify
import (
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/stretchr/testify/require"
)
// This tests that a policy can be built from a valid artifact
// Note that policy use is tested in verify_test.go in this package
func TestBuildPolicy(t *testing.T) {
ociClient := oci.MockClient{}
artifactPath := "../test/data/sigstore-js-2.1.0.tgz"
digestAlg := "sha256"
artifact, err := artifact.NewDigestedArtifact(ociClient, artifactPath, digestAlg)
require.NoError(t, err)
opts := &Options{
ArtifactPath: artifactPath,
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
SANRegex: "^https://github.com/sigstore/",
}
_, err = buildVerifyPolicy(opts, *artifact)
require.NoError(t, err)
}

View file

@ -0,0 +1,216 @@
package verify
import (
// "encoding/json"
"errors"
"fmt"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/cli/cli/v2/pkg/cmd/attestation/auth"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
)
var ErrNoMatchingSLSAPredicate = fmt.Errorf("the attestation does not have the expected SLSA predicate type: %s", SLSAPredicateType)
func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command {
opts := &Options{}
verifyCmd := &cobra.Command{
Use: "verify [<file-path> | oci://<image-uri>] [--owner | --repo]",
Args: cobra.ExactArgs(1),
Short: "Verify an artifact's integrity using attestations",
Long: heredoc.Docf(`
Verify the integrity and provenance of an artifact using its associated
cryptographically signed attestations.
The command requires either:
* a file path to an artifact, or
* a container image URI (e.g. %[1]soci://<image-uri>%[1]s)
(Note that if you provide an OCI URL, you must already be authenticated with
its container registry.)
In addition, the command requires either:
* the %[1]s--owner%[1]s flag (e.g. --owner github), or
* the %[1]s--repo%[1]s flag (e.g. --repo github/example).
The %[1]s--owner%[1]s flag value must match the name of the GitHub organization
that the artifact is associated with.
The %[1]s--repo%[1]s flag value must match the name of the GitHub repository
that the artifact is associated with.
By default, the verify command will attempt to fetch attestations associated
with the provided artifact from the GitHub API. If you would prefer to verify
the artifact using attestations stored on disk (i.e. from the download command),
provide a path to the %[1]s--bundle%[1]s flag.
To see the full results that are generated upon successful verification, i.e.
for use with a policy engine, provide the %[1]s--json-result%[1]s flag.
For more policy verification options, see the other available flags.
`, "`"),
Example: heredoc.Doc(`
# Verify a local artifact associated with a repository
$ gh attestation verify example.bin --repo github/example
# Verify a local artifact associated with an organization
$ gh attestation verify example.bin --owner github
# Verify an OCI image using locally stored attestations
$ gh attestation verify oci://<image-uri> --owner github --bundle sha256:foo.jsonl
`),
// PreRunE is used to validate flags before the command is run
// If an error is returned, its message will be printed to the terminal
// along with information about how use the command
PreRunE: func(cmd *cobra.Command, args []string) error {
// Create a logger for use throughout the verify command
opts.Logger = io.NewHandler(f.IOStreams)
// set the artifact path
opts.ArtifactPath = args[0]
// Check that the given flag combination is valid
if err := opts.AreFlagsValid(); err != nil {
return err
}
// Clean file path options
opts.Clean()
// set policy flags based on what has been provided
opts.SetPolicyFlags()
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
hc, err := f.HttpClient()
if err != nil {
return err
}
opts.APIClient = api.NewLiveClient(hc, opts.Logger)
opts.OCIClient = oci.NewLiveClient()
if err := auth.IsHostSupported(); err != nil {
return err
}
if runF != nil {
return runF(opts)
}
if err := runVerify(opts); err != nil {
return fmt.Errorf("Failed to verify the artifact: %v", err)
}
return nil
},
}
// general flags
verifyCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles")
cmdutil.StringEnumFlag(verifyCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact")
verifyCmd.Flags().StringVarP(&opts.Owner, "owner", "o", "", "GitHub organization to scope attestation lookup by")
verifyCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format <owner>/<repo>")
verifyCmd.MarkFlagsMutuallyExclusive("owner", "repo")
verifyCmd.MarkFlagsOneRequired("owner", "repo")
verifyCmd.Flags().BoolVarP(&opts.NoPublicGood, "no-public-good", "", false, "Only verify attestations signed with GitHub's Sigstore instance")
verifyCmd.Flags().StringVarP(&opts.CustomTrustedRoot, "custom-trusted-root", "", "", "Path to a custom trustedroot.json file to use for verification")
verifyCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch")
cmdutil.AddFormatFlags(verifyCmd, &opts.exporter)
// policy enforcement flags
verifyCmd.Flags().BoolVarP(&opts.DenySelfHostedRunner, "deny-self-hosted-runners", "", false, "Fail verification for attestations generated on self-hosted runners.")
verifyCmd.Flags().StringVarP(&opts.SAN, "cert-identity", "", "", "Enforce that the certificate's subject alternative name matches the provided value exactly")
verifyCmd.Flags().StringVarP(&opts.SANRegex, "cert-identity-regex", "i", "", "Enforce that the certificate's subject alternative name matches the provided regex")
verifyCmd.MarkFlagsMutuallyExclusive("cert-identity", "cert-identity-regex")
verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", GitHubOIDCIssuer, "Issuer of the OIDC token")
return verifyCmd
}
func runVerify(opts *Options) error {
artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm)
if err != nil {
return fmt.Errorf("failed to digest artifact: %s", err)
}
opts.Logger.Printf("Verifying attestations for the artifact found at %s\n", artifact.URL)
c := verification.FetchAttestationsConfig{
APIClient: opts.APIClient,
BundlePath: opts.BundlePath,
Digest: artifact.DigestWithAlg(),
Limit: opts.Limit,
Owner: opts.Owner,
Repo: opts.Repo,
}
attestations, err := verification.GetAttestations(c)
if err != nil {
if ok := errors.Is(err, api.ErrNoAttestations{}); ok {
return fmt.Errorf("no attestations found for subject: %s", artifact.DigestWithAlg())
}
return fmt.Errorf("failed to fetch attestations for subject: %s", artifact.DigestWithAlg())
}
policy, err := buildVerifyPolicy(opts, *artifact)
if err != nil {
return fmt.Errorf("failed to build policy: %v", err)
}
config := verification.SigstoreConfig{
CustomTrustedRoot: opts.CustomTrustedRoot,
Logger: opts.Logger,
NoPublicGood: opts.NoPublicGood,
}
sv, err := verification.NewSigstoreVerifier(config, policy)
if err != nil {
return err
}
sigstoreRes := sv.Verify(attestations)
if sigstoreRes.Error != nil {
return fmt.Errorf("at least one attestation failed to verify against Sigstore: %v", sigstoreRes.Error)
}
opts.Logger.VerbosePrint(opts.Logger.ColorScheme.Green(
"Successfully verified all attestations against Sigstore!\n",
))
// Try verifying the attestation's predicate type against the expect SLSA predicate type
if err = verifySLSAPredicateType(opts.Logger, sigstoreRes.VerifyResults); err != nil {
return fmt.Errorf("at least one attestation failed to verify predicate type verification: %v", err)
}
opts.Logger.VerbosePrint(opts.Logger.ColorScheme.Green("Successfully verified the SLSA predicate type of all attestations!\n"))
opts.Logger.Println(opts.Logger.ColorScheme.Green("All attestations have been successfully verified!"))
if opts.exporter != nil {
// print the results to the terminal as an array of JSON objects
if err = opts.exporter.Write(opts.Logger.IO, sigstoreRes.VerifyResults); err != nil {
return fmt.Errorf("failed to write JSON output")
}
}
// All attestations passed verification and policy evaluation
return nil
}
func verifySLSAPredicateType(logger *io.Handler, apr []*verification.AttestationProcessingResult) error {
logger.VerbosePrint("Evaluating attestations have valid SLSA predicate type")
for _, result := range apr {
if result.VerificationResult.Statement.PredicateType != SLSAPredicateType {
return ErrNoMatchingSLSAPredicate
}
}
return nil
}

View file

@ -0,0 +1,401 @@
package verify
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
"github.com/in-toto/in-toto-golang/in_toto"
"github.com/sigstore/sigstore-go/pkg/verify"
"github.com/stretchr/testify/require"
)
const (
SigstoreSanValue = "https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main"
SigstoreSanRegex = "^https://github.com/sigstore/sigstore-js/"
)
var (
artifactPath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz")
bundlePath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json")
)
func TestNewVerifyCmd(t *testing.T) {
testIO, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: testIO,
HttpClient: func() (*http.Client, error) {
reg := &httpmock.Registry{}
client := &http.Client{}
httpmock.ReplaceTripper(client, reg)
return client, nil
},
}
testcases := []struct {
name string
cli string
wants Options
wantsErr bool
wantsExporter bool
}{
{
name: "Invalid digest-alg flag",
cli: fmt.Sprintf("%s --bundle %s --digest-alg sha384 --owner sigstore", artifactPath, bundlePath),
wants: Options{
ArtifactPath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"),
BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"),
DigestAlgorithm: "sha384",
Limit: 30,
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
},
wantsErr: true,
},
{
name: "Use default digest-alg value",
cli: fmt.Sprintf("%s --bundle %s --owner sigstore", artifactPath, bundlePath),
wants: Options{
ArtifactPath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"),
BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"),
DigestAlgorithm: "sha256",
Limit: 30,
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
SANRegex: "^https://github.com/sigstore/",
},
wantsErr: false,
},
{
name: "Use custom digest-alg value",
cli: fmt.Sprintf("%s --bundle %s --owner sigstore --digest-alg sha512", artifactPath, bundlePath),
wants: Options{
ArtifactPath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"),
BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"),
DigestAlgorithm: "sha512",
Limit: 30,
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
SANRegex: "^https://github.com/sigstore/",
},
wantsErr: false,
},
{
name: "Missing owner and repo flags",
cli: artifactPath,
wants: Options{
ArtifactPath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"),
DigestAlgorithm: "sha256",
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
Limit: 30,
SANRegex: "^https://github.com/sigstore/",
},
wantsErr: true,
},
{
name: "Has both owner and repo flags",
cli: fmt.Sprintf("%s --owner sigstore --repo sigstore/sigstore-js", artifactPath),
wants: Options{
ArtifactPath: artifactPath,
DigestAlgorithm: "sha256",
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
Repo: "sigstore/sigstore-js",
Limit: 30,
},
wantsErr: true,
},
{
name: "Uses default limit flag",
cli: fmt.Sprintf("%s --owner sigstore", artifactPath),
wants: Options{
ArtifactPath: artifactPath,
DigestAlgorithm: "sha256",
Limit: 30,
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
SANRegex: "^https://github.com/sigstore/",
},
wantsErr: false,
},
{
name: "Uses custom limit flag",
cli: fmt.Sprintf("%s --owner sigstore --limit 101", artifactPath),
wants: Options{
ArtifactPath: artifactPath,
DigestAlgorithm: "sha256",
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
Limit: 101,
SANRegex: "^https://github.com/sigstore/",
},
wantsErr: false,
},
{
name: "Uses invalid limit flag",
cli: fmt.Sprintf("%s --owner sigstore --limit 0", artifactPath),
wants: Options{
ArtifactPath: artifactPath,
DigestAlgorithm: "sha256",
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
Limit: 0,
SANRegex: "^https://github.com/sigstore/",
},
wantsErr: true,
},
{
name: "Has both cert-identity and cert-identity-regex flags",
cli: fmt.Sprintf("%s --owner sigstore --cert-identity https://github.com/sigstore/ --cert-identity-regex ^https://github.com/sigstore/", artifactPath),
wants: Options{
ArtifactPath: artifactPath,
DigestAlgorithm: "sha256",
Limit: 30,
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
SAN: "https://github.com/sigstore/",
SANRegex: "^https://github.com/sigstore/",
},
wantsErr: true,
},
{
name: "Prints output in JSON format",
cli: fmt.Sprintf("%s --bundle %s --owner sigstore --format json", artifactPath, bundlePath),
wants: Options{
ArtifactPath: artifactPath,
BundlePath: bundlePath,
DigestAlgorithm: "sha256",
Limit: 30,
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
SANRegex: "^https://github.com/sigstore/",
},
wantsExporter: true,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
var opts *Options
cmd := NewVerifyCmd(f, func(o *Options) error {
opts = o
return nil
})
argv := strings.Split(tc.cli, " ")
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err := cmd.ExecuteC()
if tc.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wants.ArtifactPath, opts.ArtifactPath)
assert.Equal(t, tc.wants.BundlePath, opts.BundlePath)
assert.Equal(t, tc.wants.CustomTrustedRoot, opts.CustomTrustedRoot)
assert.Equal(t, tc.wants.DenySelfHostedRunner, opts.DenySelfHostedRunner)
assert.Equal(t, tc.wants.DigestAlgorithm, opts.DigestAlgorithm)
assert.Equal(t, tc.wants.Limit, opts.Limit)
assert.Equal(t, tc.wants.NoPublicGood, opts.NoPublicGood)
assert.Equal(t, tc.wants.OIDCIssuer, opts.OIDCIssuer)
assert.Equal(t, tc.wants.Owner, opts.Owner)
assert.Equal(t, tc.wants.Repo, opts.Repo)
assert.Equal(t, tc.wants.SAN, opts.SAN)
assert.Equal(t, tc.wants.SANRegex, opts.SANRegex)
assert.NotNil(t, opts.APIClient)
assert.NotNil(t, opts.Logger)
assert.NotNil(t, opts.OCIClient)
assert.Equal(t, tc.wantsExporter, opts.exporter != nil)
})
}
}
func TestJSONOutput(t *testing.T) {
testIO, _, out, _ := iostreams.Test()
opts := Options{
ArtifactPath: artifactPath,
BundlePath: bundlePath,
DigestAlgorithm: "sha512",
APIClient: api.NewTestClient(),
Logger: io.NewHandler(testIO),
OCIClient: oci.MockClient{},
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
SANRegex: "^https://github.com/sigstore/",
exporter: cmdutil.NewJSONExporter(),
}
require.Nil(t, runVerify(&opts))
var target []*verification.AttestationProcessingResult
err := json.Unmarshal(out.Bytes(), &target)
require.NoError(t, err)
}
func TestRunVerify(t *testing.T) {
logger := io.NewTestHandler()
publicGoodOpts := Options{
ArtifactPath: artifactPath,
BundlePath: bundlePath,
DigestAlgorithm: "sha512",
APIClient: api.NewTestClient(),
Logger: logger,
OCIClient: oci.MockClient{},
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
SANRegex: "^https://github.com/sigstore/",
}
t.Run("with valid artifact and bundle", func(t *testing.T) {
require.Nil(t, runVerify(&publicGoodOpts))
})
t.Run("with failing OCI artifact fetch", func(t *testing.T) {
opts := publicGoodOpts
opts.ArtifactPath = "oci://ghcr.io/github/test"
opts.OCIClient = oci.ReferenceFailClient{}
err := runVerify(&opts)
require.Error(t, err)
require.ErrorContains(t, err, "failed to digest artifact")
})
t.Run("with missing artifact path", func(t *testing.T) {
opts := publicGoodOpts
opts.ArtifactPath = "../test/data/non-existent-artifact.zip"
require.Error(t, runVerify(&opts))
})
t.Run("with missing bundle path", func(t *testing.T) {
opts := publicGoodOpts
opts.BundlePath = "../test/data/non-existent-sigstoreBundle.json"
require.Error(t, runVerify(&opts))
})
t.Run("with owner", func(t *testing.T) {
opts := publicGoodOpts
opts.BundlePath = ""
opts.Owner = "sigstore"
require.Nil(t, runVerify(&opts))
})
t.Run("with repo", func(t *testing.T) {
opts := publicGoodOpts
opts.BundlePath = ""
opts.Repo = "github/example"
require.Nil(t, runVerify(&opts))
})
t.Run("with invalid repo", func(t *testing.T) {
opts := publicGoodOpts
opts.BundlePath = ""
opts.Repo = "wrong/example"
opts.APIClient = api.NewFailTestClient()
err := runVerify(&opts)
require.Error(t, err)
require.ErrorContains(t, err, "failed to fetch attestations for subject")
})
t.Run("with invalid owner", func(t *testing.T) {
opts := publicGoodOpts
opts.BundlePath = ""
opts.APIClient = api.NewFailTestClient()
opts.Owner = "wrong-owner"
err := runVerify(&opts)
require.Error(t, err)
require.ErrorContains(t, err, "failed to fetch attestations for subject")
})
t.Run("with invalid OIDC issuer", func(t *testing.T) {
opts := publicGoodOpts
opts.OIDCIssuer = "not-a-real-issuer"
require.Error(t, runVerify(&opts))
})
t.Run("with SAN enforcement", func(t *testing.T) {
opts := Options{
ArtifactPath: artifactPath,
BundlePath: bundlePath,
APIClient: api.NewTestClient(),
DigestAlgorithm: "sha512",
Logger: logger,
OIDCIssuer: GitHubOIDCIssuer,
Owner: "sigstore",
SAN: SigstoreSanValue,
}
require.Nil(t, runVerify(&opts))
})
t.Run("with invalid SAN", func(t *testing.T) {
opts := publicGoodOpts
opts.SAN = "fake san"
require.Error(t, runVerify(&opts))
})
t.Run("with SAN regex enforcement", func(t *testing.T) {
opts := publicGoodOpts
opts.SANRegex = SigstoreSanRegex
require.Nil(t, runVerify(&opts))
})
t.Run("with invalid SAN regex", func(t *testing.T) {
opts := publicGoodOpts
opts.SANRegex = "^https://github.com/sigstore/not-real/"
require.Error(t, runVerify(&opts))
})
t.Run("with no matching OIDC issuer", func(t *testing.T) {
opts := publicGoodOpts
opts.OIDCIssuer = "some-other-issuer"
require.Error(t, runVerify(&opts))
})
t.Run("with missing API client", func(t *testing.T) {
customOpts := publicGoodOpts
customOpts.APIClient = nil
customOpts.BundlePath = ""
require.Error(t, runVerify(&customOpts))
})
}
func TestVerifySLSAPredicateType_InvalidPredicate(t *testing.T) {
statement := &in_toto.Statement{}
statement.PredicateType = "some-other-predicate-type"
apr := []*verification.AttestationProcessingResult{
{
VerificationResult: &verify.VerificationResult{
Statement: statement,
},
},
}
err := verifySLSAPredicateType(io.NewTestHandler(), apr)
require.Error(t, err)
require.ErrorIs(t, err, ErrNoMatchingSLSAPredicate)
}

View file

@ -10,6 +10,7 @@ import (
aliasCmd "github.com/cli/cli/v2/pkg/cmd/alias"
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
apiCmd "github.com/cli/cli/v2/pkg/cmd/api"
attestationCmd "github.com/cli/cli/v2/pkg/cmd/attestation"
authCmd "github.com/cli/cli/v2/pkg/cmd/auth"
browseCmd "github.com/cli/cli/v2/pkg/cmd/browse"
cacheCmd "github.com/cli/cli/v2/pkg/cmd/cache"
@ -124,6 +125,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command,
cmd.AddCommand(actionsCmd.NewCmdActions(f))
cmd.AddCommand(aliasCmd.NewCmdAlias(f))
cmd.AddCommand(authCmd.NewCmdAuth(f))
cmd.AddCommand(attestationCmd.NewCmdAttestation(f))
cmd.AddCommand(configCmd.NewCmdConfig(f))
cmd.AddCommand(creditsCmd.NewCmdCredits(f, nil))
cmd.AddCommand(gistCmd.NewCmdGist(f))

View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
# Get the root directory of the repository
rootDir="$(git rev-parse --show-toplevel)"
ghBuildPath="$rootDir/bin/gh"
# Compute the package and attestation URLs
labRatPackageName="sigstore"
latestPackageVersion=$(npm -s info $labRatPackageName dist-tags.latest | tr -d '\n')
packageFile="$labRatPackageName-$latestPackageVersion.tgz"
packageURL="https://registry.npmjs.org/$labRatPackageName/-/$packageFile"
attestationFile="$labRatPackageName-$latestPackageVersion.json"
attestationURL="https://registry.npmjs.org/-/npm/v1/attestations/$labRatPackageName@$latestPackageVersion"
echo "Testing with package $packageFile and attestation $attestationFile"
curl -s "$packageURL" -o "$packageFile"
curl -s "$attestationURL" | jq '.attestations[1].bundle' > "$attestationFile"
# Verify the package with the --owner flag
if ! $ghBuildPath attestation verify "$packageFile" -b "$attestationFile" --digest-alg=sha512 --owner=sigstore; then
# cleanup test data
echo "Failed to verify package with --owner flag"
rm "$packageFile" "$attestationFile"
exit 1
fi
if ! $ghBuildPath attestation verify "$packageFile" -b "$attestationFile" --digest-alg=sha512 --repo=sigstore/sigstore-js; then
# cleanup test data
echo "Failed to verify package with --repo flag"
rm "$packageFile" "$attestationFile"
exit 1
fi
# cleanup test data
rm "$packageFile" "$attestationFile"