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:
parent
ec812a16f7
commit
90b7bf97c5
58 changed files with 4893 additions and 53 deletions
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
|
|
@ -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
|
||||
|
|
|
|||
26
.github/workflows/go.yml
vendored
26
.github/workflows/go.yml
vendored
|
|
@ -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
111
go.mod
|
|
@ -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
418
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
33
pkg/cmd/attestation/api/attestation.go
Normal file
33
pkg/cmd/attestation/api/attestation.go
Normal 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"`
|
||||
}
|
||||
104
pkg/cmd/attestation/api/client.go
Normal file
104
pkg/cmd/attestation/api/client.go
Normal 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
|
||||
}
|
||||
174
pkg/cmd/attestation/api/client_test.go
Normal file
174
pkg/cmd/attestation/api/client_test.go
Normal 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)
|
||||
}
|
||||
89
pkg/cmd/attestation/api/mock_apiClient_test.go
Normal file
89
pkg/cmd/attestation/api/mock_apiClient_test.go
Normal 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")
|
||||
}
|
||||
71
pkg/cmd/attestation/api/mock_client.go
Normal file
71
pkg/cmd/attestation/api/mock_client.go
Normal 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,
|
||||
}
|
||||
}
|
||||
79
pkg/cmd/attestation/artifact/artifact.go
Normal file
79
pkg/cmd/attestation/artifact/artifact.go
Normal 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)
|
||||
}
|
||||
99
pkg/cmd/attestation/artifact/artifact_posix_test.go
Normal file
99
pkg/cmd/attestation/artifact/artifact_posix_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
59
pkg/cmd/attestation/artifact/artifact_windows_test.go
Normal file
59
pkg/cmd/attestation/artifact/artifact_windows_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
53
pkg/cmd/attestation/artifact/digest/digest.go
Normal file
53
pkg/cmd/attestation/artifact/digest/digest.go
Normal 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
|
||||
}
|
||||
46
pkg/cmd/attestation/artifact/digest/digest_test.go
Normal file
46
pkg/cmd/attestation/artifact/digest/digest_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
25
pkg/cmd/attestation/artifact/file.go
Normal file
25
pkg/cmd/attestation/artifact/file.go
Normal 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
|
||||
}
|
||||
28
pkg/cmd/attestation/artifact/image.go
Normal file
28
pkg/cmd/attestation/artifact/image.go
Normal 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
|
||||
}
|
||||
52
pkg/cmd/attestation/artifact/image_test.go
Normal file
52
pkg/cmd/attestation/artifact/image_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
70
pkg/cmd/attestation/artifact/oci/client.go
Normal file
70
pkg/cmd/attestation/artifact/oci/client.go
Normal 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,
|
||||
}
|
||||
}
|
||||
83
pkg/cmd/attestation/artifact/oci/client_test.go
Normal file
83
pkg/cmd/attestation/artifact/oci/client_test.go
Normal 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)
|
||||
}
|
||||
34
pkg/cmd/attestation/artifact/oci/mock_client.go
Normal file
34
pkg/cmd/attestation/artifact/oci/mock_client.go
Normal 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
|
||||
}
|
||||
28
pkg/cmd/attestation/attestation.go
Normal file
28
pkg/cmd/attestation/attestation.go
Normal 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
|
||||
}
|
||||
17
pkg/cmd/attestation/auth/host.go
Normal file
17
pkg/cmd/attestation/auth/host.go
Normal 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
|
||||
}
|
||||
148
pkg/cmd/attestation/download/download.go
Normal file
148
pkg/cmd/attestation/download/download.go
Normal 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
|
||||
}
|
||||
314
pkg/cmd/attestation/download/download_test.go
Normal file
314
pkg/cmd/attestation/download/download_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
70
pkg/cmd/attestation/download/metadata.go
Normal file
70
pkg/cmd/attestation/download/metadata.go
Normal 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,
|
||||
}
|
||||
}
|
||||
87
pkg/cmd/attestation/download/metadata_test.go
Normal file
87
pkg/cmd/attestation/download/metadata_test.go
Normal 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
|
||||
}
|
||||
35
pkg/cmd/attestation/download/options.go
Normal file
35
pkg/cmd/attestation/download/options.go
Normal 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
|
||||
}
|
||||
34
pkg/cmd/attestation/download/options_test.go
Normal file
34
pkg/cmd/attestation/download/options_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
128
pkg/cmd/attestation/inspect/bundle.go
Normal file
128
pkg/cmd/attestation/inspect/bundle.go
Normal 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
|
||||
}
|
||||
46
pkg/cmd/attestation/inspect/bundle_test.go
Normal file
46
pkg/cmd/attestation/inspect/bundle_test.go
Normal 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)
|
||||
}
|
||||
162
pkg/cmd/attestation/inspect/inspect.go
Normal file
162
pkg/cmd/attestation/inspect/inspect.go
Normal 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
|
||||
}
|
||||
179
pkg/cmd/attestation/inspect/inspect_test.go
Normal file
179
pkg/cmd/attestation/inspect/inspect_test.go
Normal 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)
|
||||
}
|
||||
24
pkg/cmd/attestation/inspect/options.go
Normal file
24
pkg/cmd/attestation/inspect/options.go
Normal 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)
|
||||
}
|
||||
18
pkg/cmd/attestation/inspect/policy.go
Normal file
18
pkg/cmd/attestation/inspect/policy.go
Normal 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
|
||||
}
|
||||
61
pkg/cmd/attestation/io/handler.go
Normal file
61
pkg/cmd/attestation/io/handler.go
Normal 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...)
|
||||
}
|
||||
61
pkg/cmd/attestation/test/data/sigstore-js-2.1.0-bundle.json
Normal file
61
pkg/cmd/attestation/test/data/sigstore-js-2.1.0-bundle.json
Normal 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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
pkg/cmd/attestation/test/data/sigstore-js-2.1.0.tgz
Normal file
BIN
pkg/cmd/attestation/test/data/sigstore-js-2.1.0.tgz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
|
|
@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
13
pkg/cmd/attestation/test/path.go
Normal file
13
pkg/cmd/attestation/test/path.go
Normal 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
|
||||
}
|
||||
83
pkg/cmd/attestation/tufrootverify/tufrootverify.go
Normal file
83
pkg/cmd/attestation/tufrootverify/tufrootverify.go
Normal 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
|
||||
}
|
||||
80
pkg/cmd/attestation/tufrootverify/tufrootverify_test.go
Normal file
80
pkg/cmd/attestation/tufrootverify/tufrootverify_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
115
pkg/cmd/attestation/verification/attestation.go
Normal file
115
pkg/cmd/attestation/verification/attestation.go
Normal 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")
|
||||
}
|
||||
49
pkg/cmd/attestation/verification/attestation_test.go
Normal file
49
pkg/cmd/attestation/verification/attestation_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
20
pkg/cmd/attestation/verification/policy.go
Normal file
20
pkg/cmd/attestation/verification/policy.go
Normal 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
|
||||
}
|
||||
208
pkg/cmd/attestation/verification/sigstore.go
Normal file
208
pkg/cmd/attestation/verification/sigstore.go
Normal 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
|
||||
}
|
||||
60
pkg/cmd/attestation/verification/sigstore_test.go
Normal file
60
pkg/cmd/attestation/verification/sigstore_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
41
pkg/cmd/attestation/verification/tuf.go
Normal file
41
pkg/cmd/attestation/verification/tuf.go
Normal 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
|
||||
}
|
||||
20
pkg/cmd/attestation/verification/tuf_test.go
Normal file
20
pkg/cmd/attestation/verification/tuf_test.go
Normal 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)
|
||||
}
|
||||
83
pkg/cmd/attestation/verify/options.go
Normal file
83
pkg/cmd/attestation/verify/options.go
Normal 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)
|
||||
}
|
||||
119
pkg/cmd/attestation/verify/options_test.go
Normal file
119
pkg/cmd/attestation/verify/options_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
85
pkg/cmd/attestation/verify/policy.go
Normal file
85
pkg/cmd/attestation/verify/policy.go
Normal 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
|
||||
}
|
||||
31
pkg/cmd/attestation/verify/policy_test.go
Normal file
31
pkg/cmd/attestation/verify/policy_test.go
Normal 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)
|
||||
}
|
||||
216
pkg/cmd/attestation/verify/verify.go
Normal file
216
pkg/cmd/attestation/verify/verify.go
Normal 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
|
||||
}
|
||||
401
pkg/cmd/attestation/verify/verify_test.go
Normal file
401
pkg/cmd/attestation/verify/verify_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
38
test/integration/attestation-cmd/download-and-verify-package-attestation.sh
Executable file
38
test/integration/attestation-cmd/download-and-verify-package-attestation.sh
Executable 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue