From ff9b6bb883d993e4a33cc583ccfc109694962b14 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 7 Nov 2024 14:39:11 -0700 Subject: [PATCH 01/52] refactor fetch attestations funcs Signed-off-by: Meredith Lancaster --- .../attestation/verification/attestation.go | 36 +++---------- pkg/cmd/attestation/verify/attestation.go | 51 +++++++++++++++++++ pkg/cmd/attestation/verify/verify.go | 33 ++---------- 3 files changed, 63 insertions(+), 57 deletions(-) create mode 100644 pkg/cmd/attestation/verify/attestation.go diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 0ea91c2f7..dd885a1c1 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -9,8 +9,8 @@ import ( "path/filepath" "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" - "github.com/google/go-containerregistry/pkg/name" protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" "github.com/sigstore/sigstore-go/pkg/bundle" ) @@ -21,31 +21,11 @@ var ErrUnrecognisedBundleExtension = errors.New("bundle file extension not suppo var ErrEmptyBundleFile = errors.New("provided bundle file is empty") type FetchAttestationsConfig struct { - APIClient api.Client - BundlePath string - Digest string - Limit int - Owner string - Repo string - OCIClient oci.Client - UseBundleFromRegistry bool - NameRef name.Reference -} - -func (c *FetchAttestationsConfig) IsBundleProvided() bool { - return c.BundlePath != "" -} - -func GetAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) { - if c.IsBundleProvided() { - return GetLocalAttestations(c.BundlePath) - } - - if c.UseBundleFromRegistry { - return GetOCIAttestations(c) - } - - return GetRemoteAttestations(c) + APIClient api.Client + Digest string + Limit int + Owner string + Repo string } // GetLocalAttestations returns a slice of attestations read from a local bundle file. @@ -138,8 +118,8 @@ func GetRemoteAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error return nil, fmt.Errorf("owner or repo must be provided") } -func GetOCIAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) { - attestations, err := c.OCIClient.GetAttestations(c.NameRef, c.Digest) +func GetOCIAttestations(client oci.Client, artifact artifact.DigestedArtifact) ([]*api.Attestation, error) { + attestations, err := client.GetAttestations(artifact.NameRef(), artifact.Digest()) if err != nil { return nil, fmt.Errorf("failed to fetch OCI attestations: %w", err) } diff --git a/pkg/cmd/attestation/verify/attestation.go b/pkg/cmd/attestation/verify/attestation.go new file mode 100644 index 000000000..a588d9764 --- /dev/null +++ b/pkg/cmd/attestation/verify/attestation.go @@ -0,0 +1,51 @@ +package verify + +import ( + "fmt" + + "github.com/cli/cli/v2/internal/text" + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" +) + +func getAttestations(o *Options, a artifact.DigestedArtifact) ([]*api.Attestation, string, error) { + if o.BundlePath != "" { + attestations, err := verification.GetLocalAttestations(o.BundlePath) + if err != nil { + msg := fmt.Sprintf("✗ Loading attestations from %s failed\n", a.URL) + return nil, msg, err + } + pluralAttestation := text.Pluralize(len(attestations), "attestation") + msg := fmt.Sprintf("Loaded %s from %s\n", pluralAttestation, o.BundlePath) + return attestations, msg, nil + } + + if o.UseBundleFromRegistry { + attestations, err := verification.GetOCIAttestations(o.OCIClient, a) + if err != nil { + msg := "✗ Loading attestations from OCI registry failed\n" + return nil, msg, err + } + pluralAttestation := text.Pluralize(len(attestations), "attestation") + msg := fmt.Sprintf("Loaded %s from %s\n", pluralAttestation, o.ArtifactPath) + return attestations, msg, nil + } + + c := verification.FetchAttestationsConfig{ + APIClient: o.APIClient, + Digest: a.DigestWithAlg(), + Limit: o.Limit, + Owner: o.Owner, + Repo: o.Repo, + } + + attestations, err := verification.GetRemoteAttestations(c) + if err != nil { + msg := "✗ Loading attestations from GitHub API failed\n" + return nil, msg, err + } + pluralAttestation := text.Pluralize(len(attestations), "attestation") + msg := fmt.Sprintf("Loaded %s from GitHub API\n", pluralAttestation) + return attestations, msg, nil +} diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 47b52bb30..e5dd12f59 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -6,7 +6,6 @@ import ( "regexp" "github.com/cli/cli/v2/internal/ghinstance" - "github.com/cli/cli/v2/internal/text" "github.com/cli/cli/v2/pkg/cmd/attestation/api" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" @@ -222,42 +221,18 @@ func runVerify(opts *Options) error { opts.Logger.Printf("Loaded digest %s for %s\n", artifact.DigestWithAlg(), artifact.URL) - c := verification.FetchAttestationsConfig{ - APIClient: opts.APIClient, - BundlePath: opts.BundlePath, - Digest: artifact.DigestWithAlg(), - Limit: opts.Limit, - Owner: opts.Owner, - Repo: opts.Repo, - OCIClient: opts.OCIClient, - UseBundleFromRegistry: opts.UseBundleFromRegistry, - NameRef: artifact.NameRef(), - } - attestations, err := verification.GetAttestations(c) + attestations, logMsg, err := getAttestations(opts, *artifact) if err != nil { if ok := errors.Is(err, api.ErrNoAttestations{}); ok { opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ No attestations found for subject %s\n"), artifact.DigestWithAlg()) return err } - if c.IsBundleProvided() { - opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ Loading attestations from %s failed\n"), artifact.URL) - } else if c.UseBundleFromRegistry { - opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Loading attestations from OCI registry failed")) - } else { - opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Loading attestations from GitHub API failed")) - } + opts.Logger.Printf(opts.Logger.ColorScheme.Red(logMsg)) return err } - - pluralAttestation := text.Pluralize(len(attestations), "attestation") - if c.IsBundleProvided() { - opts.Logger.Printf("Loaded %s from %s\n", pluralAttestation, opts.BundlePath) - } else if c.UseBundleFromRegistry { - opts.Logger.Printf("Loaded %s from %s\n", pluralAttestation, opts.ArtifactPath) - } else { - opts.Logger.Printf("Loaded %s from GitHub API\n", pluralAttestation) - } + // Print the message signifying success fetching attestations + opts.Logger.Printf(logMsg) // Apply predicate type filter to returned attestations filteredAttestations := verification.FilterAttestations(ec.PredicateType, attestations) From 8ab5f247aff3fe65a1864d2fd9d6aba704b7769e Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 7 Nov 2024 14:47:53 -0700 Subject: [PATCH 02/52] rename type Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/download/download.go | 2 +- pkg/cmd/attestation/verification/attestation.go | 4 ++-- pkg/cmd/attestation/verify/attestation.go | 2 +- pkg/cmd/attestation/verify/verify.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/attestation/download/download.go b/pkg/cmd/attestation/download/download.go index 77c928093..ef5a11799 100644 --- a/pkg/cmd/attestation/download/download.go +++ b/pkg/cmd/attestation/download/download.go @@ -122,7 +122,7 @@ func runDownload(opts *Options) error { opts.Logger.VerbosePrintf("Downloading trusted metadata for artifact %s\n\n", opts.ArtifactPath) - c := verification.FetchAttestationsConfig{ + c := verification.FetchRemoteAttestations{ APIClient: opts.APIClient, Digest: artifact.DigestWithAlg(), Limit: opts.Limit, diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index dd885a1c1..bdee3a014 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -20,7 +20,7 @@ const SLSAPredicateV1 = "https://slsa.dev/provenance/v1" var ErrUnrecognisedBundleExtension = errors.New("bundle file extension not supported, must be json or jsonl") var ErrEmptyBundleFile = errors.New("provided bundle file is empty") -type FetchAttestationsConfig struct { +type FetchRemoteAttestations struct { APIClient api.Client Digest string Limit int @@ -96,7 +96,7 @@ func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) { return attestations, nil } -func GetRemoteAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) { +func GetRemoteAttestations(c FetchRemoteAttestations) ([]*api.Attestation, error) { if c.APIClient == nil { return nil, fmt.Errorf("api client must be provided") } diff --git a/pkg/cmd/attestation/verify/attestation.go b/pkg/cmd/attestation/verify/attestation.go index a588d9764..483d4cf97 100644 --- a/pkg/cmd/attestation/verify/attestation.go +++ b/pkg/cmd/attestation/verify/attestation.go @@ -32,7 +32,7 @@ func getAttestations(o *Options, a artifact.DigestedArtifact) ([]*api.Attestatio return attestations, msg, nil } - c := verification.FetchAttestationsConfig{ + c := verification.FetchRemoteAttestations{ APIClient: o.APIClient, Digest: a.DigestWithAlg(), Limit: o.Limit, diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index e5dd12f59..90149f7ca 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -227,7 +227,7 @@ func runVerify(opts *Options) error { opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ No attestations found for subject %s\n"), artifact.DigestWithAlg()) return err } - + // Print the message signifying failure fetching attestations opts.Logger.Printf(opts.Logger.ColorScheme.Red(logMsg)) return err } From e4cd729a7b9dc6f0394d5ab9a7a0e1af401cdc7a Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 7 Nov 2024 14:59:21 -0700 Subject: [PATCH 03/52] simplify verifyCertExtensions Signed-off-by: Meredith Lancaster --- .../attestation/verification/extensions.go | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/pkg/cmd/attestation/verification/extensions.go b/pkg/cmd/attestation/verification/extensions.go index e302d89c9..540c96284 100644 --- a/pkg/cmd/attestation/verification/extensions.go +++ b/pkg/cmd/attestation/verification/extensions.go @@ -20,7 +20,7 @@ func VerifyCertExtensions(results []*AttestationProcessingResult, ec Enforcement var lastErr error for _, attestation := range results { - err := verifyCertExtensions(*attestation.VerificationResult.Signature.Certificate, ec) + err := verifyCertExtensions(*attestation.VerificationResult.Signature.Certificate, ec.Certificate) if err == nil { // if at least one attestation is verified, we're good as verification // is defined as successful if at least one attestation is verified @@ -34,28 +34,23 @@ func VerifyCertExtensions(results []*AttestationProcessingResult, ec Enforcement return lastErr } -func verifyCertExtensions(verifiedCert certificate.Summary, criteria EnforcementCriteria) error { - sourceRepositoryOwnerURI := verifiedCert.Extensions.SourceRepositoryOwnerURI - if !strings.EqualFold(criteria.Certificate.SourceRepositoryOwnerURI, sourceRepositoryOwnerURI) { - return fmt.Errorf("expected SourceRepositoryOwnerURI to be %s, got %s", criteria.Certificate.SourceRepositoryOwnerURI, sourceRepositoryOwnerURI) +func verifyCertExtensions(verified, expected certificate.Summary) error { + if !strings.EqualFold(expected.SourceRepositoryOwnerURI, verified.SourceRepositoryOwnerURI) { + return fmt.Errorf("expected SourceRepositoryOwnerURI to be %s, got %s", expected.SourceRepositoryOwnerURI, verified.SourceRepositoryOwnerURI) } - // if repo is set, check the SourceRepositoryURI field - if criteria.Certificate.SourceRepositoryURI != "" { - sourceRepositoryURI := verifiedCert.Extensions.SourceRepositoryURI - if !strings.EqualFold(criteria.Certificate.SourceRepositoryURI, sourceRepositoryURI) { - return fmt.Errorf("expected SourceRepositoryURI to be %s, got %s", criteria.Certificate.SourceRepositoryURI, sourceRepositoryURI) - } + // if repo is set, compare the SourceRepositoryURI fields + if expected.SourceRepositoryURI != "" && !strings.EqualFold(expected.SourceRepositoryURI, verified.SourceRepositoryURI) { + return fmt.Errorf("expected SourceRepositoryURI to be %s, got %s", expected.SourceRepositoryURI, verified.SourceRepositoryURI) } - // if issuer is anything other than the default, use the user-provided value; - // otherwise, select the appropriate default based on the tenant - certIssuer := verifiedCert.Extensions.Issuer - if !strings.EqualFold(criteria.Certificate.Issuer, certIssuer) { - if strings.Index(certIssuer, criteria.Certificate.Issuer+"/") == 0 { - return fmt.Errorf("expected Issuer to be %s, got %s -- if you have a custom OIDC issuer policy for your enterprise, use the --cert-oidc-issuer flag with your expected issuer", criteria.Certificate.Issuer, certIssuer) + // compare the OIDC issuers. If not equal, return an error depending + // on if there is a partial match + if !strings.EqualFold(expected.Issuer, verified.Issuer) { + if strings.Index(verified.Issuer, expected.Issuer+"/") == 0 { + return fmt.Errorf("expected Issuer to be %s, got %s -- if you have a custom OIDC issuer policy for your enterprise, use the --cert-oidc-issuer flag with your expected issuer", expected.Issuer, verified.Issuer) } - return fmt.Errorf("expected Issuer to be %s, got %s", criteria.Certificate.Issuer, certIssuer) + return fmt.Errorf("expected Issuer to be %s, got %s", expected.Issuer, verified.Issuer) } return nil From 43e5abbcd8c7691694dc9a64f28edb2d56b6cfb7 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 7 Nov 2024 15:50:46 -0700 Subject: [PATCH 04/52] use logger println method Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/attestation.go | 12 ++++++------ pkg/cmd/attestation/verify/verify.go | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/attestation/verify/attestation.go b/pkg/cmd/attestation/verify/attestation.go index 483d4cf97..c067afe9b 100644 --- a/pkg/cmd/attestation/verify/attestation.go +++ b/pkg/cmd/attestation/verify/attestation.go @@ -13,22 +13,22 @@ func getAttestations(o *Options, a artifact.DigestedArtifact) ([]*api.Attestatio if o.BundlePath != "" { attestations, err := verification.GetLocalAttestations(o.BundlePath) if err != nil { - msg := fmt.Sprintf("✗ Loading attestations from %s failed\n", a.URL) + msg := fmt.Sprintf("✗ Loading attestations from %s failed", a.URL) return nil, msg, err } pluralAttestation := text.Pluralize(len(attestations), "attestation") - msg := fmt.Sprintf("Loaded %s from %s\n", pluralAttestation, o.BundlePath) + msg := fmt.Sprintf("Loaded %s from %s", pluralAttestation, o.BundlePath) return attestations, msg, nil } if o.UseBundleFromRegistry { attestations, err := verification.GetOCIAttestations(o.OCIClient, a) if err != nil { - msg := "✗ Loading attestations from OCI registry failed\n" + msg := "✗ Loading attestations from OCI registry failed" return nil, msg, err } pluralAttestation := text.Pluralize(len(attestations), "attestation") - msg := fmt.Sprintf("Loaded %s from %s\n", pluralAttestation, o.ArtifactPath) + msg := fmt.Sprintf("Loaded %s from %s", pluralAttestation, o.ArtifactPath) return attestations, msg, nil } @@ -42,10 +42,10 @@ func getAttestations(o *Options, a artifact.DigestedArtifact) ([]*api.Attestatio attestations, err := verification.GetRemoteAttestations(c) if err != nil { - msg := "✗ Loading attestations from GitHub API failed\n" + msg := "✗ Loading attestations from GitHub API failed" return nil, msg, err } pluralAttestation := text.Pluralize(len(attestations), "attestation") - msg := fmt.Sprintf("Loaded %s from GitHub API\n", pluralAttestation) + msg := fmt.Sprintf("Loaded %s from GitHub API", pluralAttestation) return attestations, msg, nil } diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 90149f7ca..1d34fdf99 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -228,11 +228,11 @@ func runVerify(opts *Options) error { return err } // Print the message signifying failure fetching attestations - opts.Logger.Printf(opts.Logger.ColorScheme.Red(logMsg)) + opts.Logger.Println(opts.Logger.ColorScheme.Red(logMsg)) return err } // Print the message signifying success fetching attestations - opts.Logger.Printf(logMsg) + opts.Logger.Println(logMsg) // Apply predicate type filter to returned attestations filteredAttestations := verification.FilterAttestations(ec.PredicateType, attestations) From 68f3ef79cadfda52a5096f2651132e9f68b03d5e Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Fri, 18 Oct 2024 21:21:47 -0600 Subject: [PATCH 05/52] Handle missing "workflow" scope in createRelease --- pkg/cmd/release/create/create_test.go | 67 +++++++++++++++++++++++++++ pkg/cmd/release/create/http.go | 45 ++++++++++++++++++ pkg/httpmock/stub.go | 14 ++++++ 3 files changed, 126 insertions(+) diff --git a/pkg/cmd/release/create/create_test.go b/pkg/cmd/release/create/create_test.go index 85c1c3f3f..aded1bea9 100644 --- a/pkg/cmd/release/create/create_test.go +++ b/pkg/cmd/release/create/create_test.go @@ -10,6 +10,7 @@ import ( "path/filepath" "testing" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/gh" @@ -1082,6 +1083,72 @@ func Test_createRun(t *testing.T) { runStubs: defaultRunStubs, wantErr: "cannot generate release notes from tag v1.2.3 as it does not exist locally", }, + { + name: "API returns 404, OAuth token has no workflow scope", + isTTY: false, + opts: CreateOptions{ + TagName: "Does not matter", + }, + runStubs: func(rs *run.CommandStubber) { + rs.Register(contentCmd, 0, "some tag message") + rs.Register(signatureCmd, 0, "") + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL("RepositoryFindRef"), + httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`), + ) + reg.Register( + httpmock.REST("POST", "repos/OWNER/REPO/releases"), + httpmock.StatusScopesResponder(404, `repo,read:org`)) + }, + wantErr: heredoc.Doc(` + HTTP 404: Failed to create release, "workflow" scope may be required + To request it, run gh auth refresh -h github.com -s workflow + `), + }, + { + name: "API returns 404, OAuth token has workflow scope", + isTTY: false, + opts: CreateOptions{ + TagName: "Does not matter", + }, + runStubs: func(rs *run.CommandStubber) { + rs.Register(contentCmd, 0, "some tag message") + rs.Register(signatureCmd, 0, "") + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL("RepositoryFindRef"), + httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`), + ) + reg.Register( + httpmock.REST("POST", "repos/OWNER/REPO/releases"), + httpmock.StatusScopesResponder(404, `repo,read:org,workflow`)) + }, + wantErr: "HTTP 404 (https://api.github.com/repos/OWNER/REPO/releases)", + }, + { + name: "API returns 404, not an OAuth token", + isTTY: false, + opts: CreateOptions{ + TagName: "Does not matter", + }, + runStubs: func(rs *run.CommandStubber) { + rs.Register(contentCmd, 0, "some tag message") + rs.Register(signatureCmd, 0, "") + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL("RepositoryFindRef"), + httpmock.StringResponse(`{"data":{"repository":{"ref": {"id": "tag id"}}}}`), + ) + reg.Register( + httpmock.REST("POST", "repos/OWNER/REPO/releases"), + httpmock.StatusStringResponse(404, `HTTP 404 (https://api.github.com/repos/OWNER/REPO/releases)`)) + }, + wantErr: "HTTP 404 (https://api.github.com/repos/OWNER/REPO/releases)", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/cmd/release/create/http.go b/pkg/cmd/release/create/http.go index 84e95710f..1512b5341 100644 --- a/pkg/cmd/release/create/http.go +++ b/pkg/cmd/release/create/http.go @@ -8,12 +8,16 @@ import ( "io" "net/http" "net/url" + "strings" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmd/release/shared" "github.com/shurcooL/githubv4" + + ghauth "github.com/cli/go-gh/v2/pkg/auth" ) type tag struct { @@ -174,6 +178,26 @@ func createRelease(httpClient *http.Client, repo ghrepo.Interface, params map[st } defer resp.Body.Close() + // This code checks if we received a 404 while attempting to create a release without + // the workflow scope, and if so, returns an error message that explains a possible + // solution to the user. + // + // If the same file (with both the same path and contents) exists + // on another branch in the repo, releases with workflow file changes can be + // created without the workflow scope. Otherwise, the workflow scope is + // required to create the release. + // + // https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes + if resp.StatusCode == http.StatusNotFound && tokenMissingWorkflowScope(resp) { + normalizedHostname := ghauth.NormalizeHostname(resp.Request.URL.Hostname()) + errMissingRequiredWorkflowScope := errors.New(heredoc.Docf(` + HTTP 404: Failed to create release, "workflow" scope may be required + To request it, run gh auth refresh -h %[1]s -s workflow + `, normalizedHostname)) + + return nil, errMissingRequiredWorkflowScope + } + success := resp.StatusCode >= 200 && resp.StatusCode < 300 if !success { return nil, api.HandleHTTPError(resp) @@ -254,3 +278,24 @@ func deleteRelease(httpClient *http.Client, release *shared.Release) error { } return nil } + +// Given an http.Response, check if the token used in +// the request is missing the workflow scope. +func tokenMissingWorkflowScope(resp *http.Response) bool { + scopes := resp.Header.Get("X-Oauth-Scopes") + + // Return false when no scopes are present - no scopes in this header + // means that the user is probably authenticating with a token type other + // than an OAuth token, and we don't know what this token's scopes actually are. + if scopes == "" { + return false + } + + for _, s := range strings.Split(scopes, ",") { + if s == "workflow" { + return false + } + } + + return true +} diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index 787cdcf9d..d1a27d835 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -238,6 +238,20 @@ func ScopesResponder(scopes string) func(*http.Request) (*http.Response, error) } } +// StatusScopesResponder returns a response with the given status code and OAuth scopes. +func StatusScopesResponder(status int, scopes string) func(*http.Request) (*http.Response, error) { + return func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: status, + Request: req, + Header: map[string][]string{ + "X-Oauth-Scopes": {scopes}, + }, + Body: io.NopCloser(bytes.NewBufferString("")), + }, nil + } +} + func httpResponse(status int, req *http.Request, body io.Reader) *http.Response { return httpResponseWithHeader(status, req, body, http.Header{}) } From 9168a5d7d895b9e31a24aa338df01bd8b6ff5272 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Mon, 18 Nov 2024 14:12:44 +0100 Subject: [PATCH 06/52] Added a section on manual verification of the relases. Signed-off-by: Fredrik Skogman --- README.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 064008e3b..23b33881d 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ For more information, see [Linux & BSD installation](./docs/install_linux.md). | ------------------- | --------------------| | `winget install --id GitHub.cli` | `winget upgrade --id GitHub.cli` | -> **Note** +> **Note** > The Windows installer modifies your PATH. When using Windows Terminal, you will need to **open a new window** for the changes to take effect. (Simply opening a new tab will _not_ be sufficient.) #### scoop @@ -125,6 +125,36 @@ GitHub CLI comes pre-installed in all [GitHub-Hosted Runners](https://docs.githu Download packaged binaries from the [releases page][]. +#### Verification of binaries + +Since version 2.50.0 `gh` has been producing [Build Provenance Attestation](https://github.blog/changelog/2024-06-25-artifact-attestations-is-generally-available/) enabling a cryptographically verifiable paper-trail back to the origin GitHub repository, git revision and build instructions used. The build provenance attestations are signed and relies on Public Good [Sigstore](https://www.sigstore.dev/) for PKI. + +There are two common ways to verify a downloaded release, depending if `gh` is aready installed or not. If `gh` is installed, it's trivial to verify a new release: + +```shell +$ % gh at verify -R cli/cli gh_2.62.0_macOS_arm64.zip +Loaded digest sha256:fdb77f31b8a6dd23c3fd858758d692a45f7fc76383e37d475bdcae038df92afc for file://gh_2.62.0_macOS_arm64.zip +Loaded 1 attestation from GitHub API +✓ Verification succeeded! + +sha256:fdb77f31b8a6dd23c3fd858758d692a45f7fc76383e37d475bdcae038df92afc was attested by: +REPO PREDICATE_TYPE WORKFLOW +cli/cli https://slsa.dev/provenance/v1 .github/workflows/deployment.yml@refs/heads/trunk +``` + +If `gh` is not installed, you can use the `cosign` [binary](https://github.com/sigstore/cosign) from Sigstore to verify a release. To perform this, you must first download the [attestation](https://github.com/cli/cli/attestations) for the downloaded release, then you can invoke cosign to verify the authenticity of the downloaded file: + +```shell +$ cosign verify-blob-attestation --bundle cli-cli-attestation-3120304.sigstore.json \ + --new-bundle-format \ + --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ + --certificate-identity-regexp="^https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk$" \ + gh_2.62.0_macOS_arm64.zip +Verified OK +``` + +This will verify that the downloaded zip file is authentic, and it can be extracted to get the `gh` binary. + ### Build from source See here on how to [build GitHub CLI from source][build from source]. From 5098ea407aa88f56fe62d4c0203db6d467c95e5d Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Mon, 18 Nov 2024 14:48:34 +0100 Subject: [PATCH 07/52] Updated markdown syntax for a `note`. Signed-off-by: Fredrik Skogman --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23b33881d..42b383938 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ For more information, see [Linux & BSD installation](./docs/install_linux.md). | ------------------- | --------------------| | `winget install --id GitHub.cli` | `winget upgrade --id GitHub.cli` | -> **Note** +> [!NOTE] > The Windows installer modifies your PATH. When using Windows Terminal, you will need to **open a new window** for the changes to take effect. (Simply opening a new tab will _not_ be sufficient.) #### scoop From 601982faf187d78a6ebb05243355c46c461986be Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Mon, 18 Nov 2024 15:16:47 +0100 Subject: [PATCH 08/52] Updated formatting to be more clear Signed-off-by: Fredrik Skogman --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 42b383938..a505a201b 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,8 @@ Since version 2.50.0 `gh` has been producing [Build Provenance Attestation](http There are two common ways to verify a downloaded release, depending if `gh` is aready installed or not. If `gh` is installed, it's trivial to verify a new release: +- **Option 1: Using `gh` if already installed: + ```shell $ % gh at verify -R cli/cli gh_2.62.0_macOS_arm64.zip Loaded digest sha256:fdb77f31b8a6dd23c3fd858758d692a45f7fc76383e37d475bdcae038df92afc for file://gh_2.62.0_macOS_arm64.zip @@ -142,7 +144,9 @@ REPO PREDICATE_TYPE WORKFLOW cli/cli https://slsa.dev/provenance/v1 .github/workflows/deployment.yml@refs/heads/trunk ``` -If `gh` is not installed, you can use the `cosign` [binary](https://github.com/sigstore/cosign) from Sigstore to verify a release. To perform this, you must first download the [attestation](https://github.com/cli/cli/attestations) for the downloaded release, then you can invoke cosign to verify the authenticity of the downloaded file: +- **Option 2: Using Sigstore [`cosign`](https://github.com/sigstore/cosign): + +To perform this, download the [attestation](https://github.com/cli/cli/attestations) for the downloaded release and use cosign to verify the authenticity of the downloaded release: ```shell $ cosign verify-blob-attestation --bundle cli-cli-attestation-3120304.sigstore.json \ @@ -153,8 +157,6 @@ $ cosign verify-blob-attestation --bundle cli-cli-attestation-3120304.sigstore.j Verified OK ``` -This will verify that the downloaded zip file is authentic, and it can be extracted to get the `gh` binary. - ### Build from source See here on how to [build GitHub CLI from source][build from source]. From 075691b03e9d91c9de5dedfa8e8d104baee02a99 Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Mon, 18 Nov 2024 15:17:34 +0100 Subject: [PATCH 09/52] Formatting fix Signed-off-by: Fredrik Skogman --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a505a201b..bcd470eec 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Since version 2.50.0 `gh` has been producing [Build Provenance Attestation](http There are two common ways to verify a downloaded release, depending if `gh` is aready installed or not. If `gh` is installed, it's trivial to verify a new release: -- **Option 1: Using `gh` if already installed: +- **Option 1: Using `gh` if already installed:** ```shell $ % gh at verify -R cli/cli gh_2.62.0_macOS_arm64.zip @@ -144,7 +144,7 @@ REPO PREDICATE_TYPE WORKFLOW cli/cli https://slsa.dev/provenance/v1 .github/workflows/deployment.yml@refs/heads/trunk ``` -- **Option 2: Using Sigstore [`cosign`](https://github.com/sigstore/cosign): +- **Option 2: Using Sigstore [`cosign`](https://github.com/sigstore/cosign):** To perform this, download the [attestation](https://github.com/cli/cli/attestations) for the downloaded release and use cosign to verify the authenticity of the downloaded release: From c518a3b1f57d179b40eb40991741720b344e5d61 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 18 Nov 2024 08:18:04 -0700 Subject: [PATCH 10/52] Update pkg/cmd/attestation/verification/extensions.go Co-authored-by: Phill MV --- pkg/cmd/attestation/verification/extensions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verification/extensions.go b/pkg/cmd/attestation/verification/extensions.go index 540c96284..130a301e3 100644 --- a/pkg/cmd/attestation/verification/extensions.go +++ b/pkg/cmd/attestation/verification/extensions.go @@ -34,7 +34,7 @@ func VerifyCertExtensions(results []*AttestationProcessingResult, ec Enforcement return lastErr } -func verifyCertExtensions(verified, expected certificate.Summary) error { +func verifyCertExtensions(given, expected certificate.Summary) error { if !strings.EqualFold(expected.SourceRepositoryOwnerURI, verified.SourceRepositoryOwnerURI) { return fmt.Errorf("expected SourceRepositoryOwnerURI to be %s, got %s", expected.SourceRepositoryOwnerURI, verified.SourceRepositoryOwnerURI) } From 762e99d151c3c5e3927ad7c34aa7d758a59b05ce Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 18 Nov 2024 08:19:07 -0700 Subject: [PATCH 11/52] fix function param calls Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/extensions.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/attestation/verification/extensions.go b/pkg/cmd/attestation/verification/extensions.go index 130a301e3..a0827e9ec 100644 --- a/pkg/cmd/attestation/verification/extensions.go +++ b/pkg/cmd/attestation/verification/extensions.go @@ -35,22 +35,22 @@ func VerifyCertExtensions(results []*AttestationProcessingResult, ec Enforcement } func verifyCertExtensions(given, expected certificate.Summary) error { - if !strings.EqualFold(expected.SourceRepositoryOwnerURI, verified.SourceRepositoryOwnerURI) { - return fmt.Errorf("expected SourceRepositoryOwnerURI to be %s, got %s", expected.SourceRepositoryOwnerURI, verified.SourceRepositoryOwnerURI) + if !strings.EqualFold(expected.SourceRepositoryOwnerURI, given.SourceRepositoryOwnerURI) { + return fmt.Errorf("expected SourceRepositoryOwnerURI to be %s, got %s", expected.SourceRepositoryOwnerURI, given.SourceRepositoryOwnerURI) } // if repo is set, compare the SourceRepositoryURI fields - if expected.SourceRepositoryURI != "" && !strings.EqualFold(expected.SourceRepositoryURI, verified.SourceRepositoryURI) { - return fmt.Errorf("expected SourceRepositoryURI to be %s, got %s", expected.SourceRepositoryURI, verified.SourceRepositoryURI) + if expected.SourceRepositoryURI != "" && !strings.EqualFold(expected.SourceRepositoryURI, given.SourceRepositoryURI) { + return fmt.Errorf("expected SourceRepositoryURI to be %s, got %s", expected.SourceRepositoryURI, given.SourceRepositoryURI) } // compare the OIDC issuers. If not equal, return an error depending // on if there is a partial match - if !strings.EqualFold(expected.Issuer, verified.Issuer) { - if strings.Index(verified.Issuer, expected.Issuer+"/") == 0 { - return fmt.Errorf("expected Issuer to be %s, got %s -- if you have a custom OIDC issuer policy for your enterprise, use the --cert-oidc-issuer flag with your expected issuer", expected.Issuer, verified.Issuer) + if !strings.EqualFold(expected.Issuer, given.Issuer) { + if strings.Index(given.Issuer, expected.Issuer+"/") == 0 { + return fmt.Errorf("expected Issuer to be %s, got %s -- if you have a custom OIDC issuer policy for your enterprise, use the --cert-oidc-issuer flag with your expected issuer", expected.Issuer, given.Issuer) } - return fmt.Errorf("expected Issuer to be %s, got %s", expected.Issuer, verified.Issuer) + return fmt.Errorf("expected Issuer to be %s, got %s", expected.Issuer, given.Issuer) } return nil From 30ae1388e4e4010f3b02620506638d813f3c884d Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 18 Nov 2024 08:19:41 -0700 Subject: [PATCH 12/52] Update pkg/cmd/attestation/download/download.go Co-authored-by: Phill MV --- pkg/cmd/attestation/download/download.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/download/download.go b/pkg/cmd/attestation/download/download.go index ef5a11799..a79af5935 100644 --- a/pkg/cmd/attestation/download/download.go +++ b/pkg/cmd/attestation/download/download.go @@ -122,7 +122,7 @@ func runDownload(opts *Options) error { opts.Logger.VerbosePrintf("Downloading trusted metadata for artifact %s\n\n", opts.ArtifactPath) - c := verification.FetchRemoteAttestations{ + c := verification.FetchRemoteAttestationsParams{ APIClient: opts.APIClient, Digest: artifact.DigestWithAlg(), Limit: opts.Limit, From 63f37eb36996fbf496673052386be215b14b992d Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 18 Nov 2024 08:24:25 -0700 Subject: [PATCH 13/52] pr feedback Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/download/download.go | 13 +++++---- .../attestation/verification/attestation.go | 27 +++++++++---------- pkg/cmd/attestation/verify/attestation.go | 13 +++++---- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/pkg/cmd/attestation/download/download.go b/pkg/cmd/attestation/download/download.go index a79af5935..143912308 100644 --- a/pkg/cmd/attestation/download/download.go +++ b/pkg/cmd/attestation/download/download.go @@ -122,14 +122,13 @@ func runDownload(opts *Options) error { opts.Logger.VerbosePrintf("Downloading trusted metadata for artifact %s\n\n", opts.ArtifactPath) - c := verification.FetchRemoteAttestationsParams{ - APIClient: opts.APIClient, - Digest: artifact.DigestWithAlg(), - Limit: opts.Limit, - Owner: opts.Owner, - Repo: opts.Repo, + params := verification.FetchRemoteAttestationsParams{ + Digest: artifact.DigestWithAlg(), + Limit: opts.Limit, + Owner: opts.Owner, + Repo: opts.Repo, } - attestations, err := verification.GetRemoteAttestations(c) + attestations, err := verification.GetRemoteAttestations(opts.APIClient, params) if err != nil { if errors.Is(err, api.ErrNoAttestations{}) { fmt.Fprintf(opts.Logger.IO.Out, "No attestations found for %s\n", opts.ArtifactPath) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index bdee3a014..07083a5c0 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -20,12 +20,11 @@ const SLSAPredicateV1 = "https://slsa.dev/provenance/v1" var ErrUnrecognisedBundleExtension = errors.New("bundle file extension not supported, must be json or jsonl") var ErrEmptyBundleFile = errors.New("provided bundle file is empty") -type FetchRemoteAttestations struct { - APIClient api.Client - Digest string - Limit int - Owner string - Repo string +type FetchRemoteAttestationsParams struct { + Digest string + Limit int + Owner string + Repo string } // GetLocalAttestations returns a slice of attestations read from a local bundle file. @@ -96,22 +95,22 @@ func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) { return attestations, nil } -func GetRemoteAttestations(c FetchRemoteAttestations) ([]*api.Attestation, error) { - if c.APIClient == nil { +func GetRemoteAttestations(client api.Client, params FetchRemoteAttestationsParams) ([]*api.Attestation, error) { + if client == nil { return nil, fmt.Errorf("api client must be provided") } // check if Repo is set first because if Repo has been set, Owner will be set using the value of Repo. // If Repo is not set, the field will remain empty. It will not be populated using the value of Owner. - if c.Repo != "" { - attestations, err := c.APIClient.GetByRepoAndDigest(c.Repo, c.Digest, c.Limit) + if params.Repo != "" { + attestations, err := client.GetByRepoAndDigest(params.Repo, params.Digest, params.Limit) if err != nil { - return nil, fmt.Errorf("failed to fetch attestations from %s: %w", c.Repo, err) + return nil, fmt.Errorf("failed to fetch attestations from %s: %w", params.Repo, err) } return attestations, nil - } else if c.Owner != "" { - attestations, err := c.APIClient.GetByOwnerAndDigest(c.Owner, c.Digest, c.Limit) + } else if params.Owner != "" { + attestations, err := client.GetByOwnerAndDigest(params.Owner, params.Digest, params.Limit) if err != nil { - return nil, fmt.Errorf("failed to fetch attestations from %s: %w", c.Owner, err) + return nil, fmt.Errorf("failed to fetch attestations from %s: %w", params.Owner, err) } return attestations, nil } diff --git a/pkg/cmd/attestation/verify/attestation.go b/pkg/cmd/attestation/verify/attestation.go index c067afe9b..f3f2792c4 100644 --- a/pkg/cmd/attestation/verify/attestation.go +++ b/pkg/cmd/attestation/verify/attestation.go @@ -32,15 +32,14 @@ func getAttestations(o *Options, a artifact.DigestedArtifact) ([]*api.Attestatio return attestations, msg, nil } - c := verification.FetchRemoteAttestations{ - APIClient: o.APIClient, - Digest: a.DigestWithAlg(), - Limit: o.Limit, - Owner: o.Owner, - Repo: o.Repo, + params := verification.FetchRemoteAttestationsParams{ + Digest: a.DigestWithAlg(), + Limit: o.Limit, + Owner: o.Owner, + Repo: o.Repo, } - attestations, err := verification.GetRemoteAttestations(c) + attestations, err := verification.GetRemoteAttestations(o.APIClient, params) if err != nil { msg := "✗ Loading attestations from GitHub API failed" return nil, msg, err From f48e6b56e3c947184aebd251b72a4e31b4b7006e Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 19 Nov 2024 14:38:28 -0700 Subject: [PATCH 14/52] verify cert extensions function should return filtered result list Signed-off-by: Meredith Lancaster --- .../attestation/verification/extensions.go | 32 ++++++++++++------- .../verification/extensions_test.go | 21 ++++++++---- pkg/cmd/attestation/verify/verify.go | 9 +++--- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/pkg/cmd/attestation/verification/extensions.go b/pkg/cmd/attestation/verification/extensions.go index a0827e9ec..4f70a1c2e 100644 --- a/pkg/cmd/attestation/verification/extensions.go +++ b/pkg/cmd/attestation/verification/extensions.go @@ -13,25 +13,35 @@ var ( GitHubTenantOIDCIssuer = "https://token.actions.%s.ghe.com" ) -func VerifyCertExtensions(results []*AttestationProcessingResult, ec EnforcementCriteria) error { +func VerifyCertExtensions(results []*AttestationProcessingResult, ec EnforcementCriteria) ([]*AttestationProcessingResult, error) { if len(results) == 0 { - return errors.New("no attestations proccessing results") + return nil, errors.New("no attestations processing results") } + verified := make([]*AttestationProcessingResult, len(results)) + var verifyCount int var lastErr error for _, attestation := range results { - err := verifyCertExtensions(*attestation.VerificationResult.Signature.Certificate, ec.Certificate) - if err == nil { - // if at least one attestation is verified, we're good as verification - // is defined as successful if at least one attestation is verified - return nil + if err := verifyCertExtensions(*attestation.VerificationResult.Signature.Certificate, ec.Certificate); err != nil { + lastErr = err + // move onto the next attestation in the for loop if verification fails + continue } - lastErr = err + // otherwise, add the result to the results slice and increment verifyCount + verified[verifyCount] = attestation + verifyCount++ } - // if we have exited the for loop without returning early due to successful - // verification, we need to return an error - return lastErr + // if we have exited the for loop without verifying any attestations, + // return the last error found + if verifyCount == 0 { + return nil, lastErr + } + + // truncate the verified slice to only include verified attestations + verified = verified[:verifyCount] + return verified, nil + } func verifyCertExtensions(given, expected certificate.Summary) error { diff --git a/pkg/cmd/attestation/verification/extensions_test.go b/pkg/cmd/attestation/verification/extensions_test.go index b8ef2875f..73d808119 100644 --- a/pkg/cmd/attestation/verification/extensions_test.go +++ b/pkg/cmd/attestation/verification/extensions_test.go @@ -37,8 +37,9 @@ func TestVerifyCertExtensions(t *testing.T) { } t.Run("passes with one result", func(t *testing.T) { - err := VerifyCertExtensions(results, c) + verified, err := VerifyCertExtensions(results, c) require.NoError(t, err) + require.Len(t, verified, 1) }) t.Run("passes with 1/2 valid results", func(t *testing.T) { @@ -46,8 +47,9 @@ func TestVerifyCertExtensions(t *testing.T) { require.Len(t, twoResults, 2) twoResults[1].VerificationResult.Signature.Certificate.Extensions.SourceRepositoryOwnerURI = "https://github.com/wrong" - err := VerifyCertExtensions(twoResults, c) + verified, err := VerifyCertExtensions(twoResults, c) require.NoError(t, err) + require.Len(t, verified, 1) }) t.Run("fails when all results fail verification", func(t *testing.T) { @@ -56,35 +58,40 @@ func TestVerifyCertExtensions(t *testing.T) { twoResults[0].VerificationResult.Signature.Certificate.Extensions.SourceRepositoryOwnerURI = "https://github.com/wrong" twoResults[1].VerificationResult.Signature.Certificate.Extensions.SourceRepositoryOwnerURI = "https://github.com/wrong" - err := VerifyCertExtensions(twoResults, c) + verified, err := VerifyCertExtensions(twoResults, c) require.Error(t, err) + require.Nil(t, verified) }) t.Run("with wrong SourceRepositoryOwnerURI", func(t *testing.T) { expectedCriteria := c expectedCriteria.Certificate.SourceRepositoryOwnerURI = "https://github.com/wrong" - err := VerifyCertExtensions(results, expectedCriteria) + verified, err := VerifyCertExtensions(results, expectedCriteria) require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://github.com/wrong, got https://github.com/owner") + require.Nil(t, verified) }) t.Run("with wrong SourceRepositoryURI", func(t *testing.T) { expectedCriteria := c expectedCriteria.Certificate.SourceRepositoryURI = "https://github.com/foo/wrong" - err := VerifyCertExtensions(results, expectedCriteria) + verified, err := VerifyCertExtensions(results, expectedCriteria) require.ErrorContains(t, err, "expected SourceRepositoryURI to be https://github.com/foo/wrong, got https://github.com/owner/repo") + require.Nil(t, verified) }) t.Run("with wrong OIDCIssuer", func(t *testing.T) { expectedCriteria := c expectedCriteria.Certificate.Issuer = "wrong" - err := VerifyCertExtensions(results, expectedCriteria) + verified, err := VerifyCertExtensions(results, expectedCriteria) require.ErrorContains(t, err, "expected Issuer to be wrong, got https://token.actions.githubusercontent.com") + require.Nil(t, verified) }) t.Run("with partial OIDCIssuer match", func(t *testing.T) { expectedResults := results expectedResults[0].VerificationResult.Signature.Certificate.Extensions.Issuer = "https://token.actions.githubusercontent.com/foo-bar" - err := VerifyCertExtensions(expectedResults, c) + verified, err := VerifyCertExtensions(expectedResults, c) require.ErrorContains(t, err, "expected Issuer to be https://token.actions.githubusercontent.com, got https://token.actions.githubusercontent.com/foo-bar -- if you have a custom OIDC issuer") + require.Nil(t, verified) }) } diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 1d34fdf99..86b8c6d8d 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -250,14 +250,15 @@ func runVerify(opts *Options) error { return err } - verifyResults, err := opts.SigstoreVerifier.Verify(attestations, sp) + sigstoreVerified, err := opts.SigstoreVerifier.Verify(attestations, sp) if err != nil { opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Sigstore verification failed")) return err } // Verify extensions - if err := verification.VerifyCertExtensions(verifyResults, ec); err != nil { + certExtVerified, err := verification.VerifyCertExtensions(sigstoreVerified, ec) + if err != nil { opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Policy verification failed")) return err } @@ -267,7 +268,7 @@ func runVerify(opts *Options) error { // If an exporter is provided with the --json flag, write the results to the terminal in JSON format if opts.exporter != nil { // print the results to the terminal as an array of JSON objects - if err = opts.exporter.Write(opts.Logger.IO, verifyResults); err != nil { + if err = opts.exporter.Write(opts.Logger.IO, certExtVerified); err != nil { opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Failed to write JSON output")) return err } @@ -277,7 +278,7 @@ func runVerify(opts *Options) error { opts.Logger.Printf("%s was attested by:\n", artifact.DigestWithAlg()) // Otherwise print the results to the terminal in a table - tableContent, err := buildTableVerifyContent(opts.Tenant, verifyResults) + tableContent, err := buildTableVerifyContent(opts.Tenant, certExtVerified) if err != nil { opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to parse results")) return err From 9414930b5df4a176ad5f1dd3da1e7ffc8be44382 Mon Sep 17 00:00:00 2001 From: Dalius Dobravolskas Date: Tue, 19 Nov 2024 23:50:45 +0200 Subject: [PATCH 15/52] Adding option to return `baseRefOid` in `pr view` You need to know exact `baseRefOid` so you could show correct diff. `baseRefName` is not enough sometimes because branch from which PR was forked might have changes already. Example usage: ``` gh pr view --json headRefName,headRefOid,number,baseRefName,baseRefOid,reviewDecision ``` --- api/queries_pr.go | 1 + api/query_builder.go | 1 + pkg/cmd/pr/view/view_test.go | 1 + 3 files changed, 3 insertions(+) diff --git a/api/queries_pr.go b/api/queries_pr.go index 370f2db24..aa493b5e9 100644 --- a/api/queries_pr.go +++ b/api/queries_pr.go @@ -33,6 +33,7 @@ type PullRequest struct { Closed bool URL string BaseRefName string + BaseRefOid string HeadRefName string HeadRefOid string Body string diff --git a/api/query_builder.go b/api/query_builder.go index dbf889273..2112367e3 100644 --- a/api/query_builder.go +++ b/api/query_builder.go @@ -285,6 +285,7 @@ var PullRequestFields = append(sharedIssuePRFields, "additions", "autoMergeRequest", "baseRefName", + "baseRefOid", "changedFiles", "commits", "deletions", diff --git a/pkg/cmd/pr/view/view_test.go b/pkg/cmd/pr/view/view_test.go index 470e3bd27..e7f572c76 100644 --- a/pkg/cmd/pr/view/view_test.go +++ b/pkg/cmd/pr/view/view_test.go @@ -32,6 +32,7 @@ func TestJSONFields(t *testing.T) { "author", "autoMergeRequest", "baseRefName", + "baseRefOid", "body", "changedFiles", "closed", From b5788f252361e633192da95959ed6efdd7ee5da3 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 19 Nov 2024 16:24:17 -0700 Subject: [PATCH 16/52] wrap sigstore and cert ext verification into a single function Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/attestation.go | 18 +++ .../verify/attestation_integration_test.go | 105 ++++++++++++++++++ pkg/cmd/attestation/verify/verify.go | 15 +-- 3 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 pkg/cmd/attestation/verify/attestation_integration_test.go diff --git a/pkg/cmd/attestation/verify/attestation.go b/pkg/cmd/attestation/verify/attestation.go index f3f2792c4..add6f80fe 100644 --- a/pkg/cmd/attestation/verify/attestation.go +++ b/pkg/cmd/attestation/verify/attestation.go @@ -7,6 +7,7 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/api" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" + "github.com/sigstore/sigstore-go/pkg/verify" ) func getAttestations(o *Options, a artifact.DigestedArtifact) ([]*api.Attestation, string, error) { @@ -48,3 +49,20 @@ func getAttestations(o *Options, a artifact.DigestedArtifact) ([]*api.Attestatio msg := fmt.Sprintf("Loaded %s from GitHub API", pluralAttestation) return attestations, msg, nil } + +func verifyAttestations(attestations []*api.Attestation, sgVerifier verification.SigstoreVerifier, sgPolicy verify.PolicyBuilder, ec verification.EnforcementCriteria) ([]*verification.AttestationProcessingResult, string, error) { + sigstoreVerified, err := sgVerifier.Verify(attestations, sgPolicy) + if err != nil { + errMsg := "✗ Sigstore verification failed" + return nil, errMsg, err + } + + // Verify extensions + certExtVerified, err := verification.VerifyCertExtensions(sigstoreVerified, ec) + if err != nil { + errMsg := "✗ Policy verification failed" + return nil, errMsg, err + } + + return certExtVerified, "", nil +} diff --git a/pkg/cmd/attestation/verify/attestation_integration_test.go b/pkg/cmd/attestation/verify/attestation_integration_test.go new file mode 100644 index 000000000..5d787e15f --- /dev/null +++ b/pkg/cmd/attestation/verify/attestation_integration_test.go @@ -0,0 +1,105 @@ +//go:build integration + +package verify + +import ( + "testing" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" + "github.com/cli/cli/v2/pkg/cmd/attestation/io" + "github.com/cli/cli/v2/pkg/cmd/attestation/test" + "github.com/cli/cli/v2/pkg/cmd/attestation/verification" + "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" + "github.com/stretchr/testify/require" +) + +func getAttestationsFor(t *testing.T, bundlePath string) []*api.Attestation { + t.Helper() + + attestations, err := verification.GetLocalAttestations(bundlePath) + require.NoError(t, err) + + return attestations +} + +func TestVerifyAttestations(t *testing.T) { + sigstoreConfig := verification.SigstoreConfig{ + Logger: io.NewTestHandler(), + } + sgVerifier := verification.NewLiveSigstoreVerifier(sigstoreConfig) + + certSummary := certificate.Summary{} + certSummary.SourceRepositoryOwnerURI = "https://github.com/sigstore" + certSummary.SourceRepositoryURI = "https://github.com/sigstore/sigstore-js" + certSummary.Issuer = verification.GitHubOIDCIssuer + + ec := verification.EnforcementCriteria{ + Certificate: certSummary, + PredicateType: verification.SLSAPredicateV1, + SANRegex: "^https://github.com/sigstore/", + } + + artifactPath := test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz") + a, err := artifact.NewDigestedArtifact(nil, artifactPath, "sha512") + require.NoError(t, err) + + sp, err := buildSigstoreVerifyPolicy(ec, *a) + + t.Run("all attestations pass verification", func(t *testing.T) { + attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") + results, errMsg, err := verifyAttestations(attestations, sgVerifier, sp, ec) + require.NoError(t, err) + require.Zero(t, errMsg) + require.Len(t, results, 2) + }) + + t.Run("passes verification with 2/3 attestations passing Sigstore verification", func(t *testing.T) { + invalidBundle := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json") + attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") + attestations = append(attestations, invalidBundle[0]) + require.Len(t, attestations, 3) + + results, errMsg, err := verifyAttestations(attestations, sgVerifier, sp, ec) + require.NoError(t, err) + require.Zero(t, errMsg) + require.Len(t, results, 2) + }) + + t.Run("fails verification when Sigstore verification fails", func(t *testing.T) { + invalidBundle := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json") + invalidBundle2 := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0-bundle-v0.1.json") + attestations := append(invalidBundle, invalidBundle2...) + require.Len(t, attestations, 2) + + results, errMsg, err := verifyAttestations(attestations, sgVerifier, sp, ec) + require.Error(t, err) + require.Contains(t, errMsg, "✗ Sigstore verification failed") + require.Nil(t, results) + }) + + t.Run("passes verification with 2/3 attestations passing cert extension verification", func(t *testing.T) { + ghAttestation := getAttestationsFor(t, "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle.jsonl") + attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") + attestations = append(attestations, ghAttestation[0]) + require.Len(t, attestations, 3) + + results, errMsg, err := verifyAttestations(attestations, sgVerifier, sp, ec) + require.NoError(t, err) + require.Zero(t, errMsg) + require.Len(t, results, 2) + }) + + t.Run("fails verification when cert extension verification fails", func(t *testing.T) { + attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") + require.Len(t, attestations, 2) + + expectedCriteria := ec + expectedCriteria.Certificate.SourceRepositoryOwnerURI = "https://github.com/wrong" + + results, errMsg, err := verifyAttestations(attestations, sgVerifier, sp, expectedCriteria) + require.Error(t, err) + require.Contains(t, errMsg, "✗ Policy verification failed") + require.Nil(t, results) + }) +} diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 86b8c6d8d..4efc23f44 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -250,16 +250,9 @@ func runVerify(opts *Options) error { return err } - sigstoreVerified, err := opts.SigstoreVerifier.Verify(attestations, sp) + verified, errMsg, err := verifyAttestations(attestations, opts.SigstoreVerifier, sp, ec) if err != nil { - opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Sigstore verification failed")) - return err - } - - // Verify extensions - certExtVerified, err := verification.VerifyCertExtensions(sigstoreVerified, ec) - if err != nil { - opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Policy verification failed")) + opts.Logger.Println(opts.Logger.ColorScheme.Red(errMsg)) return err } @@ -268,7 +261,7 @@ func runVerify(opts *Options) error { // If an exporter is provided with the --json flag, write the results to the terminal in JSON format if opts.exporter != nil { // print the results to the terminal as an array of JSON objects - if err = opts.exporter.Write(opts.Logger.IO, certExtVerified); err != nil { + if err = opts.exporter.Write(opts.Logger.IO, verified); err != nil { opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Failed to write JSON output")) return err } @@ -278,7 +271,7 @@ func runVerify(opts *Options) error { opts.Logger.Printf("%s was attested by:\n", artifact.DigestWithAlg()) // Otherwise print the results to the terminal in a table - tableContent, err := buildTableVerifyContent(opts.Tenant, certExtVerified) + tableContent, err := buildTableVerifyContent(opts.Tenant, verified) if err != nil { opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to parse results")) return err From 3e6861e7e1f725f5e19ab79d5f83d659a8cace6b Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 19 Nov 2024 16:28:36 -0700 Subject: [PATCH 17/52] clean up Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/attestation_integration_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/attestation/verify/attestation_integration_test.go b/pkg/cmd/attestation/verify/attestation_integration_test.go index 5d787e15f..e7799247d 100644 --- a/pkg/cmd/attestation/verify/attestation_integration_test.go +++ b/pkg/cmd/attestation/verify/attestation_integration_test.go @@ -24,10 +24,10 @@ func getAttestationsFor(t *testing.T, bundlePath string) []*api.Attestation { } func TestVerifyAttestations(t *testing.T) { - sigstoreConfig := verification.SigstoreConfig{ + config := verification.SigstoreConfig{ Logger: io.NewTestHandler(), } - sgVerifier := verification.NewLiveSigstoreVerifier(sigstoreConfig) + sgVerifier := verification.NewLiveSigstoreVerifier(config) certSummary := certificate.Summary{} certSummary.SourceRepositoryOwnerURI = "https://github.com/sigstore" @@ -48,6 +48,7 @@ func TestVerifyAttestations(t *testing.T) { t.Run("all attestations pass verification", func(t *testing.T) { attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") + require.Len(t, attestations, 2) results, errMsg, err := verifyAttestations(attestations, sgVerifier, sp, ec) require.NoError(t, err) require.Zero(t, errMsg) From 78260967f923a6212c1dbbebab814b07c354a9ab Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Wed, 20 Nov 2024 10:26:00 -0500 Subject: [PATCH 18/52] Fix README.md code block formatting --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index bcd470eec..60ab7d6d9 100644 --- a/README.md +++ b/README.md @@ -133,29 +133,29 @@ There are two common ways to verify a downloaded release, depending if `gh` is a - **Option 1: Using `gh` if already installed:** -```shell -$ % gh at verify -R cli/cli gh_2.62.0_macOS_arm64.zip -Loaded digest sha256:fdb77f31b8a6dd23c3fd858758d692a45f7fc76383e37d475bdcae038df92afc for file://gh_2.62.0_macOS_arm64.zip -Loaded 1 attestation from GitHub API -✓ Verification succeeded! + ```shell + $ % gh at verify -R cli/cli gh_2.62.0_macOS_arm64.zip + Loaded digest sha256:fdb77f31b8a6dd23c3fd858758d692a45f7fc76383e37d475bdcae038df92afc for file://gh_2.62.0_macOS_arm64.zip + Loaded 1 attestation from GitHub API + ✓ Verification succeeded! -sha256:fdb77f31b8a6dd23c3fd858758d692a45f7fc76383e37d475bdcae038df92afc was attested by: -REPO PREDICATE_TYPE WORKFLOW -cli/cli https://slsa.dev/provenance/v1 .github/workflows/deployment.yml@refs/heads/trunk -``` + sha256:fdb77f31b8a6dd23c3fd858758d692a45f7fc76383e37d475bdcae038df92afc was attested by: + REPO PREDICATE_TYPE WORKFLOW + cli/cli https://slsa.dev/provenance/v1 .github/workflows/deployment.yml@refs/heads/trunk + ``` - **Option 2: Using Sigstore [`cosign`](https://github.com/sigstore/cosign):** -To perform this, download the [attestation](https://github.com/cli/cli/attestations) for the downloaded release and use cosign to verify the authenticity of the downloaded release: + To perform this, download the [attestation](https://github.com/cli/cli/attestations) for the downloaded release and use cosign to verify the authenticity of the downloaded release: -```shell -$ cosign verify-blob-attestation --bundle cli-cli-attestation-3120304.sigstore.json \ - --new-bundle-format \ - --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ - --certificate-identity-regexp="^https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk$" \ - gh_2.62.0_macOS_arm64.zip -Verified OK -``` + ```shell + $ cosign verify-blob-attestation --bundle cli-cli-attestation-3120304.sigstore.json \ + --new-bundle-format \ + --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ + --certificate-identity-regexp="^https://github.com/cli/cli/.github/workflows/deployment.yml@refs/heads/trunk$" \ + gh_2.62.0_macOS_arm64.zip + Verified OK + ``` ### Build from source From 4671b8d66b892a34a5c4aef44cf952447b0e0fbd Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 20 Nov 2024 12:46:06 -0700 Subject: [PATCH 19/52] update test Signed-off-by: Meredith Lancaster --- .../attestation/verify/attestation_integration_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/attestation/verify/attestation_integration_test.go b/pkg/cmd/attestation/verify/attestation_integration_test.go index e7799247d..77ea238a8 100644 --- a/pkg/cmd/attestation/verify/attestation_integration_test.go +++ b/pkg/cmd/attestation/verify/attestation_integration_test.go @@ -80,15 +80,18 @@ func TestVerifyAttestations(t *testing.T) { }) t.Run("passes verification with 2/3 attestations passing cert extension verification", func(t *testing.T) { - ghAttestation := getAttestationsFor(t, "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle.jsonl") - attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") - attestations = append(attestations, ghAttestation[0]) + ghAttestations := getAttestationsFor(t, "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle.jsonl") + sgjAttestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") + attestations := []*api.Attestation{sgjAttestations[0], ghAttestations[0], sgjAttestations[1]} require.Len(t, attestations, 3) results, errMsg, err := verifyAttestations(attestations, sgVerifier, sp, ec) require.NoError(t, err) require.Zero(t, errMsg) require.Len(t, results, 2) + for _, r := range results { + require.NotEqual(t, r.Attestation.Bundle.String(), ghAttestations[0].Bundle.String()) + } }) t.Run("fails verification when cert extension verification fails", func(t *testing.T) { From ff8844a3084e017b47f514a71dafad3b77acacf6 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 20 Nov 2024 13:22:55 -0700 Subject: [PATCH 20/52] update test Signed-off-by: Meredith Lancaster --- .../attestation/verify/attestation_integration_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/attestation/verify/attestation_integration_test.go b/pkg/cmd/attestation/verify/attestation_integration_test.go index 77ea238a8..6eb79cdd9 100644 --- a/pkg/cmd/attestation/verify/attestation_integration_test.go +++ b/pkg/cmd/attestation/verify/attestation_integration_test.go @@ -80,12 +80,16 @@ func TestVerifyAttestations(t *testing.T) { }) t.Run("passes verification with 2/3 attestations passing cert extension verification", func(t *testing.T) { - ghAttestations := getAttestationsFor(t, "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle.jsonl") + ghAttestations := getAttestationsFor(t, "../test/data/reusable-workflow-attestation.sigstore.json") sgjAttestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") attestations := []*api.Attestation{sgjAttestations[0], ghAttestations[0], sgjAttestations[1]} require.Len(t, attestations, 3) - results, errMsg, err := verifyAttestations(attestations, sgVerifier, sp, ec) + expectedCriteria := ec + expectedCriteria.SANRegex = "^https://github.com/" + esp, err := buildSigstoreVerifyPolicy(ec, *a) + + results, errMsg, err := verifyAttestations(attestations, sgVerifier, esp, expectedCriteria) require.NoError(t, err) require.Zero(t, errMsg) require.Len(t, results, 2) From 5601149c837c1a3a1d95adfbb4f545fdf61c7b73 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 20 Nov 2024 13:34:40 -0700 Subject: [PATCH 21/52] naming Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verify/attestation_integration_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/attestation/verify/attestation_integration_test.go b/pkg/cmd/attestation/verify/attestation_integration_test.go index 6eb79cdd9..46c239f61 100644 --- a/pkg/cmd/attestation/verify/attestation_integration_test.go +++ b/pkg/cmd/attestation/verify/attestation_integration_test.go @@ -80,9 +80,9 @@ func TestVerifyAttestations(t *testing.T) { }) t.Run("passes verification with 2/3 attestations passing cert extension verification", func(t *testing.T) { - ghAttestations := getAttestationsFor(t, "../test/data/reusable-workflow-attestation.sigstore.json") + rwAttestations := getAttestationsFor(t, "../test/data/reusable-workflow-attestation.sigstore.json") sgjAttestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") - attestations := []*api.Attestation{sgjAttestations[0], ghAttestations[0], sgjAttestations[1]} + attestations := []*api.Attestation{sgjAttestations[0], rwAttestations[0], sgjAttestations[1]} require.Len(t, attestations, 3) expectedCriteria := ec @@ -94,7 +94,7 @@ func TestVerifyAttestations(t *testing.T) { require.Zero(t, errMsg) require.Len(t, results, 2) for _, r := range results { - require.NotEqual(t, r.Attestation.Bundle.String(), ghAttestations[0].Bundle.String()) + require.NotEqual(t, r.Attestation.Bundle.String(), rwAttestations[0].Bundle.String()) } }) From 19afe453c7068ff568087806adce0c3b111709aa Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 20 Nov 2024 14:53:02 -0700 Subject: [PATCH 22/52] update test with new test bundle Signed-off-by: Meredith Lancaster --- ...artifact-attestations-workflow-bundle.json | 59 +++++++++++++++++++ .../verify/attestation_integration_test.go | 27 ++++++--- 2 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 pkg/cmd/attestation/test/data/gh-artifact-attestations-workflow-bundle.json diff --git a/pkg/cmd/attestation/test/data/gh-artifact-attestations-workflow-bundle.json b/pkg/cmd/attestation/test/data/gh-artifact-attestations-workflow-bundle.json new file mode 100644 index 000000000..ca17f4101 --- /dev/null +++ b/pkg/cmd/attestation/test/data/gh-artifact-attestations-workflow-bundle.json @@ -0,0 +1,59 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "tlogEntries": [ + { + "logIndex": "129027678", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "integratedTime": "1725924650", + "inclusionPromise": { + "signedEntryTimestamp": "MEQCICZHyKBtXwvRDYWke7Pp0KfrBL8YNLoDHGP7lKFgkjGbAiB6V+IfSg/SYV/ySXOUg+t/Wp8wGjXMkKWnzPT9DWNsZA==" + }, + "inclusionProof": { + "logIndex": "7123416", + "rootHash": "LZ7MbkKAnq4sU1EMcWwJWWhw778o2w5vO6+Op/DFwm4=", + "treeSize": "7123418", + "hashes": [ + "pJMeUZ4R2LEEjf0JSmsoXTfHMgBAah4aOPBdXEa9sxo=", + "4f/Xq7IIiBbWQbSZZLkEBCSuOIbUXSvzYfxTIqh9rjY=", + "gy7fFADBSd5e37RIZy86inqhsnYB29bTUxY6/EtlJDk=", + "8JY4XnAVf8weXfLSChGSEbqVSN7FKSapmaM5Xi+qowM=", + "FKlHhO4TMH2pnrZUvSKA7Drig5MbABFy2KZx4esRxJY=", + "T+Ziyo74JC0j3MWEgjiiGuTCQ0w+AzLft+r9OyRldMw=", + "naRDgCL1Ch0MNzrXrAmrV1PLa/Bi5HV5GqrqlUceCVI=", + "c4TDdYxGB0ihJtrnXDSynGSSI83D5WVHvZJxuMti4Xg=", + "bcPqJfBdq24AxJvo1LAJKwcudDBLIIyVclqFzJW5TEY=", + "7Dvc6Q8qiduX8Oka5vLLU5oWAmybo8oaecNXPgkOQvA=", + "LAdu5Ynz/wk2uMNazU0CVickvA3YhhBz6TpIl5brTko=", + "uRsmea7eVXshBNN6huh/owmfaAy9Rx4Cq2M2vFb2Ntk=", + "NeHKGVl6KVXfx3+wnQrIrxra4Pr9Fa7YDpTlf86mlTc=" + ], + "checkpoint": { + "envelope": "rekor.sigstore.dev - 1193050959916656506\n7123418\nLZ7MbkKAnq4sU1EMcWwJWWhw778o2w5vO6+Op/DFwm4=\n\n— rekor.sigstore.dev wNI9ajBGAiEAy5505Eq4vOyji5LLmRbaN4/eqwOlVpgLOnozcVCJWvoCIQD+/CNPBY1eyyNypq25OIDwTVIHVxroif3cf2MsfEfplw==\n" + } + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiYzk2YzgzYjE5NTAwOTJmOWEzYjc4NWE4ZWFlM2Y4MDY4YzcwYWQyZWJlZTFhNTRhYmIxZmJmYzk0ZTg1YzA3NSJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImZmZTQ0OTYwMWI0ZmJjODQ4ODE0MzZkMzhlZjE4ZGRhNGI4NWM4YWY0NGI3YzM3ZmQwZTRhNGZhZmI0MGM2MzgifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVVQ0lRQ0FPQmRJT3M2MnR1TzQybDhBWGNyK2t5ampDa1VqMTZRZUlwWkpkUjM4SWdJZ0x3YWlLdlg0RU9IclEvVFczOFQ4UTJiRzBvYkZ2b3NDNUlqYTFMbDlJNU09IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VoWmFrTkRRblZsWjBGM1NVSkJaMGxWWVRkNWJUSktNa1kyYVd0c1ZtdE1jMGhTUzNaVU9YTnZiR3BWZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwUmQwOVVRVFZOYWsxNlRVUlZkMWRvWTA1TmFsRjNUMVJCTlUxcVRUQk5SRlYzVjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVlFjekpOVmpNeU9HRkdRM2gyVWxKelFuQTNkR2R2WVRVcmJuUTFTbmR0TkdrM2Frc0tNR0ZUTUZFM2JVMDFZblpZZUZwdmRFdGtLMWxwTmpSMEswaFZOaXN4WnpWVlJIZzVNRFJJVjFCSlRUQkROemhwVUdGUFEwSm5XWGRuWjFsRFRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVm1iMmg2Q2s1bFRXMUNkMW9yUm5rMFN6aE5aU3MwVlc5aVkxcEpkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMlZuV1VSV1VqQlNRVkZJTDBKSVFYZGliMXB6WVVoU01HTklUVFpNZVRsdVlWaFNiMlJYU1hWWk1qbDBUREprY0dSSGFERlphVGxvWTI1U2NBcGFiVVpxWkVNeGFHUklVbXhqTTFKb1pFZHNkbUp1VFhSa01qbDVZVEphYzJJelpIcE1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU1tUnZDbGt6U1hSalNGWnBZa2RzZW1GRE5UVmlWM2hCWTIxV2JXTjVPVzlhVjBaclkzazVkRmxYYkhWTlJHdEhRMmx6UjBGUlVVSm5OemgzUVZGRlJVc3lhREFLWkVoQ2VrOXBPSFprUnpseVdsYzBkVmxYVGpCaFZ6bDFZM2sxYm1GWVVtOWtWMG94WXpKV2VWa3lPWFZrUjFaMVpFTTFhbUl5TUhkSWQxbExTM2RaUWdwQ1FVZEVkbnBCUWtGblVWSmtNamw1WVRKYWMySXpaR1phUjJ4NlkwZEdNRmt5WjNkT1oxbExTM2RaUWtKQlIwUjJla0ZDUVhkUmIxa3lUVE5QUjAwMUNsa3lVVEZOUjAweldXMUdhbHBVV21wTk1rcHNUWHBhYVZsdFVYbE5iVmw1V21wR2FVNVhXVFJOVkdkNVdXcEJlVUpuYjNKQ1owVkZRVmxQTDAxQlJVVUtRa05TUW1SSVVteGpNMUZuVTFjeGFGb3lWV2RMUldSSlNVVk9kbUp1VW1oaFZ6VnNZMmxDVTFwWFpIQmpNMUo1WlZOcmQwNUJXVXRMZDFsQ1FrRkhSQXAyZWtGQ1FsRlJiVm95YkRCaFNGWnBUREpHZVdSSGJHMVpWMDR3VEZkR01HUkhWbnBrUjBZd1lWYzVkV041TVROaU0wcHlXbTE0ZG1RelRYZElVVmxMQ2t0M1dVSkNRVWRFZG5wQlFrSm5VVkJqYlZadFkzazViMXBYUm10amVUbDBXVmRzZFUxRWMwZERhWE5IUVZGUlFtYzNPSGRCVVdkRlRGRjNjbUZJVWpBS1kwaE5Oa3g1T1RCaU1uUnNZbWsxYUZrelVuQmlNalY2VEcxa2NHUkhhREZaYmxaNldsaEthbUl5TlRCYVZ6VXdURzFPZG1KVVFqaENaMjl5UW1kRlJRcEJXVTh2VFVGRlNrSkhORTFpUjJnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemx1WVZoU2IyUlhTWFpaV0Vvd1lWZGFhRmt6VVhSWldGSXdDbHBZVGpCWldGSndZakkxZWt4WVpIWmpiWFJ0WWtjNU0yTjVPSFZhTW13d1lVaFdhVXd6WkhaamJYUnRZa2M1TTJONU9XNWhSMDU1VEZoQ01WbHRlSEFLWXpKbmRXVlhNWE5SU0Vwc1dtNU5kbUZIVm1oYVNFMTJZbGRHY0dKcVFUUkNaMjl5UW1kRlJVRlpUeTlOUVVWTFFrTnZUVXRIVG1wT2VtaHFUMWRPYXdwT1ZFSnFUakpLYUZreVZUSlplazVwV2xSTk1sbHRTbXROYWtwdFRXMVplRmxxVm0xUFJFVTBUVzFKZDBoUldVdExkMWxDUWtGSFJIWjZRVUpEZDFGUUNrUkJNVzVoV0ZKdlpGZEpkR0ZIT1hwa1IxWnJUVVZyUjBOcGMwZEJVVkZDWnpjNGQwRlJkMFZQZDNjMVlVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVUtXVEk1ZEV3eVpIQmtSMmd4V1drNWFHTnVVbkJhYlVacVpFTXhhR1JJVW14ak0xSm9aRWRzZG1KdVRYUmtNamw1WVRKYWMySXpaSHBOUkdkSFEybHpSd3BCVVZGQ1p6YzRkMEZSTUVWTFozZHZXVEpOTTA5SFRUVlpNbEV4VFVkTk0xbHRSbXBhVkZwcVRUSktiRTE2V21sWmJWRjVUVzFaZVZwcVJtbE9WMWswQ2sxVVozbFpha0ZtUW1kdmNrSm5SVVZCV1U4dlRVRkZUMEpDUlUxRU0wcHNXbTVOZG1GSFZtaGFTRTEyWWxkR2NHSnFRVnBDWjI5eVFtZEZSVUZaVHk4S1RVRkZVRUpCYzAxRFZHZDNUa1JSTWs5RVJYbFBWRUZ3UW1kdmNrSm5SVVZCV1U4dlRVRkZVVUpDYzAxSFYyZ3daRWhDZWs5cE9IWmFNbXd3WVVoV2FRcE1iVTUyWWxNNWJtRllVbTlrVjBsM1JrRlpTMHQzV1VKQ1FVZEVkbnBCUWtWUlVVZEVRVkUxVDFSRk5VMUlkMGREYVhOSFFWRlJRbWMzT0hkQlVrbEZDbUpuZUhOaFNGSXdZMGhOTmt4NU9XNWhXRkp2WkZkSmRWa3lPWFJNTW1Sd1pFZG9NVmxwT1doamJsSndXbTFHYW1SRE1XaGtTRkpzWXpOU2FHUkhiSFlLWW01TmRHUXlPWGxoTWxwellqTmtla3g1Tlc1aFdGSnZaRmRKZG1ReU9YbGhNbHB6WWpOa2Vrd3laRzlaTTBsMFkwaFdhV0pIYkhwaFF6VTFZbGQ0UVFwamJWWnRZM2s1YjFwWFJtdGplVGwwV1Zkc2RVMUVaMGREYVhOSFFWRlJRbWMzT0hkQlVrMUZTMmQzYjFreVRUTlBSMDAxV1RKUk1VMUhUVE5aYlVacUNscFVXbXBOTWtwc1RYcGFhVmx0VVhsTmJWbDVXbXBHYVU1WFdUUk5WR2Q1V1dwQmFFSm5iM0pDWjBWRlFWbFBMMDFCUlZWQ1FrMU5SVmhrZG1OdGRHMEtZa2M1TTFneVVuQmpNMEpvWkVkT2IwMUhNRWREYVhOSFFWRlJRbWMzT0hkQlVsVkZXSGQ0WkdGSVVqQmpTRTAyVEhrNWJtRllVbTlrVjBsMVdUSTVkQXBNTW1Sd1pFZG9NVmxwT1doamJsSndXbTFHYW1SRE1XaGtTRkpzWXpOU2FHUkhiSFppYmsxMFpESTVlV0V5V25OaU0yUjZUREpHYW1SSGJIWmliazEyQ21OdVZuVmplVGg0VFVSak5FMXFhM2hPVkVWNFRsTTVhR1JJVW14aVdFSXdZM2s0ZUUxQ1dVZERhWE5IUVZGUlFtYzNPSGRCVWxsRlEwRjNSMk5JVm1rS1lrZHNhazFKUjBwQ1oyOXlRbWRGUlVGa1dqVkJaMUZEUWtoelJXVlJRak5CU0ZWQk0xUXdkMkZ6WWtoRlZFcHFSMUkwWTIxWFl6TkJjVXBMV0hKcVpRcFFTek12YURSd2VXZERPSEEzYnpSQlFVRkhVakpUUVhaQlVVRkJRa0ZOUVZKcVFrVkJhVUp6UTBKUlZtTlBOVmxrZDBsQ01VNXBSVVJxU1V0aFJXdG5DblpvUm1OQ1pGWTJVbE5wTjFNM05qVlBaMGxuWWtoeldXMTVUVGRuU2xWaWFtMXNSRFIxT0M5cVdtWjFNbEZrZGpKYU1rWmFVVzQ1YUVGUFkyNWtOSGNLUTJkWlNVdHZXa2w2YWpCRlFYZE5SR0ZSUVhkYVowbDRRVWx2Wmsxd2FFMU9RbU5ZYVhGRE0wNXZZWFpWYm14d04wOVBlSE5ZTkhoUVUzcGhZbWRtVFFwc2N6Z3JiazVuTUhsRWEwSnVaazlQZVZwblZVcHFieXNyWjBsNFFVcDZOek5qTlZnMFZERm5PRmx4YWs1VVNtMUZjWG8yYW5WbFlYcEJTVzVZYTA5TUNsSmFlV0ZCYVc1RFNsVnVRVVEwVUU1SVpucEZWV0UwTDFKTGRrazNRVDA5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn1dfX0=" + } + ], + "timestampVerificationData": { + }, + "certificate": { + "rawBytes": "MIIHYjCCBuegAwIBAgIUa7ym2J2F6iklVkLsHRKvT9soljUwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwOTA5MjMzMDUwWhcNMjQwOTA5MjM0MDUwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPs2MV328aFCxvRRsBp7tgoa5+nt5Jwm4i7jK0aS0Q7mM5bvXxZotKd+Yi64t+HU6+1g5UDx904HWPIM0C78iPaOCBgYwggYCMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUfohzNeMmBwZ+Fy4K8Me+4UobcZIwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wegYDVR0RAQH/BHAwboZsaHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9hcnRpZmFjdC1hdHRlc3RhdGlvbnMtd29ya2Zsb3dzLy5naXRodWIvd29ya2Zsb3dzL2doY3ItcHVibGlzaC55bWxAcmVmcy9oZWFkcy9tYWluMDkGCisGAQQBg78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wHwYKKwYBBAGDvzABAgQRd29ya2Zsb3dfZGlzcGF0Y2gwNgYKKwYBBAGDvzABAwQoY2M3OGM5Y2Q1MGM3YmFjZTZjM2JlMzZiYmQyMmYyZjFiNWY4MTgyYjAyBgorBgEEAYO/MAEEBCRBdHRlc3QgSW1hZ2UgKEdIIENvbnRhaW5lciBSZWdpc3RyeSkwNAYKKwYBBAGDvzABBQQmZ2l0aHViL2FydGlmYWN0LWF0dGVzdGF0aW9ucy13b3JrZmxvd3MwHQYKKwYBBAGDvzABBgQPcmVmcy9oZWFkcy9tYWluMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTB8BgorBgEEAYO/MAEJBG4MbGh0dHBzOi8vZ2l0aHViLmNvbS9naXRodWIvYXJ0aWZhY3QtYXR0ZXN0YXRpb25zLXdvcmtmbG93cy8uZ2l0aHViL3dvcmtmbG93cy9naGNyLXB1Ymxpc2gueW1sQHJlZnMvaGVhZHMvbWFpbjA4BgorBgEEAYO/MAEKBCoMKGNjNzhjOWNkNTBjN2JhY2U2YzNiZTM2YmJkMjJmMmYxYjVmODE4MmIwHQYKKwYBBAGDvzABCwQPDA1naXRodWItaG9zdGVkMEkGCisGAQQBg78wAQwEOww5aHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9hcnRpZmFjdC1hdHRlc3RhdGlvbnMtd29ya2Zsb3dzMDgGCisGAQQBg78wAQ0EKgwoY2M3OGM5Y2Q1MGM3YmFjZTZjM2JlMzZiYmQyMmYyZjFiNWY4MTgyYjAfBgorBgEEAYO/MAEOBBEMD3JlZnMvaGVhZHMvbWFpbjAZBgorBgEEAYO/MAEPBAsMCTgwNDQ2ODEyOTApBgorBgEEAYO/MAEQBBsMGWh0dHBzOi8vZ2l0aHViLmNvbS9naXRodWIwFAYKKwYBBAGDvzABEQQGDAQ5OTE5MHwGCisGAQQBg78wARIEbgxsaHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9hcnRpZmFjdC1hdHRlc3RhdGlvbnMtd29ya2Zsb3dzLy5naXRodWIvd29ya2Zsb3dzL2doY3ItcHVibGlzaC55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoY2M3OGM5Y2Q1MGM3YmFjZTZjM2JlMzZiYmQyMmYyZjFiNWY4MTgyYjAhBgorBgEEAYO/MAEUBBMMEXdvcmtmbG93X2Rpc3BhdGNoMG0GCisGAQQBg78wARUEXwxdaHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9hcnRpZmFjdC1hdHRlc3RhdGlvbnMtd29ya2Zsb3dzL2FjdGlvbnMvcnVucy8xMDc4MjkxNTExNS9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwGcHVibGljMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGR2SAvAQAABAMARjBEAiBsCBQVcO5YdwIB1NiEDjIKaEkgvhFcBdV6RSi7S765OgIgbHsYmyM7gJUbjmlD4u8/jZfu2Qdv2Z2FZQn9hAOcnd4wCgYIKoZIzj0EAwMDaQAwZgIxAIofMphMNBcXiqC3NoavUnlp7OOxsX4xPSzabgfMls8+nNg0yDkBnfOOyZgUJjo++gIxAJz73c5X4T1g8YqjNTJmEqz6jueazAInXkOLRZyaAinCJUnAD4PNHfzEUa4/RKvI7A==" + } + }, + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiZ2hjci5pby9naXRodWIvYXJ0aWZhY3QtYXR0ZXN0YXRpb25zLXdvcmtmbG93cyIsImRpZ2VzdCI6eyJzaGEyNTYiOiIwNmU2ODE4MzI0Y2IyMGUxZWE2ZjIyZGE3ZmI5MjUzMjNmYjU3MjUxYzk4NzY4Yzg4YjkzODcyYjUwNTkwZjkyIn19XSwicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vc2xzYS5kZXYvcHJvdmVuYW5jZS92MSIsInByZWRpY2F0ZSI6eyJidWlsZERlZmluaXRpb24iOnsiYnVpbGRUeXBlIjoiaHR0cHM6Ly9hY3Rpb25zLmdpdGh1Yi5pby9idWlsZHR5cGVzL3dvcmtmbG93L3YxIiwiZXh0ZXJuYWxQYXJhbWV0ZXJzIjp7IndvcmtmbG93Ijp7InJlZiI6InJlZnMvaGVhZHMvbWFpbiIsInJlcG9zaXRvcnkiOiJodHRwczovL2dpdGh1Yi5jb20vZ2l0aHViL2FydGlmYWN0LWF0dGVzdGF0aW9ucy13b3JrZmxvd3MiLCJwYXRoIjoiLmdpdGh1Yi93b3JrZmxvd3MvZ2hjci1wdWJsaXNoLnltbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoid29ya2Zsb3dfZGlzcGF0Y2giLCJyZXBvc2l0b3J5X2lkIjoiODA0NDY4MTI5IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6Ijk5MTkiLCJydW5uZXJfZW52aXJvbm1lbnQiOiJnaXRodWItaG9zdGVkIn19LCJyZXNvbHZlZERlcGVuZGVuY2llcyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9naXRodWIvYXJ0aWZhY3QtYXR0ZXN0YXRpb25zLXdvcmtmbG93c0ByZWZzL2hlYWRzL21haW4iLCJkaWdlc3QiOnsiZ2l0Q29tbWl0IjoiY2M3OGM5Y2Q1MGM3YmFjZTZjM2JlMzZiYmQyMmYyZjFiNWY4MTgyYiJ9fV19LCJydW5EZXRhaWxzIjp7ImJ1aWxkZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vZ2l0aHViL2FydGlmYWN0LWF0dGVzdGF0aW9ucy13b3JrZmxvd3MvLmdpdGh1Yi93b3JrZmxvd3MvZ2hjci1wdWJsaXNoLnltbEByZWZzL2hlYWRzL21haW4ifSwibWV0YWRhdGEiOnsiaW52b2NhdGlvbklkIjoiaHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9hcnRpZmFjdC1hdHRlc3RhdGlvbnMtd29ya2Zsb3dzL2FjdGlvbnMvcnVucy8xMDc4MjkxNTExNS9hdHRlbXB0cy8xIn19fX0=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEUCIQCAOBdIOs62tuO42l8AXcr+kyjjCkUj16QeIpZJdR38IgIgLwaiKvX4EOHrQ/TW38T8Q2bG0obFvosC5Ija1Ll9I5M=" + } + ] + } +} \ No newline at end of file diff --git a/pkg/cmd/attestation/verify/attestation_integration_test.go b/pkg/cmd/attestation/verify/attestation_integration_test.go index 46c239f61..480368519 100644 --- a/pkg/cmd/attestation/verify/attestation_integration_test.go +++ b/pkg/cmd/attestation/verify/attestation_integration_test.go @@ -80,21 +80,32 @@ func TestVerifyAttestations(t *testing.T) { }) t.Run("passes verification with 2/3 attestations passing cert extension verification", func(t *testing.T) { - rwAttestations := getAttestationsFor(t, "../test/data/reusable-workflow-attestation.sigstore.json") - sgjAttestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") - attestations := []*api.Attestation{sgjAttestations[0], rwAttestations[0], sgjAttestations[1]} + customArtifactPath := test.NormalizeRelativePath("../test/data/reusable-workflow-artifact") + customArtifact, err := artifact.NewDigestedArtifact(nil, customArtifactPath, "sha256") + require.NoError(t, err) + + mlDemoAttestAttestation := getAttestationsFor(t, "../test/data/reusable-workflow-attestation.sigstore.json") + aareusableAttestation := getAttestationsFor(t, "../test/data/gh-artifact-attestations-workflow-bundle.json") + attestations := []*api.Attestation{mlDemoAttestAttestation[0], aareusableAttestation[0], mlDemoAttestAttestation[0]} require.Len(t, attestations, 3) - expectedCriteria := ec - expectedCriteria.SANRegex = "^https://github.com/" - esp, err := buildSigstoreVerifyPolicy(ec, *a) + certSummary := certificate.Summary{} + certSummary.SourceRepositoryOwnerURI = "https://github.com/malancas" + certSummary.Issuer = verification.GitHubOIDCIssuer - results, errMsg, err := verifyAttestations(attestations, sgVerifier, esp, expectedCriteria) + customEc := verification.EnforcementCriteria{ + Certificate: certSummary, + PredicateType: verification.SLSAPredicateV1, + SANRegex: "^https://github.com/github/artifact-attestations-workflows/", + } + esp, err := buildSigstoreVerifyPolicy(customEc, *customArtifact) + + results, errMsg, err := verifyAttestations(attestations, sgVerifier, esp, customEc) require.NoError(t, err) require.Zero(t, errMsg) require.Len(t, results, 2) for _, r := range results { - require.NotEqual(t, r.Attestation.Bundle.String(), rwAttestations[0].Bundle.String()) + require.NotEqual(t, r.Attestation.Bundle.String(), aareusableAttestation[0].Bundle.String()) } }) From 4d277df559c8b5a6c349738bf1c7ef740e0b8ae8 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 21 Nov 2024 08:43:21 -0700 Subject: [PATCH 23/52] add more testing testing fixtures Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/inspect/inspect_test.go | 14 ++--- .../attestation/verification/mock_verifier.go | 55 +++++++++++++++++-- pkg/cmd/attestation/verification/sigstore.go | 2 +- pkg/cmd/attestation/verify/attestation.go | 19 ++++--- .../verify/attestation_integration_test.go | 45 +++++---------- pkg/cmd/attestation/verify/verify.go | 8 +-- pkg/cmd/attestation/verify/verify_test.go | 30 +++++----- 7 files changed, 101 insertions(+), 72 deletions(-) diff --git a/pkg/cmd/attestation/inspect/inspect_test.go b/pkg/cmd/attestation/inspect/inspect_test.go index 368cc54f5..29ca9aec7 100644 --- a/pkg/cmd/attestation/inspect/inspect_test.go +++ b/pkg/cmd/attestation/inspect/inspect_test.go @@ -58,7 +58,7 @@ func TestNewInspectCmd(t *testing.T) { BundlePath: bundlePath, DigestAlgorithm: "sha384", OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -70,7 +70,7 @@ func TestNewInspectCmd(t *testing.T) { BundlePath: bundlePath, DigestAlgorithm: "sha256", OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -82,7 +82,7 @@ func TestNewInspectCmd(t *testing.T) { BundlePath: bundlePath, DigestAlgorithm: "sha512", OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -93,7 +93,7 @@ func TestNewInspectCmd(t *testing.T) { ArtifactPath: artifactPath, DigestAlgorithm: "sha256", OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -105,7 +105,7 @@ func TestNewInspectCmd(t *testing.T) { BundlePath: bundlePath, DigestAlgorithm: "sha256", OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsExporter: true, }, @@ -148,7 +148,7 @@ func TestRunInspect(t *testing.T) { DigestAlgorithm: "sha512", Logger: io.NewTestHandler(), OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), } t.Run("with valid artifact and bundle", func(t *testing.T) { @@ -176,7 +176,7 @@ func TestJSONOutput(t *testing.T) { DigestAlgorithm: "sha512", Logger: io.NewHandler(testIO), OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), exporter: cmdutil.NewJSONExporter(), } require.Nil(t, runInspect(&opts)) diff --git a/pkg/cmd/attestation/verification/mock_verifier.go b/pkg/cmd/attestation/verification/mock_verifier.go index 41332dc62..44ed9405e 100644 --- a/pkg/cmd/attestation/verification/mock_verifier.go +++ b/pkg/cmd/attestation/verification/mock_verifier.go @@ -6,6 +6,7 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/api" "github.com/cli/cli/v2/pkg/cmd/attestation/test/data" + "github.com/sigstore/sigstore-go/pkg/bundle" "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" in_toto "github.com/in-toto/attestation/go/v1" @@ -13,10 +14,15 @@ import ( ) type MockSigstoreVerifier struct { - t *testing.T + t *testing.T + mockResults []*AttestationProcessingResult } -func (v *MockSigstoreVerifier) Verify(attestations []*api.Attestation, policy verify.PolicyBuilder) ([]*AttestationProcessingResult, error) { +func (v *MockSigstoreVerifier) Verify([]*api.Attestation, verify.PolicyBuilder) ([]*AttestationProcessingResult, error) { + if v.mockResults != nil { + return v.mockResults, nil + } + statement := &in_toto.Statement{} statement.PredicateType = SLSAPredicateV1 @@ -44,12 +50,51 @@ func (v *MockSigstoreVerifier) Verify(attestations []*api.Attestation, policy ve return results, nil } -func NewMockSigstoreVerifier(t *testing.T) *MockSigstoreVerifier { - return &MockSigstoreVerifier{t} +func NewMockSigstoreVerifier(t *testing.T, mockResults []*AttestationProcessingResult) *MockSigstoreVerifier { + return &MockSigstoreVerifier{t, mockResults} +} + +func NewDefaultMockSigstoreVerifier(t *testing.T) *MockSigstoreVerifier { + result := BuildDefaultMockResult(t) + results := []*AttestationProcessingResult{&result} + return &MockSigstoreVerifier{t, results} } type FailSigstoreVerifier struct{} -func (v *FailSigstoreVerifier) Verify(attestations []*api.Attestation, policy verify.PolicyBuilder) ([]*AttestationProcessingResult, error) { +func (v *FailSigstoreVerifier) Verify([]*api.Attestation, verify.PolicyBuilder) ([]*AttestationProcessingResult, error) { return nil, fmt.Errorf("failed to verify attestations") } + +func BuildMockResult(b *bundle.Bundle, buildSignerURI, sourceRepoOwnerURI, sourceRepoURI, issuer string) AttestationProcessingResult { + statement := &in_toto.Statement{} + statement.PredicateType = SLSAPredicateV1 + + return AttestationProcessingResult{ + Attestation: &api.Attestation{ + Bundle: b, + }, + VerificationResult: &verify.VerificationResult{ + Statement: statement, + Signature: &verify.SignatureVerificationResult{ + Certificate: &certificate.Summary{ + Extensions: certificate.Extensions{ + BuildSignerURI: buildSignerURI, + SourceRepositoryOwnerURI: sourceRepoOwnerURI, + SourceRepositoryURI: sourceRepoURI, + Issuer: issuer, + }, + }, + }, + }, + } +} + +func BuildDefaultMockResult(t *testing.T) AttestationProcessingResult { + bundle := data.SigstoreBundle(t) + buildSignerURI := "https://github.com/github/example/.github/workflows/release.yml@refs/heads/main" + sourceRepoOwnerURI := "https://github.com/sigstore" + sourceRepoURI := "https://github.com/sigstore/sigstore-js" + issuer := "https://token.actions.githubusercontent.com" + return BuildMockResult(bundle, buildSignerURI, sourceRepoOwnerURI, sourceRepoURI, issuer) +} diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go index 66005d62e..da4a45e2e 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -180,7 +180,7 @@ func (v *LiveSigstoreVerifier) verify(attestation *api.Attestation, policy verif // if verification fails, create the error and exit verification early if err != nil { v.config.Logger.VerbosePrint(v.config.Logger.ColorScheme.Redf( - "Failed to verify against issuer \"%s\" \n\n", issuer, + "Failed to verify against issuer \"%s\": %v\n\n", issuer, err, )) return nil, fmt.Errorf("verifying with issuer \"%s\"", issuer) diff --git a/pkg/cmd/attestation/verify/attestation.go b/pkg/cmd/attestation/verify/attestation.go index add6f80fe..bb96c9526 100644 --- a/pkg/cmd/attestation/verify/attestation.go +++ b/pkg/cmd/attestation/verify/attestation.go @@ -7,7 +7,6 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/api" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" - "github.com/sigstore/sigstore-go/pkg/verify" ) func getAttestations(o *Options, a artifact.DigestedArtifact) ([]*api.Attestation, string, error) { @@ -50,18 +49,24 @@ func getAttestations(o *Options, a artifact.DigestedArtifact) ([]*api.Attestatio return attestations, msg, nil } -func verifyAttestations(attestations []*api.Attestation, sgVerifier verification.SigstoreVerifier, sgPolicy verify.PolicyBuilder, ec verification.EnforcementCriteria) ([]*verification.AttestationProcessingResult, string, error) { - sigstoreVerified, err := sgVerifier.Verify(attestations, sgPolicy) +func verifyAttestations(art artifact.DigestedArtifact, att []*api.Attestation, sgVerifier verification.SigstoreVerifier, ec verification.EnforcementCriteria) ([]*verification.AttestationProcessingResult, string, error) { + sgPolicy, err := buildSigstoreVerifyPolicy(ec, art) if err != nil { - errMsg := "✗ Sigstore verification failed" - return nil, errMsg, err + logMsg := "✗ Failed to build Sigstore verification policy" + return nil, logMsg, err + } + + sigstoreVerified, err := sgVerifier.Verify(att, sgPolicy) + if err != nil { + logMsg := "✗ Sigstore verification failed" + return nil, logMsg, err } // Verify extensions certExtVerified, err := verification.VerifyCertExtensions(sigstoreVerified, ec) if err != nil { - errMsg := "✗ Policy verification failed" - return nil, errMsg, err + logMsg := "✗ Policy verification failed" + return nil, logMsg, err } return certExtVerified, "", nil diff --git a/pkg/cmd/attestation/verify/attestation_integration_test.go b/pkg/cmd/attestation/verify/attestation_integration_test.go index 480368519..bd358355e 100644 --- a/pkg/cmd/attestation/verify/attestation_integration_test.go +++ b/pkg/cmd/attestation/verify/attestation_integration_test.go @@ -24,10 +24,9 @@ func getAttestationsFor(t *testing.T, bundlePath string) []*api.Attestation { } func TestVerifyAttestations(t *testing.T) { - config := verification.SigstoreConfig{ + sgVerifier := verification.NewLiveSigstoreVerifier(verification.SigstoreConfig{ Logger: io.NewTestHandler(), - } - sgVerifier := verification.NewLiveSigstoreVerifier(config) + }) certSummary := certificate.Summary{} certSummary.SourceRepositoryOwnerURI = "https://github.com/sigstore" @@ -39,17 +38,16 @@ func TestVerifyAttestations(t *testing.T) { PredicateType: verification.SLSAPredicateV1, SANRegex: "^https://github.com/sigstore/", } + require.NoError(t, ec.Valid()) artifactPath := test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz") a, err := artifact.NewDigestedArtifact(nil, artifactPath, "sha512") require.NoError(t, err) - sp, err := buildSigstoreVerifyPolicy(ec, *a) - t.Run("all attestations pass verification", func(t *testing.T) { attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") require.Len(t, attestations, 2) - results, errMsg, err := verifyAttestations(attestations, sgVerifier, sp, ec) + results, errMsg, err := verifyAttestations(*a, attestations, sgVerifier, ec) require.NoError(t, err) require.Zero(t, errMsg) require.Len(t, results, 2) @@ -61,7 +59,7 @@ func TestVerifyAttestations(t *testing.T) { attestations = append(attestations, invalidBundle[0]) require.Len(t, attestations, 3) - results, errMsg, err := verifyAttestations(attestations, sgVerifier, sp, ec) + results, errMsg, err := verifyAttestations(*a, attestations, sgVerifier, ec) require.NoError(t, err) require.Zero(t, errMsg) require.Len(t, results, 2) @@ -73,40 +71,27 @@ func TestVerifyAttestations(t *testing.T) { attestations := append(invalidBundle, invalidBundle2...) require.Len(t, attestations, 2) - results, errMsg, err := verifyAttestations(attestations, sgVerifier, sp, ec) + results, errMsg, err := verifyAttestations(*a, attestations, sgVerifier, ec) require.Error(t, err) require.Contains(t, errMsg, "✗ Sigstore verification failed") require.Nil(t, results) }) t.Run("passes verification with 2/3 attestations passing cert extension verification", func(t *testing.T) { - customArtifactPath := test.NormalizeRelativePath("../test/data/reusable-workflow-artifact") - customArtifact, err := artifact.NewDigestedArtifact(nil, customArtifactPath, "sha256") - require.NoError(t, err) - - mlDemoAttestAttestation := getAttestationsFor(t, "../test/data/reusable-workflow-attestation.sigstore.json") - aareusableAttestation := getAttestationsFor(t, "../test/data/gh-artifact-attestations-workflow-bundle.json") - attestations := []*api.Attestation{mlDemoAttestAttestation[0], aareusableAttestation[0], mlDemoAttestAttestation[0]} + sgjAttestation := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") + reusableWorkflowAttestations := getAttestationsFor(t, "../test/data/reusable-workflow-attestation.sigstore.json") + attestations := []*api.Attestation{sgjAttestation[0], reusableWorkflowAttestations[0], sgjAttestation[1]} require.Len(t, attestations, 3) - certSummary := certificate.Summary{} - certSummary.SourceRepositoryOwnerURI = "https://github.com/malancas" - certSummary.Issuer = verification.GitHubOIDCIssuer + rwfResult := verification.BuildMockResult(reusableWorkflowAttestations[0].Bundle, "", "https://github.com/malancas", "", verification.GitHubOIDCIssuer) + sgjResult := verification.BuildDefaultMockResult(t) + mockResults := []*verification.AttestationProcessingResult{&sgjResult, &rwfResult, &sgjResult} - customEc := verification.EnforcementCriteria{ - Certificate: certSummary, - PredicateType: verification.SLSAPredicateV1, - SANRegex: "^https://github.com/github/artifact-attestations-workflows/", - } - esp, err := buildSigstoreVerifyPolicy(customEc, *customArtifact) - - results, errMsg, err := verifyAttestations(attestations, sgVerifier, esp, customEc) + mockSgVerifier := verification.NewMockSigstoreVerifier(t, mockResults) + results, errMsg, err := verifyAttestations(*a, attestations, mockSgVerifier, ec) require.NoError(t, err) require.Zero(t, errMsg) require.Len(t, results, 2) - for _, r := range results { - require.NotEqual(t, r.Attestation.Bundle.String(), aareusableAttestation[0].Bundle.String()) - } }) t.Run("fails verification when cert extension verification fails", func(t *testing.T) { @@ -116,7 +101,7 @@ func TestVerifyAttestations(t *testing.T) { expectedCriteria := ec expectedCriteria.Certificate.SourceRepositoryOwnerURI = "https://github.com/wrong" - results, errMsg, err := verifyAttestations(attestations, sgVerifier, sp, expectedCriteria) + results, errMsg, err := verifyAttestations(*a, attestations, sgVerifier, expectedCriteria) require.Error(t, err) require.Contains(t, errMsg, "✗ Policy verification failed") require.Nil(t, results) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 4efc23f44..5b31371ff 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -244,13 +244,7 @@ func runVerify(opts *Options) error { opts.Logger.VerbosePrintf("Verifying attestations with predicate type: %s\n", ec.PredicateType) - sp, err := buildSigstoreVerifyPolicy(ec, *artifact) - if err != nil { - opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Failed to build Sigstore verification policy")) - return err - } - - verified, errMsg, err := verifyAttestations(attestations, opts.SigstoreVerifier, sp, ec) + verified, errMsg, err := verifyAttestations(*artifact, attestations, opts.SigstoreVerifier, ec) if err != nil { opts.Logger.Println(opts.Logger.ColorScheme.Red(errMsg)) return err diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go index 9a2e9f18c..75fd18c44 100644 --- a/pkg/cmd/attestation/verify/verify_test.go +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -75,7 +75,7 @@ func TestNewVerifyCmd(t *testing.T) { OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -92,7 +92,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -109,7 +109,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://foo.ghe.com/sigstore/", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -126,7 +126,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -143,7 +143,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -159,7 +159,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -175,7 +175,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, Repo: "sigstore/sigstore-js", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -191,7 +191,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -207,7 +207,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -223,7 +223,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -240,7 +240,7 @@ func TestNewVerifyCmd(t *testing.T) { PredicateType: verification.SLSAPredicateV1, SAN: "https://github.com/sigstore/", SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -257,7 +257,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsExporter: true, }, @@ -274,7 +274,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: "https://spdx.dev/Document/v2.3", SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), }, wantsExporter: true, }, @@ -365,7 +365,7 @@ func TestJSONOutput(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "^https://github.com/sigstore/", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), exporter: cmdutil.NewJSONExporter(), } require.NoError(t, runVerify(&opts)) @@ -389,7 +389,7 @@ func TestRunVerify(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "^https://github.com/sigstore/", - SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), } t.Run("with valid artifact and bundle", func(t *testing.T) { From 7a271b008a06df8d62e60ae8e6e32f42c803303e Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 21 Nov 2024 08:58:23 -0700 Subject: [PATCH 24/52] undo change Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/sigstore.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go index da4a45e2e..66005d62e 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -180,7 +180,7 @@ func (v *LiveSigstoreVerifier) verify(attestation *api.Attestation, policy verif // if verification fails, create the error and exit verification early if err != nil { v.config.Logger.VerbosePrint(v.config.Logger.ColorScheme.Redf( - "Failed to verify against issuer \"%s\": %v\n\n", issuer, err, + "Failed to verify against issuer \"%s\" \n\n", issuer, )) return nil, fmt.Errorf("verifying with issuer \"%s\"", issuer) From 28565dc1f83f1e8c7f409faf90b8cfbcc64ee2ce Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 21 Nov 2024 08:58:55 -0700 Subject: [PATCH 25/52] remove unused test file Signed-off-by: Meredith Lancaster --- ...artifact-attestations-workflow-bundle.json | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 pkg/cmd/attestation/test/data/gh-artifact-attestations-workflow-bundle.json diff --git a/pkg/cmd/attestation/test/data/gh-artifact-attestations-workflow-bundle.json b/pkg/cmd/attestation/test/data/gh-artifact-attestations-workflow-bundle.json deleted file mode 100644 index ca17f4101..000000000 --- a/pkg/cmd/attestation/test/data/gh-artifact-attestations-workflow-bundle.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", - "verificationMaterial": { - "tlogEntries": [ - { - "logIndex": "129027678", - "logId": { - "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" - }, - "kindVersion": { - "kind": "dsse", - "version": "0.0.1" - }, - "integratedTime": "1725924650", - "inclusionPromise": { - "signedEntryTimestamp": "MEQCICZHyKBtXwvRDYWke7Pp0KfrBL8YNLoDHGP7lKFgkjGbAiB6V+IfSg/SYV/ySXOUg+t/Wp8wGjXMkKWnzPT9DWNsZA==" - }, - "inclusionProof": { - "logIndex": "7123416", - "rootHash": "LZ7MbkKAnq4sU1EMcWwJWWhw778o2w5vO6+Op/DFwm4=", - "treeSize": "7123418", - "hashes": [ - "pJMeUZ4R2LEEjf0JSmsoXTfHMgBAah4aOPBdXEa9sxo=", - "4f/Xq7IIiBbWQbSZZLkEBCSuOIbUXSvzYfxTIqh9rjY=", - "gy7fFADBSd5e37RIZy86inqhsnYB29bTUxY6/EtlJDk=", - "8JY4XnAVf8weXfLSChGSEbqVSN7FKSapmaM5Xi+qowM=", - "FKlHhO4TMH2pnrZUvSKA7Drig5MbABFy2KZx4esRxJY=", - "T+Ziyo74JC0j3MWEgjiiGuTCQ0w+AzLft+r9OyRldMw=", - "naRDgCL1Ch0MNzrXrAmrV1PLa/Bi5HV5GqrqlUceCVI=", - "c4TDdYxGB0ihJtrnXDSynGSSI83D5WVHvZJxuMti4Xg=", - "bcPqJfBdq24AxJvo1LAJKwcudDBLIIyVclqFzJW5TEY=", - "7Dvc6Q8qiduX8Oka5vLLU5oWAmybo8oaecNXPgkOQvA=", - "LAdu5Ynz/wk2uMNazU0CVickvA3YhhBz6TpIl5brTko=", - "uRsmea7eVXshBNN6huh/owmfaAy9Rx4Cq2M2vFb2Ntk=", - "NeHKGVl6KVXfx3+wnQrIrxra4Pr9Fa7YDpTlf86mlTc=" - ], - "checkpoint": { - "envelope": "rekor.sigstore.dev - 1193050959916656506\n7123418\nLZ7MbkKAnq4sU1EMcWwJWWhw778o2w5vO6+Op/DFwm4=\n\n— rekor.sigstore.dev wNI9ajBGAiEAy5505Eq4vOyji5LLmRbaN4/eqwOlVpgLOnozcVCJWvoCIQD+/CNPBY1eyyNypq25OIDwTVIHVxroif3cf2MsfEfplw==\n" - } - }, - "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiYzk2YzgzYjE5NTAwOTJmOWEzYjc4NWE4ZWFlM2Y4MDY4YzcwYWQyZWJlZTFhNTRhYmIxZmJmYzk0ZTg1YzA3NSJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImZmZTQ0OTYwMWI0ZmJjODQ4ODE0MzZkMzhlZjE4ZGRhNGI4NWM4YWY0NGI3YzM3ZmQwZTRhNGZhZmI0MGM2MzgifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVVQ0lRQ0FPQmRJT3M2MnR1TzQybDhBWGNyK2t5ampDa1VqMTZRZUlwWkpkUjM4SWdJZ0x3YWlLdlg0RU9IclEvVFczOFQ4UTJiRzBvYkZ2b3NDNUlqYTFMbDlJNU09IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VoWmFrTkRRblZsWjBGM1NVSkJaMGxWWVRkNWJUSktNa1kyYVd0c1ZtdE1jMGhTUzNaVU9YTnZiR3BWZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwUmQwOVVRVFZOYWsxNlRVUlZkMWRvWTA1TmFsRjNUMVJCTlUxcVRUQk5SRlYzVjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVlFjekpOVmpNeU9HRkdRM2gyVWxKelFuQTNkR2R2WVRVcmJuUTFTbmR0TkdrM2Frc0tNR0ZUTUZFM2JVMDFZblpZZUZwdmRFdGtLMWxwTmpSMEswaFZOaXN4WnpWVlJIZzVNRFJJVjFCSlRUQkROemhwVUdGUFEwSm5XWGRuWjFsRFRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVm1iMmg2Q2s1bFRXMUNkMW9yUm5rMFN6aE5aU3MwVlc5aVkxcEpkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMlZuV1VSV1VqQlNRVkZJTDBKSVFYZGliMXB6WVVoU01HTklUVFpNZVRsdVlWaFNiMlJYU1hWWk1qbDBUREprY0dSSGFERlphVGxvWTI1U2NBcGFiVVpxWkVNeGFHUklVbXhqTTFKb1pFZHNkbUp1VFhSa01qbDVZVEphYzJJelpIcE1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU1tUnZDbGt6U1hSalNGWnBZa2RzZW1GRE5UVmlWM2hCWTIxV2JXTjVPVzlhVjBaclkzazVkRmxYYkhWTlJHdEhRMmx6UjBGUlVVSm5OemgzUVZGRlJVc3lhREFLWkVoQ2VrOXBPSFprUnpseVdsYzBkVmxYVGpCaFZ6bDFZM2sxYm1GWVVtOWtWMG94WXpKV2VWa3lPWFZrUjFaMVpFTTFhbUl5TUhkSWQxbExTM2RaUWdwQ1FVZEVkbnBCUWtGblVWSmtNamw1WVRKYWMySXpaR1phUjJ4NlkwZEdNRmt5WjNkT1oxbExTM2RaUWtKQlIwUjJla0ZDUVhkUmIxa3lUVE5QUjAwMUNsa3lVVEZOUjAweldXMUdhbHBVV21wTk1rcHNUWHBhYVZsdFVYbE5iVmw1V21wR2FVNVhXVFJOVkdkNVdXcEJlVUpuYjNKQ1owVkZRVmxQTDAxQlJVVUtRa05TUW1SSVVteGpNMUZuVTFjeGFGb3lWV2RMUldSSlNVVk9kbUp1VW1oaFZ6VnNZMmxDVTFwWFpIQmpNMUo1WlZOcmQwNUJXVXRMZDFsQ1FrRkhSQXAyZWtGQ1FsRlJiVm95YkRCaFNGWnBUREpHZVdSSGJHMVpWMDR3VEZkR01HUkhWbnBrUjBZd1lWYzVkV041TVROaU0wcHlXbTE0ZG1RelRYZElVVmxMQ2t0M1dVSkNRVWRFZG5wQlFrSm5VVkJqYlZadFkzazViMXBYUm10amVUbDBXVmRzZFUxRWMwZERhWE5IUVZGUlFtYzNPSGRCVVdkRlRGRjNjbUZJVWpBS1kwaE5Oa3g1T1RCaU1uUnNZbWsxYUZrelVuQmlNalY2VEcxa2NHUkhhREZaYmxaNldsaEthbUl5TlRCYVZ6VXdURzFPZG1KVVFqaENaMjl5UW1kRlJRcEJXVTh2VFVGRlNrSkhORTFpUjJnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemx1WVZoU2IyUlhTWFpaV0Vvd1lWZGFhRmt6VVhSWldGSXdDbHBZVGpCWldGSndZakkxZWt4WVpIWmpiWFJ0WWtjNU0yTjVPSFZhTW13d1lVaFdhVXd6WkhaamJYUnRZa2M1TTJONU9XNWhSMDU1VEZoQ01WbHRlSEFLWXpKbmRXVlhNWE5SU0Vwc1dtNU5kbUZIVm1oYVNFMTJZbGRHY0dKcVFUUkNaMjl5UW1kRlJVRlpUeTlOUVVWTFFrTnZUVXRIVG1wT2VtaHFUMWRPYXdwT1ZFSnFUakpLYUZreVZUSlplazVwV2xSTk1sbHRTbXROYWtwdFRXMVplRmxxVm0xUFJFVTBUVzFKZDBoUldVdExkMWxDUWtGSFJIWjZRVUpEZDFGUUNrUkJNVzVoV0ZKdlpGZEpkR0ZIT1hwa1IxWnJUVVZyUjBOcGMwZEJVVkZDWnpjNGQwRlJkMFZQZDNjMVlVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVUtXVEk1ZEV3eVpIQmtSMmd4V1drNWFHTnVVbkJhYlVacVpFTXhhR1JJVW14ak0xSm9aRWRzZG1KdVRYUmtNamw1WVRKYWMySXpaSHBOUkdkSFEybHpSd3BCVVZGQ1p6YzRkMEZSTUVWTFozZHZXVEpOTTA5SFRUVlpNbEV4VFVkTk0xbHRSbXBhVkZwcVRUSktiRTE2V21sWmJWRjVUVzFaZVZwcVJtbE9WMWswQ2sxVVozbFpha0ZtUW1kdmNrSm5SVVZCV1U4dlRVRkZUMEpDUlUxRU0wcHNXbTVOZG1GSFZtaGFTRTEyWWxkR2NHSnFRVnBDWjI5eVFtZEZSVUZaVHk4S1RVRkZVRUpCYzAxRFZHZDNUa1JSTWs5RVJYbFBWRUZ3UW1kdmNrSm5SVVZCV1U4dlRVRkZVVUpDYzAxSFYyZ3daRWhDZWs5cE9IWmFNbXd3WVVoV2FRcE1iVTUyWWxNNWJtRllVbTlrVjBsM1JrRlpTMHQzV1VKQ1FVZEVkbnBCUWtWUlVVZEVRVkUxVDFSRk5VMUlkMGREYVhOSFFWRlJRbWMzT0hkQlVrbEZDbUpuZUhOaFNGSXdZMGhOTmt4NU9XNWhXRkp2WkZkSmRWa3lPWFJNTW1Sd1pFZG9NVmxwT1doamJsSndXbTFHYW1SRE1XaGtTRkpzWXpOU2FHUkhiSFlLWW01TmRHUXlPWGxoTWxwellqTmtla3g1Tlc1aFdGSnZaRmRKZG1ReU9YbGhNbHB6WWpOa2Vrd3laRzlaTTBsMFkwaFdhV0pIYkhwaFF6VTFZbGQ0UVFwamJWWnRZM2s1YjFwWFJtdGplVGwwV1Zkc2RVMUVaMGREYVhOSFFWRlJRbWMzT0hkQlVrMUZTMmQzYjFreVRUTlBSMDAxV1RKUk1VMUhUVE5aYlVacUNscFVXbXBOTWtwc1RYcGFhVmx0VVhsTmJWbDVXbXBHYVU1WFdUUk5WR2Q1V1dwQmFFSm5iM0pDWjBWRlFWbFBMMDFCUlZWQ1FrMU5SVmhrZG1OdGRHMEtZa2M1TTFneVVuQmpNMEpvWkVkT2IwMUhNRWREYVhOSFFWRlJRbWMzT0hkQlVsVkZXSGQ0WkdGSVVqQmpTRTAyVEhrNWJtRllVbTlrVjBsMVdUSTVkQXBNTW1Sd1pFZG9NVmxwT1doamJsSndXbTFHYW1SRE1XaGtTRkpzWXpOU2FHUkhiSFppYmsxMFpESTVlV0V5V25OaU0yUjZUREpHYW1SSGJIWmliazEyQ21OdVZuVmplVGg0VFVSak5FMXFhM2hPVkVWNFRsTTVhR1JJVW14aVdFSXdZM2s0ZUUxQ1dVZERhWE5IUVZGUlFtYzNPSGRCVWxsRlEwRjNSMk5JVm1rS1lrZHNhazFKUjBwQ1oyOXlRbWRGUlVGa1dqVkJaMUZEUWtoelJXVlJRak5CU0ZWQk0xUXdkMkZ6WWtoRlZFcHFSMUkwWTIxWFl6TkJjVXBMV0hKcVpRcFFTek12YURSd2VXZERPSEEzYnpSQlFVRkhVakpUUVhaQlVVRkJRa0ZOUVZKcVFrVkJhVUp6UTBKUlZtTlBOVmxrZDBsQ01VNXBSVVJxU1V0aFJXdG5DblpvUm1OQ1pGWTJVbE5wTjFNM05qVlBaMGxuWWtoeldXMTVUVGRuU2xWaWFtMXNSRFIxT0M5cVdtWjFNbEZrZGpKYU1rWmFVVzQ1YUVGUFkyNWtOSGNLUTJkWlNVdHZXa2w2YWpCRlFYZE5SR0ZSUVhkYVowbDRRVWx2Wmsxd2FFMU9RbU5ZYVhGRE0wNXZZWFpWYm14d04wOVBlSE5ZTkhoUVUzcGhZbWRtVFFwc2N6Z3JiazVuTUhsRWEwSnVaazlQZVZwblZVcHFieXNyWjBsNFFVcDZOek5qTlZnMFZERm5PRmx4YWs1VVNtMUZjWG8yYW5WbFlYcEJTVzVZYTA5TUNsSmFlV0ZCYVc1RFNsVnVRVVEwVUU1SVpucEZWV0UwTDFKTGRrazNRVDA5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn1dfX0=" - } - ], - "timestampVerificationData": { - }, - "certificate": { - "rawBytes": "MIIHYjCCBuegAwIBAgIUa7ym2J2F6iklVkLsHRKvT9soljUwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwOTA5MjMzMDUwWhcNMjQwOTA5MjM0MDUwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPs2MV328aFCxvRRsBp7tgoa5+nt5Jwm4i7jK0aS0Q7mM5bvXxZotKd+Yi64t+HU6+1g5UDx904HWPIM0C78iPaOCBgYwggYCMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUfohzNeMmBwZ+Fy4K8Me+4UobcZIwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wegYDVR0RAQH/BHAwboZsaHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9hcnRpZmFjdC1hdHRlc3RhdGlvbnMtd29ya2Zsb3dzLy5naXRodWIvd29ya2Zsb3dzL2doY3ItcHVibGlzaC55bWxAcmVmcy9oZWFkcy9tYWluMDkGCisGAQQBg78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wHwYKKwYBBAGDvzABAgQRd29ya2Zsb3dfZGlzcGF0Y2gwNgYKKwYBBAGDvzABAwQoY2M3OGM5Y2Q1MGM3YmFjZTZjM2JlMzZiYmQyMmYyZjFiNWY4MTgyYjAyBgorBgEEAYO/MAEEBCRBdHRlc3QgSW1hZ2UgKEdIIENvbnRhaW5lciBSZWdpc3RyeSkwNAYKKwYBBAGDvzABBQQmZ2l0aHViL2FydGlmYWN0LWF0dGVzdGF0aW9ucy13b3JrZmxvd3MwHQYKKwYBBAGDvzABBgQPcmVmcy9oZWFkcy9tYWluMDsGCisGAQQBg78wAQgELQwraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTB8BgorBgEEAYO/MAEJBG4MbGh0dHBzOi8vZ2l0aHViLmNvbS9naXRodWIvYXJ0aWZhY3QtYXR0ZXN0YXRpb25zLXdvcmtmbG93cy8uZ2l0aHViL3dvcmtmbG93cy9naGNyLXB1Ymxpc2gueW1sQHJlZnMvaGVhZHMvbWFpbjA4BgorBgEEAYO/MAEKBCoMKGNjNzhjOWNkNTBjN2JhY2U2YzNiZTM2YmJkMjJmMmYxYjVmODE4MmIwHQYKKwYBBAGDvzABCwQPDA1naXRodWItaG9zdGVkMEkGCisGAQQBg78wAQwEOww5aHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9hcnRpZmFjdC1hdHRlc3RhdGlvbnMtd29ya2Zsb3dzMDgGCisGAQQBg78wAQ0EKgwoY2M3OGM5Y2Q1MGM3YmFjZTZjM2JlMzZiYmQyMmYyZjFiNWY4MTgyYjAfBgorBgEEAYO/MAEOBBEMD3JlZnMvaGVhZHMvbWFpbjAZBgorBgEEAYO/MAEPBAsMCTgwNDQ2ODEyOTApBgorBgEEAYO/MAEQBBsMGWh0dHBzOi8vZ2l0aHViLmNvbS9naXRodWIwFAYKKwYBBAGDvzABEQQGDAQ5OTE5MHwGCisGAQQBg78wARIEbgxsaHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9hcnRpZmFjdC1hdHRlc3RhdGlvbnMtd29ya2Zsb3dzLy5naXRodWIvd29ya2Zsb3dzL2doY3ItcHVibGlzaC55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoY2M3OGM5Y2Q1MGM3YmFjZTZjM2JlMzZiYmQyMmYyZjFiNWY4MTgyYjAhBgorBgEEAYO/MAEUBBMMEXdvcmtmbG93X2Rpc3BhdGNoMG0GCisGAQQBg78wARUEXwxdaHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9hcnRpZmFjdC1hdHRlc3RhdGlvbnMtd29ya2Zsb3dzL2FjdGlvbnMvcnVucy8xMDc4MjkxNTExNS9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwGcHVibGljMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGR2SAvAQAABAMARjBEAiBsCBQVcO5YdwIB1NiEDjIKaEkgvhFcBdV6RSi7S765OgIgbHsYmyM7gJUbjmlD4u8/jZfu2Qdv2Z2FZQn9hAOcnd4wCgYIKoZIzj0EAwMDaQAwZgIxAIofMphMNBcXiqC3NoavUnlp7OOxsX4xPSzabgfMls8+nNg0yDkBnfOOyZgUJjo++gIxAJz73c5X4T1g8YqjNTJmEqz6jueazAInXkOLRZyaAinCJUnAD4PNHfzEUa4/RKvI7A==" - } - }, - "dsseEnvelope": { - "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiZ2hjci5pby9naXRodWIvYXJ0aWZhY3QtYXR0ZXN0YXRpb25zLXdvcmtmbG93cyIsImRpZ2VzdCI6eyJzaGEyNTYiOiIwNmU2ODE4MzI0Y2IyMGUxZWE2ZjIyZGE3ZmI5MjUzMjNmYjU3MjUxYzk4NzY4Yzg4YjkzODcyYjUwNTkwZjkyIn19XSwicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vc2xzYS5kZXYvcHJvdmVuYW5jZS92MSIsInByZWRpY2F0ZSI6eyJidWlsZERlZmluaXRpb24iOnsiYnVpbGRUeXBlIjoiaHR0cHM6Ly9hY3Rpb25zLmdpdGh1Yi5pby9idWlsZHR5cGVzL3dvcmtmbG93L3YxIiwiZXh0ZXJuYWxQYXJhbWV0ZXJzIjp7IndvcmtmbG93Ijp7InJlZiI6InJlZnMvaGVhZHMvbWFpbiIsInJlcG9zaXRvcnkiOiJodHRwczovL2dpdGh1Yi5jb20vZ2l0aHViL2FydGlmYWN0LWF0dGVzdGF0aW9ucy13b3JrZmxvd3MiLCJwYXRoIjoiLmdpdGh1Yi93b3JrZmxvd3MvZ2hjci1wdWJsaXNoLnltbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoid29ya2Zsb3dfZGlzcGF0Y2giLCJyZXBvc2l0b3J5X2lkIjoiODA0NDY4MTI5IiwicmVwb3NpdG9yeV9vd25lcl9pZCI6Ijk5MTkiLCJydW5uZXJfZW52aXJvbm1lbnQiOiJnaXRodWItaG9zdGVkIn19LCJyZXNvbHZlZERlcGVuZGVuY2llcyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9naXRodWIvYXJ0aWZhY3QtYXR0ZXN0YXRpb25zLXdvcmtmbG93c0ByZWZzL2hlYWRzL21haW4iLCJkaWdlc3QiOnsiZ2l0Q29tbWl0IjoiY2M3OGM5Y2Q1MGM3YmFjZTZjM2JlMzZiYmQyMmYyZjFiNWY4MTgyYiJ9fV19LCJydW5EZXRhaWxzIjp7ImJ1aWxkZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vZ2l0aHViL2FydGlmYWN0LWF0dGVzdGF0aW9ucy13b3JrZmxvd3MvLmdpdGh1Yi93b3JrZmxvd3MvZ2hjci1wdWJsaXNoLnltbEByZWZzL2hlYWRzL21haW4ifSwibWV0YWRhdGEiOnsiaW52b2NhdGlvbklkIjoiaHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9hcnRpZmFjdC1hdHRlc3RhdGlvbnMtd29ya2Zsb3dzL2FjdGlvbnMvcnVucy8xMDc4MjkxNTExNS9hdHRlbXB0cy8xIn19fX0=", - "payloadType": "application/vnd.in-toto+json", - "signatures": [ - { - "sig": "MEUCIQCAOBdIOs62tuO42l8AXcr+kyjjCkUj16QeIpZJdR38IgIgLwaiKvX4EOHrQ/TW38T8Q2bG0obFvosC5Ija1Ll9I5M=" - } - ] - } -} \ No newline at end of file From 2d41225dd557abfc8c4bfa89937146aaff1dc56e Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 21 Nov 2024 09:11:18 -0700 Subject: [PATCH 26/52] pr feedback Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/extensions.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/attestation/verification/extensions.go b/pkg/cmd/attestation/verification/extensions.go index 4f70a1c2e..cc21d5ea0 100644 --- a/pkg/cmd/attestation/verification/extensions.go +++ b/pkg/cmd/attestation/verification/extensions.go @@ -18,8 +18,7 @@ func VerifyCertExtensions(results []*AttestationProcessingResult, ec Enforcement return nil, errors.New("no attestations processing results") } - verified := make([]*AttestationProcessingResult, len(results)) - var verifyCount int + verified := make([]*AttestationProcessingResult, 0, len(results)) var lastErr error for _, attestation := range results { if err := verifyCertExtensions(*attestation.VerificationResult.Signature.Certificate, ec.Certificate); err != nil { @@ -28,20 +27,16 @@ func VerifyCertExtensions(results []*AttestationProcessingResult, ec Enforcement continue } // otherwise, add the result to the results slice and increment verifyCount - verified[verifyCount] = attestation - verifyCount++ + verified = append(verified, attestation) } // if we have exited the for loop without verifying any attestations, // return the last error found - if verifyCount == 0 { + if len(verified) == 0 { return nil, lastErr } - // truncate the verified slice to only include verified attestations - verified = verified[:verifyCount] return verified, nil - } func verifyCertExtensions(given, expected certificate.Summary) error { From 677ed2cdcfd6cefef65f1a59a6f44b9a8f0ba1c4 Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:20:30 -0700 Subject: [PATCH 27/52] Refactor command documentation to use heredoc --- pkg/cmd/cache/delete/delete.go | 24 ++++++++++++------------ pkg/cmd/codespace/rebuild.go | 10 +++++++--- pkg/cmd/repo/delete/delete.go | 13 ++++++++----- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/pkg/cmd/cache/delete/delete.go b/pkg/cmd/cache/delete/delete.go index 4794d1fbe..1835d87b1 100644 --- a/pkg/cmd/cache/delete/delete.go +++ b/pkg/cmd/cache/delete/delete.go @@ -35,23 +35,23 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co cmd := &cobra.Command{ Use: "delete [| | --all]", Short: "Delete GitHub Actions caches", - Long: ` - Delete GitHub Actions caches. + Long: heredoc.Doc(` + Delete GitHub Actions caches. - Deletion requires authorization with the "repo" scope. -`, + Deletion requires authorization with the "repo" scope. + `), Example: heredoc.Doc(` - # Delete a cache by id - $ gh cache delete 1234 + # Delete a cache by id + $ gh cache delete 1234 - # Delete a cache by key - $ gh cache delete cache-key + # Delete a cache by key + $ gh cache delete cache-key - # Delete a cache by id in a specific repo - $ gh cache delete 1234 --repo cli/cli + # Delete a cache by id in a specific repo + $ gh cache delete 1234 --repo cli/cli - # Delete all caches - $ gh cache delete --all + # Delete all caches + $ gh cache delete --all `), Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/pkg/cmd/codespace/rebuild.go b/pkg/cmd/codespace/rebuild.go index 93d43bf29..565406edc 100644 --- a/pkg/cmd/codespace/rebuild.go +++ b/pkg/cmd/codespace/rebuild.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/codespaces" "github.com/cli/cli/v2/internal/codespaces/api" "github.com/cli/cli/v2/internal/codespaces/portforwarder" @@ -20,9 +21,12 @@ func newRebuildCmd(app *App) *cobra.Command { rebuildCmd := &cobra.Command{ Use: "rebuild", Short: "Rebuild a codespace", - Long: `Rebuilding recreates your codespace. Your code and any current changes will be -preserved. Your codespace will be rebuilt using your working directory's -dev container. A full rebuild also removes cached Docker images.`, + Long: heredoc.Doc(` + Rebuilding recreates your codespace. + + Your code and any current changes will be preserved. Your codespace will be rebuilt using + your working directory's dev container. A full rebuild also removes cached Docker images. + `), Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return app.Rebuild(cmd.Context(), selector, fullRebuild) diff --git a/pkg/cmd/repo/delete/delete.go b/pkg/cmd/repo/delete/delete.go index 722d748b9..e6fbf6363 100644 --- a/pkg/cmd/repo/delete/delete.go +++ b/pkg/cmd/repo/delete/delete.go @@ -6,6 +6,7 @@ import ( "net/http" "strings" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmdutil" @@ -38,12 +39,14 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co cmd := &cobra.Command{ Use: "delete []", Short: "Delete a repository", - Long: `Delete a GitHub repository. + Long: heredoc.Doc(` + Delete a GitHub repository. + + With no argument, deletes the current repository. Otherwise, deletes the specified repository. -With no argument, deletes the current repository. Otherwise, deletes the specified repository. - -Deletion requires authorization with the "delete_repo" scope. -To authorize, run "gh auth refresh -s delete_repo"`, + Deletion requires authorization with the "delete_repo" scope. + To authorize, run "gh auth refresh -s delete_repo" + `), Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { From 74f13a9b4f8e735a14ca227a21c31231db4fc41c Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:55:35 -0700 Subject: [PATCH 28/52] Apply suggestions from code review Co-authored-by: Andy Feller --- pkg/cmd/cache/delete/delete.go | 6 +++--- pkg/cmd/repo/delete/delete.go | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/cache/delete/delete.go b/pkg/cmd/cache/delete/delete.go index 1835d87b1..65a9d696a 100644 --- a/pkg/cmd/cache/delete/delete.go +++ b/pkg/cmd/cache/delete/delete.go @@ -35,11 +35,11 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co cmd := &cobra.Command{ Use: "delete [| | --all]", Short: "Delete GitHub Actions caches", - Long: heredoc.Doc(` + Long: heredoc.Docf(` Delete GitHub Actions caches. - Deletion requires authorization with the "repo" scope. - `), + Deletion requires authorization with the %[1]srepo%[1]s scope. + `, "`"), Example: heredoc.Doc(` # Delete a cache by id $ gh cache delete 1234 diff --git a/pkg/cmd/repo/delete/delete.go b/pkg/cmd/repo/delete/delete.go index e6fbf6363..7c6476f1d 100644 --- a/pkg/cmd/repo/delete/delete.go +++ b/pkg/cmd/repo/delete/delete.go @@ -39,14 +39,14 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co cmd := &cobra.Command{ Use: "delete []", Short: "Delete a repository", - Long: heredoc.Doc(` + Long: heredoc.Docf(` Delete a GitHub repository. With no argument, deletes the current repository. Otherwise, deletes the specified repository. - Deletion requires authorization with the "delete_repo" scope. - To authorize, run "gh auth refresh -s delete_repo" - `), + Deletion requires authorization with the %[1]sdelete_repo%[1]s scope. + To authorize, run %[1]sgh auth refresh -s delete_repo%[1]s + `, "`"), Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { From fed3c8142c92c81a8f666b312e65f1da4aac53b7 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 21 Nov 2024 15:20:44 -0700 Subject: [PATCH 29/52] Update pkg/cmd/attestation/verify/attestation_integration_test.go Co-authored-by: Phill MV --- pkg/cmd/attestation/verify/attestation_integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verify/attestation_integration_test.go b/pkg/cmd/attestation/verify/attestation_integration_test.go index bd358355e..64d02984d 100644 --- a/pkg/cmd/attestation/verify/attestation_integration_test.go +++ b/pkg/cmd/attestation/verify/attestation_integration_test.go @@ -77,7 +77,7 @@ func TestVerifyAttestations(t *testing.T) { require.Nil(t, results) }) - t.Run("passes verification with 2/3 attestations passing cert extension verification", func(t *testing.T) { + t.Run("attestations fail to verify when cert extensions don't match enforcement criteria", func(t *testing.T) { sgjAttestation := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") reusableWorkflowAttestations := getAttestationsFor(t, "../test/data/reusable-workflow-attestation.sigstore.json") attestations := []*api.Attestation{sgjAttestation[0], reusableWorkflowAttestations[0], sgjAttestation[1]} From 0fd09eb5ffe42fa780979f134ab9e42fa41e2a2d Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 21 Nov 2024 15:30:41 -0700 Subject: [PATCH 30/52] pr feedback Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/extensions.go | 1 + .../attestation/verify/attestation_integration_test.go | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verification/extensions.go b/pkg/cmd/attestation/verification/extensions.go index cc21d5ea0..2958408d0 100644 --- a/pkg/cmd/attestation/verification/extensions.go +++ b/pkg/cmd/attestation/verification/extensions.go @@ -13,6 +13,7 @@ var ( GitHubTenantOIDCIssuer = "https://token.actions.%s.ghe.com" ) +// VerifyCertExtensions allows us to perform case insensitive comparisons of certificate extensions func VerifyCertExtensions(results []*AttestationProcessingResult, ec EnforcementCriteria) ([]*AttestationProcessingResult, error) { if len(results) == 0 { return nil, errors.New("no attestations processing results") diff --git a/pkg/cmd/attestation/verify/attestation_integration_test.go b/pkg/cmd/attestation/verify/attestation_integration_test.go index 64d02984d..f07fb2c1e 100644 --- a/pkg/cmd/attestation/verify/attestation_integration_test.go +++ b/pkg/cmd/attestation/verify/attestation_integration_test.go @@ -86,12 +86,20 @@ func TestVerifyAttestations(t *testing.T) { rwfResult := verification.BuildMockResult(reusableWorkflowAttestations[0].Bundle, "", "https://github.com/malancas", "", verification.GitHubOIDCIssuer) sgjResult := verification.BuildDefaultMockResult(t) mockResults := []*verification.AttestationProcessingResult{&sgjResult, &rwfResult, &sgjResult} - mockSgVerifier := verification.NewMockSigstoreVerifier(t, mockResults) + + // we want to test that attestations that pass Sigstore verification but fail + // cert extension verification are filtered out properly in the second step + // in verifyAttestations. By using a mock Sigstore verifier, we can ensure + // that the call to verification.VerifyCertExtensions in verifyAttestations + // is filtering out attestations as expected results, errMsg, err := verifyAttestations(*a, attestations, mockSgVerifier, ec) require.NoError(t, err) require.Zero(t, errMsg) require.Len(t, results, 2) + for _, result := range results { + require.NotEqual(t, result.Attestation.Bundle, reusableWorkflowAttestations[0].Bundle) + } }) t.Run("fails verification when cert extension verification fails", func(t *testing.T) { From f92d7035541bfa62eb33488adc047ce81215b1d3 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 21 Nov 2024 15:40:15 -0700 Subject: [PATCH 31/52] pr feedback Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/inspect/inspect_test.go | 14 ++++----- .../attestation/verification/mock_verifier.go | 15 +++++----- .../verify/attestation_integration_test.go | 4 +-- pkg/cmd/attestation/verify/verify_test.go | 30 +++++++++---------- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/pkg/cmd/attestation/inspect/inspect_test.go b/pkg/cmd/attestation/inspect/inspect_test.go index 29ca9aec7..368cc54f5 100644 --- a/pkg/cmd/attestation/inspect/inspect_test.go +++ b/pkg/cmd/attestation/inspect/inspect_test.go @@ -58,7 +58,7 @@ func TestNewInspectCmd(t *testing.T) { BundlePath: bundlePath, DigestAlgorithm: "sha384", OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -70,7 +70,7 @@ func TestNewInspectCmd(t *testing.T) { BundlePath: bundlePath, DigestAlgorithm: "sha256", OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -82,7 +82,7 @@ func TestNewInspectCmd(t *testing.T) { BundlePath: bundlePath, DigestAlgorithm: "sha512", OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -93,7 +93,7 @@ func TestNewInspectCmd(t *testing.T) { ArtifactPath: artifactPath, DigestAlgorithm: "sha256", OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -105,7 +105,7 @@ func TestNewInspectCmd(t *testing.T) { BundlePath: bundlePath, DigestAlgorithm: "sha256", OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsExporter: true, }, @@ -148,7 +148,7 @@ func TestRunInspect(t *testing.T) { DigestAlgorithm: "sha512", Logger: io.NewTestHandler(), OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), } t.Run("with valid artifact and bundle", func(t *testing.T) { @@ -176,7 +176,7 @@ func TestJSONOutput(t *testing.T) { DigestAlgorithm: "sha512", Logger: io.NewHandler(testIO), OCIClient: oci.MockClient{}, - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), exporter: cmdutil.NewJSONExporter(), } require.Nil(t, runInspect(&opts)) diff --git a/pkg/cmd/attestation/verification/mock_verifier.go b/pkg/cmd/attestation/verification/mock_verifier.go index 44ed9405e..be828e66f 100644 --- a/pkg/cmd/attestation/verification/mock_verifier.go +++ b/pkg/cmd/attestation/verification/mock_verifier.go @@ -50,14 +50,15 @@ func (v *MockSigstoreVerifier) Verify([]*api.Attestation, verify.PolicyBuilder) return results, nil } -func NewMockSigstoreVerifier(t *testing.T, mockResults []*AttestationProcessingResult) *MockSigstoreVerifier { - return &MockSigstoreVerifier{t, mockResults} +func NewMockSigstoreVerifier(t *testing.T) *MockSigstoreVerifier { + result := BuildSigstoreJsMockResult(t) + results := []*AttestationProcessingResult{&result} + + return &MockSigstoreVerifier{t, results} } -func NewDefaultMockSigstoreVerifier(t *testing.T) *MockSigstoreVerifier { - result := BuildDefaultMockResult(t) - results := []*AttestationProcessingResult{&result} - return &MockSigstoreVerifier{t, results} +func NewMockSigstoreVerifierWithMockResults(t *testing.T, mockResults []*AttestationProcessingResult) *MockSigstoreVerifier { + return &MockSigstoreVerifier{t, mockResults} } type FailSigstoreVerifier struct{} @@ -90,7 +91,7 @@ func BuildMockResult(b *bundle.Bundle, buildSignerURI, sourceRepoOwnerURI, sourc } } -func BuildDefaultMockResult(t *testing.T) AttestationProcessingResult { +func BuildSigstoreJsMockResult(t *testing.T) AttestationProcessingResult { bundle := data.SigstoreBundle(t) buildSignerURI := "https://github.com/github/example/.github/workflows/release.yml@refs/heads/main" sourceRepoOwnerURI := "https://github.com/sigstore" diff --git a/pkg/cmd/attestation/verify/attestation_integration_test.go b/pkg/cmd/attestation/verify/attestation_integration_test.go index f07fb2c1e..caa02281f 100644 --- a/pkg/cmd/attestation/verify/attestation_integration_test.go +++ b/pkg/cmd/attestation/verify/attestation_integration_test.go @@ -84,9 +84,9 @@ func TestVerifyAttestations(t *testing.T) { require.Len(t, attestations, 3) rwfResult := verification.BuildMockResult(reusableWorkflowAttestations[0].Bundle, "", "https://github.com/malancas", "", verification.GitHubOIDCIssuer) - sgjResult := verification.BuildDefaultMockResult(t) + sgjResult := verification.BuildSigstoreJsMockResult(t) mockResults := []*verification.AttestationProcessingResult{&sgjResult, &rwfResult, &sgjResult} - mockSgVerifier := verification.NewMockSigstoreVerifier(t, mockResults) + mockSgVerifier := verification.NewMockSigstoreVerifierWithMockResults(t, mockResults) // we want to test that attestations that pass Sigstore verification but fail // cert extension verification are filtered out properly in the second step diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go index 75fd18c44..9a2e9f18c 100644 --- a/pkg/cmd/attestation/verify/verify_test.go +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -75,7 +75,7 @@ func TestNewVerifyCmd(t *testing.T) { OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -92,7 +92,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -109,7 +109,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://foo.ghe.com/sigstore/", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -126,7 +126,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -143,7 +143,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -159,7 +159,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -175,7 +175,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, Repo: "sigstore/sigstore-js", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -191,7 +191,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -207,7 +207,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: false, }, @@ -223,7 +223,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -240,7 +240,7 @@ func TestNewVerifyCmd(t *testing.T) { PredicateType: verification.SLSAPredicateV1, SAN: "https://github.com/sigstore/", SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsErr: true, }, @@ -257,7 +257,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsExporter: true, }, @@ -274,7 +274,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", PredicateType: "https://spdx.dev/Document/v2.3", SANRegex: "(?i)^https://github.com/sigstore/", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), }, wantsExporter: true, }, @@ -365,7 +365,7 @@ func TestJSONOutput(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "^https://github.com/sigstore/", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), exporter: cmdutil.NewJSONExporter(), } require.NoError(t, runVerify(&opts)) @@ -389,7 +389,7 @@ func TestRunVerify(t *testing.T) { Owner: "sigstore", PredicateType: verification.SLSAPredicateV1, SANRegex: "^https://github.com/sigstore/", - SigstoreVerifier: verification.NewDefaultMockSigstoreVerifier(t), + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), } t.Run("with valid artifact and bundle", func(t *testing.T) { From a4f96d29e32214c13a0f5c5830a39a714bea94ba Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:52:15 -0700 Subject: [PATCH 32/52] Refactor `workflow` scope checking Refactor the logic for checking `workflow` scope checking in releases to be in the positive - check if the scope is there, not check if it isn't there. Then, when the function is called we invert it. Also update comments to be more imperative. This refactor also incorporates @andyfeller's suggestion to use `slices`. Co-Authored-By: Andy Feller --- pkg/cmd/release/create/http.go | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/release/create/http.go b/pkg/cmd/release/create/http.go index 1512b5341..1162e7080 100644 --- a/pkg/cmd/release/create/http.go +++ b/pkg/cmd/release/create/http.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/url" + "slices" "strings" "github.com/MakeNowJust/heredoc" @@ -178,17 +179,18 @@ func createRelease(httpClient *http.Client, repo ghrepo.Interface, params map[st } defer resp.Body.Close() - // This code checks if we received a 404 while attempting to create a release without - // the workflow scope, and if so, returns an error message that explains a possible + // Check if we received a 404 while attempting to create a release without + // the workflow scope, and if so, return an error message that explains a possible // solution to the user. // // If the same file (with both the same path and contents) exists // on another branch in the repo, releases with workflow file changes can be // created without the workflow scope. Otherwise, the workflow scope is - // required to create the release. + // required to create the release, but the API does not indicate this criteria + // beyond returning a 404. // // https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes - if resp.StatusCode == http.StatusNotFound && tokenMissingWorkflowScope(resp) { + if resp.StatusCode == http.StatusNotFound && !tokenHasWorkflowScope(resp) { normalizedHostname := ghauth.NormalizeHostname(resp.Request.URL.Hostname()) errMissingRequiredWorkflowScope := errors.New(heredoc.Docf(` HTTP 404: Failed to create release, "workflow" scope may be required @@ -279,23 +281,17 @@ func deleteRelease(httpClient *http.Client, release *shared.Release) error { return nil } -// Given an http.Response, check if the token used in -// the request is missing the workflow scope. -func tokenMissingWorkflowScope(resp *http.Response) bool { +// tokenHasWorkflowScope checks if the given http.Response's token has the workflow scope. +// Tokens that do not have OAuth scopes are assumed to have the workflow scope. +func tokenHasWorkflowScope(resp *http.Response) bool { scopes := resp.Header.Get("X-Oauth-Scopes") - // Return false when no scopes are present - no scopes in this header + // Return true when no scopes are present - no scopes in this header // means that the user is probably authenticating with a token type other // than an OAuth token, and we don't know what this token's scopes actually are. if scopes == "" { - return false + return true } - for _, s := range strings.Split(scopes, ",") { - if s == "workflow" { - return false - } - } - - return true + return slices.Contains(strings.Split(scopes, ","), "workflow") } From 11dc6df88b93e5e767c4f82ca3245693afddc86a Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Sat, 23 Nov 2024 13:19:45 -0700 Subject: [PATCH 33/52] ScopesResponder wraps StatusScopesResponder --- pkg/httpmock/stub.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index d1a27d835..196a047d8 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -225,17 +225,9 @@ func GraphQLQuery(body string, cb func(string, map[string]interface{})) Responde } } +// ScopesResponder returns a response with a 200 status code and the given OAuth scopes. func ScopesResponder(scopes string) func(*http.Request) (*http.Response, error) { - return func(req *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: 200, - Request: req, - Header: map[string][]string{ - "X-Oauth-Scopes": {scopes}, - }, - Body: io.NopCloser(bytes.NewBufferString("")), - }, nil - } + return StatusScopesResponder(http.StatusOK, scopes) } // StatusScopesResponder returns a response with the given status code and OAuth scopes. From deb34d6456d14efe73ccd61fcb88e063da5f4794 Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Sat, 23 Nov 2024 16:59:49 -0700 Subject: [PATCH 34/52] Refactor error handling for missing "workflow" scope in createRelease --- pkg/cmd/release/create/create.go | 15 +++++++++++++++ pkg/cmd/release/create/create_test.go | 9 +++++---- pkg/cmd/release/create/http.go | 18 +++++++++++------- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/release/create/create.go b/pkg/cmd/release/create/create.go index 0b7c99b23..8e77546d0 100644 --- a/pkg/cmd/release/create/create.go +++ b/pkg/cmd/release/create/create.go @@ -477,6 +477,21 @@ func createRun(opts *CreateOptions) error { } newRelease, err := createRelease(httpClient, baseRepo, params) + + var errMissingRequiredWorkflowScope *errMissingRequiredWorkflowScope + if errors.As(err, &errMissingRequiredWorkflowScope) { + host := errMissingRequiredWorkflowScope.Hostname + refreshInstructions := fmt.Sprintf("gh auth refresh -h %[1]s -s workflow", host) + cs := opts.IO.ColorScheme() + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("%s Failed to create release, \"workflow\" scope may be required.\n", cs.WarningIcon())) + sb.WriteString(fmt.Sprintf("To request it, run:\n%s\n", cs.Bold(refreshInstructions))) + fmt.Fprint(opts.IO.ErrOut, sb.String()) + + return cmdutil.SilentError + } + if err != nil { return err } diff --git a/pkg/cmd/release/create/create_test.go b/pkg/cmd/release/create/create_test.go index aded1bea9..c9e9a8c8a 100644 --- a/pkg/cmd/release/create/create_test.go +++ b/pkg/cmd/release/create/create_test.go @@ -1102,10 +1102,12 @@ func Test_createRun(t *testing.T) { httpmock.REST("POST", "repos/OWNER/REPO/releases"), httpmock.StatusScopesResponder(404, `repo,read:org`)) }, - wantErr: heredoc.Doc(` - HTTP 404: Failed to create release, "workflow" scope may be required - To request it, run gh auth refresh -h github.com -s workflow + wantStderr: heredoc.Doc(` + ! Failed to create release, "workflow" scope may be required. + To request it, run: + gh auth refresh -h github.com -s workflow `), + wantErr: cmdutil.SilentError.Error(), }, { name: "API returns 404, OAuth token has workflow scope", @@ -1182,7 +1184,6 @@ func Test_createRun(t *testing.T) { err := createRun(&tt.opts) if tt.wantErr != "" { require.EqualError(t, err, tt.wantErr) - return } else { require.NoError(t, err) } diff --git a/pkg/cmd/release/create/http.go b/pkg/cmd/release/create/http.go index 1162e7080..3bb55f39e 100644 --- a/pkg/cmd/release/create/http.go +++ b/pkg/cmd/release/create/http.go @@ -11,7 +11,6 @@ import ( "slices" "strings" - "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/ghrepo" @@ -32,6 +31,14 @@ type releaseNotes struct { var notImplementedError = errors.New("not implemented") +type errMissingRequiredWorkflowScope struct { + Hostname string +} + +func (e errMissingRequiredWorkflowScope) Error() string { + return "workflow scope may be required" +} + func remoteTagExists(httpClient *http.Client, repo ghrepo.Interface, tagName string) (bool, error) { gql := api.NewClientFromHTTP(httpClient) qualifiedTagName := fmt.Sprintf("refs/tags/%s", tagName) @@ -192,12 +199,9 @@ func createRelease(httpClient *http.Client, repo ghrepo.Interface, params map[st // https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes if resp.StatusCode == http.StatusNotFound && !tokenHasWorkflowScope(resp) { normalizedHostname := ghauth.NormalizeHostname(resp.Request.URL.Hostname()) - errMissingRequiredWorkflowScope := errors.New(heredoc.Docf(` - HTTP 404: Failed to create release, "workflow" scope may be required - To request it, run gh auth refresh -h %[1]s -s workflow - `, normalizedHostname)) - - return nil, errMissingRequiredWorkflowScope + return nil, &errMissingRequiredWorkflowScope{ + Hostname: normalizedHostname, + } } success := resp.StatusCode >= 200 && resp.StatusCode < 300 From 46922694dcd0754a9852c26468026e69659d1d66 Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 26 Nov 2024 13:31:34 +0100 Subject: [PATCH 35/52] Support secure credential pattern --- git/client.go | 82 ++++++- git/client_test.go | 308 +++++++++++++++++++------- git/command.go | 6 + internal/run/stub.go | 2 +- pkg/cmd/issue/develop/develop_test.go | 8 +- pkg/cmd/pr/checkout/checkout.go | 13 +- pkg/cmd/pr/checkout/checkout_test.go | 14 ++ pkg/cmd/pr/create/create_test.go | 6 + pkg/cmd/pr/merge/merge_test.go | 6 + pkg/cmd/repo/clone/clone_test.go | 2 + pkg/cmd/repo/create/create.go | 7 +- pkg/cmd/repo/create/create_test.go | 3 + pkg/cmd/repo/fork/fork_test.go | 8 + pkg/cmd/repo/sync/git.go | 7 +- 14 files changed, 379 insertions(+), 93 deletions(-) diff --git a/git/client.go b/git/client.go index 560eccfdd..ce204c50e 100644 --- a/git/client.go +++ b/git/client.go @@ -16,6 +16,7 @@ import ( "strings" "sync" + "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/safeexec" ) @@ -94,16 +95,27 @@ func (c *Client) Command(ctx context.Context, args ...string) (*Command, error) return &Command{cmd}, nil } +// WM-TODO: not sure about this type, but I want to ensure that all call sites are provding the host, +// which is hard if the signature of AuthenticatedCommand is (context.Context, host string, args ...string) +// because this means AuthenticatedCommand(ctx, "fetch") will not be a compile error. +type CredentialPattern struct { + pattern string +} + // AuthenticatedCommand is a wrapper around Command that included configuration to use gh // as the credential helper for git. -func (c *Client) AuthenticatedCommand(ctx context.Context, args ...string) (*Command, error) { - preArgs := []string{"-c", "credential.helper="} +func (c *Client) AuthenticatedCommand(ctx context.Context, credentialPattern CredentialPattern, args ...string) (*Command, error) { + if credentialPattern.pattern == "" { + panic("get your shit together") + } + + preArgs := []string{"-c", fmt.Sprintf("credential.%s.helper=", credentialPattern.pattern)} if c.GhPath == "" { // Assumes that gh is in PATH. c.GhPath = "gh" } credHelper := fmt.Sprintf("!%q auth git-credential", c.GhPath) - preArgs = append(preArgs, "-c", fmt.Sprintf("credential.helper=%s", credHelper)) + preArgs = append(preArgs, "-c", fmt.Sprintf("credential.%s.helper=%s", credentialPattern.pattern, credHelper)) args = append(preArgs, args...) return c.Command(ctx, args...) } @@ -152,6 +164,19 @@ func (c *Client) UpdateRemoteURL(ctx context.Context, name, url string) error { return nil } +func (c *Client) GetRemoteURL(ctx context.Context, name string) (string, error) { + args := []string{"remote", "get-url", name} + cmd, err := c.Command(ctx, args...) + if err != nil { + return "", err + } + out, err := cmd.Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(out)), nil +} + func (c *Client) SetRemoteResolution(ctx context.Context, name, resolution string) error { args := []string{"config", "--add", fmt.Sprintf("remote.%s.gh-resolved", name), resolution} cmd, err := c.Command(ctx, args...) @@ -545,11 +570,16 @@ func (c *Client) AddRemote(ctx context.Context, name, urlStr string, trackingBra // Below are commands that make network calls and need authentication credentials supplied from gh. func (c *Client) Fetch(ctx context.Context, remote string, refspec string, mods ...CommandModifier) error { + host, err := c.CredentialPatternFromRemote(ctx, remote) + if err != nil { + return err + } + args := []string{"fetch", remote} if refspec != "" { args = append(args, refspec) } - cmd, err := c.AuthenticatedCommand(ctx, args...) + cmd, err := c.AuthenticatedCommand(ctx, host, args...) if err != nil { return err } @@ -560,11 +590,16 @@ func (c *Client) Fetch(ctx context.Context, remote string, refspec string, mods } func (c *Client) Pull(ctx context.Context, remote, branch string, mods ...CommandModifier) error { + host, err := c.CredentialPatternFromRemote(ctx, remote) + if err != nil { + return err + } + args := []string{"pull", "--ff-only"} if remote != "" && branch != "" { args = append(args, remote, branch) } - cmd, err := c.AuthenticatedCommand(ctx, args...) + cmd, err := c.AuthenticatedCommand(ctx, host, args...) if err != nil { return err } @@ -575,8 +610,13 @@ func (c *Client) Pull(ctx context.Context, remote, branch string, mods ...Comman } func (c *Client) Push(ctx context.Context, remote string, ref string, mods ...CommandModifier) error { + host, err := c.CredentialPatternFromRemote(ctx, remote) + if err != nil { + return err + } + args := []string{"push", "--set-upstream", remote, ref} - cmd, err := c.AuthenticatedCommand(ctx, args...) + cmd, err := c.AuthenticatedCommand(ctx, host, args...) if err != nil { return err } @@ -587,6 +627,11 @@ func (c *Client) Push(ctx context.Context, remote string, ref string, mods ...Co } func (c *Client) Clone(ctx context.Context, cloneURL string, args []string, mods ...CommandModifier) (string, error) { + host, err := CredentialPatternFromGitURL(cloneURL) + if err != nil { + return "", err + } + cloneArgs, target := parseCloneArgs(args) cloneArgs = append(cloneArgs, cloneURL) // If the args contain an explicit target, pass it to clone otherwise, @@ -601,7 +646,7 @@ func (c *Client) Clone(ctx context.Context, cloneURL string, args []string, mods } } cloneArgs = append([]string{"clone"}, cloneArgs...) - cmd, err := c.AuthenticatedCommand(ctx, cloneArgs...) + cmd, err := c.AuthenticatedCommand(ctx, host, cloneArgs...) if err != nil { return "", err } @@ -615,6 +660,17 @@ func (c *Client) Clone(ctx context.Context, cloneURL string, args []string, mods return target, nil } +// WM-TODO: Bit of a weird method to hang off the client? +// WM-TODO: We need to make sure this handles command modifiers everywhere... +// WM-TODO: Are there any funny refspec usages that might not resolve via get-url +func (c *Client) CredentialPatternFromRemote(ctx context.Context, remote string) (CredentialPattern, error) { + gitURL, err := c.GetRemoteURL(ctx, remote) + if err != nil { + return CredentialPattern{}, err + } + return CredentialPatternFromGitURL(gitURL) +} + func resolveGitPath() (string, error) { path, err := safeexec.LookPath("git") if err != nil { @@ -729,3 +785,15 @@ var globReplacer = strings.NewReplacer( func escapeGlob(p string) string { return globReplacer.Replace(p) } + +// Cool cool cool... +// YOLO +func CredentialPatternFromGitURL(gitURL string) (CredentialPattern, error) { + normalizedURL, err := ParseURL(gitURL) + if err != nil { + return CredentialPattern{}, fmt.Errorf("failed to parse remote URL: %w", err) + } + return CredentialPattern{ + pattern: strings.TrimSuffix(ghinstance.HostPrefix(normalizedURL.Host), "/"), + }, nil +} diff --git a/git/client_test.go b/git/client_test.go index 1e3032317..2bafaf78f 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -69,11 +69,11 @@ func TestClientAuthenticatedCommand(t *testing.T) { { name: "adds credential helper config options", path: "path/to/gh", - wantArgs: []string{"path/to/git", "-c", "credential.helper=", "-c", `credential.helper=!"path/to/gh" auth git-credential`, "fetch"}, + wantArgs: []string{"path/to/git", "-c", "credential.https://github.com.helper=", "-c", `credential.https://github.com.helper=!"path/to/gh" auth git-credential`, "fetch"}, }, { name: "fallback when GhPath is not set", - wantArgs: []string{"path/to/git", "-c", "credential.helper=", "-c", `credential.helper=!"gh" auth git-credential`, "fetch"}, + wantArgs: []string{"path/to/git", "-c", "credential.https://github.com.helper=", "-c", `credential.https://github.com.helper=!"gh" auth git-credential`, "fetch"}, }, } for _, tt := range tests { @@ -82,7 +82,7 @@ func TestClientAuthenticatedCommand(t *testing.T) { GhPath: tt.path, GitPath: "path/to/git", } - cmd, err := client.AuthenticatedCommand(context.Background(), "fetch") + cmd, err := client.AuthenticatedCommand(context.Background(), CredentialPattern{pattern: "https://github.com"}, "fetch") assert.NoError(t, err) assert.Equal(t, tt.wantArgs, cmd.Args) }) @@ -1064,13 +1064,13 @@ func TestClientUnsetRemoteResolution(t *testing.T) { name: "unset remote resolution", wantCmdArgs: `path/to/git config --unset remote.origin.gh-resolved`, }, - { - name: "git error", - cmdExitStatus: 1, - cmdStderr: "git error message", - wantCmdArgs: `path/to/git config --unset remote.origin.gh-resolved`, - wantErrorMsg: "failed to run git: git error message", - }, + // { + // name: "git error", + // cmdExitStatus: 1, + // cmdStderr: "git error message", + // wantCmdArgs: `path/to/git config --unset remote.origin.gh-resolved`, + // wantErrorMsg: "failed to run git: git error message", + // }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1131,44 +1131,74 @@ func TestClientSetRemoteBranches(t *testing.T) { func TestClientFetch(t *testing.T) { tests := []struct { - name string - mods []CommandModifier - cmdExitStatus int - cmdStdout string - cmdStderr string - wantCmdArgs string - wantErrorMsg string + name string + mods []CommandModifier + commands mockedCommands + wantErrorMsg string }{ { - name: "fetch", - wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential fetch origin trunk`, + name: "fetch", + commands: map[args]commandResult{ + `path/to/git remote get-url origin`: { + ExitStatus: 0, + Stdout: "https://github.com/cli/nonexistent.git", + }, + `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential fetch origin trunk`: { + ExitStatus: 0, + }, + }, }, { - name: "accepts command modifiers", - mods: []CommandModifier{WithRepoDir("/path/to/repo")}, - wantCmdArgs: `path/to/git -C /path/to/repo -c credential.helper= -c credential.helper=!"gh" auth git-credential fetch origin trunk`, + name: "accepts command modifiers", + mods: []CommandModifier{WithRepoDir("/path/to/repo")}, + commands: map[args]commandResult{ + `path/to/git remote get-url origin`: { + ExitStatus: 0, + Stdout: "https://github.com/cli/nonexistent.git", + }, + `path/to/git -C /path/to/repo -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential fetch origin trunk`: { + ExitStatus: 0, + }, + }, }, { - name: "git error", - cmdExitStatus: 1, - cmdStderr: "git error message", - wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential fetch origin trunk`, - wantErrorMsg: "failed to run git: git error message", + name: "git error on get-url", + commands: map[args]commandResult{ + `path/to/git remote get-url origin`: { + ExitStatus: 1, + Stderr: "get-url error message", + }, + }, + wantErrorMsg: "failed to run git: get-url error message", + }, + { + name: "git error on fetch", + commands: map[args]commandResult{ + `path/to/git remote get-url origin`: { + ExitStatus: 0, + Stdout: "https://github.com/cli/nonexistent.git", + }, + `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential fetch origin trunk`: { + ExitStatus: 1, + Stderr: "fetch error message", + }, + }, + wantErrorMsg: "failed to run git: fetch error message", }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + cmdCtx := createMockedCommandContext(t, tt.commands) client := Client{ GitPath: "path/to/git", commandContext: cmdCtx, } err := client.Fetch(context.Background(), "origin", "trunk", tt.mods...) - assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) if tt.wantErrorMsg == "" { - assert.NoError(t, err) + require.NoError(t, err) } else { - assert.EqualError(t, err, tt.wantErrorMsg) + require.EqualError(t, err, tt.wantErrorMsg) } }) } @@ -1176,44 +1206,74 @@ func TestClientFetch(t *testing.T) { func TestClientPull(t *testing.T) { tests := []struct { - name string - mods []CommandModifier - cmdExitStatus int - cmdStdout string - cmdStderr string - wantCmdArgs string - wantErrorMsg string + name string + mods []CommandModifier + commands mockedCommands + wantErrorMsg string }{ { - name: "pull", - wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential pull --ff-only origin trunk`, + name: "pull", + commands: map[args]commandResult{ + `path/to/git remote get-url origin`: { + ExitStatus: 0, + Stdout: "https://github.com/cli/nonexistent.git", + }, + `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential pull --ff-only origin trunk`: { + ExitStatus: 0, + }, + }, }, { - name: "accepts command modifiers", - mods: []CommandModifier{WithRepoDir("/path/to/repo")}, - wantCmdArgs: `path/to/git -C /path/to/repo -c credential.helper= -c credential.helper=!"gh" auth git-credential pull --ff-only origin trunk`, + name: "accepts command modifiers", + mods: []CommandModifier{WithRepoDir("/path/to/repo")}, + commands: map[args]commandResult{ + `path/to/git remote get-url origin`: { + ExitStatus: 0, + Stdout: "https://github.com/cli/nonexistent.git", + }, + `path/to/git -C /path/to/repo -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential pull --ff-only origin trunk`: { + ExitStatus: 0, + }, + }, }, { - name: "git error", - cmdExitStatus: 1, - cmdStderr: "git error message", - wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential pull --ff-only origin trunk`, - wantErrorMsg: "failed to run git: git error message", + name: "git error on get-url", + commands: map[args]commandResult{ + `path/to/git remote get-url origin`: { + ExitStatus: 1, + Stderr: "get-url error message", + }, + }, + wantErrorMsg: "failed to run git: get-url error message", + }, + { + name: "git error on fetch", + commands: map[args]commandResult{ + `path/to/git remote get-url origin`: { + ExitStatus: 0, + Stdout: "https://github.com/cli/nonexistent.git", + }, + `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential pull --ff-only origin trunk`: { + ExitStatus: 1, + Stderr: "fetch error message", + }, + }, + wantErrorMsg: "failed to run git: fetch error message", }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + cmdCtx := createMockedCommandContext(t, tt.commands) client := Client{ GitPath: "path/to/git", commandContext: cmdCtx, } err := client.Pull(context.Background(), "origin", "trunk", tt.mods...) - assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) if tt.wantErrorMsg == "" { - assert.NoError(t, err) + require.NoError(t, err) } else { - assert.EqualError(t, err, tt.wantErrorMsg) + require.EqualError(t, err, tt.wantErrorMsg) } }) } @@ -1221,44 +1281,74 @@ func TestClientPull(t *testing.T) { func TestClientPush(t *testing.T) { tests := []struct { - name string - mods []CommandModifier - cmdExitStatus int - cmdStdout string - cmdStderr string - wantCmdArgs string - wantErrorMsg string + name string + mods []CommandModifier + commands mockedCommands + wantErrorMsg string }{ { - name: "push", - wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential push --set-upstream origin trunk`, + name: "push", + commands: map[args]commandResult{ + `path/to/git remote get-url origin`: { + ExitStatus: 0, + Stdout: "https://github.com/cli/nonexistent.git", + }, + `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential push --set-upstream origin trunk`: { + ExitStatus: 0, + }, + }, }, { - name: "accepts command modifiers", - mods: []CommandModifier{WithRepoDir("/path/to/repo")}, - wantCmdArgs: `path/to/git -C /path/to/repo -c credential.helper= -c credential.helper=!"gh" auth git-credential push --set-upstream origin trunk`, + name: "accepts command modifiers", + mods: []CommandModifier{WithRepoDir("/path/to/repo")}, + commands: map[args]commandResult{ + `path/to/git remote get-url origin`: { + ExitStatus: 0, + Stdout: "https://github.com/cli/nonexistent.git", + }, + `path/to/git -C /path/to/repo -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential push --set-upstream origin trunk`: { + ExitStatus: 0, + }, + }, }, { - name: "git error", - cmdExitStatus: 1, - cmdStderr: "git error message", - wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential push --set-upstream origin trunk`, - wantErrorMsg: "failed to run git: git error message", + name: "git error on get-url", + commands: map[args]commandResult{ + `path/to/git remote get-url origin`: { + ExitStatus: 1, + Stderr: "get-url error message", + }, + }, + wantErrorMsg: "failed to run git: get-url error message", + }, + { + name: "git error on fetch", + commands: map[args]commandResult{ + `path/to/git remote get-url origin`: { + ExitStatus: 0, + Stdout: "https://github.com/cli/nonexistent.git", + }, + `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential push --set-upstream origin trunk`: { + ExitStatus: 1, + Stderr: "fetch error message", + }, + }, + wantErrorMsg: "failed to run git: fetch error message", }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus, tt.cmdStdout, tt.cmdStderr) + cmdCtx := createMockedCommandContext(t, tt.commands) client := Client{ GitPath: "path/to/git", commandContext: cmdCtx, } err := client.Push(context.Background(), "origin", "trunk", tt.mods...) - assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) if tt.wantErrorMsg == "" { - assert.NoError(t, err) + require.NoError(t, err) } else { - assert.EqualError(t, err, tt.wantErrorMsg) + require.EqualError(t, err, tt.wantErrorMsg) } }) } @@ -1279,14 +1369,14 @@ func TestClientClone(t *testing.T) { { name: "clone", args: []string{}, - wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential clone github.com/cli/cli`, + wantCmdArgs: `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential clone https://github.com/cli/cli`, wantTarget: "cli", }, { name: "accepts command modifiers", args: []string{}, mods: []CommandModifier{WithRepoDir("/path/to/repo")}, - wantCmdArgs: `path/to/git -C /path/to/repo -c credential.helper= -c credential.helper=!"gh" auth git-credential clone github.com/cli/cli`, + wantCmdArgs: `path/to/git -C /path/to/repo -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential clone https://github.com/cli/cli`, wantTarget: "cli", }, { @@ -1294,19 +1384,19 @@ func TestClientClone(t *testing.T) { args: []string{}, cmdExitStatus: 1, cmdStderr: "git error message", - wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential clone github.com/cli/cli`, + wantCmdArgs: `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential clone https://github.com/cli/cli`, wantErrorMsg: "failed to run git: git error message", }, { name: "bare clone", args: []string{"--bare"}, - wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential clone --bare github.com/cli/cli`, + wantCmdArgs: `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential clone --bare https://github.com/cli/cli`, wantTarget: "cli.git", }, { name: "bare clone with explicit target", args: []string{"cli-bare", "--bare"}, - wantCmdArgs: `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential clone --bare github.com/cli/cli cli-bare`, + wantCmdArgs: `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential clone --bare https://github.com/cli/cli cli-bare`, wantTarget: "cli-bare", }, } @@ -1317,7 +1407,7 @@ func TestClientClone(t *testing.T) { GitPath: "path/to/git", commandContext: cmdCtx, } - target, err := client.Clone(context.Background(), "github.com/cli/cli", tt.args, tt.mods...) + target, err := client.Clone(context.Background(), "https://github.com/cli/cli", tt.args, tt.mods...) assert.Equal(t, tt.wantCmdArgs, strings.Join(cmd.Args[3:], " ")) if tt.wantErrorMsg == "" { assert.NoError(t, err) @@ -1442,6 +1532,49 @@ func initRepo(t *testing.T, dir string) { assert.NoError(t, err) } +type args string + +type commandResult struct { + ExitStatus int `json:"exitStatus"` + Stdout string `json:"out"` + Stderr string `json:"err"` +} + +type mockedCommands map[args]commandResult + +func TestCommandMocking(t *testing.T) { + if os.Getenv("GH_WANT_HELPER_PROCESS_RICH") != "1" { + return + } + + // WM-TODO: maybe don't use 255, I only picked it because it was distinct + jsonVar, ok := os.LookupEnv("GH_HELPER_PROCESS_RICH_COMMANDS") + if !ok { + fmt.Fprint(os.Stderr, "missing GH_HELPER_PROCESS_RICH_COMMANDS") + os.Exit(255) + } + + var commands mockedCommands + if err := json.Unmarshal([]byte(jsonVar), &commands); err != nil { + fmt.Fprint(os.Stderr, "failed to unmarshal GH_HELPER_PROCESS_RICH_COMMANDS") + os.Exit(255) + } + + // The discarded args are those for the go test binary itself, e.g. `-test.run=TestHelperProcessRich` + realArgs := os.Args[3:] + + commandResult, ok := commands[args(strings.Join(realArgs, " "))] + if !ok { + fmt.Fprintf(os.Stderr, "unexpected command: %s\n", strings.Join(realArgs, " ")) + os.Exit(255) + } + + // WM-TODO: maybe pointer on these fields, or only print if not-empty + fmt.Fprint(os.Stdout, commandResult.Stdout) + fmt.Fprint(os.Stderr, commandResult.Stderr) + os.Exit(commandResult.ExitStatus) +} + func TestHelperProcess(t *testing.T) { if os.Getenv("GH_WANT_HELPER_PROCESS") != "1" { return @@ -1479,3 +1612,20 @@ func createCommandContext(t *testing.T, exitStatus int, stdout, stderr string) ( return cmd } } + +func createMockedCommandContext(t *testing.T, commands mockedCommands) commandCtx { + marshaledCommands, err := json.Marshal(commands) + require.NoError(t, err) + + return func(ctx context.Context, exe string, args ...string) *exec.Cmd { + cmd := exec.CommandContext(context.Background(), os.Args[0], "-test.run=TestCommandMocking", "--") + cmd.Env = []string{ + "GH_WANT_HELPER_PROCESS_RICH=1", + fmt.Sprintf("GH_HELPER_PROCESS_RICH_COMMANDS=%s", string(marshaledCommands)), + } + + cmd.Args = append(cmd.Args, exe) + cmd.Args = append(cmd.Args, args...) + return cmd + } +} diff --git a/git/command.go b/git/command.go index 8065ffd86..70a156912 100644 --- a/git/command.go +++ b/git/command.go @@ -98,3 +98,9 @@ func WithRepoDir(repoDir string) CommandModifier { gc.setRepoDir(repoDir) } } + +func WithExtraArgs(args ...string) CommandModifier { + return func(gc *Command) { + gc.Args = append(gc.Args, args...) + } +} diff --git a/internal/run/stub.go b/internal/run/stub.go index 49bf62d29..9499da8e2 100644 --- a/internal/run/stub.go +++ b/internal/run/stub.go @@ -9,7 +9,7 @@ import ( ) const ( - gitAuthRE = `-c credential.helper= -c credential.helper=!"[^"]+" auth git-credential ` + gitAuthRE = `-c credential\..+\.helper= -c credential\..+\.helper=!"[^"]+" auth git-credential ` ) type T interface { diff --git a/pkg/cmd/issue/develop/develop_test.go b/pkg/cmd/issue/develop/develop_test.go index c35bcb845..28642eb51 100644 --- a/pkg/cmd/issue/develop/develop_test.go +++ b/pkg/cmd/issue/develop/develop_test.go @@ -249,7 +249,7 @@ func TestDevelopRun(t *testing.T) { expectedOut: heredoc.Doc(` Showing linked branches for OWNER/REPO#42 - + BRANCH URL foo https://github.com/OWNER/REPO/tree/foo bar https://github.com/OWNER/OTHER-REPO/tree/bar @@ -314,6 +314,7 @@ func TestDevelopRun(t *testing.T) { ) }, runStubs: func(cs *run.CommandStubber) { + cs.Register(`git remote get-url origin`, 0, "https://github.com/cli/cli.git") cs.Register(`git fetch origin \+refs/heads/my-issue-1:refs/remotes/origin/my-issue-1`, 0, "") }, expectedOut: "github.com/OWNER/REPO/tree/my-issue-1\n", @@ -360,6 +361,7 @@ func TestDevelopRun(t *testing.T) { ) }, runStubs: func(cs *run.CommandStubber) { + cs.Register(`git remote get-url origin`, 0, "https://github.com/cli/cli.git") cs.Register(`git fetch origin \+refs/heads/my-issue-1:refs/remotes/origin/my-issue-1`, 0, "") }, expectedOut: "github.com/OWNER2/REPO/tree/my-issue-1\n", @@ -398,6 +400,7 @@ func TestDevelopRun(t *testing.T) { ) }, runStubs: func(cs *run.CommandStubber) { + cs.Register(`git remote get-url origin`, 0, "https://github.com/cli/cli.git") cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") }, expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", @@ -467,9 +470,11 @@ func TestDevelopRun(t *testing.T) { ) }, runStubs: func(cs *run.CommandStubber) { + cs.Register(`git remote get-url origin`, 0, "https://github.com/cli/cli.git") cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") cs.Register(`git rev-parse --verify refs/heads/my-branch`, 0, "") cs.Register(`git checkout my-branch`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/cli/cli.git") cs.Register(`git pull --ff-only origin my-branch`, 0, "") }, expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", @@ -509,6 +514,7 @@ func TestDevelopRun(t *testing.T) { ) }, runStubs: func(cs *run.CommandStubber) { + cs.Register(`git remote get-url origin`, 0, "https://github.com/cli/cli.git") cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") cs.Register(`git rev-parse --verify refs/heads/my-branch`, 1, "") cs.Register(`git checkout -b my-branch --track origin/my-branch`, 0, "") diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index f566cbe1d..4a732a409 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -130,7 +130,14 @@ func checkoutRun(opts *CheckoutOptions) error { cmdQueue = append(cmdQueue, []string{"submodule", "update", "--init", "--recursive"}) } - err = executeCmds(opts.GitClient, cmdQueue) + // Note that although we will probably be fetching from the headRemote, in practice, PR checkout can only + // ever point to one host, and we know baseRemote must be populated, where headRemote might be nil (e.g. when + // it was deleted). + credentialPattern, err := opts.GitClient.CredentialPatternFromRemote(context.Background(), baseRemote.Name) + if err != nil { + return err + } + err = executeCmds(opts.GitClient, credentialPattern, cmdQueue) if err != nil { return err } @@ -240,12 +247,12 @@ func localBranchExists(client *git.Client, b string) bool { return err == nil } -func executeCmds(client *git.Client, cmdQueue [][]string) error { +func executeCmds(client *git.Client, credentialPattern git.CredentialPattern, cmdQueue [][]string) error { for _, args := range cmdQueue { var err error var cmd *git.Command if args[0] == "fetch" || args[0] == "submodule" { - cmd, err = client.AuthenticatedCommand(context.Background(), args...) + cmd, err = client.AuthenticatedCommand(context.Background(), credentialPattern, args...) } else { cmd, err = client.Command(context.Background(), args...) } diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index 1eda5dda2..55123fd59 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -94,6 +94,7 @@ func Test_checkoutRun(t *testing.T) { "origin": "OWNER/REPO", }, runStubs: func(cs *run.CommandStubber) { + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "") cs.Register(`git config branch\.feature\.merge`, 1, "") cs.Register(`git checkout feature`, 0, "") @@ -124,6 +125,7 @@ func Test_checkoutRun(t *testing.T) { }, runStubs: func(cs *run.CommandStubber) { cs.Register(`git show-ref --verify -- refs/heads/foobar`, 1, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "") cs.Register(`git checkout -b foobar --track origin/feature`, 0, "") }, @@ -151,6 +153,7 @@ func Test_checkoutRun(t *testing.T) { }, runStubs: func(cs *run.CommandStubber) { cs.Register(`git config branch\.foobar\.merge`, 1, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/hubot/REPO.git") cs.Register(`git fetch origin refs/pull/123/head:foobar`, 0, "") cs.Register(`git checkout foobar`, 0, "") cs.Register(`git config branch\.foobar\.remote https://github.com/hubot/REPO.git`, 0, "") @@ -276,6 +279,7 @@ func TestPRCheckout_sameRepo(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "") cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "") cs.Register(`git checkout -b feature --track origin/feature`, 0, "") @@ -296,6 +300,7 @@ func TestPRCheckout_existingBranch(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "") cs.Register(`git show-ref --verify -- refs/heads/feature`, 0, "") cs.Register(`git checkout feature`, 0, "") @@ -329,6 +334,7 @@ func TestPRCheckout_differentRepo_remoteExists(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch robot-fork \+refs/heads/feature:refs/remotes/robot-fork/feature`, 0, "") cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "") cs.Register(`git checkout -b feature --track robot-fork/feature`, 0, "") @@ -350,6 +356,7 @@ func TestPRCheckout_differentRepo(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "") cs.Register(`git config branch\.feature\.merge`, 1, "") cs.Register(`git checkout feature`, 0, "") @@ -373,6 +380,7 @@ func TestPRCheckout_differentRepo_existingBranch(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "") cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n") cs.Register(`git checkout feature`, 0, "") @@ -393,6 +401,7 @@ func TestPRCheckout_detachedHead(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "") cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n") cs.Register(`git checkout feature`, 0, "") @@ -413,6 +422,7 @@ func TestPRCheckout_differentRepo_currentBranch(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch origin refs/pull/123/head`, 0, "") cs.Register(`git config branch\.feature\.merge`, 0, "refs/heads/feature\n") cs.Register(`git merge --ff-only FETCH_HEAD`, 0, "") @@ -450,6 +460,7 @@ func TestPRCheckout_maintainerCanModify(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch origin refs/pull/123/head:feature`, 0, "") cs.Register(`git config branch\.feature\.merge`, 1, "") cs.Register(`git checkout feature`, 0, "") @@ -472,6 +483,7 @@ func TestPRCheckout_recurseSubmodules(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "") cs.Register(`git show-ref --verify -- refs/heads/feature`, 0, "") cs.Register(`git checkout feature`, 0, "") @@ -494,6 +506,7 @@ func TestPRCheckout_force(t *testing.T) { cs, cmdTeardown := run.Stub() defer cmdTeardown(t) + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "") cs.Register(`git show-ref --verify -- refs/heads/feature`, 0, "") cs.Register(`git checkout feature`, 0, "") @@ -517,6 +530,7 @@ func TestPRCheckout_detach(t *testing.T) { defer cmdTeardown(t) cs.Register(`git checkout --detach FETCH_HEAD`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/hubot/REPO.git") cs.Register(`git fetch origin refs/pull/123/head`, 0, "") output, err := runCommand(http, nil, "", `123 --detach`) diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index d31174999..7a2ccb7e6 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -628,6 +628,7 @@ func Test_createRun(t *testing.T) { cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -692,6 +693,7 @@ func Test_createRun(t *testing.T) { cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -739,6 +741,7 @@ func Test_createRun(t *testing.T) { cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -791,6 +794,7 @@ func Test_createRun(t *testing.T) { cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register("git remote rename origin upstream", 0, "") cs.Register(`git remote add origin https://github.com/monalisa/REPO.git`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -1069,6 +1073,7 @@ func Test_createRun(t *testing.T) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -1102,6 +1107,7 @@ func Test_createRun(t *testing.T) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { diff --git a/pkg/cmd/pr/merge/merge_test.go b/pkg/cmd/pr/merge/merge_test.go index 21df882b4..ff9f6ef5e 100644 --- a/pkg/cmd/pr/merge/merge_test.go +++ b/pkg/cmd/pr/merge/merge_test.go @@ -644,6 +644,7 @@ func TestPrMerge_deleteBranch(t *testing.T) { cs.Register(`git checkout main`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git pull --ff-only`, 0, "") output, err := runCommand(http, nil, "blueberries", true, `pr merge --merge --delete-branch`) @@ -695,6 +696,7 @@ func TestPrMerge_deleteBranch_nonDefault(t *testing.T) { cs.Register(`git checkout fruit`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git pull --ff-only`, 0, "") output, err := runCommand(http, nil, "blueberries", true, `pr merge --merge --delete-branch`) @@ -744,6 +746,7 @@ func TestPrMerge_deleteBranch_onlyLocally(t *testing.T) { cs.Register(`git checkout main`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git pull --ff-only`, 0, "") output, err := runCommand(http, nil, "blueberries", true, `pr merge --merge --delete-branch`) @@ -794,6 +797,7 @@ func TestPrMerge_deleteBranch_checkoutNewBranch(t *testing.T) { cs.Register(`git checkout -b fruit --track origin/fruit`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git pull --ff-only`, 0, "") output, err := runCommand(http, nil, "blueberries", true, `pr merge --merge --delete-branch`) @@ -1081,6 +1085,7 @@ func TestPrMerge_alreadyMerged(t *testing.T) { cs.Register(`git checkout main`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git pull --ff-only`, 0, "") pm := &prompter.PrompterMock{ @@ -1324,6 +1329,7 @@ func TestPRMergeTTY_withDeleteBranch(t *testing.T) { cs.Register(`git checkout main`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git pull --ff-only`, 0, "") pm := &prompter.PrompterMock{ diff --git a/pkg/cmd/repo/clone/clone_test.go b/pkg/cmd/repo/clone/clone_test.go index 471ed05dd..fe122690b 100644 --- a/pkg/cmd/repo/clone/clone_test.go +++ b/pkg/cmd/repo/clone/clone_test.go @@ -259,6 +259,7 @@ func Test_RepoClone_hasParent(t *testing.T) { cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "") cs.Register(`git -C REPO remote add -t trunk upstream https://github.com/hubot/ORIG.git`, 0, "") + cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/hubot/ORIG.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO remote set-branches upstream *`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") @@ -299,6 +300,7 @@ func Test_RepoClone_hasParent_upstreamRemoteName(t *testing.T) { cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "") cs.Register(`git -C REPO remote add -t trunk test https://github.com/hubot/ORIG.git`, 0, "") + cs.Register(`git -C REPO remote get-url test`, 0, "https://github.com/hubot/ORIG.git") cs.Register(`git -C REPO fetch test`, 0, "") cs.Register(`git -C REPO remote set-branches test *`, 0, "") cs.Register(`git -C REPO config --add remote.test.gh-resolved base`, 0, "") diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 79c349aa4..29cf453c7 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -676,7 +676,12 @@ func createFromLocal(opts *CreateOptions) error { } if opts.Push && repoType == bare { - cmd, err := opts.GitClient.AuthenticatedCommand(context.Background(), "push", baseRemote, "--mirror") + // WM-TODO: can we collapse this into opts.GitClient.Push? + credentialPattern, err := opts.GitClient.CredentialPatternFromRemote(context.Background(), baseRemote) + if err != nil { + return err + } + cmd, err := opts.GitClient.AuthenticatedCommand(context.Background(), credentialPattern, "push", baseRemote, "--mirror") if err != nil { return err } diff --git a/pkg/cmd/repo/create/create_test.go b/pkg/cmd/repo/create/create_test.go index c33cfdad6..501e721fc 100644 --- a/pkg/cmd/repo/create/create_test.go +++ b/pkg/cmd/repo/create/create_test.go @@ -507,6 +507,7 @@ func Test_createRun(t *testing.T) { cs.Register(`git -C . rev-parse --git-dir`, 0, ".") cs.Register(`git -C . rev-parse HEAD`, 0, "commithash") cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "") + cs.Register(`git -C . remote get-url origin`, 0, "https://github.com/OWNER/REPO") cs.Register(`git -C . push origin --mirror`, 0, "") }, wantStdout: "✓ Created repository OWNER/REPO on GitHub\n https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Mirrored all refs to https://github.com/OWNER/REPO.git\n", @@ -575,6 +576,7 @@ func Test_createRun(t *testing.T) { cs.Register(`git -C . rev-parse --git-dir`, 0, ".git") cs.Register(`git -C . rev-parse HEAD`, 0, "commithash") cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "") + cs.Register(`git -C . remote get-url origin`, 0, "https://github.com/OWNER/REPO") cs.Register(`git -C . push --set-upstream origin HEAD`, 0, "") }, wantStdout: "✓ Created repository OWNER/REPO on GitHub\n https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Pushed commits to https://github.com/OWNER/REPO.git\n", @@ -795,6 +797,7 @@ func Test_createRun(t *testing.T) { cs.Register(`git -C . rev-parse --git-dir`, 0, ".") cs.Register(`git -C . rev-parse HEAD`, 0, "commithash") cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "") + cs.Register(`git -C . remote get-url origin`, 0, "https://github.com/OWNER/REPO") cs.Register(`git -C . push origin --mirror`, 0, "") }, wantStdout: "https://github.com/OWNER/REPO\n", diff --git a/pkg/cmd/repo/fork/fork_test.go b/pkg/cmd/repo/fork/fork_test.go index 0f94496f0..95b27ae60 100644 --- a/pkg/cmd/repo/fork/fork_test.go +++ b/pkg/cmd/repo/fork/fork_test.go @@ -442,6 +442,7 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone --depth 1 https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") + cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -474,6 +475,7 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone https://github.com/gamehendge/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") + cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -490,6 +492,7 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") + cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -526,6 +529,7 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") + cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -550,6 +554,7 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") + cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -586,6 +591,7 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") + cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -601,6 +607,7 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") + cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -691,6 +698,7 @@ func TestRepoFork(t *testing.T) { cs.Register(`git clone https://github.com/someone/REPO\.git`, 128, "") cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") + cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, diff --git a/pkg/cmd/repo/sync/git.go b/pkg/cmd/repo/sync/git.go index 1f18d0fdb..3d48ad7b3 100644 --- a/pkg/cmd/repo/sync/git.go +++ b/pkg/cmd/repo/sync/git.go @@ -54,8 +54,13 @@ func (g *gitExecuter) CurrentBranch() (string, error) { } func (g *gitExecuter) Fetch(remote, ref string) error { + host, err := g.client.CredentialPatternFromRemote(context.Background(), remote) + if err != nil { + return err + } + args := []string{"fetch", "-q", remote, ref} - cmd, err := g.client.AuthenticatedCommand(context.Background(), args...) + cmd, err := g.client.AuthenticatedCommand(context.Background(), host, args...) if err != nil { return err } From 5f5c5270c9fe3f61b88ce53f0cc136b47b4c924b Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 26 Nov 2024 21:38:25 +0100 Subject: [PATCH 36/52] Allow opt-in to insecure pattern --- git/client.go | 27 +++++++++++++++++---------- git/client_test.go | 28 +++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/git/client.go b/git/client.go index ce204c50e..8a8159134 100644 --- a/git/client.go +++ b/git/client.go @@ -95,27 +95,34 @@ func (c *Client) Command(ctx context.Context, args ...string) (*Command, error) return &Command{cmd}, nil } -// WM-TODO: not sure about this type, but I want to ensure that all call sites are provding the host, -// which is hard if the signature of AuthenticatedCommand is (context.Context, host string, args ...string) -// because this means AuthenticatedCommand(ctx, "fetch") will not be a compile error. type CredentialPattern struct { - pattern string + insecure bool // should only be constructable via InsecureAllMatchingCredentialPattern + pattern string } +var InsecureAllMatchingCredentialsPattern = CredentialPattern{insecure: true, pattern: ""} +var disallowedCredentialPattern = CredentialPattern{insecure: false, pattern: ""} + // AuthenticatedCommand is a wrapper around Command that included configuration to use gh // as the credential helper for git. func (c *Client) AuthenticatedCommand(ctx context.Context, credentialPattern CredentialPattern, args ...string) (*Command, error) { - if credentialPattern.pattern == "" { - panic("get your shit together") - } - - preArgs := []string{"-c", fmt.Sprintf("credential.%s.helper=", credentialPattern.pattern)} if c.GhPath == "" { // Assumes that gh is in PATH. c.GhPath = "gh" } credHelper := fmt.Sprintf("!%q auth git-credential", c.GhPath) - preArgs = append(preArgs, "-c", fmt.Sprintf("credential.%s.helper=%s", credentialPattern.pattern, credHelper)) + + var preArgs []string + if credentialPattern == disallowedCredentialPattern { + return nil, fmt.Errorf("empty credential pattern is not allowed execept explicitly") + } else if credentialPattern == InsecureAllMatchingCredentialsPattern { + preArgs = []string{"-c", "credential.helper="} + preArgs = append(preArgs, "-c", fmt.Sprintf("credential.helper=%s", credHelper)) + } else { + preArgs = []string{"-c", fmt.Sprintf("credential.%s.helper=", credentialPattern.pattern)} + preArgs = append(preArgs, "-c", fmt.Sprintf("credential.%s.helper=%s", credentialPattern.pattern, credHelper)) + } + args = append(preArgs, args...) return c.Command(ctx, args...) } diff --git a/git/client_test.go b/git/client_test.go index 2bafaf78f..cf9abc54c 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -64,16 +64,31 @@ func TestClientAuthenticatedCommand(t *testing.T) { tests := []struct { name string path string + pattern CredentialPattern wantArgs []string + wantErr error }{ { - name: "adds credential helper config options", + name: "when credential pattern is TODO, credential helper matches everything", path: "path/to/gh", + pattern: InsecureAllMatchingCredentialsPattern, + wantArgs: []string{"path/to/git", "-c", "credential.helper=", "-c", `credential.helper=!"path/to/gh" auth git-credential`, "fetch"}, + }, + { + name: "when credential pattern is set, credential helper only matches that pattern", + path: "path/to/gh", + pattern: CredentialPattern{pattern: "https://github.com"}, wantArgs: []string{"path/to/git", "-c", "credential.https://github.com.helper=", "-c", `credential.https://github.com.helper=!"path/to/gh" auth git-credential`, "fetch"}, }, { name: "fallback when GhPath is not set", - wantArgs: []string{"path/to/git", "-c", "credential.https://github.com.helper=", "-c", `credential.https://github.com.helper=!"gh" auth git-credential`, "fetch"}, + pattern: InsecureAllMatchingCredentialsPattern, + wantArgs: []string{"path/to/git", "-c", "credential.helper=", "-c", `credential.helper=!"gh" auth git-credential`, "fetch"}, + }, + { + name: "errors when attempting to use an empty pattern that isn't marked insecure", + pattern: CredentialPattern{insecure: false, pattern: ""}, + wantErr: fmt.Errorf("empty credential pattern is not allowed execept explicitly"), }, } for _, tt := range tests { @@ -82,9 +97,12 @@ func TestClientAuthenticatedCommand(t *testing.T) { GhPath: tt.path, GitPath: "path/to/git", } - cmd, err := client.AuthenticatedCommand(context.Background(), CredentialPattern{pattern: "https://github.com"}, "fetch") - assert.NoError(t, err) - assert.Equal(t, tt.wantArgs, cmd.Args) + cmd, err := client.AuthenticatedCommand(context.Background(), tt.pattern, "fetch") + if tt.wantErr != nil { + require.Equal(t, tt.wantErr, err) + return + } + require.Equal(t, tt.wantArgs, cmd.Args) }) } } From 75712de712cdca8ad11b585c67febf23dd742b24 Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 26 Nov 2024 21:50:19 +0100 Subject: [PATCH 37/52] Allow client pull to use insecure credential pattern --- git/client.go | 7 +---- git/client_test.go | 38 ++++++--------------------- internal/run/stub.go | 2 +- pkg/cmd/issue/develop/develop_test.go | 3 +-- pkg/cmd/pr/merge/merge_test.go | 6 ----- 5 files changed, 11 insertions(+), 45 deletions(-) diff --git a/git/client.go b/git/client.go index 8a8159134..658fb40cd 100644 --- a/git/client.go +++ b/git/client.go @@ -597,16 +597,11 @@ func (c *Client) Fetch(ctx context.Context, remote string, refspec string, mods } func (c *Client) Pull(ctx context.Context, remote, branch string, mods ...CommandModifier) error { - host, err := c.CredentialPatternFromRemote(ctx, remote) - if err != nil { - return err - } - args := []string{"pull", "--ff-only"} if remote != "" && branch != "" { args = append(args, remote, branch) } - cmd, err := c.AuthenticatedCommand(ctx, host, args...) + cmd, err := c.AuthenticatedCommand(ctx, InsecureAllMatchingCredentialsPattern, args...) if err != nil { return err } diff --git a/git/client_test.go b/git/client_test.go index cf9abc54c..d48a6af08 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -1157,11 +1157,7 @@ func TestClientFetch(t *testing.T) { { name: "fetch", commands: map[args]commandResult{ - `path/to/git remote get-url origin`: { - ExitStatus: 0, - Stdout: "https://github.com/cli/nonexistent.git", - }, - `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential fetch origin trunk`: { + `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential fetch origin trunk`: { ExitStatus: 0, }, }, @@ -1232,11 +1228,7 @@ func TestClientPull(t *testing.T) { { name: "pull", commands: map[args]commandResult{ - `path/to/git remote get-url origin`: { - ExitStatus: 0, - Stdout: "https://github.com/cli/nonexistent.git", - }, - `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential pull --ff-only origin trunk`: { + `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential pull --ff-only origin trunk`: { ExitStatus: 0, }, }, @@ -1249,34 +1241,20 @@ func TestClientPull(t *testing.T) { ExitStatus: 0, Stdout: "https://github.com/cli/nonexistent.git", }, - `path/to/git -C /path/to/repo -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential pull --ff-only origin trunk`: { + `path/to/git -C /path/to/repo -c credential.helper= -c credential.helper=!"gh" auth git-credential pull --ff-only origin trunk`: { ExitStatus: 0, }, }, }, { - name: "git error on get-url", + name: "git error on pull", commands: map[args]commandResult{ - `path/to/git remote get-url origin`: { + `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential pull --ff-only origin trunk`: { ExitStatus: 1, - Stderr: "get-url error message", + Stderr: "pull error message", }, }, - wantErrorMsg: "failed to run git: get-url error message", - }, - { - name: "git error on fetch", - commands: map[args]commandResult{ - `path/to/git remote get-url origin`: { - ExitStatus: 0, - Stdout: "https://github.com/cli/nonexistent.git", - }, - `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential pull --ff-only origin trunk`: { - ExitStatus: 1, - Stderr: "fetch error message", - }, - }, - wantErrorMsg: "failed to run git: fetch error message", + wantErrorMsg: "failed to run git: pull error message", }, } @@ -1348,7 +1326,7 @@ func TestClientPush(t *testing.T) { }, `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential push --set-upstream origin trunk`: { ExitStatus: 1, - Stderr: "fetch error message", + Stderr: "push error message", }, }, wantErrorMsg: "failed to run git: fetch error message", diff --git a/internal/run/stub.go b/internal/run/stub.go index 9499da8e2..5cd3c6de5 100644 --- a/internal/run/stub.go +++ b/internal/run/stub.go @@ -9,7 +9,7 @@ import ( ) const ( - gitAuthRE = `-c credential\..+\.helper= -c credential\..+\.helper=!"[^"]+" auth git-credential ` + gitAuthRE = `-c credential(?:\..+)?\.helper= -c credential(?:\..+)?\.helper=!"[^"]+" auth git-credential ` ) type T interface { diff --git a/pkg/cmd/issue/develop/develop_test.go b/pkg/cmd/issue/develop/develop_test.go index 28642eb51..655668b46 100644 --- a/pkg/cmd/issue/develop/develop_test.go +++ b/pkg/cmd/issue/develop/develop_test.go @@ -314,7 +314,7 @@ func TestDevelopRun(t *testing.T) { ) }, runStubs: func(cs *run.CommandStubber) { - cs.Register(`git remote get-url origin`, 0, "https://github.com/cli/cli.git") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch origin \+refs/heads/my-issue-1:refs/remotes/origin/my-issue-1`, 0, "") }, expectedOut: "github.com/OWNER/REPO/tree/my-issue-1\n", @@ -474,7 +474,6 @@ func TestDevelopRun(t *testing.T) { cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") cs.Register(`git rev-parse --verify refs/heads/my-branch`, 0, "") cs.Register(`git checkout my-branch`, 0, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/cli/cli.git") cs.Register(`git pull --ff-only origin my-branch`, 0, "") }, expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", diff --git a/pkg/cmd/pr/merge/merge_test.go b/pkg/cmd/pr/merge/merge_test.go index ff9f6ef5e..21df882b4 100644 --- a/pkg/cmd/pr/merge/merge_test.go +++ b/pkg/cmd/pr/merge/merge_test.go @@ -644,7 +644,6 @@ func TestPrMerge_deleteBranch(t *testing.T) { cs.Register(`git checkout main`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git pull --ff-only`, 0, "") output, err := runCommand(http, nil, "blueberries", true, `pr merge --merge --delete-branch`) @@ -696,7 +695,6 @@ func TestPrMerge_deleteBranch_nonDefault(t *testing.T) { cs.Register(`git checkout fruit`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git pull --ff-only`, 0, "") output, err := runCommand(http, nil, "blueberries", true, `pr merge --merge --delete-branch`) @@ -746,7 +744,6 @@ func TestPrMerge_deleteBranch_onlyLocally(t *testing.T) { cs.Register(`git checkout main`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git pull --ff-only`, 0, "") output, err := runCommand(http, nil, "blueberries", true, `pr merge --merge --delete-branch`) @@ -797,7 +794,6 @@ func TestPrMerge_deleteBranch_checkoutNewBranch(t *testing.T) { cs.Register(`git checkout -b fruit --track origin/fruit`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git pull --ff-only`, 0, "") output, err := runCommand(http, nil, "blueberries", true, `pr merge --merge --delete-branch`) @@ -1085,7 +1081,6 @@ func TestPrMerge_alreadyMerged(t *testing.T) { cs.Register(`git checkout main`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git pull --ff-only`, 0, "") pm := &prompter.PrompterMock{ @@ -1329,7 +1324,6 @@ func TestPRMergeTTY_withDeleteBranch(t *testing.T) { cs.Register(`git checkout main`, 0, "") cs.Register(`git rev-parse --verify refs/heads/blueberries`, 0, "") cs.Register(`git branch -D blueberries`, 0, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git pull --ff-only`, 0, "") pm := &prompter.PrompterMock{ From 7affcadb5e247f6408018a6d6865b8bc94ff5940 Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 26 Nov 2024 21:55:15 +0100 Subject: [PATCH 38/52] Allow client push to use insecure credential pattern --- git/client.go | 7 +---- git/client_test.go | 46 ++++++++---------------------- git/command.go | 6 ---- pkg/cmd/pr/create/create_test.go | 6 ---- pkg/cmd/repo/create/create.go | 7 +---- pkg/cmd/repo/create/create_test.go | 3 -- 6 files changed, 14 insertions(+), 61 deletions(-) diff --git a/git/client.go b/git/client.go index 658fb40cd..fc386d76f 100644 --- a/git/client.go +++ b/git/client.go @@ -612,13 +612,8 @@ func (c *Client) Pull(ctx context.Context, remote, branch string, mods ...Comman } func (c *Client) Push(ctx context.Context, remote string, ref string, mods ...CommandModifier) error { - host, err := c.CredentialPatternFromRemote(ctx, remote) - if err != nil { - return err - } - args := []string{"push", "--set-upstream", remote, ref} - cmd, err := c.AuthenticatedCommand(ctx, host, args...) + cmd, err := c.AuthenticatedCommand(ctx, InsecureAllMatchingCredentialsPattern, args...) if err != nil { return err } diff --git a/git/client_test.go b/git/client_test.go index d48a6af08..d680c2f00 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -1082,13 +1082,13 @@ func TestClientUnsetRemoteResolution(t *testing.T) { name: "unset remote resolution", wantCmdArgs: `path/to/git config --unset remote.origin.gh-resolved`, }, - // { - // name: "git error", - // cmdExitStatus: 1, - // cmdStderr: "git error message", - // wantCmdArgs: `path/to/git config --unset remote.origin.gh-resolved`, - // wantErrorMsg: "failed to run git: git error message", - // }, + { + name: "git error", + cmdExitStatus: 1, + cmdStderr: "git error message", + wantCmdArgs: `path/to/git config --unset remote.origin.gh-resolved`, + wantErrorMsg: "failed to run git: git error message", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1285,11 +1285,7 @@ func TestClientPush(t *testing.T) { { name: "push", commands: map[args]commandResult{ - `path/to/git remote get-url origin`: { - ExitStatus: 0, - Stdout: "https://github.com/cli/nonexistent.git", - }, - `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential push --set-upstream origin trunk`: { + `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential push --set-upstream origin trunk`: { ExitStatus: 0, }, }, @@ -1298,38 +1294,20 @@ func TestClientPush(t *testing.T) { name: "accepts command modifiers", mods: []CommandModifier{WithRepoDir("/path/to/repo")}, commands: map[args]commandResult{ - `path/to/git remote get-url origin`: { - ExitStatus: 0, - Stdout: "https://github.com/cli/nonexistent.git", - }, - `path/to/git -C /path/to/repo -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential push --set-upstream origin trunk`: { + `path/to/git -C /path/to/repo -c credential.helper= -c credential.helper=!"gh" auth git-credential push --set-upstream origin trunk`: { ExitStatus: 0, }, }, }, { - name: "git error on get-url", + name: "git error on push", commands: map[args]commandResult{ - `path/to/git remote get-url origin`: { - ExitStatus: 1, - Stderr: "get-url error message", - }, - }, - wantErrorMsg: "failed to run git: get-url error message", - }, - { - name: "git error on fetch", - commands: map[args]commandResult{ - `path/to/git remote get-url origin`: { - ExitStatus: 0, - Stdout: "https://github.com/cli/nonexistent.git", - }, - `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential push --set-upstream origin trunk`: { + `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential push --set-upstream origin trunk`: { ExitStatus: 1, Stderr: "push error message", }, }, - wantErrorMsg: "failed to run git: fetch error message", + wantErrorMsg: "failed to run git: push error message", }, } diff --git a/git/command.go b/git/command.go index 70a156912..8065ffd86 100644 --- a/git/command.go +++ b/git/command.go @@ -98,9 +98,3 @@ func WithRepoDir(repoDir string) CommandModifier { gc.setRepoDir(repoDir) } } - -func WithExtraArgs(args ...string) CommandModifier { - return func(gc *Command) { - gc.Args = append(gc.Args, args...) - } -} diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index 7a2ccb7e6..d31174999 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -628,7 +628,6 @@ func Test_createRun(t *testing.T) { cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -693,7 +692,6 @@ func Test_createRun(t *testing.T) { cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -741,7 +739,6 @@ func Test_createRun(t *testing.T) { cmdStubs: func(cs *run.CommandStubber) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -794,7 +791,6 @@ func Test_createRun(t *testing.T) { cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register("git remote rename origin upstream", 0, "") cs.Register(`git remote add origin https://github.com/monalisa/REPO.git`, 0, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -1073,7 +1069,6 @@ func Test_createRun(t *testing.T) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { @@ -1107,7 +1102,6 @@ func Test_createRun(t *testing.T) { cs.Register(`git config --get-regexp.+branch\\\.feature\\\.`, 0, "") cs.Register(`git show-ref --verify -- HEAD refs/remotes/origin/feature`, 0, "") cs.Register(`git( .+)? log( .+)? origin/master\.\.\.feature`, 0, "") - cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git push --set-upstream origin HEAD:refs/heads/feature`, 0, "") }, promptStubs: func(pm *prompter.PrompterMock) { diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 29cf453c7..c732a3759 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -676,12 +676,7 @@ func createFromLocal(opts *CreateOptions) error { } if opts.Push && repoType == bare { - // WM-TODO: can we collapse this into opts.GitClient.Push? - credentialPattern, err := opts.GitClient.CredentialPatternFromRemote(context.Background(), baseRemote) - if err != nil { - return err - } - cmd, err := opts.GitClient.AuthenticatedCommand(context.Background(), credentialPattern, "push", baseRemote, "--mirror") + cmd, err := opts.GitClient.AuthenticatedCommand(context.Background(), git.InsecureAllMatchingCredentialsPattern, "push", baseRemote, "--mirror") if err != nil { return err } diff --git a/pkg/cmd/repo/create/create_test.go b/pkg/cmd/repo/create/create_test.go index 501e721fc..c33cfdad6 100644 --- a/pkg/cmd/repo/create/create_test.go +++ b/pkg/cmd/repo/create/create_test.go @@ -507,7 +507,6 @@ func Test_createRun(t *testing.T) { cs.Register(`git -C . rev-parse --git-dir`, 0, ".") cs.Register(`git -C . rev-parse HEAD`, 0, "commithash") cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "") - cs.Register(`git -C . remote get-url origin`, 0, "https://github.com/OWNER/REPO") cs.Register(`git -C . push origin --mirror`, 0, "") }, wantStdout: "✓ Created repository OWNER/REPO on GitHub\n https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Mirrored all refs to https://github.com/OWNER/REPO.git\n", @@ -576,7 +575,6 @@ func Test_createRun(t *testing.T) { cs.Register(`git -C . rev-parse --git-dir`, 0, ".git") cs.Register(`git -C . rev-parse HEAD`, 0, "commithash") cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "") - cs.Register(`git -C . remote get-url origin`, 0, "https://github.com/OWNER/REPO") cs.Register(`git -C . push --set-upstream origin HEAD`, 0, "") }, wantStdout: "✓ Created repository OWNER/REPO on GitHub\n https://github.com/OWNER/REPO\n✓ Added remote https://github.com/OWNER/REPO.git\n✓ Pushed commits to https://github.com/OWNER/REPO.git\n", @@ -797,7 +795,6 @@ func Test_createRun(t *testing.T) { cs.Register(`git -C . rev-parse --git-dir`, 0, ".") cs.Register(`git -C . rev-parse HEAD`, 0, "commithash") cs.Register(`git -C . remote add origin https://github.com/OWNER/REPO`, 0, "") - cs.Register(`git -C . remote get-url origin`, 0, "https://github.com/OWNER/REPO") cs.Register(`git -C . push origin --mirror`, 0, "") }, wantStdout: "https://github.com/OWNER/REPO\n", From 6b7f1ff06072858daa35b378be3544f932e6eabd Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 26 Nov 2024 22:08:45 +0100 Subject: [PATCH 39/52] Allow client fetch to use insecure credentials pattern --- git/client.go | 7 +------ git/client_test.go | 26 ++------------------------ pkg/cmd/issue/develop/develop_test.go | 5 ----- pkg/cmd/pr/checkout/checkout.go | 7 +++++-- pkg/cmd/repo/clone/clone_test.go | 2 -- pkg/cmd/repo/fork/fork_test.go | 8 -------- 6 files changed, 8 insertions(+), 47 deletions(-) diff --git a/git/client.go b/git/client.go index fc386d76f..d2919ad22 100644 --- a/git/client.go +++ b/git/client.go @@ -577,16 +577,11 @@ func (c *Client) AddRemote(ctx context.Context, name, urlStr string, trackingBra // Below are commands that make network calls and need authentication credentials supplied from gh. func (c *Client) Fetch(ctx context.Context, remote string, refspec string, mods ...CommandModifier) error { - host, err := c.CredentialPatternFromRemote(ctx, remote) - if err != nil { - return err - } - args := []string{"fetch", remote} if refspec != "" { args = append(args, refspec) } - cmd, err := c.AuthenticatedCommand(ctx, host, args...) + cmd, err := c.AuthenticatedCommand(ctx, InsecureAllMatchingCredentialsPattern, args...) if err != nil { return err } diff --git a/git/client_test.go b/git/client_test.go index d680c2f00..614f05995 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -1166,33 +1166,15 @@ func TestClientFetch(t *testing.T) { name: "accepts command modifiers", mods: []CommandModifier{WithRepoDir("/path/to/repo")}, commands: map[args]commandResult{ - `path/to/git remote get-url origin`: { - ExitStatus: 0, - Stdout: "https://github.com/cli/nonexistent.git", - }, - `path/to/git -C /path/to/repo -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential fetch origin trunk`: { + `path/to/git -C /path/to/repo -c credential.helper= -c credential.helper=!"gh" auth git-credential fetch origin trunk`: { ExitStatus: 0, }, }, }, - { - name: "git error on get-url", - commands: map[args]commandResult{ - `path/to/git remote get-url origin`: { - ExitStatus: 1, - Stderr: "get-url error message", - }, - }, - wantErrorMsg: "failed to run git: get-url error message", - }, { name: "git error on fetch", commands: map[args]commandResult{ - `path/to/git remote get-url origin`: { - ExitStatus: 0, - Stdout: "https://github.com/cli/nonexistent.git", - }, - `path/to/git -c credential.https://github.com.helper= -c credential.https://github.com.helper=!"gh" auth git-credential fetch origin trunk`: { + `path/to/git -c credential.helper= -c credential.helper=!"gh" auth git-credential fetch origin trunk`: { ExitStatus: 1, Stderr: "fetch error message", }, @@ -1237,10 +1219,6 @@ func TestClientPull(t *testing.T) { name: "accepts command modifiers", mods: []CommandModifier{WithRepoDir("/path/to/repo")}, commands: map[args]commandResult{ - `path/to/git remote get-url origin`: { - ExitStatus: 0, - Stdout: "https://github.com/cli/nonexistent.git", - }, `path/to/git -C /path/to/repo -c credential.helper= -c credential.helper=!"gh" auth git-credential pull --ff-only origin trunk`: { ExitStatus: 0, }, diff --git a/pkg/cmd/issue/develop/develop_test.go b/pkg/cmd/issue/develop/develop_test.go index 655668b46..abdebf0c8 100644 --- a/pkg/cmd/issue/develop/develop_test.go +++ b/pkg/cmd/issue/develop/develop_test.go @@ -314,7 +314,6 @@ func TestDevelopRun(t *testing.T) { ) }, runStubs: func(cs *run.CommandStubber) { - cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git fetch origin \+refs/heads/my-issue-1:refs/remotes/origin/my-issue-1`, 0, "") }, expectedOut: "github.com/OWNER/REPO/tree/my-issue-1\n", @@ -361,7 +360,6 @@ func TestDevelopRun(t *testing.T) { ) }, runStubs: func(cs *run.CommandStubber) { - cs.Register(`git remote get-url origin`, 0, "https://github.com/cli/cli.git") cs.Register(`git fetch origin \+refs/heads/my-issue-1:refs/remotes/origin/my-issue-1`, 0, "") }, expectedOut: "github.com/OWNER2/REPO/tree/my-issue-1\n", @@ -400,7 +398,6 @@ func TestDevelopRun(t *testing.T) { ) }, runStubs: func(cs *run.CommandStubber) { - cs.Register(`git remote get-url origin`, 0, "https://github.com/cli/cli.git") cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") }, expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", @@ -470,7 +467,6 @@ func TestDevelopRun(t *testing.T) { ) }, runStubs: func(cs *run.CommandStubber) { - cs.Register(`git remote get-url origin`, 0, "https://github.com/cli/cli.git") cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") cs.Register(`git rev-parse --verify refs/heads/my-branch`, 0, "") cs.Register(`git checkout my-branch`, 0, "") @@ -513,7 +509,6 @@ func TestDevelopRun(t *testing.T) { ) }, runStubs: func(cs *run.CommandStubber) { - cs.Register(`git remote get-url origin`, 0, "https://github.com/cli/cli.git") cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") cs.Register(`git rev-parse --verify refs/heads/my-branch`, 1, "") cs.Register(`git checkout -b my-branch --track origin/my-branch`, 0, "") diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index 4a732a409..a4cb405c1 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -251,9 +251,12 @@ func executeCmds(client *git.Client, credentialPattern git.CredentialPattern, cm for _, args := range cmdQueue { var err error var cmd *git.Command - if args[0] == "fetch" || args[0] == "submodule" { + switch args[0] { + case "submodule": cmd, err = client.AuthenticatedCommand(context.Background(), credentialPattern, args...) - } else { + case "fetch": + cmd, err = client.AuthenticatedCommand(context.Background(), git.InsecureAllMatchingCredentialsPattern, args...) + default: cmd, err = client.Command(context.Background(), args...) } if err != nil { diff --git a/pkg/cmd/repo/clone/clone_test.go b/pkg/cmd/repo/clone/clone_test.go index fe122690b..471ed05dd 100644 --- a/pkg/cmd/repo/clone/clone_test.go +++ b/pkg/cmd/repo/clone/clone_test.go @@ -259,7 +259,6 @@ func Test_RepoClone_hasParent(t *testing.T) { cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "") cs.Register(`git -C REPO remote add -t trunk upstream https://github.com/hubot/ORIG.git`, 0, "") - cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/hubot/ORIG.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO remote set-branches upstream *`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") @@ -300,7 +299,6 @@ func Test_RepoClone_hasParent_upstreamRemoteName(t *testing.T) { cs.Register(`git clone https://github.com/OWNER/REPO.git`, 0, "") cs.Register(`git -C REPO remote add -t trunk test https://github.com/hubot/ORIG.git`, 0, "") - cs.Register(`git -C REPO remote get-url test`, 0, "https://github.com/hubot/ORIG.git") cs.Register(`git -C REPO fetch test`, 0, "") cs.Register(`git -C REPO remote set-branches test *`, 0, "") cs.Register(`git -C REPO config --add remote.test.gh-resolved base`, 0, "") diff --git a/pkg/cmd/repo/fork/fork_test.go b/pkg/cmd/repo/fork/fork_test.go index 95b27ae60..0f94496f0 100644 --- a/pkg/cmd/repo/fork/fork_test.go +++ b/pkg/cmd/repo/fork/fork_test.go @@ -442,7 +442,6 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone --depth 1 https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") - cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -475,7 +474,6 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone https://github.com/gamehendge/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") - cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -492,7 +490,6 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") - cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -529,7 +526,6 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") - cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -554,7 +550,6 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") - cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -591,7 +586,6 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") - cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -607,7 +601,6 @@ func TestRepoFork(t *testing.T) { execStubs: func(cs *run.CommandStubber) { cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") - cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, @@ -698,7 +691,6 @@ func TestRepoFork(t *testing.T) { cs.Register(`git clone https://github.com/someone/REPO\.git`, 128, "") cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "") cs.Register(`git -C REPO remote add upstream https://github\.com/OWNER/REPO\.git`, 0, "") - cs.Register(`git -C REPO remote get-url upstream`, 0, "https://github.com/OWNER/REPO.git") cs.Register(`git -C REPO fetch upstream`, 0, "") cs.Register(`git -C REPO config --add remote.upstream.gh-resolved base`, 0, "") }, From 19d62826d6a53b28bdc626641fe032e59e412581 Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 26 Nov 2024 22:19:55 +0100 Subject: [PATCH 40/52] Allow repo sync fetch to use insecure credentials pattern --- pkg/cmd/repo/sync/git.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/cmd/repo/sync/git.go b/pkg/cmd/repo/sync/git.go index 3d48ad7b3..b0bd26abd 100644 --- a/pkg/cmd/repo/sync/git.go +++ b/pkg/cmd/repo/sync/git.go @@ -54,13 +54,8 @@ func (g *gitExecuter) CurrentBranch() (string, error) { } func (g *gitExecuter) Fetch(remote, ref string) error { - host, err := g.client.CredentialPatternFromRemote(context.Background(), remote) - if err != nil { - return err - } - args := []string{"fetch", "-q", remote, ref} - cmd, err := g.client.AuthenticatedCommand(context.Background(), host, args...) + cmd, err := g.client.AuthenticatedCommand(context.Background(), git.InsecureAllMatchingCredentialsPattern, args...) if err != nil { return err } From efd8ff6d469a80e2cb4f9e714be116b01bdf94f6 Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 26 Nov 2024 22:21:05 +0100 Subject: [PATCH 41/52] General cleanup and docs --- git/client.go | 33 ++++++++++++++++++++++----------- git/client_test.go | 18 +++++++++++------- pkg/cmd/pr/checkout/checkout.go | 2 +- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/git/client.go b/git/client.go index d2919ad22..f1b357634 100644 --- a/git/client.go +++ b/git/client.go @@ -95,14 +95,36 @@ func (c *Client) Command(ctx context.Context, args ...string) (*Command, error) return &Command{cmd}, nil } +// CredentialPattern is used to indicate to AuthenticatedCommand which patterns git should match +// against when trying to find credentials. It is a little overengineered as a type because we +// want AuthenticatedCommand to have a clear complication error when this is not provided, +// as opposed to using a string which might compile with `client.AuthenticatedCommand(ctx, "fetch")`. +// +// It is only usable when constructed by another function in the package because the empty pattern, +// without insecure set to true, will result in an error in AuthenticatedCommand. +// +// Callers can currently opt-in to an insecure mode for backwards compatability by using +// InsecureAllMatchingCredentialsPattern. type CredentialPattern struct { insecure bool // should only be constructable via InsecureAllMatchingCredentialPattern pattern string } +// InsecureAllMatchingCredentialsPattern allows for opting in to an insecure mode for backwards compatability. var InsecureAllMatchingCredentialsPattern = CredentialPattern{insecure: true, pattern: ""} var disallowedCredentialPattern = CredentialPattern{insecure: false, pattern: ""} +// WM-TODO: Should this handle command modifiers, e.g. RepoDir being set in Clone +// WM-TODO: Are there any funny remotes that might not resolve to a URL? +// WM-TODO: This should probably have its own tests +func CredentialPatternFromRemote(ctx context.Context, c *Client, remote string) (CredentialPattern, error) { + gitURL, err := c.GetRemoteURL(ctx, remote) + if err != nil { + return CredentialPattern{}, err + } + return CredentialPatternFromGitURL(gitURL) +} + // AuthenticatedCommand is a wrapper around Command that included configuration to use gh // as the credential helper for git. func (c *Client) AuthenticatedCommand(ctx context.Context, credentialPattern CredentialPattern, args ...string) (*Command, error) { @@ -652,17 +674,6 @@ func (c *Client) Clone(ctx context.Context, cloneURL string, args []string, mods return target, nil } -// WM-TODO: Bit of a weird method to hang off the client? -// WM-TODO: We need to make sure this handles command modifiers everywhere... -// WM-TODO: Are there any funny refspec usages that might not resolve via get-url -func (c *Client) CredentialPatternFromRemote(ctx context.Context, remote string) (CredentialPattern, error) { - gitURL, err := c.GetRemoteURL(ctx, remote) - if err != nil { - return CredentialPattern{}, err - } - return CredentialPatternFromGitURL(gitURL) -} - func resolveGitPath() (string, error) { path, err := safeexec.LookPath("git") if err != nil { diff --git a/git/client_test.go b/git/client_test.go index 614f05995..9cacf4e65 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -1499,17 +1499,16 @@ func TestCommandMocking(t *testing.T) { return } - // WM-TODO: maybe don't use 255, I only picked it because it was distinct jsonVar, ok := os.LookupEnv("GH_HELPER_PROCESS_RICH_COMMANDS") if !ok { fmt.Fprint(os.Stderr, "missing GH_HELPER_PROCESS_RICH_COMMANDS") - os.Exit(255) + os.Exit(1) } var commands mockedCommands if err := json.Unmarshal([]byte(jsonVar), &commands); err != nil { fmt.Fprint(os.Stderr, "failed to unmarshal GH_HELPER_PROCESS_RICH_COMMANDS") - os.Exit(255) + os.Exit(1) } // The discarded args are those for the go test binary itself, e.g. `-test.run=TestHelperProcessRich` @@ -1518,12 +1517,17 @@ func TestCommandMocking(t *testing.T) { commandResult, ok := commands[args(strings.Join(realArgs, " "))] if !ok { fmt.Fprintf(os.Stderr, "unexpected command: %s\n", strings.Join(realArgs, " ")) - os.Exit(255) + os.Exit(1) + } + + if commandResult.Stdout != "" { + fmt.Fprint(os.Stdout, commandResult.Stdout) + } + + if commandResult.Stderr != "" { + fmt.Fprint(os.Stderr, commandResult.Stderr) } - // WM-TODO: maybe pointer on these fields, or only print if not-empty - fmt.Fprint(os.Stdout, commandResult.Stdout) - fmt.Fprint(os.Stderr, commandResult.Stderr) os.Exit(commandResult.ExitStatus) } diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index a4cb405c1..d7243af60 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -133,7 +133,7 @@ func checkoutRun(opts *CheckoutOptions) error { // Note that although we will probably be fetching from the headRemote, in practice, PR checkout can only // ever point to one host, and we know baseRemote must be populated, where headRemote might be nil (e.g. when // it was deleted). - credentialPattern, err := opts.GitClient.CredentialPatternFromRemote(context.Background(), baseRemote.Name) + credentialPattern, err := git.CredentialPatternFromRemote(context.Background(), opts.GitClient, baseRemote.Name) if err != nil { return err } From 0db05ff022aff65a3a3132c7445b865a9b5f8f1b Mon Sep 17 00:00:00 2001 From: William Martin Date: Tue, 26 Nov 2024 22:27:36 +0100 Subject: [PATCH 42/52] Add SSH remote todo --- git/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/git/client.go b/git/client.go index f1b357634..8cf4dff33 100644 --- a/git/client.go +++ b/git/client.go @@ -117,6 +117,7 @@ var disallowedCredentialPattern = CredentialPattern{insecure: false, pattern: "" // WM-TODO: Should this handle command modifiers, e.g. RepoDir being set in Clone // WM-TODO: Are there any funny remotes that might not resolve to a URL? // WM-TODO: This should probably have its own tests +// WM-TODO: Consider what to do if remote was SSH, it's probably not breaking, but maybe we want to do something better func CredentialPatternFromRemote(ctx context.Context, c *Client, remote string) (CredentialPattern, error) { gitURL, err := c.GetRemoteURL(ctx, remote) if err != nil { From ad397bd0a66e6865ea0ffc4d06c0d1ca204a49c8 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Tue, 26 Nov 2024 16:08:51 -0800 Subject: [PATCH 43/52] Fix typos and add tests for CredentialPatternFrom* functions --- git/client.go | 32 +++++++++--------- git/client_test.go | 83 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 17 deletions(-) diff --git a/git/client.go b/git/client.go index 8cf4dff33..9526d1249 100644 --- a/git/client.go +++ b/git/client.go @@ -96,21 +96,21 @@ func (c *Client) Command(ctx context.Context, args ...string) (*Command, error) } // CredentialPattern is used to indicate to AuthenticatedCommand which patterns git should match -// against when trying to find credentials. It is a little overengineered as a type because we +// against when trying to find credentials. It is a little over-engineered as a type because we // want AuthenticatedCommand to have a clear complication error when this is not provided, // as opposed to using a string which might compile with `client.AuthenticatedCommand(ctx, "fetch")`. // // It is only usable when constructed by another function in the package because the empty pattern, // without insecure set to true, will result in an error in AuthenticatedCommand. // -// Callers can currently opt-in to an insecure mode for backwards compatability by using +// Callers can currently opt-in to an insecure mode for backwards compatibility by using // InsecureAllMatchingCredentialsPattern. type CredentialPattern struct { insecure bool // should only be constructable via InsecureAllMatchingCredentialPattern pattern string } -// InsecureAllMatchingCredentialsPattern allows for opting in to an insecure mode for backwards compatability. +// InsecureAllMatchingCredentialsPattern allows for opting in to an insecure mode for backwards compatibility. var InsecureAllMatchingCredentialsPattern = CredentialPattern{insecure: true, pattern: ""} var disallowedCredentialPattern = CredentialPattern{insecure: false, pattern: ""} @@ -126,6 +126,18 @@ func CredentialPatternFromRemote(ctx context.Context, c *Client, remote string) return CredentialPatternFromGitURL(gitURL) } +// Cool cool cool... +// YOLO +func CredentialPatternFromGitURL(gitURL string) (CredentialPattern, error) { + normalizedURL, err := ParseURL(gitURL) + if err != nil { + return CredentialPattern{}, fmt.Errorf("failed to parse remote URL: %w", err) + } + return CredentialPattern{ + pattern: strings.TrimSuffix(ghinstance.HostPrefix(normalizedURL.Host), "/"), + }, nil +} + // AuthenticatedCommand is a wrapper around Command that included configuration to use gh // as the credential helper for git. func (c *Client) AuthenticatedCommand(ctx context.Context, credentialPattern CredentialPattern, args ...string) (*Command, error) { @@ -137,7 +149,7 @@ func (c *Client) AuthenticatedCommand(ctx context.Context, credentialPattern Cre var preArgs []string if credentialPattern == disallowedCredentialPattern { - return nil, fmt.Errorf("empty credential pattern is not allowed execept explicitly") + return nil, fmt.Errorf("empty credential pattern is not allowed unless provided explicitly") } else if credentialPattern == InsecureAllMatchingCredentialsPattern { preArgs = []string{"-c", "credential.helper="} preArgs = append(preArgs, "-c", fmt.Sprintf("credential.helper=%s", credHelper)) @@ -789,15 +801,3 @@ var globReplacer = strings.NewReplacer( func escapeGlob(p string) string { return globReplacer.Replace(p) } - -// Cool cool cool... -// YOLO -func CredentialPatternFromGitURL(gitURL string) (CredentialPattern, error) { - normalizedURL, err := ParseURL(gitURL) - if err != nil { - return CredentialPattern{}, fmt.Errorf("failed to parse remote URL: %w", err) - } - return CredentialPattern{ - pattern: strings.TrimSuffix(ghinstance.HostPrefix(normalizedURL.Host), "/"), - }, nil -} diff --git a/git/client_test.go b/git/client_test.go index 9cacf4e65..72a780ae6 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -88,7 +88,7 @@ func TestClientAuthenticatedCommand(t *testing.T) { { name: "errors when attempting to use an empty pattern that isn't marked insecure", pattern: CredentialPattern{insecure: false, pattern: ""}, - wantErr: fmt.Errorf("empty credential pattern is not allowed execept explicitly"), + wantErr: fmt.Errorf("empty credential pattern is not allowed except explicitly"), }, } for _, tt := range tests { @@ -1554,6 +1554,87 @@ func TestHelperProcess(t *testing.T) { os.Exit(0) } +func TestCredentialPatternFromGitURL(t *testing.T) { + tests := []struct { + name string + gitURL string + wantErr bool + wantCredentialPattern CredentialPattern + }{ + { + name: "Given a well formed gitURL, it returns the corresponding CredentialPattern", + gitURL: "https://github.com/OWNER/REPO", + wantCredentialPattern: CredentialPattern{ + pattern: "https://github.com", + insecure: false, + }, + }, + { + name: "Given a malformed gitURL, it returns an error", + // This pattern is copied from the tests in ParseURL + // Unexpectedly, a non URL-like string did not error in ParseURL + gitURL: "ssh://git@[/tmp/git-repo", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + credentialPattern, err := CredentialPatternFromGitURL(tt.gitURL) + if tt.wantErr { + assert.ErrorContains(t, err, "failed to parse remote URL") + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantCredentialPattern, credentialPattern) + } + }) + } +} + +func TestCredentialPatternFromRemote(t *testing.T) { + tests := []struct { + name string + remote string + wantCredentialPattern CredentialPattern + wantErr bool + }{ + { + name: "Given a well formed remote, it returns the corresponding CredentialPattern", + remote: "https://github.com/OWNER/REPO", + wantCredentialPattern: CredentialPattern{ + pattern: "https://github.com", + insecure: false, + }, + }, + { + name: "Given an error from GetRemoteURL, it returns that error", + remote: "foo remote", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var cmdCtx func(ctx context.Context, name string, args ...string) *exec.Cmd + if tt.wantErr { + _, cmdCtx = createCommandContext(t, 1, tt.remote, "GetRemoteURL error") + } else { + _, cmdCtx = createCommandContext(t, 0, tt.remote, "") + } + + client := Client{ + GitPath: "path/to/git", + commandContext: cmdCtx, + } + credentialPattern, err := CredentialPatternFromRemote(context.Background(), &client, tt.remote) + if tt.wantErr { + assert.ErrorContains(t, err, "GetRemoteURL error") + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantCredentialPattern, credentialPattern) + } + }) + } +} + func createCommandContext(t *testing.T, exitStatus int, stdout, stderr string) (*exec.Cmd, commandCtx) { cmd := exec.CommandContext(context.Background(), os.Args[0], "-test.run=TestHelperProcess", "--") cmd.Env = []string{ From 3773068f58ba7550a4b56d857abbeb67b2653278 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 27 Nov 2024 12:06:17 +0100 Subject: [PATCH 44/52] Remove TODOs --- git/client.go | 11 ++++------- git/client_test.go | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/git/client.go b/git/client.go index 9526d1249..909de88d1 100644 --- a/git/client.go +++ b/git/client.go @@ -114,10 +114,7 @@ type CredentialPattern struct { var InsecureAllMatchingCredentialsPattern = CredentialPattern{insecure: true, pattern: ""} var disallowedCredentialPattern = CredentialPattern{insecure: false, pattern: ""} -// WM-TODO: Should this handle command modifiers, e.g. RepoDir being set in Clone // WM-TODO: Are there any funny remotes that might not resolve to a URL? -// WM-TODO: This should probably have its own tests -// WM-TODO: Consider what to do if remote was SSH, it's probably not breaking, but maybe we want to do something better func CredentialPatternFromRemote(ctx context.Context, c *Client, remote string) (CredentialPattern, error) { gitURL, err := c.GetRemoteURL(ctx, remote) if err != nil { @@ -126,8 +123,6 @@ func CredentialPatternFromRemote(ctx context.Context, c *Client, remote string) return CredentialPatternFromGitURL(gitURL) } -// Cool cool cool... -// YOLO func CredentialPatternFromGitURL(gitURL string) (CredentialPattern, error) { normalizedURL, err := ParseURL(gitURL) if err != nil { @@ -654,7 +649,9 @@ func (c *Client) Push(ctx context.Context, remote string, ref string, mods ...Co } func (c *Client) Clone(ctx context.Context, cloneURL string, args []string, mods ...CommandModifier) (string, error) { - host, err := CredentialPatternFromGitURL(cloneURL) + // Note that even if this is an SSH clone URL, we are setting the pattern anyway. + // We could write some code to prevent this, but it also doesn't seem harmful. + pattern, err := CredentialPatternFromGitURL(cloneURL) if err != nil { return "", err } @@ -673,7 +670,7 @@ func (c *Client) Clone(ctx context.Context, cloneURL string, args []string, mods } } cloneArgs = append([]string{"clone"}, cloneArgs...) - cmd, err := c.AuthenticatedCommand(ctx, host, cloneArgs...) + cmd, err := c.AuthenticatedCommand(ctx, pattern, cloneArgs...) if err != nil { return "", err } diff --git a/git/client_test.go b/git/client_test.go index 72a780ae6..4ba6f0843 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -88,7 +88,7 @@ func TestClientAuthenticatedCommand(t *testing.T) { { name: "errors when attempting to use an empty pattern that isn't marked insecure", pattern: CredentialPattern{insecure: false, pattern: ""}, - wantErr: fmt.Errorf("empty credential pattern is not allowed except explicitly"), + wantErr: fmt.Errorf("empty credential pattern is not allowed unless provided explicitly"), }, } for _, tt := range tests { From 0b4f087b466c47062ce6fa6a915558de207f22c7 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 27 Nov 2024 12:07:04 +0100 Subject: [PATCH 45/52] Fix CredentialPattern doc typos Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com> --- git/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/git/client.go b/git/client.go index 909de88d1..439199a46 100644 --- a/git/client.go +++ b/git/client.go @@ -95,9 +95,9 @@ func (c *Client) Command(ctx context.Context, args ...string) (*Command, error) return &Command{cmd}, nil } -// CredentialPattern is used to indicate to AuthenticatedCommand which patterns git should match +// CredentialPattern is used to inform AuthenticatedCommand which patterns Git should match // against when trying to find credentials. It is a little over-engineered as a type because we -// want AuthenticatedCommand to have a clear complication error when this is not provided, +// want AuthenticatedCommand to have a clear compilation error when this is not provided, // as opposed to using a string which might compile with `client.AuthenticatedCommand(ctx, "fetch")`. // // It is only usable when constructed by another function in the package because the empty pattern, From 72a6fd00a47afc40f2e3030341223e8a2fc10c17 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 27 Nov 2024 12:21:55 +0100 Subject: [PATCH 46/52] Rename backwards compatible credentials pattern --- git/client.go | 25 +++++++++++++------------ git/client_test.go | 16 ++++++++-------- pkg/cmd/pr/checkout/checkout.go | 2 +- pkg/cmd/repo/create/create.go | 2 +- pkg/cmd/repo/sync/git.go | 2 +- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/git/client.go b/git/client.go index 439199a46..b2cfbce45 100644 --- a/git/client.go +++ b/git/client.go @@ -101,18 +101,19 @@ func (c *Client) Command(ctx context.Context, args ...string) (*Command, error) // as opposed to using a string which might compile with `client.AuthenticatedCommand(ctx, "fetch")`. // // It is only usable when constructed by another function in the package because the empty pattern, -// without insecure set to true, will result in an error in AuthenticatedCommand. +// without allMatching set to true, will result in an error in AuthenticatedCommand. // -// Callers can currently opt-in to an insecure mode for backwards compatibility by using -// InsecureAllMatchingCredentialsPattern. +// Callers can currently opt-in to an slightly less secure mode for backwards compatibility by using +// AllMatchingCredentialsPattern. type CredentialPattern struct { - insecure bool // should only be constructable via InsecureAllMatchingCredentialPattern - pattern string + allMatching bool // should only be constructable via AllMatchingCredentialsPattern + pattern string } -// InsecureAllMatchingCredentialsPattern allows for opting in to an insecure mode for backwards compatibility. -var InsecureAllMatchingCredentialsPattern = CredentialPattern{insecure: true, pattern: ""} -var disallowedCredentialPattern = CredentialPattern{insecure: false, pattern: ""} +// AllMatchingCredentialsPattern allows for setting gh as credential helper for all hosts. +// However, we should endeavour to remove it as it's less secure. +var AllMatchingCredentialsPattern = CredentialPattern{allMatching: true, pattern: ""} +var disallowedCredentialPattern = CredentialPattern{allMatching: false, pattern: ""} // WM-TODO: Are there any funny remotes that might not resolve to a URL? func CredentialPatternFromRemote(ctx context.Context, c *Client, remote string) (CredentialPattern, error) { @@ -145,7 +146,7 @@ func (c *Client) AuthenticatedCommand(ctx context.Context, credentialPattern Cre var preArgs []string if credentialPattern == disallowedCredentialPattern { return nil, fmt.Errorf("empty credential pattern is not allowed unless provided explicitly") - } else if credentialPattern == InsecureAllMatchingCredentialsPattern { + } else if credentialPattern == AllMatchingCredentialsPattern { preArgs = []string{"-c", "credential.helper="} preArgs = append(preArgs, "-c", fmt.Sprintf("credential.helper=%s", credHelper)) } else { @@ -611,7 +612,7 @@ func (c *Client) Fetch(ctx context.Context, remote string, refspec string, mods if refspec != "" { args = append(args, refspec) } - cmd, err := c.AuthenticatedCommand(ctx, InsecureAllMatchingCredentialsPattern, args...) + cmd, err := c.AuthenticatedCommand(ctx, AllMatchingCredentialsPattern, args...) if err != nil { return err } @@ -626,7 +627,7 @@ func (c *Client) Pull(ctx context.Context, remote, branch string, mods ...Comman if remote != "" && branch != "" { args = append(args, remote, branch) } - cmd, err := c.AuthenticatedCommand(ctx, InsecureAllMatchingCredentialsPattern, args...) + cmd, err := c.AuthenticatedCommand(ctx, AllMatchingCredentialsPattern, args...) if err != nil { return err } @@ -638,7 +639,7 @@ func (c *Client) Pull(ctx context.Context, remote, branch string, mods ...Comman func (c *Client) Push(ctx context.Context, remote string, ref string, mods ...CommandModifier) error { args := []string{"push", "--set-upstream", remote, ref} - cmd, err := c.AuthenticatedCommand(ctx, InsecureAllMatchingCredentialsPattern, args...) + cmd, err := c.AuthenticatedCommand(ctx, AllMatchingCredentialsPattern, args...) if err != nil { return err } diff --git a/git/client_test.go b/git/client_test.go index 4ba6f0843..f643caa90 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -71,7 +71,7 @@ func TestClientAuthenticatedCommand(t *testing.T) { { name: "when credential pattern is TODO, credential helper matches everything", path: "path/to/gh", - pattern: InsecureAllMatchingCredentialsPattern, + pattern: AllMatchingCredentialsPattern, wantArgs: []string{"path/to/git", "-c", "credential.helper=", "-c", `credential.helper=!"path/to/gh" auth git-credential`, "fetch"}, }, { @@ -82,12 +82,12 @@ func TestClientAuthenticatedCommand(t *testing.T) { }, { name: "fallback when GhPath is not set", - pattern: InsecureAllMatchingCredentialsPattern, + pattern: AllMatchingCredentialsPattern, wantArgs: []string{"path/to/git", "-c", "credential.helper=", "-c", `credential.helper=!"gh" auth git-credential`, "fetch"}, }, { - name: "errors when attempting to use an empty pattern that isn't marked insecure", - pattern: CredentialPattern{insecure: false, pattern: ""}, + name: "errors when attempting to use an empty pattern that isn't marked all matching", + pattern: CredentialPattern{allMatching: false, pattern: ""}, wantErr: fmt.Errorf("empty credential pattern is not allowed unless provided explicitly"), }, } @@ -1565,8 +1565,8 @@ func TestCredentialPatternFromGitURL(t *testing.T) { name: "Given a well formed gitURL, it returns the corresponding CredentialPattern", gitURL: "https://github.com/OWNER/REPO", wantCredentialPattern: CredentialPattern{ - pattern: "https://github.com", - insecure: false, + pattern: "https://github.com", + allMatching: false, }, }, { @@ -1601,8 +1601,8 @@ func TestCredentialPatternFromRemote(t *testing.T) { name: "Given a well formed remote, it returns the corresponding CredentialPattern", remote: "https://github.com/OWNER/REPO", wantCredentialPattern: CredentialPattern{ - pattern: "https://github.com", - insecure: false, + pattern: "https://github.com", + allMatching: false, }, }, { diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index d7243af60..c4549d89b 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -255,7 +255,7 @@ func executeCmds(client *git.Client, credentialPattern git.CredentialPattern, cm case "submodule": cmd, err = client.AuthenticatedCommand(context.Background(), credentialPattern, args...) case "fetch": - cmd, err = client.AuthenticatedCommand(context.Background(), git.InsecureAllMatchingCredentialsPattern, args...) + cmd, err = client.AuthenticatedCommand(context.Background(), git.AllMatchingCredentialsPattern, args...) default: cmd, err = client.Command(context.Background(), args...) } diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index c732a3759..ec7026759 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -676,7 +676,7 @@ func createFromLocal(opts *CreateOptions) error { } if opts.Push && repoType == bare { - cmd, err := opts.GitClient.AuthenticatedCommand(context.Background(), git.InsecureAllMatchingCredentialsPattern, "push", baseRemote, "--mirror") + cmd, err := opts.GitClient.AuthenticatedCommand(context.Background(), git.AllMatchingCredentialsPattern, "push", baseRemote, "--mirror") if err != nil { return err } diff --git a/pkg/cmd/repo/sync/git.go b/pkg/cmd/repo/sync/git.go index b0bd26abd..0a0be1ed6 100644 --- a/pkg/cmd/repo/sync/git.go +++ b/pkg/cmd/repo/sync/git.go @@ -55,7 +55,7 @@ func (g *gitExecuter) CurrentBranch() (string, error) { func (g *gitExecuter) Fetch(remote, ref string) error { args := []string{"fetch", "-q", remote, ref} - cmd, err := g.client.AuthenticatedCommand(context.Background(), git.InsecureAllMatchingCredentialsPattern, args...) + cmd, err := g.client.AuthenticatedCommand(context.Background(), git.AllMatchingCredentialsPattern, args...) if err != nil { return err } From bd44d33eaabe6dc12db3e8f5a5d9b2298b19e736 Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 27 Nov 2024 13:06:35 +0100 Subject: [PATCH 47/52] Add checkout test that uses ssh git remote url --- pkg/cmd/pr/checkout/checkout_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/cmd/pr/checkout/checkout_test.go b/pkg/cmd/pr/checkout/checkout_test.go index 55123fd59..32f7b5b80 100644 --- a/pkg/cmd/pr/checkout/checkout_test.go +++ b/pkg/cmd/pr/checkout/checkout_test.go @@ -72,6 +72,32 @@ func Test_checkoutRun(t *testing.T) { wantStderr string wantErr bool }{ + { + name: "checkout with ssh remote URL", + opts: &CheckoutOptions{ + SelectorArg: "123", + Finder: func() shared.PRFinder { + baseRepo, pr := stubPR("OWNER/REPO:master", "OWNER/REPO:feature") + finder := shared.NewMockFinder("123", pr, baseRepo) + return finder + }(), + Config: func() (gh.Config, error) { + return config.NewBlankConfig(), nil + }, + Branch: func() (string, error) { + return "main", nil + }, + }, + remotes: map[string]string{ + "origin": "OWNER/REPO", + }, + runStubs: func(cs *run.CommandStubber) { + cs.Register(`git show-ref --verify -- refs/heads/feature`, 1, "") + cs.Register(`git remote get-url origin`, 0, "https://github.com/OWNER/REPO.git") + cs.Register(`git fetch origin \+refs/heads/feature:refs/remotes/origin/feature`, 0, "") + cs.Register(`git checkout -b feature --track origin/feature`, 0, "") + }, + }, { name: "fork repo was deleted", opts: &CheckoutOptions{ From 21a14a7d1a6dc9561346b956c8ddc047d04fee9d Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 27 Nov 2024 15:50:54 +0100 Subject: [PATCH 48/52] Update git/client_test.go Co-authored-by: Andy Feller --- git/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/client_test.go b/git/client_test.go index f643caa90..b39ae7acb 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -69,7 +69,7 @@ func TestClientAuthenticatedCommand(t *testing.T) { wantErr error }{ { - name: "when credential pattern is TODO, credential helper matches everything", + name: "when credential pattern allows for anything, credential helper matches everything", path: "path/to/gh", pattern: AllMatchingCredentialsPattern, wantArgs: []string{"path/to/git", "-c", "credential.helper=", "-c", `credential.helper=!"path/to/gh" auth git-credential`, "fetch"}, From 622e283d2b3243686933c3a87d57344a7e56e6fd Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 27 Nov 2024 15:51:38 +0100 Subject: [PATCH 49/52] Update git/client_test.go Co-authored-by: Andy Feller --- git/client_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/git/client_test.go b/git/client_test.go index b39ae7acb..65adf96da 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -1494,6 +1494,7 @@ type commandResult struct { type mockedCommands map[args]commandResult +// TestCommandMocking is an invoked test helper that emulates expected behavior for predefined shell commands, erroring when unexpected conditions are encountered. func TestCommandMocking(t *testing.T) { if os.Getenv("GH_WANT_HELPER_PROCESS_RICH") != "1" { return From ec086a021b9feddf600c3fc0d35ef8e17023ee6b Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 27 Nov 2024 15:51:44 +0100 Subject: [PATCH 50/52] Update git/client_test.go Co-authored-by: Andy Feller --- git/client_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/git/client_test.go b/git/client_test.go index 65adf96da..0014ddc3f 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -1655,6 +1655,7 @@ func createMockedCommandContext(t *testing.T, commands mockedCommands) commandCt marshaledCommands, err := json.Marshal(commands) require.NoError(t, err) + // invokes helper within current test binary, emulating desired behavior return func(ctx context.Context, exe string, args ...string) *exec.Cmd { cmd := exec.CommandContext(context.Background(), os.Args[0], "-test.run=TestCommandMocking", "--") cmd.Env = []string{ From c94def8b515b113f38475f2e33b2520200a9854e Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Wed, 27 Nov 2024 15:54:38 -0500 Subject: [PATCH 51/52] Bump cli/go-gh for codespace fix --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 40193c4f8..a95ccacc0 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c - github.com/cli/go-gh/v2 v2.11.0 + github.com/cli/go-gh/v2 v2.11.1 github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 github.com/cli/oauth v1.1.1 github.com/cli/safeexec v1.0.1 diff --git a/go.sum b/go.sum index d068d9d11..a7d87de7d 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,8 @@ github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f/go.mod h1 github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/cli/go-gh/v2 v2.11.0 h1:TERLYMMWderKBO3lBff/JIu2+eSly2oFRgN2WvO+3eA= -github.com/cli/go-gh/v2 v2.11.0/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE= +github.com/cli/go-gh/v2 v2.11.1 h1:amAyfqMWQTBdue8iTmDUegGZK7c8kk6WCxD9l/wLtGI= +github.com/cli/go-gh/v2 v2.11.1/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE= github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 h1:QDrhR4JA2n3ij9YQN0u5ZeuvRIIvsUGmf5yPlTS0w8E= github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24/go.mod h1:rr9GNING0onuVw8MnracQHn7PcchnFlP882Y0II2KZk= github.com/cli/oauth v1.1.1 h1:459gD3hSjlKX9B1uXBuiAMdpXBUQ9QGf/NDcCpoQxPs= From 76f1553fb910d5dbc99687b43359000034cdbef4 Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:19:27 -0700 Subject: [PATCH 52/52] Fix formatting in client_test.go comments for linter --- git/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/client_test.go b/git/client_test.go index 0014ddc3f..41b651d0a 100644 --- a/git/client_test.go +++ b/git/client_test.go @@ -1655,7 +1655,7 @@ func createMockedCommandContext(t *testing.T, commands mockedCommands) commandCt marshaledCommands, err := json.Marshal(commands) require.NoError(t, err) - // invokes helper within current test binary, emulating desired behavior + // invokes helper within current test binary, emulating desired behavior return func(ctx context.Context, exe string, args ...string) *exec.Cmd { cmd := exec.CommandContext(context.Background(), os.Args[0], "-test.run=TestCommandMocking", "--") cmd.Env = []string{