Merge branch 'trunk' into andyfeller/9422-license-compliance

This commit is contained in:
Andy Feller 2025-06-20 16:23:00 -04:00
commit 98ea250ede
32 changed files with 1265 additions and 146 deletions

4
.github/CODEOWNERS vendored
View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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 }}

View file

@ -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")"

View file

@ -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

View file

@ -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

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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)
}

View file

@ -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 {

View file

@ -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)

View 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)
}

View file

@ -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
}

View 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="
}
]
}
}

View file

@ -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

View file

@ -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 {

View file

@ -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) {

View file

@ -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)

View file

@ -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

View 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
}

View file

@ -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)),
)
}

View 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
}

View 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)
})
}
}

View 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
}

View 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")
}

View file

@ -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

View file

@ -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",

View file

@ -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
}