Merge branch 'trunk' into andyfeller/9422-license-compliance
This commit is contained in:
commit
98ea250ede
32 changed files with 1265 additions and 146 deletions
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
|
|
@ -5,6 +5,10 @@ internal/codespaces/ @cli/codespaces
|
|||
|
||||
# Limit Package Security team ownership to the attestation command package and related integration tests
|
||||
pkg/cmd/attestation/ @cli/package-security
|
||||
pkg/cmd/release/attestation @cli/package-security
|
||||
pkg/cmd/release/verify @cli/package-security
|
||||
pkg/cmd/release/verify-asset @cli/package-security
|
||||
|
||||
test/integration/attestation-cmd @cli/package-security
|
||||
|
||||
pkg/cmd/attestation/verification/embed/tuf-repo.github.com/ @cli/tuf-root-reviewers
|
||||
|
|
|
|||
12
.github/workflows/codeql.yml
vendored
12
.github/workflows/codeql.yml
vendored
|
|
@ -27,6 +27,12 @@ jobs:
|
|||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
if: matrix.language == 'go'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
|
|
@ -37,12 +43,6 @@ jobs:
|
|||
- 'third-party/**'
|
||||
- 'third-party-licenses.*.md'
|
||||
|
||||
- name: Setup Go
|
||||
if: matrix.language == 'go'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
|
|
|
|||
22
.github/workflows/lint.yml
vendored
22
.github/workflows/lint.yml
vendored
|
|
@ -31,17 +31,7 @@ jobs:
|
|||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Verify dependencies
|
||||
run: |
|
||||
go mod verify
|
||||
go mod download
|
||||
|
||||
LINT_VERSION=1.63.4
|
||||
curl -fsSL https://github.com/golangci/golangci-lint/releases/download/v${LINT_VERSION}/golangci-lint-${LINT_VERSION}-linux-amd64.tar.gz | \
|
||||
tar xz --strip-components 1 --wildcards \*/golangci-lint
|
||||
mkdir -p bin && mv golangci-lint bin/
|
||||
|
||||
- name: Run checks
|
||||
- name: Ensure go.mod and go.sum are up to date
|
||||
run: |
|
||||
STATUS=0
|
||||
assert-nothing-changed() {
|
||||
|
|
@ -53,14 +43,14 @@ jobs:
|
|||
STATUS=1
|
||||
fi
|
||||
}
|
||||
|
||||
assert-nothing-changed go fmt ./...
|
||||
assert-nothing-changed go mod tidy
|
||||
|
||||
bin/golangci-lint run --out-format=colored-line-number --timeout=3m || STATUS=$?
|
||||
|
||||
exit $STATUS
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
||||
with:
|
||||
version: v2.1.6
|
||||
|
||||
# actions/setup-go does not setup the installed toolchain to be preferred over the system install,
|
||||
# which causes go-licenses to raise "Package ... does not have module info" errors.
|
||||
# for more information, https://github.com/google/go-licenses/issues/244#issuecomment-1885098633
|
||||
|
|
|
|||
6
.github/workflows/pr-help-wanted.yml
vendored
6
.github/workflows/pr-help-wanted.yml
vendored
|
|
@ -20,12 +20,8 @@ jobs:
|
|||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
PR_AUTHOR_TYPE: ${{ github.event.pull_request.user.type }}
|
||||
PR_AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }}
|
||||
if: "!github.event.pull_request.draft"
|
||||
run: |
|
||||
# Skip if PR is from a bot or org member
|
||||
if [ "$PR_AUTHOR_TYPE" = "Bot" ] || gh api orgs/cli/public_members/"${PR_AUTHOR}" --silent 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run the script to check for issues without help-wanted label
|
||||
bash .github/workflows/scripts/check-help-wanted.sh ${{ github.event.pull_request.html_url }}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ if [ -z "$PR_URL" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# Skip if PR is from a bot or org member
|
||||
if [ "$PR_AUTHOR_TYPE" = "Bot" ] || [ "$PR_AUTHOR_ASSOCIATION" = "MEMBER" ] || [ "$PR_AUTHOR_ASSOCIATION" = "OWNER" ]; then
|
||||
echo "Skipping check for PR #$PR_URL as it is from a bot ($PR_AUTHOR_TYPE) or an org member ($PR_AUTHOR_ASSOCIATION: MEMBER/OWNER)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract PR number from URL for logging
|
||||
PR_NUM="$(basename "$PR_URL")"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
version: "2"
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- gofmt
|
||||
- nolintlint
|
||||
- nolintlint
|
||||
disable:
|
||||
# The following linters are disabled purely because this config was migrated to v2 where they are in the default
|
||||
# set, and we should have separate work to enable them if we truly want them.
|
||||
- staticcheck
|
||||
- errcheck
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
|||
|
||||
# Attempt to rename the repo with a slash in the name
|
||||
! exec gh repo rename $ORG/new-name --repo=$ORG/$SCRIPT_NAME-$RANDOM_STRING --yes
|
||||
stderr 'New repository name cannot contain '/' character - to transfer a repository to a new owner, you must follow additional steps on GitHub.com. For more information on transferring repository ownership, see <https://docs.github.com/en/repositories/creating-and-managing-repositories/transferring-a-repository>.'
|
||||
stderr 'New repository name cannot contain \''/\'' character - to transfer a repository to a new owner, you must follow additional steps on <github.com>. For more information on transferring repository ownership, see <https://docs.github.com/en/repositories/creating-and-managing-repositories/transferring-a-repository>.'
|
||||
|
||||
# Defer repo deletion
|
||||
defer gh repo delete $ORG/$SCRIPT_NAME-$RANDOM_STRING --yes
|
||||
|
|
@ -19,6 +19,7 @@ Install:
|
|||
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||
&& cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
|
||||
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& sudo mkdir -p -m 755 /etc/apt/sources.list.d \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
||||
&& sudo apt update \
|
||||
&& sudo apt install gh -y
|
||||
|
|
@ -163,6 +164,20 @@ Or via [pkg(8)](https://www.freebsd.org/cgi/man.cgi?pkg(8)):
|
|||
pkg install gh
|
||||
```
|
||||
|
||||
### MidnightBSD
|
||||
|
||||
MidnightBSD users can install from [mports](https://www.midnightbsd.org/documentation/mports/index.html)
|
||||
|
||||
```bash
|
||||
cd /usr/mports/devel/gh/ && make install clean
|
||||
```
|
||||
|
||||
Or via [mport(1)](http://man.midnightbsd.org/cgi-bin/man.cgi/mport):
|
||||
|
||||
```bash
|
||||
mport install gh
|
||||
```
|
||||
|
||||
### NetBSD/pkgsrc
|
||||
|
||||
NetBSD users and those on [platforms supported by pkgsrc](https://pkgsrc.org/#index4h1) can install the [gh package](https://pkgsrc.se/net/gh):
|
||||
|
|
|
|||
40
go.mod
40
go.mod
|
|
@ -1,8 +1,8 @@
|
|||
module github.com/cli/cli/v2
|
||||
|
||||
go 1.23.0
|
||||
go 1.24
|
||||
|
||||
toolchain go1.23.5
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
|
|
@ -26,14 +26,14 @@ require (
|
|||
github.com/gdamore/tcell/v2 v2.5.4
|
||||
github.com/golang/snappy v0.0.4
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/go-containerregistry v0.20.3
|
||||
github.com/google/go-containerregistry v0.20.6
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-version v1.3.0
|
||||
github.com/henvic/httpretty v0.1.4
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec
|
||||
github.com/in-toto/attestation v1.1.1
|
||||
github.com/in-toto/attestation v1.1.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-colorable v0.1.14
|
||||
|
|
@ -52,10 +52,10 @@ require (
|
|||
github.com/theupdateframework/go-tuf/v2 v2.1.1
|
||||
github.com/yuin/goldmark v1.7.12
|
||||
github.com/zalando/go-keyring v0.2.5
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/sync v0.15.0
|
||||
golang.org/x/term v0.32.0
|
||||
golang.org/x/text v0.25.0
|
||||
golang.org/x/text v0.26.0
|
||||
google.golang.org/grpc v1.72.2
|
||||
google.golang.org/protobuf v1.36.6
|
||||
gopkg.in/h2non/gock.v1 v1.1.2
|
||||
|
|
@ -86,13 +86,13 @@ require (
|
|||
github.com/cli/shurcooL-graphql v0.0.4 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 // indirect
|
||||
github.com/danieljoos/wincred v1.2.1 // indirect
|
||||
github.com/danieljoos/wincred v1.2.2 // 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/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/docker/cli v27.5.0+incompatible // indirect
|
||||
github.com/docker/cli v28.2.2+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
|
|
@ -100,7 +100,7 @@ require (
|
|||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||
github.com/go-openapi/errors v0.22.1 // indirect
|
||||
|
|
@ -126,7 +126,7 @@ require (
|
|||
github.com/itchyny/timefmt-go v0.1.5 // 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.11 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
|
|
@ -144,7 +144,7 @@ require (
|
|||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
|
|
@ -171,21 +171,21 @@ require (
|
|||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
|
||||
github.com/transparency-dev/merkle v0.0.2 // indirect
|
||||
github.com/vbatts/tar-split v0.11.6 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
go.mongodb.org/mongo-driver v1.14.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
|
|
|
|||
84
go.sum
84
go.sum
|
|
@ -4,8 +4,8 @@ cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU=
|
|||
cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
|
||||
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
|
||||
cloud.google.com/go/iam v1.5.0 h1:QlLcVMhbLGOjRcGe6VTGGTyQib8dRLK2B/kYNV0+2xs=
|
||||
cloud.google.com/go/iam v1.5.0/go.mod h1:U+DOtKQltF/LxPEtcDLoobcsZMilSRwR7mgNL7knOpo=
|
||||
cloud.google.com/go/kms v1.21.2 h1:c/PRUSMNQ8zXrc1sdAUnsenWWaNXN+PzTXfXOcSFdoE=
|
||||
|
|
@ -160,8 +160,8 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
|||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
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/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
|
||||
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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=
|
||||
|
|
@ -175,12 +175,12 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM=
|
||||
github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=
|
||||
github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
|
|
@ -205,8 +205,8 @@ github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm
|
|||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/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.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
|
||||
|
|
@ -247,8 +247,8 @@ github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeW
|
|||
github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
|
||||
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
|
||||
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
|
||||
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
|
||||
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.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
|
|
@ -304,8 +304,8 @@ github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb
|
|||
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/in-toto/attestation v1.1.1 h1:QD3d+oATQ0dFsWoNh5oT0udQ3tUrOsZZ0Fc3tSgWbzI=
|
||||
github.com/in-toto/attestation v1.1.1/go.mod h1:Dcq1zVwA2V7Qin8I7rgOi+i837wEf/mOZwRm047Sjys=
|
||||
github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E=
|
||||
github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM=
|
||||
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=
|
||||
|
|
@ -338,8 +338,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
|||
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/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
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=
|
||||
|
|
@ -402,8 +402,8 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
|||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
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 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
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.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
|
|
@ -515,8 +515,8 @@ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C
|
|||
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/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
|
||||
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
|
||||
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
|
||||
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
|
|
@ -533,18 +533,18 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
|
|||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.step.sm/crypto v0.63.0 h1:U1QGELQqJ85oDfeNFE2V52cow1rvy0m3MekG3wFmyXY=
|
||||
go.step.sm/crypto v0.63.0/go.mod h1:aj3LETmCZeSil1DMq3BlbhDBcN86+mmKrHZtXWyc0L4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
|
|
@ -555,24 +555,24 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
|||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
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=
|
||||
|
|
@ -594,15 +594,15 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
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=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.230.0 h1:2u1hni3E+UXAXrONrrkfWpi/V6cyKVAbfGVeGtC3OxM=
|
||||
google.golang.org/api v0.230.0/go.mod h1:aqvtoMk7YkiXx+6U12arQFExiRV9D/ekvMCwCd/TksQ=
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/cmd/attestation/test/data"
|
||||
)
|
||||
|
||||
func makeTestReleaseAttestation() Attestation {
|
||||
return Attestation{
|
||||
Bundle: data.GitHubReleaseBundle(nil),
|
||||
BundleURL: "https://example.com",
|
||||
}
|
||||
}
|
||||
|
||||
func makeTestAttestation() Attestation {
|
||||
return Attestation{Bundle: data.SigstoreBundle(nil), BundleURL: "https://example.com"}
|
||||
}
|
||||
|
|
@ -26,8 +33,12 @@ func (m MockClient) GetTrustDomain() (string, error) {
|
|||
func OnGetByDigestSuccess(params FetchParams) ([]*Attestation, error) {
|
||||
att1 := makeTestAttestation()
|
||||
att2 := makeTestAttestation()
|
||||
att3 := makeTestReleaseAttestation()
|
||||
attestations := []*Attestation{&att1, &att2}
|
||||
if params.PredicateType != "" {
|
||||
if params.PredicateType == "https://in-toto.io/attestation/release/v0.1" {
|
||||
attestations = append(attestations, &att3)
|
||||
}
|
||||
return FilterAttestations(params.PredicateType, attestations)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,13 @@ func normalizeReference(reference string, pathSeparator rune) (normalized string
|
|||
return filepath.Clean(reference), fileArtifactType, nil
|
||||
}
|
||||
|
||||
func NewDigestedArtifactForRelease(digest string, digestAlg string) (artifact *DigestedArtifact) {
|
||||
return &DigestedArtifact{
|
||||
digest: digest,
|
||||
digestAlg: digestAlg,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDigestedArtifact(client oci.Client, reference, digestAlg string) (artifact *DigestedArtifact, err error) {
|
||||
normalized, artifactType, err := normalizeReference(reference, os.PathSeparator)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
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)
|
||||
return nil, fmt.Errorf("failed to open local artifact: %v", err)
|
||||
}
|
||||
defer data.Close()
|
||||
digest, err := digest.CalculateDigestWithAlgorithm(data, digestAlg)
|
||||
|
|
|
|||
23
pkg/cmd/attestation/artifact/file_test.go
Normal file
23
pkg/cmd/attestation/artifact/file_test.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package artifact
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_digestLocalFileArtifact_withRealZip(t *testing.T) {
|
||||
// Path to the test artifact
|
||||
artifactPath := test.NormalizeRelativePath("../../attestation/test/data/github_release_artifact.zip")
|
||||
|
||||
// Calculate expected digest using the same algorithm as the function under test
|
||||
expectedDigest := "e15b593c6ab8d7725a3cc82226ef816cac6bf9c70eed383bd459295cc65f5ec3"
|
||||
|
||||
// Call the function under test
|
||||
artifact, err := digestLocalFileArtifact(artifactPath, "sha256")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "file://"+artifactPath, artifact.URL)
|
||||
require.Equal(t, expectedDigest, artifact.digest)
|
||||
require.Equal(t, "sha256", artifact.digestAlg)
|
||||
}
|
||||
|
|
@ -10,6 +10,9 @@ import (
|
|||
//go:embed sigstore-js-2.1.0-bundle.json
|
||||
var SigstoreBundleRaw []byte
|
||||
|
||||
//go:embed github_release_bundle.json
|
||||
var GitHubReleaseBundleRaw []byte
|
||||
|
||||
// SigstoreBundle returns a test sigstore-go bundle.Bundle
|
||||
func SigstoreBundle(t *testing.T) *bundle.Bundle {
|
||||
b := &bundle.Bundle{}
|
||||
|
|
@ -19,3 +22,12 @@ func SigstoreBundle(t *testing.T) *bundle.Bundle {
|
|||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func GitHubReleaseBundle(t *testing.T) *bundle.Bundle {
|
||||
b := &bundle.Bundle{}
|
||||
err := b.UnmarshalJSON(GitHubReleaseBundleRaw)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal GitHub release bundle: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
|
|
|||
BIN
pkg/cmd/attestation/test/data/github_release_artifact.zip
Normal file
BIN
pkg/cmd/attestation/test/data/github_release_artifact.zip
Normal file
Binary file not shown.
Binary file not shown.
24
pkg/cmd/attestation/test/data/github_release_bundle.json
Normal file
24
pkg/cmd/attestation/test/data/github_release_bundle.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
|
||||
"verificationMaterial": {
|
||||
"timestampVerificationData": {
|
||||
"rfc3161Timestamps": [
|
||||
{
|
||||
"signedTimestamp": "MIIC0DADAgEAMIICxwYJKoZIhvcNAQcCoIICuDCCArQCAQMxDTALBglghkgBZQMEAgIwgbwGCyqGSIb3DQEJEAEEoIGsBIGpMIGmAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQg1c3kQQpo4Adf2E+nx78lNg8EjRSLpIRERpPF0HIavogCFQCOfZuxr0DOc1LsM+y+sjQCMFrtbxgPMjAyNTA1MzAyMDEzMzlaMAMCAQGgNqQ0MDIxFTATBgNVBAoTDEdpdEh1YiwgSW5jLjEZMBcGA1UEAxMQVFNBIFRpbWVzdGFtcGluZ6AAMYIB3TCCAdkCAQEwSjAyMRUwEwYDVQQKEwxHaXRIdWIsIEluYy4xGTAXBgNVBAMTEFRTQSBpbnRlcm1lZGlhdGUCFB+7MIjE5/rL4XA4fNDnmXHA04+wMAsGCWCGSAFlAwQCAqCCAQUwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNTA1MzAyMDEzMzlaMD8GCSqGSIb3DQEJBDEyBDDkw1fXMZ6l/uWne+PcdzhLl2ckTZftcUuHcnYCwjhyYMeGOcgbpNNMDem46JCxItwwgYcGCyqGSIb3DQEJEAIvMXgwdjB0MHIEIHuISsKSyiJtlhGjT+RyS+tYQ7iwCMsMCTGmz2NK3D7DME4wNqQ0MDIxFTATBgNVBAoTDEdpdEh1YiwgSW5jLjEZMBcGA1UEAxMQVFNBIGludGVybWVkaWF0ZQIUH7swiMTn+svhcDh80OeZccDTj7AwCgYIKoZIzj0EAwMEZjBkAjBhE76zis18/xOtQdx6rJUuaRoZCflXHCjH6BqEk1B29r9C8STztZhAKalXL+Wy4rsCMFFaGPKF1uOl5JADiKMg5/7chJbWrfwyO9oa0tbmvcGrtBCdFeJ1Ic0tIi1sOVvq5Q=="
|
||||
}
|
||||
]
|
||||
},
|
||||
"certificate": {
|
||||
"rawBytes": "MIICKjCCAbCgAwIBAgIUaa62dj98DUB+TpyvKtVaR4vGSM0wCgYIKoZIzj0EAwMwODEVMBMGA1UEChMMR2l0SHViLCBJbmMuMR8wHQYDVQQDExZGdWxjaW8gSW50ZXJtZWRpYXRlIGwxMB4XDTI1MDMxMDE1MDMwMloXDTI2MDMxMDE1MDMwMlowKjEVMBMGA1UEChMMR2l0SHViLCBJbmMuMREwDwYDVQQDEwhBdHRlc3RlcjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIMB7plPnZvBRlC2lvAocKTAqAPMJqstEqYk26e9vDJDC1yqoiHxZfPV4W/1RqUMZD1dFKm9t4RiSmm73/QnQKajgaUwgaIwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFOqaGpr5SbdYk5CQXsmmDZCBHR+XMB8GA1UdIwQYMBaAFMDhuFKkS08+3no4EQbPSY6hRZszMC0GA1UdEQQmMCSGImh0dHBzOi8vZG90Y29tLnJlbGVhc2VzLmdpdGh1Yi5jb20wCgYIKoZIzj0EAwMDaAAwZQIwWFdF6xcXazHVPHEAtd1SeaizLdY1erRl5hK+XlwhfpnasQHHZ9bdu4Zj8ARhW/AhAjEArujhmJGo7Fi4/Ek1RN8bufs6UhIQneQd/pxE8QdorwZkj2C8nf2EzrUYzlxKfktC"
|
||||
}
|
||||
},
|
||||
"dsseEnvelope": {
|
||||
"payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCAic3ViamVjdCI6W3sidXJpIjoicGtnOmdpdGh1Yi9iZGVoYW1lci9kZWxtZUB2NiIsICJkaWdlc3QiOnsic2hhMSI6ImM1ZTE3YTYyZTA2YTFkMjAxNTcwMjQ5YzYxZmFlNTMxZTkyNDRlMWIifX0sIHsibmFtZSI6ImFydGlmYWN0LnppcCIsICJkaWdlc3QiOnsic2hhMjU2IjoiZTE1YjU5M2M2YWI4ZDc3MjVhM2NjODIyMjZlZjgxNmNhYzZiZjljNzBlZWQzODNiZDQ1OTI5NWNjNjVmNWVjMyJ9fV0sICJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9pbi10b3RvLmlvL2F0dGVzdGF0aW9uL3JlbGVhc2UvdjAuMSIsICJwcmVkaWNhdGUiOnsib3duZXJJZCI6IjM5ODAyNyIsICJwdXJsIjoicGtnOmdpdGh1Yi9iZGVoYW1lci9kZWxtZUB2NiIsICJyZWxlYXNlSWQiOiIyMjIxNTg2NzEiLCAicmVwb3NpdG9yeSI6ImJkZWhhbWVyL2RlbG1lIiwgInJlcG9zaXRvcnlJZCI6IjkwNTk4ODA0NCIsICJ0YWciOiJ2NiJ9fQ==",
|
||||
"payloadType": "application/vnd.in-toto+json",
|
||||
"signatures": [
|
||||
{
|
||||
"sig": "MEUCIGq+T2g2gV2+lcmgyCaVPrjO1tj86RxitwiEOjU5dH/GAiEAvKaT/7H0sIVdAY7EzLq1IFaF8LmlW6eV68eZQvtuA0c="
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -23,15 +23,14 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewCmdExtension(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
oldWd, _ := os.Getwd()
|
||||
localExtensionTempDir := filepath.Join(tempDir, "gh-hello")
|
||||
assert.NoError(t, os.MkdirAll(localExtensionTempDir, 0755))
|
||||
assert.NoError(t, os.Chdir(localExtensionTempDir))
|
||||
t.Cleanup(func() { _ = os.Chdir(oldWd) })
|
||||
require.NoError(t, os.MkdirAll(localExtensionTempDir, 0755))
|
||||
t.Chdir(localExtensionTempDir)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -1251,9 +1251,10 @@ func TestManager_repo_not_found(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestManager_Create(t *testing.T) {
|
||||
chdirTemp(t)
|
||||
tempDir := t.TempDir()
|
||||
t.Chdir(tempDir)
|
||||
err := os.MkdirAll("gh-test", 0755)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
|
|
@ -1279,9 +1280,10 @@ func TestManager_Create(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestManager_Create_go_binary(t *testing.T) {
|
||||
chdirTemp(t)
|
||||
tempDir := t.TempDir()
|
||||
t.Chdir(tempDir)
|
||||
err := os.MkdirAll("gh-test", 0755)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
reg := httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
|
|
@ -1329,9 +1331,10 @@ func TestManager_Create_go_binary(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestManager_Create_other_binary(t *testing.T) {
|
||||
chdirTemp(t)
|
||||
tempDir := t.TempDir()
|
||||
t.Chdir(tempDir)
|
||||
err := os.MkdirAll("gh-test", 0755)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
|
|
@ -1392,18 +1395,6 @@ func Test_ensurePrefixed(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// chdirTemp changes the current working directory to a temporary directory for the duration of the test.
|
||||
func chdirTemp(t *testing.T) {
|
||||
oldWd, _ := os.Getwd()
|
||||
tempDir := t.TempDir()
|
||||
if err := os.Chdir(tempDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = os.Chdir(oldWd)
|
||||
})
|
||||
}
|
||||
|
||||
func fileNames(files []os.DirEntry) []string {
|
||||
names := make([]string, len(files))
|
||||
for i, f := range files {
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ func NewPullRequestFindRefsResolver(gitConfigClient GitConfigClient, remotesFn f
|
|||
}
|
||||
}
|
||||
|
||||
// ResolvePullRequests takes a base repository, a base branch name and a local branch name and uses the git configuration to
|
||||
// ResolvePullRequestRefs takes a base repository, a base branch name and a local branch name and uses the git configuration to
|
||||
// determine the head repository and remote branch name. If we were unable to determine this from git, we default the head
|
||||
// repository to the base repository.
|
||||
func (r *PullRequestFindRefsResolver) ResolvePullRequestRefs(baseRepo ghrepo.Interface, baseBranchName, localBranchName string) (PRFindRefs, error) {
|
||||
|
|
|
|||
|
|
@ -174,11 +174,6 @@ func Test_NewCmdDownload(t *testing.T) {
|
|||
}
|
||||
|
||||
func Test_downloadRun(t *testing.T) {
|
||||
oldwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("could not determine working directory: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
isTTY bool
|
||||
|
|
@ -526,11 +521,7 @@ func Test_downloadRun(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
if err := os.Chdir(tempDir); err == nil {
|
||||
t.Cleanup(func() { _ = os.Chdir(oldwd) })
|
||||
} else {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Chdir(tempDir)
|
||||
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdoutTTY(tt.isTTY)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import (
|
|||
cmdUpdate "github.com/cli/cli/v2/pkg/cmd/release/edit"
|
||||
cmdList "github.com/cli/cli/v2/pkg/cmd/release/list"
|
||||
cmdUpload "github.com/cli/cli/v2/pkg/cmd/release/upload"
|
||||
cmdVerify "github.com/cli/cli/v2/pkg/cmd/release/verify"
|
||||
cmdVerifyAsset "github.com/cli/cli/v2/pkg/cmd/release/verify-asset"
|
||||
cmdView "github.com/cli/cli/v2/pkg/cmd/release/view"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -34,6 +36,8 @@ func NewCmdRelease(f *cmdutil.Factory) *cobra.Command {
|
|||
cmdDownload.NewCmdDownload(f, nil),
|
||||
cmdDelete.NewCmdDelete(f, nil),
|
||||
cmdDeleteAsset.NewCmdDeleteAsset(f, nil),
|
||||
cmdVerify.NewCmdVerify(f, nil),
|
||||
cmdVerifyAsset.NewCmdVerifyAsset(f, nil),
|
||||
)
|
||||
|
||||
return cmd
|
||||
|
|
|
|||
128
pkg/cmd/release/shared/attestation.go
Normal file
128
pkg/cmd/release/shared/attestation.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
|
||||
att_io "github.com/cli/cli/v2/pkg/cmd/attestation/io"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/test/data"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
|
||||
"github.com/sigstore/sigstore-go/pkg/verify"
|
||||
|
||||
v1 "github.com/in-toto/attestation/go/v1"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
const ReleasePredicateType = "https://in-toto.io/attestation/release/v0.1"
|
||||
|
||||
type Verifier interface {
|
||||
// VerifyAttestation verifies the attestation for a given artifact
|
||||
VerifyAttestation(art *artifact.DigestedArtifact, att *api.Attestation) (*verification.AttestationProcessingResult, error)
|
||||
}
|
||||
|
||||
type AttestationVerifier struct {
|
||||
AttClient api.Client
|
||||
HttpClient *http.Client
|
||||
IO *iostreams.IOStreams
|
||||
}
|
||||
|
||||
func (v *AttestationVerifier) VerifyAttestation(art *artifact.DigestedArtifact, att *api.Attestation) (*verification.AttestationProcessingResult, error) {
|
||||
td, err := v.AttClient.GetTrustDomain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
verifier, err := verification.NewLiveSigstoreVerifier(verification.SigstoreConfig{
|
||||
HttpClient: v.HttpClient,
|
||||
Logger: att_io.NewHandler(v.IO),
|
||||
NoPublicGood: true,
|
||||
TrustDomain: td,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policy := buildVerificationPolicy(*art)
|
||||
sigstoreVerified, err := verifier.Verify([]*api.Attestation{att}, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sigstoreVerified[0], nil
|
||||
}
|
||||
|
||||
func FilterAttestationsByTag(attestations []*api.Attestation, tagName string) ([]*api.Attestation, error) {
|
||||
var filtered []*api.Attestation
|
||||
for _, att := range attestations {
|
||||
statement := att.Bundle.Bundle.GetDsseEnvelope().Payload
|
||||
var statementData v1.Statement
|
||||
err := protojson.Unmarshal([]byte(statement), &statementData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal statement: %w", err)
|
||||
}
|
||||
tagValue := statementData.Predicate.GetFields()["tag"].GetStringValue()
|
||||
|
||||
if tagValue == tagName {
|
||||
filtered = append(filtered, att)
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func FilterAttestationsByFileDigest(attestations []*api.Attestation, fileDigest string) ([]*api.Attestation, error) {
|
||||
var filtered []*api.Attestation
|
||||
for _, att := range attestations {
|
||||
statement := att.Bundle.Bundle.GetDsseEnvelope().Payload
|
||||
var statementData v1.Statement
|
||||
err := protojson.Unmarshal([]byte(statement), &statementData)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal statement: %w", err)
|
||||
}
|
||||
subjects := statementData.Subject
|
||||
for _, subject := range subjects {
|
||||
digestMap := subject.GetDigest()
|
||||
alg := "sha256"
|
||||
|
||||
digest := digestMap[alg]
|
||||
if digest == fileDigest {
|
||||
filtered = append(filtered, att)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// buildVerificationPolicy constructs a verification policy for GitHub releases
|
||||
func buildVerificationPolicy(a artifact.DigestedArtifact) verify.PolicyBuilder {
|
||||
// SAN must match the GitHub releases domain. No issuer extension (match anything)
|
||||
sanMatcher, _ := verify.NewSANMatcher("", "^https://.*\\.releases\\.github\\.com$")
|
||||
issuerMatcher, _ := verify.NewIssuerMatcher("", ".*")
|
||||
certId, _ := verify.NewCertificateIdentity(sanMatcher, issuerMatcher, certificate.Extensions{})
|
||||
|
||||
artifactDigestPolicyOption, _ := verification.BuildDigestPolicyOption(a)
|
||||
return verify.NewPolicy(artifactDigestPolicyOption, verify.WithCertificateIdentity(certId))
|
||||
}
|
||||
|
||||
type MockVerifier struct {
|
||||
mockResult *verification.AttestationProcessingResult
|
||||
}
|
||||
|
||||
func NewMockVerifier(mockResult *verification.AttestationProcessingResult) *MockVerifier {
|
||||
return &MockVerifier{mockResult: mockResult}
|
||||
}
|
||||
|
||||
func (v *MockVerifier) VerifyAttestation(art *artifact.DigestedArtifact, att *api.Attestation) (*verification.AttestationProcessingResult, error) {
|
||||
return &verification.AttestationProcessingResult{
|
||||
Attestation: &api.Attestation{
|
||||
Bundle: data.GitHubReleaseBundle(nil),
|
||||
BundleURL: "https://example.com",
|
||||
},
|
||||
VerificationResult: nil,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -133,6 +133,41 @@ type fetchResult struct {
|
|||
error error
|
||||
}
|
||||
|
||||
func FetchRefSHA(ctx context.Context, httpClient *http.Client, repo ghrepo.Interface, tagName string) (string, error) {
|
||||
path := fmt.Sprintf("repos/%s/%s/git/refs/tags/%s", repo.RepoOwner(), repo.RepoName(), tagName)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", ghinstance.RESTPrefix(repo.RepoHost())+path, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
// ErrRefNotFound
|
||||
return "", ErrReleaseNotFound
|
||||
}
|
||||
|
||||
if resp.StatusCode > 299 {
|
||||
return "", api.HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
var ref struct {
|
||||
Object struct {
|
||||
SHA string `json:"sha"`
|
||||
} `json:"object"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&ref); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ref.Object.SHA, nil
|
||||
}
|
||||
|
||||
// FetchRelease finds a published repository release by its tagName, or a draft release by its pending tag name.
|
||||
func FetchRelease(ctx context.Context, httpClient *http.Client, repo ghrepo.Interface, tagName string) (*Release, error) {
|
||||
cc, cancel := context.WithCancel(ctx)
|
||||
|
|
@ -213,7 +248,7 @@ func fetchReleasePath(ctx context.Context, httpClient *http.Client, host string,
|
|||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
return nil, ErrReleaseNotFound
|
||||
} else if resp.StatusCode > 299 {
|
||||
|
|
@ -248,3 +283,11 @@ func StubFetchRelease(t *testing.T, reg *httpmock.Registry, owner, repoName, tag
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
func StubFetchRefSHA(t *testing.T, reg *httpmock.Registry, owner, repoName, tagName, sha string) {
|
||||
path := fmt.Sprintf("repos/%s/%s/git/refs/tags/%s", owner, repoName, tagName)
|
||||
reg.Register(
|
||||
httpmock.REST("GET", path),
|
||||
httpmock.StringResponse(fmt.Sprintf(`{"object": {"sha": "%s"}}`, sha)),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
211
pkg/cmd/release/verify-asset/verify_asset.go
Normal file
211
pkg/cmd/release/verify-asset/verify_asset.go
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
package verifyasset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
|
||||
att_io "github.com/cli/cli/v2/pkg/cmd/attestation/io"
|
||||
"github.com/cli/cli/v2/pkg/cmd/release/shared"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type VerifyAssetOptions struct {
|
||||
TagName string
|
||||
BaseRepo ghrepo.Interface
|
||||
Exporter cmdutil.Exporter
|
||||
AssetFilePath string
|
||||
}
|
||||
|
||||
type VerifyAssetConfig struct {
|
||||
HttpClient *http.Client
|
||||
IO *iostreams.IOStreams
|
||||
Opts *VerifyAssetOptions
|
||||
AttClient api.Client
|
||||
AttVerifier shared.Verifier
|
||||
}
|
||||
|
||||
func NewCmdVerifyAsset(f *cmdutil.Factory, runF func(*VerifyAssetConfig) error) *cobra.Command {
|
||||
opts := &VerifyAssetOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "verify-asset [<tag>] <file-path>",
|
||||
Short: "Verify that a given asset originated from a specific GitHub Release.",
|
||||
Long: heredoc.Doc(`
|
||||
Verify that a given asset file originated from a specific GitHub Release using cryptographically signed attestations.
|
||||
|
||||
## Understanding Verification
|
||||
|
||||
An attestation is a claim made by GitHub regarding a release and its assets.
|
||||
|
||||
## What This Command Does
|
||||
|
||||
This command checks that the asset you provide matches an attestation produced by GitHub for a particular release.
|
||||
It ensures the asset's integrity by validating:
|
||||
* The asset's digest matches the subject in the attestation
|
||||
* The attestation is associated with the specified release
|
||||
`),
|
||||
Hidden: true,
|
||||
Args: cobra.MaximumNArgs(2),
|
||||
Example: heredoc.Doc(`
|
||||
# Verify an asset from the latest release
|
||||
$ gh release verify-asset ./dist/my-asset.zip
|
||||
|
||||
# Verify an asset from a specific release tag
|
||||
$ gh release verify-asset v1.2.3 ./dist/my-asset.zip
|
||||
|
||||
# Verify an asset from a specific release tag and output the attestation in JSON format
|
||||
$ gh release verify-asset v1.2.3 ./dist/my-asset.zip --format json
|
||||
`),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 2 {
|
||||
opts.TagName = args[0]
|
||||
opts.AssetFilePath = args[1]
|
||||
} else if len(args) == 1 {
|
||||
opts.AssetFilePath = args[0]
|
||||
} else {
|
||||
return cmdutil.FlagErrorf("you must specify an asset filepath")
|
||||
}
|
||||
|
||||
opts.AssetFilePath = filepath.Clean(opts.AssetFilePath)
|
||||
|
||||
baseRepo, err := f.BaseRepo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine base repository: %w", err)
|
||||
}
|
||||
opts.BaseRepo = baseRepo
|
||||
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io := f.IOStreams
|
||||
attClient := api.NewLiveClient(httpClient, baseRepo.RepoHost(), att_io.NewHandler(io))
|
||||
|
||||
attVerifier := &shared.AttestationVerifier{
|
||||
AttClient: attClient,
|
||||
HttpClient: httpClient,
|
||||
IO: io,
|
||||
}
|
||||
|
||||
config := &VerifyAssetConfig{
|
||||
Opts: opts,
|
||||
HttpClient: httpClient,
|
||||
AttClient: attClient,
|
||||
AttVerifier: attVerifier,
|
||||
IO: io,
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
|
||||
return verifyAssetRun(config)
|
||||
},
|
||||
}
|
||||
cmdutil.AddFormatFlags(cmd, &opts.Exporter)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func verifyAssetRun(config *VerifyAssetConfig) error {
|
||||
ctx := context.Background()
|
||||
opts := config.Opts
|
||||
baseRepo := opts.BaseRepo
|
||||
tagName := opts.TagName
|
||||
|
||||
if tagName == "" {
|
||||
release, err := shared.FetchLatestRelease(ctx, config.HttpClient, baseRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tagName = release.TagName
|
||||
}
|
||||
|
||||
fileName := getFileName(opts.AssetFilePath)
|
||||
|
||||
// Calculate the digest of the file
|
||||
fileDigest, err := artifact.NewDigestedArtifact(nil, opts.AssetFilePath, "sha256")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref, err := shared.FetchRefSHA(ctx, config.HttpClient, baseRepo, tagName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
releaseRefDigest := artifact.NewDigestedArtifactForRelease(ref, "sha1")
|
||||
|
||||
// Find attestations for the release tag SHA
|
||||
attestations, err := config.AttClient.GetByDigest(api.FetchParams{
|
||||
Digest: releaseRefDigest.DigestWithAlg(),
|
||||
PredicateType: shared.ReleasePredicateType,
|
||||
Owner: baseRepo.RepoOwner(),
|
||||
Repo: baseRepo.RepoOwner() + "/" + baseRepo.RepoName(),
|
||||
// TODO: Allow this value to be set via a flag.
|
||||
// The limit is set to 100 to ensure we fetch all attestations for a given SHA.
|
||||
// While multiple attestations can exist for a single SHA,
|
||||
// only one attestation is associated with each release tag.
|
||||
Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("no attestations found for tag %s (%s)", tagName, releaseRefDigest.DigestWithAlg())
|
||||
}
|
||||
|
||||
// Filter attestations by tag name
|
||||
filteredAttestations, err := shared.FilterAttestationsByTag(attestations, opts.TagName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing attestations for tag %s: %w", tagName, err)
|
||||
}
|
||||
|
||||
if len(filteredAttestations) == 0 {
|
||||
return fmt.Errorf("no attestations found for release %s in %s/%s", tagName, baseRepo.RepoOwner(), baseRepo.RepoName())
|
||||
}
|
||||
|
||||
// Filter attestations by subject digest
|
||||
filteredAttestations, err = shared.FilterAttestationsByFileDigest(filteredAttestations, fileDigest.Digest())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing attestations for digest %s: %w", fileDigest.DigestWithAlg(), err)
|
||||
}
|
||||
|
||||
if len(filteredAttestations) == 0 {
|
||||
return fmt.Errorf("attestation for %s does not contain subject %s", tagName, fileDigest.DigestWithAlg())
|
||||
}
|
||||
|
||||
// Verify attestation
|
||||
verified, err := config.AttVerifier.VerifyAttestation(releaseRefDigest, filteredAttestations[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify attestation for tag %s: %w", tagName, err)
|
||||
}
|
||||
|
||||
// If an exporter is provided with the --json flag, write the results to the terminal in JSON format
|
||||
if opts.Exporter != nil {
|
||||
return opts.Exporter.Write(config.IO, verified)
|
||||
}
|
||||
|
||||
io := config.IO
|
||||
cs := io.ColorScheme()
|
||||
fmt.Fprintf(io.Out, "Calculated digest for %s: %s\n", fileName, fileDigest.DigestWithAlg())
|
||||
fmt.Fprintf(io.Out, "Resolved tag %s to %s\n", opts.TagName, releaseRefDigest.DigestWithAlg())
|
||||
fmt.Fprint(io.Out, "Loaded attestation from GitHub API\n\n")
|
||||
fmt.Fprintf(io.Out, cs.Green("%s Verification succeeded! %s is present in release %s\n"), cs.SuccessIcon(), fileName, opts.TagName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFileName(filePath string) string {
|
||||
// Get the file name from the file path
|
||||
_, fileName := filepath.Split(filePath)
|
||||
return fileName
|
||||
}
|
||||
267
pkg/cmd/release/verify-asset/verify_asset_test.go
Normal file
267
pkg/cmd/release/verify-asset/verify_asset_test.go
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
package verifyasset
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/test"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/test/data"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
|
||||
"github.com/cli/cli/v2/pkg/cmd/release/shared"
|
||||
"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"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
)
|
||||
|
||||
func TestNewCmdVerifyAsset_Args(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantTag string
|
||||
wantFile string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid args",
|
||||
args: []string{"v1.2.3", "../../attestation/test/data/github_release_artifact.zip"},
|
||||
wantTag: "v1.2.3",
|
||||
wantFile: test.NormalizeRelativePath("../../attestation/test/data/github_release_artifact.zip"),
|
||||
},
|
||||
{
|
||||
name: "valid flag with no tag",
|
||||
|
||||
args: []string{"../../attestation/test/data/github_release_artifact.zip"},
|
||||
wantFile: test.NormalizeRelativePath("../../attestation/test/data/github_release_artifact.zip"),
|
||||
},
|
||||
{
|
||||
name: "no args",
|
||||
args: []string{},
|
||||
wantErr: "you must specify an asset filepath",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testIO, _, _, _ := iostreams.Test()
|
||||
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: testIO,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return nil, nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("owner/repo")
|
||||
},
|
||||
}
|
||||
|
||||
var cfg *VerifyAssetConfig
|
||||
cmd := NewCmdVerifyAsset(f, func(c *VerifyAssetConfig) error {
|
||||
cfg = c
|
||||
return nil
|
||||
})
|
||||
cmd.SetArgs(tt.args)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err := cmd.ExecuteC()
|
||||
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantTag, cfg.Opts.TagName)
|
||||
assert.Equal(t, tt.wantFile, cfg.Opts.AssetFilePath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_verifyAssetRun_Success(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
tagName := "v6"
|
||||
|
||||
fakeHTTP := &httpmock.Registry{}
|
||||
defer fakeHTTP.Verify(t)
|
||||
|
||||
fakeSHA := "1234567890abcdef1234567890abcdef12345678"
|
||||
shared.StubFetchRefSHA(t, fakeHTTP, "owner", "repo", tagName, fakeSHA)
|
||||
|
||||
baseRepo, err := ghrepo.FromFullName("owner/repo")
|
||||
require.NoError(t, err)
|
||||
|
||||
result := &verification.AttestationProcessingResult{
|
||||
Attestation: &api.Attestation{
|
||||
Bundle: data.GitHubReleaseBundle(t),
|
||||
BundleURL: "https://example.com",
|
||||
},
|
||||
VerificationResult: nil,
|
||||
}
|
||||
|
||||
releaseAssetPath := test.NormalizeRelativePath("../../attestation/test/data/github_release_artifact.zip")
|
||||
|
||||
cfg := &VerifyAssetConfig{
|
||||
Opts: &VerifyAssetOptions{
|
||||
AssetFilePath: releaseAssetPath,
|
||||
TagName: tagName,
|
||||
BaseRepo: baseRepo,
|
||||
Exporter: nil,
|
||||
},
|
||||
IO: ios,
|
||||
HttpClient: &http.Client{Transport: fakeHTTP},
|
||||
AttClient: api.NewTestClient(),
|
||||
AttVerifier: shared.NewMockVerifier(result),
|
||||
}
|
||||
|
||||
err = verifyAssetRun(cfg)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_verifyAssetRun_FailedNoAttestations(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
tagName := "v1"
|
||||
|
||||
fakeHTTP := &httpmock.Registry{}
|
||||
defer fakeHTTP.Verify(t)
|
||||
fakeSHA := "1234567890abcdef1234567890abcdef12345678"
|
||||
shared.StubFetchRefSHA(t, fakeHTTP, "owner", "repo", tagName, fakeSHA)
|
||||
|
||||
baseRepo, err := ghrepo.FromFullName("owner/repo")
|
||||
require.NoError(t, err)
|
||||
|
||||
releaseAssetPath := test.NormalizeRelativePath("../../attestation/test/data/github_release_artifact.zip")
|
||||
|
||||
cfg := &VerifyAssetConfig{
|
||||
Opts: &VerifyAssetOptions{
|
||||
AssetFilePath: releaseAssetPath,
|
||||
TagName: tagName,
|
||||
BaseRepo: baseRepo,
|
||||
Exporter: nil,
|
||||
},
|
||||
IO: ios,
|
||||
HttpClient: &http.Client{Transport: fakeHTTP},
|
||||
AttClient: api.NewFailTestClient(),
|
||||
AttVerifier: nil,
|
||||
}
|
||||
|
||||
err = verifyAssetRun(cfg)
|
||||
require.ErrorContains(t, err, "no attestations found for tag v1")
|
||||
}
|
||||
|
||||
func Test_verifyAssetRun_FailedTagNotInAttestation(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
// Tag name does not match the one present in the attestation which
|
||||
// will be returned by the mock client. Simulates a scenario where
|
||||
// multiple releases may point to the same commit SHA, but not all
|
||||
// of them are attested.
|
||||
tagName := "v1.2.3"
|
||||
|
||||
fakeHTTP := &httpmock.Registry{}
|
||||
defer fakeHTTP.Verify(t)
|
||||
fakeSHA := "1234567890abcdef1234567890abcdef12345678"
|
||||
shared.StubFetchRefSHA(t, fakeHTTP, "owner", "repo", tagName, fakeSHA)
|
||||
|
||||
baseRepo, err := ghrepo.FromFullName("owner/repo")
|
||||
require.NoError(t, err)
|
||||
|
||||
releaseAssetPath := test.NormalizeRelativePath("../../attestation/test/data/github_release_artifact.zip")
|
||||
|
||||
cfg := &VerifyAssetConfig{
|
||||
Opts: &VerifyAssetOptions{
|
||||
AssetFilePath: releaseAssetPath,
|
||||
TagName: tagName,
|
||||
BaseRepo: baseRepo,
|
||||
Exporter: nil,
|
||||
},
|
||||
IO: ios,
|
||||
HttpClient: &http.Client{Transport: fakeHTTP},
|
||||
AttClient: api.NewTestClient(),
|
||||
AttVerifier: nil,
|
||||
}
|
||||
|
||||
err = verifyAssetRun(cfg)
|
||||
require.ErrorContains(t, err, "no attestations found for release v1.2.3")
|
||||
}
|
||||
|
||||
func Test_verifyAssetRun_FailedInvalidAsset(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
tagName := "v6"
|
||||
|
||||
fakeHTTP := &httpmock.Registry{}
|
||||
defer fakeHTTP.Verify(t)
|
||||
fakeSHA := "1234567890abcdef1234567890abcdef12345678"
|
||||
shared.StubFetchRefSHA(t, fakeHTTP, "owner", "repo", tagName, fakeSHA)
|
||||
|
||||
baseRepo, err := ghrepo.FromFullName("owner/repo")
|
||||
require.NoError(t, err)
|
||||
|
||||
releaseAssetPath := test.NormalizeRelativePath("../../attestation/test/data/github_release_artifact_invalid.zip")
|
||||
|
||||
cfg := &VerifyAssetConfig{
|
||||
Opts: &VerifyAssetOptions{
|
||||
AssetFilePath: releaseAssetPath,
|
||||
TagName: tagName,
|
||||
BaseRepo: baseRepo,
|
||||
Exporter: nil,
|
||||
},
|
||||
IO: ios,
|
||||
HttpClient: &http.Client{Transport: fakeHTTP},
|
||||
AttClient: api.NewTestClient(),
|
||||
AttVerifier: nil,
|
||||
}
|
||||
|
||||
err = verifyAssetRun(cfg)
|
||||
require.ErrorContains(t, err, "attestation for v6 does not contain subject")
|
||||
}
|
||||
|
||||
func Test_verifyAssetRun_NoSuchAsset(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
tagName := "v6"
|
||||
|
||||
fakeHTTP := &httpmock.Registry{}
|
||||
fakeSHA := "1234567890abcdef1234567890abcdef12345678"
|
||||
shared.StubFetchRefSHA(t, fakeHTTP, "owner", "repo", tagName, fakeSHA)
|
||||
|
||||
baseRepo, err := ghrepo.FromFullName("owner/repo")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &VerifyAssetConfig{
|
||||
Opts: &VerifyAssetOptions{
|
||||
AssetFilePath: "artifact.zip",
|
||||
TagName: tagName,
|
||||
BaseRepo: baseRepo,
|
||||
Exporter: nil,
|
||||
},
|
||||
IO: ios,
|
||||
HttpClient: &http.Client{Transport: fakeHTTP},
|
||||
AttClient: api.NewTestClient(),
|
||||
AttVerifier: nil,
|
||||
}
|
||||
|
||||
err = verifyAssetRun(cfg)
|
||||
require.ErrorContains(t, err, "failed to open local artifact")
|
||||
}
|
||||
|
||||
func Test_getFileName(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"foo/bar/baz.txt", "baz.txt"},
|
||||
{"baz.txt", "baz.txt"},
|
||||
{"/tmp/foo.tar.gz", "foo.tar.gz"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got := getFileName(tt.input)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
233
pkg/cmd/release/verify/verify.go
Normal file
233
pkg/cmd/release/verify/verify.go
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
v1 "github.com/in-toto/attestation/go/v1"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/artifact"
|
||||
att_io "github.com/cli/cli/v2/pkg/cmd/attestation/io"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
|
||||
"github.com/cli/cli/v2/pkg/cmd/release/shared"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type VerifyOptions struct {
|
||||
TagName string
|
||||
BaseRepo ghrepo.Interface
|
||||
Exporter cmdutil.Exporter
|
||||
}
|
||||
|
||||
type VerifyConfig struct {
|
||||
HttpClient *http.Client
|
||||
IO *iostreams.IOStreams
|
||||
Opts *VerifyOptions
|
||||
AttClient api.Client
|
||||
AttVerifier shared.Verifier
|
||||
}
|
||||
|
||||
func NewCmdVerify(f *cmdutil.Factory, runF func(config *VerifyConfig) error) *cobra.Command {
|
||||
opts := &VerifyOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "verify [<tag>]",
|
||||
Short: "Verify the attestation for a GitHub Release.",
|
||||
Hidden: true,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Long: heredoc.Doc(`
|
||||
Verify that a GitHub Release is accompanied by a valid cryptographically signed attestation.
|
||||
|
||||
## Understanding Verification
|
||||
|
||||
An attestation is a claim made by GitHub regarding a release and its assets.
|
||||
|
||||
## What This Command Does
|
||||
|
||||
This command checks that the specified release (or the latest release, if no tag is given) has a valid attestation.
|
||||
It fetches the attestation for the release and prints out metadata about all assets referenced in the attestation, including their digests.
|
||||
`),
|
||||
Example: heredoc.Doc(`
|
||||
# Verify the latest release
|
||||
gh release verify
|
||||
|
||||
# Verify a specific release by tag
|
||||
gh release verify v1.2.3
|
||||
|
||||
# Verify a specific release by tag and output the attestation in JSON format
|
||||
gh release verify v1.2.3 --format json
|
||||
`),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
opts.TagName = args[0]
|
||||
}
|
||||
|
||||
baseRepo, err := f.BaseRepo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine base repository: %w", err)
|
||||
}
|
||||
|
||||
opts.BaseRepo = baseRepo
|
||||
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io := f.IOStreams
|
||||
attClient := api.NewLiveClient(httpClient, baseRepo.RepoHost(), att_io.NewHandler(io))
|
||||
|
||||
attVerifier := &shared.AttestationVerifier{
|
||||
AttClient: attClient,
|
||||
HttpClient: httpClient,
|
||||
IO: io,
|
||||
}
|
||||
|
||||
config := &VerifyConfig{
|
||||
Opts: opts,
|
||||
HttpClient: httpClient,
|
||||
AttClient: attClient,
|
||||
AttVerifier: attVerifier,
|
||||
IO: io,
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
return verifyRun(config)
|
||||
},
|
||||
}
|
||||
cmdutil.AddFormatFlags(cmd, &opts.Exporter)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func verifyRun(config *VerifyConfig) error {
|
||||
ctx := context.Background()
|
||||
opts := config.Opts
|
||||
baseRepo := opts.BaseRepo
|
||||
tagName := opts.TagName
|
||||
|
||||
if tagName == "" {
|
||||
release, err := shared.FetchLatestRelease(ctx, config.HttpClient, baseRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tagName = release.TagName
|
||||
}
|
||||
|
||||
// Retrieve the ref for the release tag
|
||||
ref, err := shared.FetchRefSHA(ctx, config.HttpClient, baseRepo, tagName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
releaseRefDigest := artifact.NewDigestedArtifactForRelease(ref, "sha1")
|
||||
|
||||
// Find all the attestations for the release tag SHA
|
||||
attestations, err := config.AttClient.GetByDigest(api.FetchParams{
|
||||
Digest: releaseRefDigest.DigestWithAlg(),
|
||||
PredicateType: shared.ReleasePredicateType,
|
||||
Owner: baseRepo.RepoOwner(),
|
||||
Repo: baseRepo.RepoOwner() + "/" + baseRepo.RepoName(),
|
||||
// TODO: Allow this value to be set via a flag.
|
||||
// The limit is set to 100 to ensure we fetch all attestations for a given SHA.
|
||||
// While multiple attestations can exist for a single SHA,
|
||||
// only one attestation is associated with each release tag.
|
||||
Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("no attestations for tag %s (%s)", tagName, releaseRefDigest.DigestWithAlg())
|
||||
}
|
||||
|
||||
// Filter attestations by tag name
|
||||
filteredAttestations, err := shared.FilterAttestationsByTag(attestations, tagName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing attestations for tag %s: %w", tagName, err)
|
||||
}
|
||||
|
||||
if len(filteredAttestations) == 0 {
|
||||
return fmt.Errorf("no attestations found for release %s in %s", tagName, baseRepo.RepoName())
|
||||
}
|
||||
|
||||
if len(filteredAttestations) > 1 {
|
||||
return fmt.Errorf("duplicate attestations found for release %s in %s", tagName, baseRepo.RepoName())
|
||||
}
|
||||
|
||||
// Verify attestation
|
||||
verified, err := config.AttVerifier.VerifyAttestation(releaseRefDigest, filteredAttestations[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify attestations for tag %s: %w", tagName, err)
|
||||
}
|
||||
|
||||
// If an exporter is provided with the --json flag, write the results to the terminal in JSON format
|
||||
if opts.Exporter != nil {
|
||||
return opts.Exporter.Write(config.IO, verified)
|
||||
}
|
||||
|
||||
io := config.IO
|
||||
cs := io.ColorScheme()
|
||||
fmt.Fprintf(io.Out, "Resolved tag %s to %s\n", tagName, releaseRefDigest.DigestWithAlg())
|
||||
fmt.Fprint(io.Out, "Loaded attestation from GitHub API\n")
|
||||
fmt.Fprintf(io.Out, cs.Green("%s Release %s verified!\n"), cs.SuccessIcon(), tagName)
|
||||
fmt.Fprintln(io.Out)
|
||||
|
||||
if err := printVerifiedSubjects(io, verified); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printVerifiedSubjects(io *iostreams.IOStreams, att *verification.AttestationProcessingResult) error {
|
||||
cs := io.ColorScheme()
|
||||
w := io.Out
|
||||
|
||||
statement := att.Attestation.Bundle.GetDsseEnvelope().Payload
|
||||
var statementData v1.Statement
|
||||
|
||||
err := protojson.Unmarshal([]byte(statement), &statementData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If there aren't at least two subjects, there are no assets to display
|
||||
if len(statementData.Subject) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, cs.Bold("Assets"))
|
||||
table := tableprinter.New(io, tableprinter.WithHeader("Name", "Digest"))
|
||||
|
||||
for _, s := range statementData.Subject {
|
||||
name := s.Name
|
||||
digest := s.Digest
|
||||
|
||||
if name != "" {
|
||||
digestStr := ""
|
||||
for key, value := range digest {
|
||||
digestStr = key + ":" + value
|
||||
}
|
||||
|
||||
table.AddField(name)
|
||||
table.AddField(digestStr)
|
||||
table.EndRow()
|
||||
}
|
||||
}
|
||||
err = table.Render()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
|
||||
return nil
|
||||
}
|
||||
165
pkg/cmd/release/verify/verify_test.go
Normal file
165
pkg/cmd/release/verify/verify_test.go
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
package verify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/api"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/test/data"
|
||||
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
|
||||
"github.com/cli/cli/v2/pkg/cmd/release/shared"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestNewCmdVerify_Args(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantTag string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid tag arg",
|
||||
args: []string{"v1.2.3"},
|
||||
wantTag: "v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "no tag arg",
|
||||
args: []string{},
|
||||
wantTag: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testIO, _, _, _ := iostreams.Test()
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: testIO,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
return nil, nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
return ghrepo.FromFullName("owner/repo")
|
||||
},
|
||||
}
|
||||
|
||||
var cfg *VerifyConfig
|
||||
cmd := NewCmdVerify(f, func(c *VerifyConfig) error {
|
||||
cfg = c
|
||||
return nil
|
||||
})
|
||||
cmd.SetArgs(tt.args)
|
||||
cmd.SetIn(&bytes.Buffer{})
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
_, err := cmd.ExecuteC()
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantTag, cfg.Opts.TagName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_verifyRun_Success(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
tagName := "v6"
|
||||
|
||||
fakeHTTP := &httpmock.Registry{}
|
||||
defer fakeHTTP.Verify(t)
|
||||
|
||||
fakeSHA := "1234567890abcdef1234567890abcdef12345678"
|
||||
shared.StubFetchRefSHA(t, fakeHTTP, "owner", "repo", tagName, fakeSHA)
|
||||
|
||||
baseRepo, err := ghrepo.FromFullName("owner/repo")
|
||||
require.NoError(t, err)
|
||||
|
||||
result := &verification.AttestationProcessingResult{
|
||||
Attestation: &api.Attestation{
|
||||
Bundle: data.GitHubReleaseBundle(t),
|
||||
BundleURL: "https://example.com",
|
||||
},
|
||||
VerificationResult: nil,
|
||||
}
|
||||
|
||||
cfg := &VerifyConfig{
|
||||
Opts: &VerifyOptions{
|
||||
TagName: tagName,
|
||||
BaseRepo: baseRepo,
|
||||
Exporter: nil,
|
||||
},
|
||||
IO: ios,
|
||||
HttpClient: &http.Client{Transport: fakeHTTP},
|
||||
AttClient: api.NewTestClient(),
|
||||
AttVerifier: shared.NewMockVerifier(result),
|
||||
}
|
||||
|
||||
err = verifyRun(cfg)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_verifyRun_FailedNoAttestations(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
tagName := "v1"
|
||||
|
||||
fakeHTTP := &httpmock.Registry{}
|
||||
defer fakeHTTP.Verify(t)
|
||||
fakeSHA := "1234567890abcdef1234567890abcdef12345678"
|
||||
shared.StubFetchRefSHA(t, fakeHTTP, "owner", "repo", tagName, fakeSHA)
|
||||
|
||||
baseRepo, err := ghrepo.FromFullName("owner/repo")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &VerifyConfig{
|
||||
Opts: &VerifyOptions{
|
||||
TagName: tagName,
|
||||
BaseRepo: baseRepo,
|
||||
Exporter: nil,
|
||||
},
|
||||
IO: ios,
|
||||
HttpClient: &http.Client{Transport: fakeHTTP},
|
||||
AttClient: api.NewFailTestClient(),
|
||||
AttVerifier: nil,
|
||||
}
|
||||
|
||||
err = verifyRun(cfg)
|
||||
require.ErrorContains(t, err, "no attestations for tag v1")
|
||||
}
|
||||
|
||||
func Test_verifyRun_FailedTagNotInAttestation(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
|
||||
// Tag name does not match the one present in the attestation which
|
||||
// will be returned by the mock client. Simulates a scenario where
|
||||
// multiple releases may point to the same commit SHA, but not all
|
||||
// of them are attested.
|
||||
tagName := "v1.2.3"
|
||||
|
||||
fakeHTTP := &httpmock.Registry{}
|
||||
defer fakeHTTP.Verify(t)
|
||||
fakeSHA := "1234567890abcdef1234567890abcdef12345678"
|
||||
shared.StubFetchRefSHA(t, fakeHTTP, "owner", "repo", tagName, fakeSHA)
|
||||
|
||||
baseRepo, err := ghrepo.FromFullName("owner/repo")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &VerifyConfig{
|
||||
Opts: &VerifyOptions{
|
||||
TagName: tagName,
|
||||
BaseRepo: baseRepo,
|
||||
Exporter: nil,
|
||||
},
|
||||
IO: ios,
|
||||
HttpClient: &http.Client{Transport: fakeHTTP},
|
||||
AttClient: api.NewTestClient(),
|
||||
AttVerifier: nil,
|
||||
}
|
||||
|
||||
err = verifyRun(cfg)
|
||||
require.ErrorContains(t, err, "no attestations found for release v1.2.3")
|
||||
}
|
||||
|
|
@ -330,7 +330,7 @@ func runRun(opts *RunOptions) error {
|
|||
fmt.Fprintln(out)
|
||||
|
||||
fmt.Fprintf(out, "To see runs for this workflow, try: %s\n",
|
||||
cs.Boldf("gh run list --workflow=%s", workflow.Base()))
|
||||
cs.Boldf("gh run list --workflow=%q", workflow.Base()))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -447,7 +447,7 @@ jobs:
|
|||
"ref": "trunk",
|
||||
},
|
||||
httpStubs: stubs,
|
||||
wantOut: "✓ Created workflow_dispatch event for workflow.yml at trunk\n\nTo see runs for this workflow, try: gh run list --workflow=workflow.yml\n",
|
||||
wantOut: "✓ Created workflow_dispatch event for workflow.yml at trunk\n\nTo see runs for this workflow, try: gh run list --workflow=\"workflow.yml\"\n",
|
||||
},
|
||||
{
|
||||
name: "nontty good JSON",
|
||||
|
|
@ -494,7 +494,7 @@ jobs:
|
|||
"ref": "good-branch",
|
||||
},
|
||||
httpStubs: stubs,
|
||||
wantOut: "✓ Created workflow_dispatch event for workflow.yml at good-branch\n\nTo see runs for this workflow, try: gh run list --workflow=workflow.yml\n",
|
||||
wantOut: "✓ Created workflow_dispatch event for workflow.yml at good-branch\n\nTo see runs for this workflow, try: gh run list --workflow=\"workflow.yml\"\n",
|
||||
},
|
||||
{
|
||||
// TODO this test is somewhat silly; it's more of a placeholder in case I decide to handle the API error more elegantly
|
||||
|
|
@ -634,7 +634,7 @@ jobs:
|
|||
"inputs": map[string]interface{}{},
|
||||
"ref": "trunk",
|
||||
},
|
||||
wantOut: "✓ Created workflow_dispatch event for minimal.yml at trunk\n\nTo see runs for this workflow, try: gh run list --workflow=minimal.yml\n",
|
||||
wantOut: "✓ Created workflow_dispatch event for minimal.yml at trunk\n\nTo see runs for this workflow, try: gh run list --workflow=\"minimal.yml\"\n",
|
||||
},
|
||||
{
|
||||
name: "prompt",
|
||||
|
|
@ -682,7 +682,7 @@ jobs:
|
|||
},
|
||||
"ref": "trunk",
|
||||
},
|
||||
wantOut: "✓ Created workflow_dispatch event for workflow.yml at trunk\n\nTo see runs for this workflow, try: gh run list --workflow=workflow.yml\n",
|
||||
wantOut: "✓ Created workflow_dispatch event for workflow.yml at trunk\n\nTo see runs for this workflow, try: gh run list --workflow=\"workflow.yml\"\n",
|
||||
},
|
||||
{
|
||||
name: "prompt, workflow choice input",
|
||||
|
|
@ -731,7 +731,7 @@ jobs:
|
|||
},
|
||||
"ref": "trunk",
|
||||
},
|
||||
wantOut: "✓ Created workflow_dispatch event for workflow.yml at trunk\n\nTo see runs for this workflow, try: gh run list --workflow=workflow.yml\n",
|
||||
wantOut: "✓ Created workflow_dispatch event for workflow.yml at trunk\n\nTo see runs for this workflow, try: gh run list --workflow=\"workflow.yml\"\n",
|
||||
},
|
||||
{
|
||||
name: "prompt, workflow choice missing input",
|
||||
|
|
|
|||
|
|
@ -210,17 +210,10 @@ func createTestDir(t *testing.T) (cleanupFn func()) {
|
|||
rootDir := t.TempDir()
|
||||
|
||||
// Move workspace to temporary directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.Chdir(rootDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Chdir(rootDir)
|
||||
|
||||
// Make subdirectories
|
||||
err = os.Mkdir(filepath.Join(rootDir, "subDir1"), 0755)
|
||||
err := os.Mkdir(filepath.Join(rootDir, "subDir1"), 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -253,10 +246,6 @@ func createTestDir(t *testing.T) (cleanupFn func()) {
|
|||
|
||||
cleanupFn = func() {
|
||||
os.RemoveAll(rootDir)
|
||||
err = os.Chdir(cwd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return cleanupFn
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue