diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e69a804ee..20bf83882 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,5 @@ { - "image": "mcr.microsoft.com/devcontainers/go:1.21", + "image": "mcr.microsoft.com/devcontainers/go:1.22", "features": { "ghcr.io/devcontainers/features/sshd:1": {} }, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cd5695abc..8464d95a5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,3 +2,8 @@ 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 + +pkg/cmd/attestation/verification/embed/tuf-repo.github.com/ @cli/tuf-root-reviewers diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 73174db65..9f5d56376 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -24,7 +24,7 @@ Please avoid: ## Building the project Prerequisites: -- Go 1.21+ +- Go 1.22+ Build with: * Unix-like systems: `make` diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c45c65e34..8cd5ecbee 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,5 +29,10 @@ jobs: languages: go queries: security-and-quality + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index bf138c95d..51059a669 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -1,5 +1,5 @@ name: Deployment -run-name: ${{ inputs.tag_name }} / go ${{ inputs.go_version }} / ${{ inputs.environment }} +run-name: ${{ inputs.tag_name }} / ${{ inputs.environment }} concurrency: group: ${{ github.workflow }}-${{ github.ref_name }} @@ -17,9 +17,6 @@ on: environment: default: production type: environment - go_version: - default: "1.21" - type: string platforms: default: "linux,macos,windows" type: string @@ -39,7 +36,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: ${{ inputs.go_version }} + go-version-file: 'go.mod' - name: Install GoReleaser uses: goreleaser/goreleaser-action@v5 with: @@ -73,7 +70,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: ${{ inputs.go_version }} + go-version-file: 'go.mod' - name: Configure macOS signing if: inputs.environment == 'production' env: @@ -130,7 +127,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: ${{ inputs.go_version }} + go-version-file: 'go.mod' - name: Install GoReleaser uses: goreleaser/goreleaser-action@v5 with: diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 13b598d52..1dded32b0 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,4 +1,4 @@ -name: Tests +name: Unit and Integration Tests on: [push, pull_request] permissions: @@ -13,14 +13,14 @@ jobs: 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: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + - name: Restore Go modules cache uses: actions/cache@v4 with: @@ -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: Check out code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Build executable + run: make + + - name: Run attestation command integration Tests + run: ./test/integration/attestation-cmd/download-and-verify-package-attestation.sh diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 57f584e00..52f99cd81 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,14 +19,14 @@ jobs: runs-on: ubuntu-latest 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: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + - name: Restore Go modules cache uses: actions/cache@v4 with: diff --git a/api/http_client_test.go b/api/http_client_test.go index dca032e6f..ce20a2684 100644 --- a/api/http_client_test.go +++ b/api/http_client_test.go @@ -222,7 +222,7 @@ func TestHTTPClientSanitizeJSONControlCharactersC0(t *testing.T) { err = json.Unmarshal(body, &issue) require.NoError(t, err) assert.Equal(t, "^[[31mRed Title^[[0m", issue.Title) - assert.Equal(t, "1^A 2^B 3^C 4^D 5^E 6^F 7^G 8^H 9\t A\r\n B\v C^L D\r\n E^N F^O", issue.Body) + assert.Equal(t, "1^A 2^B 3^C 4^D 5^E 6^F 7^G 8\b 9\t A\r\n B\v C\f D\r\n E^N F^O", issue.Body) assert.Equal(t, "10^P 11^Q 12^R 13^S 14^T 15^U 16^V 17^W 18^X 19^Y 1A^Z 1B^[ 1C^\\ 1D^] 1E^^ 1F^_", issue.Author.Name) assert.Equal(t, "monalisa \\u00^[", issue.Author.Login) assert.Equal(t, "Escaped ^[ \\^[ \\^[ \\\\^[", issue.ActiveLockReason) diff --git a/context/remote.go b/context/remote.go index 3540ad95d..6094179f4 100644 --- a/context/remote.go +++ b/context/remote.go @@ -31,7 +31,7 @@ func (r Remotes) FindByRepo(owner, name string) (*Remote, error) { return rem, nil } } - return nil, fmt.Errorf("no matching remote found") + return nil, fmt.Errorf("no matching remote found; looking for %s/%s", owner, name) } // Filter remotes by given hostnames, maintains original order diff --git a/context/remote_test.go b/context/remote_test.go index 7db0473a0..d57e2e1e1 100644 --- a/context/remote_test.go +++ b/context/remote_test.go @@ -28,6 +28,71 @@ func Test_Remotes_FindByName(t *testing.T) { assert.Error(t, err, "no GitHub remotes found") } +func Test_Remotes_FindByRepo(t *testing.T) { + list := Remotes{ + &Remote{Remote: &git.Remote{Name: "remote-0"}, Repo: ghrepo.New("owner", "repo")}, + &Remote{Remote: &git.Remote{Name: "remote-1"}, Repo: ghrepo.New("another-owner", "another-repo")}, + } + + tests := []struct { + name string + owner string + repo string + wantsRemote *Remote + wantsError string + }{ + { + name: "exact match (owner/repo)", + owner: "owner", + repo: "repo", + wantsRemote: list[0], + }, + { + name: "exact match (another-owner/another-repo)", + owner: "another-owner", + repo: "another-repo", + wantsRemote: list[1], + }, + { + name: "case-insensitive match", + owner: "OWNER", + repo: "REPO", + wantsRemote: list[0], + }, + { + name: "non-match (owner)", + owner: "unknown-owner", + repo: "repo", + wantsError: "no matching remote found; looking for unknown-owner/repo", + }, + { + name: "non-match (repo)", + owner: "owner", + repo: "unknown-repo", + wantsError: "no matching remote found; looking for owner/unknown-repo", + }, + { + name: "non-match (owner, repo)", + owner: "unknown-owner", + repo: "unknown-repo", + wantsError: "no matching remote found; looking for unknown-owner/unknown-repo", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, err := list.FindByRepo(tt.owner, tt.repo) + if tt.wantsError != "" { + assert.Error(t, err, tt.wantsError) + assert.Nil(t, r) + } else { + assert.NoError(t, err) + assert.Equal(t, r, tt.wantsRemote) + } + }) + } +} + type identityTranslator struct{} func (it identityTranslator) Translate(u *url.URL) *url.URL { diff --git a/docs/multiple-accounts.md b/docs/multiple-accounts.md index 67b6fa0b5..29c83b703 100644 --- a/docs/multiple-accounts.md +++ b/docs/multiple-accounts.md @@ -8,7 +8,7 @@ A particular shoutout to @gabe565 and his long term community support for https: With the release of `v2.40.0`, `gh` has begun supporting multiple accounts for some use cases on github.com and in GitHub Enterprise. We recognise that there are a number of missing quality of life features, and we've opted -not to address the use case of automatic account switching based on some context (e.g. `pwd`, `git remote`, etc). +not to address the use case of automatic account switching based on some context (e.g. `pwd`, `git remote`). However, we hope many of those using these custom solutions will now find it easier to obtain and update tokens (via the standard OAuth flow rather than as a PAT), and to store them securely in the system keyring managed by `gh`. @@ -111,7 +111,7 @@ if there are any remaining logged into the host: As mentioned above, we know that this only addresses some of the requests around supporting multiple accounts. While these are not out of scope forever, for this release some of the big things we have intentionally not included are: - * Automatic account switching based on some context (e.g. `pwd`, `git remote`, etc) + * Automatic account switching based on some context (e.g. `pwd`, `git remote`) * Automatic configuration of git config such as `user.name` and `user.email` when switching * User level configuration e.g. `williammartin` uses `vim` but `wilmartin_microsoft` uses `emacs` diff --git a/docs/source.md b/docs/source.md index cc1605b8c..d2b7c5b31 100644 --- a/docs/source.md +++ b/docs/source.md @@ -1,6 +1,6 @@ # Installation from source -1. Verify that you have Go 1.21+ installed +1. Verify that you have Go 1.22+ installed ```sh $ go version diff --git a/git/url.go b/git/url.go index 1a3e97fd6..890ccbe1c 100644 --- a/git/url.go +++ b/git/url.go @@ -56,9 +56,7 @@ func ParseURL(rawURL string) (u *url.URL, err error) { u.Path = strings.TrimPrefix(u.Path, "/") } - if idx := strings.Index(u.Host, ":"); idx >= 0 { - u.Host = u.Host[0:idx] - } + u.Host = strings.TrimSuffix(u.Host, ":"+u.Port()) return } diff --git a/git/url_test.go b/git/url_test.go index f5b3b50d0..d31c1b817 100644 --- a/git/url_test.go +++ b/git/url_test.go @@ -126,6 +126,26 @@ func TestParseURL(t *testing.T) { Path: "/owner/repo.git", }, }, + { + name: "ssh, ipv6", + url: "ssh://git@[::1]/owner/repo.git", + want: url{ + Scheme: "ssh", + User: "git", + Host: "[::1]", + Path: "/owner/repo.git", + }, + }, + { + name: "ssh with port, ipv6", + url: "ssh://git@[::1]:22/owner/repo.git", + want: url{ + Scheme: "ssh", + User: "git", + Host: "[::1]", + Path: "/owner/repo.git", + }, + }, { name: "git+ssh", url: "git+ssh://example.com/owner/repo.git", diff --git a/go.mod b/go.mod index 1f0610ebc..e779ae75f 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,30 @@ module github.com/cli/cli/v2 -go 1.21 +go 1.22 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/MakeNowJust/heredoc v1.0.0 github.com/briandowns/spinner v1.18.1 github.com/cenkalti/backoff/v4 v4.2.1 - github.com/charmbracelet/glamour v0.6.0 + github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/lipgloss v0.5.0 - github.com/cli/go-gh/v2 v2.6.0 + github.com/cli/go-gh/v2 v2.8.0 github.com/cli/oauth v1.0.1 github.com/cli/safeexec v1.0.1 - github.com/cpuguy83/go-md2man/v2 v2.0.3 + 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.1 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/gorilla/websocket v1.4.2 + github.com/gorilla/websocket v1.5.1 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,61 +32,135 @@ 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.1 + 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.3 - golang.org/x/crypto v0.17.0 - golang.org/x/sync v0.1.0 - golang.org/x/term v0.15.0 + github.com/zalando/go-keyring v0.2.4 + 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.2 google.golang.org/protobuf v1.33.0 gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/alecthomas/chroma v0.10.0 // indirect - github.com/alessio/shellescape v1.4.1 // indirect - github.com/aymanbagabas/go-osc52 v1.0.3 // indirect + github.com/alecthomas/chroma/v2 v2.8.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/v2 v2.0.1 // 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/danieljoos/wincred v1.2.0 // indirect - github.com/davecgh/go-spew v1.1.1 // 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.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.9+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/itchyny/gojq v0.12.13 // 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.15 // 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/mattn/go-runewidth v0.0.14 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-runewidth v0.0.15 // 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/muesli/termenv v0.15.2 // 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/rivo/uniseg v0.4.4 // 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.7 // 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/yuin/goldmark v1.5.2 // indirect - github.com/yuin/goldmark-emoji v1.0.1 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.15.0 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // 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.4 // indirect + github.com/yuin/goldmark-emoji v1.0.2 // 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-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.3 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + k8s.io/klog/v2 v2.120.0 // indirect ) diff --git a/go.sum b/go.sum index 0ac2b9277..5cff3f2cf 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,100 @@ +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= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= -github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= -github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= -github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= -github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -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/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= +github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264= +github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +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/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 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/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc= -github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc= +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.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= +github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8= github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/cli/go-gh/v2 v2.6.0 h1:1zXwr7mW6JDCPwXQLLtCdKnp+pNc7ZixyDeLpM1pf9I= -github.com/cli/go-gh/v2 v2.6.0/go.mod h1:h3salfqqooVpzKmHp6aUdeNx62UmxQRpLbagFSHTJGQ= +github.com/cli/go-gh/v2 v2.8.0 h1:xFDgnhRiVMFanqACszdyRduUaFkoMOiasOrox0sKpKo= +github.com/cli/go-gh/v2 v2.8.0/go.mod h1:U6GukjNwSqD6coCKP6FTp/yrPk5ttNYjAe+ZYSOFCVc= github.com/cli/oauth v1.0.1 h1:pXnTFl/qUegXHK531Dv0LNjW4mLx626eS42gnzfXJPA= github.com/cli/oauth v1.0.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= @@ -32,70 +102,201 @@ 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 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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.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/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= -github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= +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.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= +github.com/docker/docker v24.0.9+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.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY= +github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +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.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 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/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU= -github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4= +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.15 h1:WC1Nxbx4Ifw5U2oQWACYz32JK8G9qxNtHzrvW4KEcqI= +github.com/itchyny/gojq v0.12.15/go.mod h1:uWAHCbCIla1jiNxmeT5/B5mOjSdfkCq6p8vxWg+BM10= 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= @@ -108,111 +309,243 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 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/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/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= -github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= 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= github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= -github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/muhammadmuzzammil1998/jsonc v0.0.0-20201229145248-615b0916ca38 h1:0FrBxrkJ0hVembTb/e4EU5Ml6vLcOusAqymmYISg5Uo= 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= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/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.1 h1:9aJQrPq7iRDSLBNg//zsP7tAzxdHnD1sA+1FyCCrkrQ= +github.com/sigstore/protobuf-specs v0.3.1/go.mod h1:HfkcPi5QXteuew4+c5ONz8vYQ8aOH//ZTQ3gg0X8ZUA= +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/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= -github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= -github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= -github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= -github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= +github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= +github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= +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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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 +553,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.2 h1:TzJay21lXCf7BiNFKl7mSskt5DlkKAumAYTs52SpJeo= +google.golang.org/grpc v1.61.2/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.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= +gopkg.in/go-jose/go-jose.v2 v2.6.3/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= diff --git a/pkg/cmd/attestation/api/attestation.go b/pkg/cmd/attestation/api/attestation.go new file mode 100644 index 000000000..2b96a51fc --- /dev/null +++ b/pkg/cmd/attestation/api/attestation.go @@ -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"` +} diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go new file mode 100644 index 000000000..b0cafa6e9 --- /dev/null +++ b/pkg/cmd/attestation/api/client.go @@ -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 +} diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go new file mode 100644 index 000000000..7044a1cc5 --- /dev/null +++ b/pkg/cmd/attestation/api/client_test.go @@ -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) +} diff --git a/pkg/cmd/attestation/api/mock_apiClient_test.go b/pkg/cmd/attestation/api/mock_apiClient_test.go new file mode 100644 index 000000000..1d4f61cd9 --- /dev/null +++ b/pkg/cmd/attestation/api/mock_apiClient_test.go @@ -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") +} diff --git a/pkg/cmd/attestation/api/mock_client.go b/pkg/cmd/attestation/api/mock_client.go new file mode 100644 index 000000000..96a64e4fc --- /dev/null +++ b/pkg/cmd/attestation/api/mock_client.go @@ -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, + } +} diff --git a/pkg/cmd/attestation/artifact/artifact.go b/pkg/cmd/attestation/artifact/artifact.go new file mode 100644 index 000000000..de354b947 --- /dev/null +++ b/pkg/cmd/attestation/artifact/artifact.go @@ -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) +} diff --git a/pkg/cmd/attestation/artifact/artifact_posix_test.go b/pkg/cmd/attestation/artifact/artifact_posix_test.go new file mode 100644 index 000000000..31e9cb7e7 --- /dev/null +++ b/pkg/cmd/attestation/artifact/artifact_posix_test.go @@ -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) + } + }) + } +} diff --git a/pkg/cmd/attestation/artifact/artifact_windows_test.go b/pkg/cmd/attestation/artifact/artifact_windows_test.go new file mode 100644 index 000000000..46995f226 --- /dev/null +++ b/pkg/cmd/attestation/artifact/artifact_windows_test.go @@ -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) + } + }) + } +} diff --git a/pkg/cmd/attestation/artifact/digest/digest.go b/pkg/cmd/attestation/artifact/digest/digest.go new file mode 100644 index 000000000..e48fb1d0d --- /dev/null +++ b/pkg/cmd/attestation/artifact/digest/digest.go @@ -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 +} diff --git a/pkg/cmd/attestation/artifact/digest/digest_test.go b/pkg/cmd/attestation/artifact/digest/digest_test.go new file mode 100644 index 000000000..bcfd2c1ac --- /dev/null +++ b/pkg/cmd/attestation/artifact/digest/digest_test.go @@ -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") + }) +} diff --git a/pkg/cmd/attestation/artifact/file.go b/pkg/cmd/attestation/artifact/file.go new file mode 100644 index 000000000..789a92a5d --- /dev/null +++ b/pkg/cmd/attestation/artifact/file.go @@ -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 +} diff --git a/pkg/cmd/attestation/artifact/image.go b/pkg/cmd/attestation/artifact/image.go new file mode 100644 index 000000000..2af13e723 --- /dev/null +++ b/pkg/cmd/attestation/artifact/image.go @@ -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 +} diff --git a/pkg/cmd/attestation/artifact/image_test.go b/pkg/cmd/attestation/artifact/image_test.go new file mode 100644 index 000000000..5ea5f9a37 --- /dev/null +++ b/pkg/cmd/attestation/artifact/image_test.go @@ -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) + } +} diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go new file mode 100644 index 000000000..5b8d8cf7a --- /dev/null +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -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, + } +} diff --git a/pkg/cmd/attestation/artifact/oci/client_test.go b/pkg/cmd/attestation/artifact/oci/client_test.go new file mode 100644 index 000000000..9aa415c47 --- /dev/null +++ b/pkg/cmd/attestation/artifact/oci/client_test.go @@ -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) +} diff --git a/pkg/cmd/attestation/artifact/oci/mock_client.go b/pkg/cmd/attestation/artifact/oci/mock_client.go new file mode 100644 index 000000000..24368dec8 --- /dev/null +++ b/pkg/cmd/attestation/artifact/oci/mock_client.go @@ -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 +} diff --git a/pkg/cmd/attestation/attestation.go b/pkg/cmd/attestation/attestation.go new file mode 100644 index 000000000..9b3a0cd2c --- /dev/null +++ b/pkg/cmd/attestation/attestation.go @@ -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 +} diff --git a/pkg/cmd/attestation/auth/host.go b/pkg/cmd/attestation/auth/host.go new file mode 100644 index 000000000..998dcb7f5 --- /dev/null +++ b/pkg/cmd/attestation/auth/host.go @@ -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 +} diff --git a/pkg/cmd/attestation/download/download.go b/pkg/cmd/attestation/download/download.go new file mode 100644 index 000000000..2f40970fc --- /dev/null +++ b/pkg/cmd/attestation/download/download.go @@ -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 [ | oci://] [--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://%[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 /") + 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 +} diff --git a/pkg/cmd/attestation/download/download_test.go b/pkg/cmd/attestation/download/download_test.go new file mode 100644 index 000000000..0762a29da --- /dev/null +++ b/pkg/cmd/attestation/download/download_test.go @@ -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) + }) +} diff --git a/pkg/cmd/attestation/download/metadata.go b/pkg/cmd/attestation/download/metadata.go new file mode 100644 index 000000000..4096be001 --- /dev/null +++ b/pkg/cmd/attestation/download/metadata.go @@ -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, + } +} diff --git a/pkg/cmd/attestation/download/metadata_test.go b/pkg/cmd/attestation/download/metadata_test.go new file mode 100644 index 000000000..8ee3b2a45 --- /dev/null +++ b/pkg/cmd/attestation/download/metadata_test.go @@ -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 +} diff --git a/pkg/cmd/attestation/download/options.go b/pkg/cmd/attestation/download/options.go new file mode 100644 index 000000000..6a64ecc30 --- /dev/null +++ b/pkg/cmd/attestation/download/options.go @@ -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 +} diff --git a/pkg/cmd/attestation/download/options_test.go b/pkg/cmd/attestation/download/options_test.go new file mode 100644 index 000000000..800691d79 --- /dev/null +++ b/pkg/cmd/attestation/download/options_test.go @@ -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) + } +} diff --git a/pkg/cmd/attestation/inspect/bundle.go b/pkg/cmd/attestation/inspect/bundle.go new file mode 100644 index 000000000..283b2c14e --- /dev/null +++ b/pkg/cmd/attestation/inspect/bundle.go @@ -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 +} diff --git a/pkg/cmd/attestation/inspect/bundle_test.go b/pkg/cmd/attestation/inspect/bundle_test.go new file mode 100644 index 000000000..d7e9babd8 --- /dev/null +++ b/pkg/cmd/attestation/inspect/bundle_test.go @@ -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) +} diff --git a/pkg/cmd/attestation/inspect/inspect.go b/pkg/cmd/attestation/inspect/inspect.go new file mode 100644 index 000000000..1b2105da4 --- /dev/null +++ b/pkg/cmd/attestation/inspect/inspect.go @@ -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 [ | oci://] --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://%[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 --bundle + + # Inspect a Sigstore bundle and print the results in JSON format + $ gh attestation inspect --bundle --json-result + + # Inspect a Sigsore bundle for an OCI artifact, and print the results in table format + $ gh attestation inspect oci:// --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 +} diff --git a/pkg/cmd/attestation/inspect/inspect_test.go b/pkg/cmd/attestation/inspect/inspect_test.go new file mode 100644 index 000000000..e42bb2620 --- /dev/null +++ b/pkg/cmd/attestation/inspect/inspect_test.go @@ -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) +} diff --git a/pkg/cmd/attestation/inspect/options.go b/pkg/cmd/attestation/inspect/options.go new file mode 100644 index 000000000..56199e06b --- /dev/null +++ b/pkg/cmd/attestation/inspect/options.go @@ -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) +} diff --git a/pkg/cmd/attestation/inspect/policy.go b/pkg/cmd/attestation/inspect/policy.go new file mode 100644 index 000000000..49313d35a --- /dev/null +++ b/pkg/cmd/attestation/inspect/policy.go @@ -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 +} diff --git a/pkg/cmd/attestation/io/handler.go b/pkg/cmd/attestation/io/handler.go new file mode 100644 index 000000000..b41d4ddbf --- /dev/null +++ b/pkg/cmd/attestation/io/handler.go @@ -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...) +} diff --git a/pkg/cmd/attestation/test/data/sigstore-js-2.1.0-bundle.json b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0-bundle.json new file mode 100644 index 000000000..d91176e09 --- /dev/null +++ b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0-bundle.json @@ -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": "" + } + ] + } + } diff --git a/pkg/cmd/attestation/test/data/sigstore-js-2.1.0.tgz b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0.tgz new file mode 100644 index 000000000..390b823fd Binary files /dev/null and b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0.tgz differ diff --git a/pkg/cmd/attestation/test/data/sigstore-js-2.1.0_with_2_bundles.jsonl b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0_with_2_bundles.jsonl new file mode 100644 index 000000000..a4aca34d0 --- /dev/null +++ b/pkg/cmd/attestation/test/data/sigstore-js-2.1.0_with_2_bundles.jsonl @@ -0,0 +1,2 @@ +{"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":""}]}} +{"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":""}]}} diff --git a/pkg/cmd/attestation/test/data/sigstoreBundle-invalid-signature.json b/pkg/cmd/attestation/test/data/sigstoreBundle-invalid-signature.json new file mode 100644 index 000000000..b1275cebc --- /dev/null +++ b/pkg/cmd/attestation/test/data/sigstoreBundle-invalid-signature.json @@ -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": "" + } + ] + } +} diff --git a/pkg/cmd/attestation/test/path.go b/pkg/cmd/attestation/test/path.go new file mode 100644 index 000000000..5b6282b7d --- /dev/null +++ b/pkg/cmd/attestation/test/path.go @@ -0,0 +1,13 @@ +package test + +import ( + "runtime" + "strings" +) + +func NormalizeRelativePath(posixPath string) string { + if runtime.GOOS == "windows" { + return strings.ReplaceAll(posixPath, "/", "\\") + } + return posixPath +} diff --git a/pkg/cmd/attestation/tufrootverify/tufrootverify.go b/pkg/cmd/attestation/tufrootverify/tufrootverify.go new file mode 100644 index 000000000..05971da54 --- /dev/null +++ b/pkg/cmd/attestation/tufrootverify/tufrootverify.go @@ -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 --root ", + 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: . + `, "`"), + 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 +} diff --git a/pkg/cmd/attestation/tufrootverify/tufrootverify_test.go b/pkg/cmd/attestation/tufrootverify/tufrootverify_test.go new file mode 100644 index 000000000..004f25618 --- /dev/null +++ b/pkg/cmd/attestation/tufrootverify/tufrootverify_test.go @@ -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") + }) +} diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go new file mode 100644 index 000000000..7fb4a1615 --- /dev/null +++ b/pkg/cmd/attestation/verification/attestation.go @@ -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") +} diff --git a/pkg/cmd/attestation/verification/attestation_test.go b/pkg/cmd/attestation/verification/attestation_test.go new file mode 100644 index 000000000..b178a0682 --- /dev/null +++ b/pkg/cmd/attestation/verification/attestation_test.go @@ -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) + }) +} diff --git a/pkg/cmd/attestation/verification/embed/tuf-repo.github.com/root.json b/pkg/cmd/attestation/verification/embed/tuf-repo.github.com/root.json new file mode 100644 index 000000000..2990a2665 --- /dev/null +++ b/pkg/cmd/attestation/verification/embed/tuf-repo.github.com/root.json @@ -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 + } +} diff --git a/pkg/cmd/attestation/verification/policy.go b/pkg/cmd/attestation/verification/policy.go new file mode 100644 index 000000000..91b54c75e --- /dev/null +++ b/pkg/cmd/attestation/verification/policy.go @@ -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 +} diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go new file mode 100644 index 000000000..daaacf628 --- /dev/null +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -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 +} diff --git a/pkg/cmd/attestation/verification/sigstore_test.go b/pkg/cmd/attestation/verification/sigstore_test.go new file mode 100644 index 000000000..204b5e583 --- /dev/null +++ b/pkg/cmd/attestation/verification/sigstore_test.go @@ -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) + }) +} diff --git a/pkg/cmd/attestation/verification/tuf.go b/pkg/cmd/attestation/verification/tuf.go new file mode 100644 index 000000000..b87f466e8 --- /dev/null +++ b/pkg/cmd/attestation/verification/tuf.go @@ -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 +} diff --git a/pkg/cmd/attestation/verification/tuf_test.go b/pkg/cmd/attestation/verification/tuf_test.go new file mode 100644 index 000000000..7d816bf82 --- /dev/null +++ b/pkg/cmd/attestation/verification/tuf_test.go @@ -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) +} diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go new file mode 100644 index 000000000..d7742bf3a --- /dev/null +++ b/pkg/cmd/attestation/verify/options.go @@ -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 / + 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 / + 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) +} diff --git a/pkg/cmd/attestation/verify/options_test.go b/pkg/cmd/attestation/verify/options_test.go new file mode 100644 index 000000000..a7430017e --- /dev/null +++ b/pkg/cmd/attestation/verify/options_test.go @@ -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) + }) +} diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go new file mode 100644 index 000000000..83d1f62a7 --- /dev/null +++ b/pkg/cmd/attestation/verify/policy.go @@ -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 +} diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go new file mode 100644 index 000000000..f15144314 --- /dev/null +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -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) +} diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go new file mode 100644 index 000000000..718f893a2 --- /dev/null +++ b/pkg/cmd/attestation/verify/verify.go @@ -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 [ | oci://] [--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://%[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:// --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 /") + 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 +} diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go new file mode 100644 index 000000000..b4cd864fc --- /dev/null +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -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) +} diff --git a/pkg/cmd/auth/refresh/refresh.go b/pkg/cmd/auth/refresh/refresh.go index a918493e8..b77c45bac 100644 --- a/pkg/cmd/auth/refresh/refresh.go +++ b/pkg/cmd/auth/refresh/refresh.go @@ -55,7 +55,7 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra. Use: "refresh", Args: cobra.ExactArgs(0), Short: "Refresh stored authentication credentials", - Long: heredoc.Docf(`Expand or fix the permission scopes for stored credentials. + Long: heredoc.Docf(`Expand or fix the permission scopes for stored credentials for active account. The %[1]s--scopes%[1]s flag accepts a comma separated list of scopes you want your gh credentials to have. If no scopes are provided, the command @@ -67,6 +67,10 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra. The %[1]s--reset-scopes%[1]s flag resets the scopes for your gh credentials to the default set of scopes for your auth flow. + + If you have multiple accounts in %[1]sgh auth status%[1]s and want to refresh the credentials for an + inactive account, you will have to use %[1]sgh auth switch%[1]s to that account first before using + this command, and then switch back when you are done. `, "`"), Example: heredoc.Doc(` $ gh auth refresh --scopes write:org,read:public_key diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index fa027e193..c28c3f48e 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -140,7 +140,7 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co cmd := &cobra.Command{ Use: "status", Args: cobra.ExactArgs(0), - Short: "View authentication status", + Short: "View all accounts and authentication status", Long: heredoc.Doc(`Verifies and displays information about your authentication state. This command will test your authentication state for each GitHub host that gh knows about and diff --git a/pkg/cmd/auth/switch/switch.go b/pkg/cmd/auth/switch/switch.go index 129fe9fd4..d2aa73a9d 100644 --- a/pkg/cmd/auth/switch/switch.go +++ b/pkg/cmd/auth/switch/switch.go @@ -32,18 +32,24 @@ func NewCmdSwitch(f *cmdutil.Factory, runF func(*SwitchOptions) error) *cobra.Co Use: "switch", Args: cobra.ExactArgs(0), Short: "Switch active GitHub account", - Long: heredoc.Doc(` + Long: heredoc.Docf(` Switch the active account for a GitHub host. This command changes the authentication configuration that will be used when running commands targeting the specified GitHub host. - `), + + If the specified host has two accounts, the active account will be switched + automatically. If there are more than two accounts, disambiguation will be + required either through the %[1]s--user%[1]s flag or an interactive prompt. + + For a list of authenticated accounts you can run %[1]sgh auth status%[1]s. + `, "`"), Example: heredoc.Doc(` # Select what host and account to switch to via a prompt $ gh auth switch # Switch to a specific host and specific account - $ gh auth logout --hostname enterprise.internal --user monalisa + $ gh auth switch --hostname enterprise.internal --user monalisa `), RunE: func(c *cobra.Command, args []string) error { if runF != nil { diff --git a/pkg/cmd/project/item-list/item_list_test.go b/pkg/cmd/project/item-list/item_list_test.go index 58d4beeeb..e3f10e655 100644 --- a/pkg/cmd/project/item-list/item_list_test.go +++ b/pkg/cmd/project/item-list/item_list_test.go @@ -164,6 +164,7 @@ func TestRunList_User_tty(t *testing.T) { { "id": "draft issue ID", "content": map[string]interface{}{ + "id": "draft issue ID", "title": "draft issue", "__typename": "DraftIssue", }, @@ -273,6 +274,7 @@ func TestRunList_User(t *testing.T) { { "id": "draft issue ID", "content": map[string]interface{}{ + "id": "draft issue ID", "title": "draft issue", "__typename": "DraftIssue", }, @@ -379,6 +381,7 @@ func TestRunList_Org(t *testing.T) { { "id": "draft issue ID", "content": map[string]interface{}{ + "id": "draft issue ID", "title": "draft issue", "__typename": "DraftIssue", }, @@ -475,6 +478,7 @@ func TestRunList_Me(t *testing.T) { { "id": "draft issue ID", "content": map[string]interface{}{ + "id": "draft issue ID", "title": "draft issue", "__typename": "DraftIssue", }, @@ -581,6 +585,7 @@ func TestRunList_JSON(t *testing.T) { { "id": "draft issue ID", "content": map[string]interface{}{ + "id": "draft issue ID", "title": "draft issue", "__typename": "DraftIssue", }, @@ -610,6 +615,6 @@ func TestRunList_JSON(t *testing.T) { assert.NoError(t, err) assert.JSONEq( t, - `{"items":[{"content":{"type":"Issue","body":"","title":"an issue","number":1,"repository":"cli/go-gh","url":""},"id":"issue ID"},{"content":{"type":"PullRequest","body":"","title":"a pull request","number":2,"repository":"cli/go-gh","url":""},"id":"pull request ID"},{"content":{"type":"DraftIssue","body":"","title":"draft issue"},"id":"draft issue ID"}],"totalCount":3}`, + `{"items":[{"content":{"type":"Issue","body":"","title":"an issue","number":1,"repository":"cli/go-gh","url":""},"id":"issue ID"},{"content":{"type":"PullRequest","body":"","title":"a pull request","number":2,"repository":"cli/go-gh","url":""},"id":"pull request ID"},{"content":{"type":"DraftIssue","body":"","title":"draft issue","id":"draft issue ID"},"id":"draft issue ID"}],"totalCount":3}`, stdout.String()) } diff --git a/pkg/cmd/project/shared/queries/export_data_test.go b/pkg/cmd/project/shared/queries/export_data_test.go index 976839d43..adfdce63f 100644 --- a/pkg/cmd/project/shared/queries/export_data_test.go +++ b/pkg/cmd/project/shared/queries/export_data_test.go @@ -259,6 +259,7 @@ func TestJSONProjectDetailedItems(t *testing.T) { Content: ProjectItemContent{ TypeName: "DraftIssue", DraftIssue: DraftIssue{ + ID: "draftIssueId", Title: "Pull Request title", Body: "a body", }, @@ -270,7 +271,7 @@ func TestJSONProjectDetailedItems(t *testing.T) { assert.NoError(t, err) assert.JSONEq( t, - `{"items":[{"content":{"type":"Issue","body":"a body","title":"Issue title","number":1,"repository":"cli/go-gh","url":"issue-url"},"id":"issueId"},{"content":{"type":"PullRequest","body":"a body","title":"Pull Request title","number":2,"repository":"cli/go-gh","url":"pr-url"},"id":"pullRequestId"},{"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title"},"id":"draftIssueId"}],"totalCount":5}`, + `{"items":[{"content":{"type":"Issue","body":"a body","title":"Issue title","number":1,"repository":"cli/go-gh","url":"issue-url"},"id":"issueId"},{"content":{"type":"PullRequest","body":"a body","title":"Pull Request title","number":2,"repository":"cli/go-gh","url":"pr-url"},"id":"pullRequestId"},{"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title","id":"draftIssueId"},"id":"draftIssueId"}],"totalCount":5}`, string(out)) } @@ -300,6 +301,7 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldIterationValue(t *testing. Content: ProjectItemContent{ TypeName: "DraftIssue", DraftIssue: DraftIssue{ + ID: "draftIssueId", Title: "Pull Request title", Body: "a body", }, @@ -319,7 +321,7 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldIterationValue(t *testing. assert.NoError(t, err) assert.JSONEq( t, - `{"items":[{"sprint":{"title":"Iteration Title","startDate":"","duration":0},"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title"},"id":"draftIssueId"}],"totalCount":5}`, + `{"items":[{"sprint":{"title":"Iteration Title","startDate":"","duration":0},"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title","id":"draftIssueId"},"id":"draftIssueId"}],"totalCount":5}`, string(out)) } @@ -338,6 +340,7 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldMilestoneValue(t *testing. Content: ProjectItemContent{ TypeName: "DraftIssue", DraftIssue: DraftIssue{ + ID: "draftIssueId", Title: "Pull Request title", Body: "a body", }, @@ -357,7 +360,7 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldMilestoneValue(t *testing. assert.NoError(t, err) assert.JSONEq( t, - `{"items":[{"milestone":{"title":"Milestone Title","dueOn":"","description":""},"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title"},"id":"draftIssueId"}],"totalCount":5}`, + `{"items":[{"milestone":{"title":"Milestone Title","dueOn":"","description":""},"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title","id":"draftIssueId"},"id":"draftIssueId"}],"totalCount":5}`, string(out)) } diff --git a/pkg/cmd/project/shared/queries/queries.go b/pkg/cmd/project/shared/queries/queries.go index e54a3fb17..080a43097 100644 --- a/pkg/cmd/project/shared/queries/queries.go +++ b/pkg/cmd/project/shared/queries/queries.go @@ -400,6 +400,7 @@ func (p ProjectItem) DetailedItem() exportable { switch p.Type() { case "DraftIssue": return DraftIssue{ + ID: p.Content.DraftIssue.ID, Body: p.Body(), Title: p.Title(), } diff --git a/pkg/cmd/repo/rename/rename.go b/pkg/cmd/repo/rename/rename.go index d15f2efdf..df56af951 100644 --- a/pkg/cmd/repo/rename/rename.go +++ b/pkg/cmd/repo/rename/rename.go @@ -135,7 +135,11 @@ func renameRun(opts *RenameOptions) error { remote, err := updateRemote(currRepo, newRepo, opts) if err != nil { - fmt.Fprintf(opts.IO.ErrOut, "%s Warning: unable to update remote %q: %v\n", cs.WarningIcon(), remote.Name, err) + if remote != nil { + fmt.Fprintf(opts.IO.ErrOut, "%s Warning: unable to update remote %q: %v\n", cs.WarningIcon(), remote.Name, err) + } else { + fmt.Fprintf(opts.IO.ErrOut, "%s Warning: unable to update remote: %v\n", cs.WarningIcon(), err) + } } else if opts.IO.IsStdoutTTY() { fmt.Fprintf(opts.IO.Out, "%s Updated the %q remote\n", cs.SuccessIcon(), remote.Name) } diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index db591c27b..4b744e192 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -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)) diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index d6cb008d0..c54905422 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -96,7 +96,13 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman cmd := &cobra.Command{ Use: "view []", Short: "View a summary of a workflow run", - Args: cobra.MaximumNArgs(1), + Long: heredoc.Docf(` + View a summary of a workflow run. + + This command does not support authenticating via fine grained PATs + as it is not currently possible to create a PAT with the %[1]schecks:read%[1]s permission. + `, "`"), + Args: cobra.MaximumNArgs(1), Example: heredoc.Doc(` # Interactively select a run to view, optionally selecting a single job $ gh run view diff --git a/pkg/cmd/run/watch/watch.go b/pkg/cmd/run/watch/watch.go index b4b51f20c..92120dc38 100644 --- a/pkg/cmd/run/watch/watch.go +++ b/pkg/cmd/run/watch/watch.go @@ -45,6 +45,12 @@ func NewCmdWatch(f *cmdutil.Factory, runF func(*WatchOptions) error) *cobra.Comm cmd := &cobra.Command{ Use: "watch ", Short: "Watch a run until it completes, showing its progress", + Long: heredoc.Docf(` + Watch a run until it completes, showing its progress. + + This command does not support authenticating via fine grained PATs + as it is not currently possible to create a PAT with the %[1]schecks:read%[1]s permission. + `, "`"), Example: heredoc.Doc(` # Watch a run until it's done gh run watch diff --git a/test/integration/attestation-cmd/download-and-verify-package-attestation.sh b/test/integration/attestation-cmd/download-and-verify-package-attestation.sh new file mode 100755 index 000000000..6774b8e2d --- /dev/null +++ b/test/integration/attestation-cmd/download-and-verify-package-attestation.sh @@ -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"