From 20d39314279502ec3bc1bba40f4a4614f9d218c0 Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Mon, 5 Aug 2024 09:11:25 -0700 Subject: [PATCH 001/301] tmp --- pkg/cmd/attestation/artifact/artifact.go | 13 ++- pkg/cmd/attestation/artifact/image.go | 9 +- pkg/cmd/attestation/artifact/oci/client.go | 130 +++++++++++++++++++-- pkg/cmd/attestation/verify/verify.go | 10 ++ 4 files changed, 146 insertions(+), 16 deletions(-) diff --git a/pkg/cmd/attestation/artifact/artifact.go b/pkg/cmd/attestation/artifact/artifact.go index de354b947..f9302b439 100644 --- a/pkg/cmd/attestation/artifact/artifact.go +++ b/pkg/cmd/attestation/artifact/artifact.go @@ -7,6 +7,8 @@ import ( "path/filepath" "strings" + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" ) @@ -19,9 +21,10 @@ const ( // DigestedArtifact abstracts the software artifact being verified type DigestedArtifact struct { - URL string - digest string - digestAlg string + URL string + digest string + digestAlg string + attestations []*api.Attestation } func normalizeReference(reference string, pathSeparator rune) (normalized string, artifactType artifactType, err error) { @@ -77,3 +80,7 @@ func (a *DigestedArtifact) Algorithm() string { func (a *DigestedArtifact) DigestWithAlg() string { return fmt.Sprintf("%s:%s", a.digestAlg, a.digest) } + +func (a *DigestedArtifact) Attestations() []*api.Attestation { + return a.attestations +} diff --git a/pkg/cmd/attestation/artifact/image.go b/pkg/cmd/attestation/artifact/image.go index 2af13e723..45e3104c2 100644 --- a/pkg/cmd/attestation/artifact/image.go +++ b/pkg/cmd/attestation/artifact/image.go @@ -15,14 +15,15 @@ func digestContainerImageArtifact(url string, client oci.Client) (*DigestedArtif return nil, fmt.Errorf("artifact %s is not a valid registry reference: %v", url, err) } - digest, err := client.GetImageDigest(named.String()) + digest, attestations, err := client.GetImageDigest(named.String()) if err != nil { return nil, err } return &DigestedArtifact{ - URL: fmt.Sprintf("oci://%s", named.String()), - digest: digest.Hex, - digestAlg: digest.Algorithm, + URL: fmt.Sprintf("oci://%s", named.String()), + digest: digest.Hex, + digestAlg: digest.Algorithm, + attestations: attestations, }, nil } diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go index 5b8d8cf7a..e0a497e40 100644 --- a/pkg/cmd/attestation/artifact/oci/client.go +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -1,21 +1,27 @@ package oci import ( + "bytes" "errors" "fmt" + "io" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1" + "github.com/sigstore/sigstore-go/pkg/bundle" + + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote/transport" + protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" ) var ErrDenied = errors.New("the provided token was denied access to the requested resource, please check the token's expiration and repository access") var ErrRegistryAuthz = errors.New("remote registry authorization failed, please authenticate with the registry and try again") type Client interface { - GetImageDigest(imgName string) (*v1.Hash, error) + GetImageDigest(imgName string) (*v1.Hash, []*api.Attestation, error) } func checkForUnauthorizedOrDeniedErr(err transport.Error) error { @@ -36,27 +42,133 @@ type LiveClient struct { } // where name is formed like ghcr.io/github/my-image-repo -func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, error) { - name, err := c.parseReference(imgName) +func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, []*api.Attestation, error) { + nameFirst, err := c.parseReference(imgName) + var buf bytes.Buffer + attestations := []*api.Attestation{} + if err != nil { - return nil, fmt.Errorf("failed to create image tag: %v", err) + return nil, attestations, fmt.Errorf("failed to create image tag: %v", err) } // The user must already be authenticated with the container registry // The authn.DefaultKeychain argument indicates that Get should checks the // user's configuration for the registry credentials - desc, err := c.get(name, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + desc, err := c.get(nameFirst, remote.WithAuthFromKeychain(authn.DefaultKeychain)) if err != nil { var transportErr *transport.Error if errors.As(err, &transportErr) { if accessErr := checkForUnauthorizedOrDeniedErr(*transportErr); accessErr != nil { - return nil, accessErr + return nil, attestations, accessErr } } - return nil, fmt.Errorf("failed to fetch remote image: %v", err) + return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) } - return &desc.Digest, nil + dgst := nameFirst.Context().Digest(desc.Digest.String()) + + ref, err := remote.Referrers(dgst, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + indexManifest, err := ref.IndexManifest() + if err != nil { + return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) + } + manifests := indexManifest.Manifests + + for _, m := range manifests { + allowedArtifactTypes := []string{"application/vnd.dev.sigstore.bundle.v0.3+json"} + + for _, allowedType := range allowedArtifactTypes { + if allowedType == m.ArtifactType { + manifestDigest := m.Digest.String() + + manifestURL := fmt.Sprintf("%s/manifests/%s", imgName, manifestDigest) + fmt.Println(manifestURL) + + digest2 := nameFirst.Context().Digest(manifestDigest) + // replace to use GET for more correc type + img2, err := remote.Image(digest2, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) + } + // manifest2, err := ref2.Manifest() + // if err != nil { + // return nil, fmt.Errorf("failed to fetch remote image: %v", err) + // } + + // fmt.Println(manifest2.MediaType) + // Step 4: Get the layers + layers, err := img2.Layers() + if err != nil { + fmt.Println("Error getting layers: %v", err) + return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) + + } + + // For simplicity, we'll just fetch the first layer + if len(layers) > 0 { + fmt.Println("how many layers?", len(layers)) + layer := layers[0] + + // Step 5: Read the blob (layer) content + rc, err := layer.Compressed() + if err != nil { + fmt.Println("Error getting compressed layer: %v", err) + return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) + + } + defer rc.Close() + + layerBytes, err := io.ReadAll(rc) + + if err != nil { + // If creating a gzip reader fails, it might not be compressed + fmt.Println("Layer is not gzip-compressed. Reading raw content.") + // fmt.Println("Blob content:", buf.String()) + + var bundle bundle.ProtobufBundle + bundle.Bundle = new(protobundle.Bundle) + err = bundle.UnmarshalJSON(layerBytes) + fmt.Println("") + fmt.Println("JSON Content:", string(layerBytes)) + fmt.Println("") + + if err != nil { + fmt.Println("failed to unmarshal bundle from JSON: %v", err) + } else { + fmt.Println("Bundle content:", bundle.String()) + } + + a := api.Attestation{Bundle: &bundle} + attestations = append(attestations, &a) + + } else { + defer gz.Close() + + var decompressed bytes.Buffer + if _, err := io.Copy(&decompressed, gz); err != nil { + fmt.Println("Error decompressing layer content: %v", err) + } + + // Now you have the decompressed blob content in 'decompressed' buffer + fmt.Println("Decompressed blob content:", decompressed.String()) + } + + // Now you have the decompressed blob content in 'decompressed' buffer + // fmt.Println("Blob content:", decompressed.String()) + } else { + fmt.Println("No layers found in the image.") + } + } + } + } + + // msgName, err := c.parseReference(msg) + // fmt.Println() + // if err != nil { + // return nil, err + // } + + return &desc.Digest, attestations, nil } // Unlike other parts of this command set, we cannot pass a custom HTTP client diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index e1a5b1c50..990de3985 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -204,6 +204,16 @@ func runVerify(opts *Options) error { return err } + attestationsFromOCI := artifact.Attestations() + if len(attestationsFromOCI) > 0 { + attestations = append(attestations, attestationsFromOCI...) + + for _, attestation := range attestations { + // ja, err := attestation.Bundle.String() + opts.Logger.Printf("Loaded attestation from OCI registry: %s\n", attestation.Bundle.String()) + } + } + pluralAttestation := text.Pluralize(len(attestations), "attestation") if c.IsBundleProvided() { opts.Logger.Printf("Loaded %s from %s\n", pluralAttestation, opts.BundlePath) From 8d17896080cb6e0b117203be9c7014b7f3a533f0 Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Mon, 5 Aug 2024 12:25:52 -0700 Subject: [PATCH 002/301] refactor the logic and logging --- pkg/cmd/attestation/artifact/artifact.go | 4 +- pkg/cmd/attestation/artifact/image.go | 35 +++++- pkg/cmd/attestation/artifact/oci/client.go | 114 ++++++------------ pkg/cmd/attestation/download/download.go | 2 +- pkg/cmd/attestation/inspect/inspect.go | 2 +- .../attestation/verification/attestation.go | 18 ++- pkg/cmd/attestation/verify/options.go | 43 +++---- pkg/cmd/attestation/verify/verify.go | 30 ++--- 8 files changed, 120 insertions(+), 128 deletions(-) diff --git a/pkg/cmd/attestation/artifact/artifact.go b/pkg/cmd/attestation/artifact/artifact.go index f9302b439..e8e0ea095 100644 --- a/pkg/cmd/attestation/artifact/artifact.go +++ b/pkg/cmd/attestation/artifact/artifact.go @@ -54,14 +54,14 @@ func normalizeReference(reference string, pathSeparator rune) (normalized string return filepath.Clean(reference), fileArtifactType, nil } -func NewDigestedArtifact(client oci.Client, reference, digestAlg string) (artifact *DigestedArtifact, err error) { +func NewDigestedArtifact(client oci.Client, reference, digestAlg string, useBundleFromRegistry bool) (artifact *DigestedArtifact, err error) { normalized, artifactType, err := normalizeReference(reference, os.PathSeparator) if err != nil { return nil, err } if artifactType == ociArtifactType { // TODO: should we allow custom digestAlg for OCI artifacts? - return digestContainerImageArtifact(normalized, client) + return digestContainerImageArtifact(normalized, client, useBundleFromRegistry) } return digestLocalFileArtifact(normalized, digestAlg) } diff --git a/pkg/cmd/attestation/artifact/image.go b/pkg/cmd/attestation/artifact/image.go index 45e3104c2..e1fba97bf 100644 --- a/pkg/cmd/attestation/artifact/image.go +++ b/pkg/cmd/attestation/artifact/image.go @@ -7,7 +7,7 @@ import ( "github.com/distribution/reference" ) -func digestContainerImageArtifact(url string, client oci.Client) (*DigestedArtifact, error) { +func digestContainerImageArtifact(url string, client oci.Client, useBundleFromRegistry bool) (*DigestedArtifact, error) { // try to parse the url as a valid registry reference named, err := reference.Parse(url) if err != nil { @@ -15,15 +15,38 @@ func digestContainerImageArtifact(url string, client oci.Client) (*DigestedArtif return nil, fmt.Errorf("artifact %s is not a valid registry reference: %v", url, err) } - digest, attestations, err := client.GetImageDigest(named.String()) + name, err := client.ParseReference(named.String()) + if err != nil { + return nil, err + } + + digest, err := client.GetImageDigest(name) + + if err != nil { + return nil, err + } + if useBundleFromRegistry { + attestations, err := client.GetAttestations(name, digest) + + if err != nil { + return nil, err + } + + return &DigestedArtifact{ + URL: fmt.Sprintf("oci://%s", named.String()), + digest: digest.Hex, + digestAlg: digest.Algorithm, + attestations: attestations, + }, nil + } + if err != nil { return nil, err } return &DigestedArtifact{ - URL: fmt.Sprintf("oci://%s", named.String()), - digest: digest.Hex, - digestAlg: digest.Algorithm, - attestations: attestations, + URL: fmt.Sprintf("oci://%s", named.String()), + digest: digest.Hex, + digestAlg: digest.Algorithm, }, nil } diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go index e0a497e40..9e53c6f2b 100644 --- a/pkg/cmd/attestation/artifact/oci/client.go +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -1,7 +1,6 @@ package oci import ( - "bytes" "errors" "fmt" "io" @@ -21,7 +20,9 @@ var ErrDenied = errors.New("the provided token was denied access to the requeste var ErrRegistryAuthz = errors.New("remote registry authorization failed, please authenticate with the registry and try again") type Client interface { - GetImageDigest(imgName string) (*v1.Hash, []*api.Attestation, error) + GetImageDigest(name name.Reference) (*v1.Hash, error) + GetAttestations(name name.Reference, digest *v1.Hash) ([]*api.Attestation, error) + ParseReference(ref string) (name.Reference, error) } func checkForUnauthorizedOrDeniedErr(err transport.Error) error { @@ -41,36 +42,40 @@ type LiveClient struct { get func(name.Reference, ...remote.Option) (*remote.Descriptor, error) } +func (c LiveClient) ParseReference(ref string) (name.Reference, error) { + return c.parseReference(ref) +} + // where name is formed like ghcr.io/github/my-image-repo -func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, []*api.Attestation, error) { - nameFirst, err := c.parseReference(imgName) - var buf bytes.Buffer - attestations := []*api.Attestation{} - - if err != nil { - return nil, attestations, fmt.Errorf("failed to create image tag: %v", err) - } - +func (c LiveClient) GetImageDigest(name name.Reference) (*v1.Hash, error) { // The user must already be authenticated with the container registry // The authn.DefaultKeychain argument indicates that Get should checks the // user's configuration for the registry credentials - desc, err := c.get(nameFirst, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + desc, err := c.get(name, remote.WithAuthFromKeychain(authn.DefaultKeychain)) if err != nil { var transportErr *transport.Error if errors.As(err, &transportErr) { if accessErr := checkForUnauthorizedOrDeniedErr(*transportErr); accessErr != nil { - return nil, attestations, accessErr + return nil, accessErr } } - return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) + return nil, fmt.Errorf("failed to fetch remote image: %v", err) } - dgst := nameFirst.Context().Digest(desc.Digest.String()) + return &desc.Digest, nil +} - ref, err := remote.Referrers(dgst, remote.WithAuthFromKeychain(authn.DefaultKeychain)) +func (c LiveClient) GetAttestations(name name.Reference, digest *v1.Hash) ([]*api.Attestation, error) { + attestations := []*api.Attestation{} + nameDegist := name.Context().Digest(digest.String()) + + ref, err := remote.Referrers(nameDegist, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) + } indexManifest, err := ref.IndexManifest() if err != nil { - return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } manifests := indexManifest.Manifests @@ -81,94 +86,55 @@ func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, []*api.Attestation if allowedType == m.ArtifactType { manifestDigest := m.Digest.String() - manifestURL := fmt.Sprintf("%s/manifests/%s", imgName, manifestDigest) - fmt.Println(manifestURL) - - digest2 := nameFirst.Context().Digest(manifestDigest) + digest2 := nameDegist.Context().Digest(manifestDigest) // replace to use GET for more correc type img2, err := remote.Image(digest2, remote.WithAuthFromKeychain(authn.DefaultKeychain)) if err != nil { - return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } - // manifest2, err := ref2.Manifest() - // if err != nil { - // return nil, fmt.Errorf("failed to fetch remote image: %v", err) - // } - // fmt.Println(manifest2.MediaType) // Step 4: Get the layers layers, err := img2.Layers() if err != nil { - fmt.Println("Error getting layers: %v", err) - return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } // For simplicity, we'll just fetch the first layer if len(layers) > 0 { - fmt.Println("how many layers?", len(layers)) layer := layers[0] // Step 5: Read the blob (layer) content rc, err := layer.Compressed() if err != nil { - fmt.Println("Error getting compressed layer: %v", err) - return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) - + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } defer rc.Close() layerBytes, err := io.ReadAll(rc) if err != nil { - // If creating a gzip reader fails, it might not be compressed - fmt.Println("Layer is not gzip-compressed. Reading raw content.") - // fmt.Println("Blob content:", buf.String()) - - var bundle bundle.ProtobufBundle - bundle.Bundle = new(protobundle.Bundle) - err = bundle.UnmarshalJSON(layerBytes) - fmt.Println("") - fmt.Println("JSON Content:", string(layerBytes)) - fmt.Println("") - - if err != nil { - fmt.Println("failed to unmarshal bundle from JSON: %v", err) - } else { - fmt.Println("Bundle content:", bundle.String()) - } - - a := api.Attestation{Bundle: &bundle} - attestations = append(attestations, &a) - - } else { - defer gz.Close() - - var decompressed bytes.Buffer - if _, err := io.Copy(&decompressed, gz); err != nil { - fmt.Println("Error decompressing layer content: %v", err) - } - - // Now you have the decompressed blob content in 'decompressed' buffer - fmt.Println("Decompressed blob content:", decompressed.String()) + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } - // Now you have the decompressed blob content in 'decompressed' buffer - // fmt.Println("Blob content:", decompressed.String()) + var bundle bundle.ProtobufBundle + bundle.Bundle = new(protobundle.Bundle) + err = bundle.UnmarshalJSON(layerBytes) + + if err != nil { + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) + } + + a := api.Attestation{Bundle: &bundle} + attestations = append(attestations, &a) + } else { - fmt.Println("No layers found in the image.") + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } } } } - - // msgName, err := c.parseReference(msg) - // fmt.Println() - // if err != nil { - // return nil, err - // } - - return &desc.Digest, attestations, nil + return attestations, nil } // Unlike other parts of this command set, we cannot pass a custom HTTP client diff --git a/pkg/cmd/attestation/download/download.go b/pkg/cmd/attestation/download/download.go index 3af3b6200..6d74027ae 100644 --- a/pkg/cmd/attestation/download/download.go +++ b/pkg/cmd/attestation/download/download.go @@ -111,7 +111,7 @@ func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Comman } func runDownload(opts *Options) error { - artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm, false) if err != nil { return fmt.Errorf("failed to digest artifact: %v", err) } diff --git a/pkg/cmd/attestation/inspect/inspect.go b/pkg/cmd/attestation/inspect/inspect.go index 2731ee7a4..f38181b77 100644 --- a/pkg/cmd/attestation/inspect/inspect.go +++ b/pkg/cmd/attestation/inspect/inspect.go @@ -97,7 +97,7 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command } func runInspect(opts *Options) error { - artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm, false) if err != nil { return fmt.Errorf("failed to digest artifact: %s", err) } diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 52a8ff025..902657d41 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -16,12 +16,13 @@ import ( var ErrUnrecognisedBundleExtension = errors.New("bundle file extension not supported, must be json or jsonl") type FetchAttestationsConfig struct { - APIClient api.Client - BundlePath string - Digest string - Limit int - Owner string - Repo string + APIClient api.Client + BundlePath string + Digest string + Limit int + Owner string + Repo string + AttestationsFromOCI []*api.Attestation } func (c *FetchAttestationsConfig) IsBundleProvided() bool { @@ -32,6 +33,11 @@ func GetAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) { if c.IsBundleProvided() { return GetLocalAttestations(c.BundlePath) } + + if len(c.AttestationsFromOCI) > 0 { + return c.AttestationsFromOCI, nil + } + return GetRemoteAttestations(c) } diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index da2c7bb4e..902024b90 100644 --- a/pkg/cmd/attestation/verify/options.go +++ b/pkg/cmd/attestation/verify/options.go @@ -15,27 +15,28 @@ import ( // Options captures the options for the verify command type Options struct { - ArtifactPath string - BundlePath string - Config func() (gh.Config, error) - TrustedRoot string - DenySelfHostedRunner bool - DigestAlgorithm string - Limit int - NoPublicGood bool - OIDCIssuer string - Owner string - PredicateType string - Repo string - SAN string - SANRegex string - SignerRepo string - SignerWorkflow string - APIClient api.Client - Logger *io.Handler - OCIClient oci.Client - SigstoreVerifier verification.SigstoreVerifier - exporter cmdutil.Exporter + ArtifactPath string + BundlePath string + UseBundleFromRegistry bool + Config func() (gh.Config, error) + TrustedRoot string + DenySelfHostedRunner bool + DigestAlgorithm string + Limit int + NoPublicGood bool + OIDCIssuer string + Owner string + PredicateType string + Repo string + SAN string + SANRegex string + SignerRepo string + SignerWorkflow string + APIClient api.Client + Logger *io.Handler + OCIClient oci.Client + SigstoreVerifier verification.SigstoreVerifier + exporter cmdutil.Exporter } // Clean cleans the file path option values diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 990de3985..ffa87a383 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -150,6 +150,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command // general flags verifyCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles") cmdutil.DisableAuthCheckFlag(verifyCmd.Flags().Lookup("bundle")) + verifyCmd.Flags().BoolVarP(&opts.UseBundleFromRegistry, "bundle-from-registry", "", false, "Use the bundle from the OCI registry") cmdutil.StringEnumFlag(verifyCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact") verifyCmd.Flags().StringVarP(&opts.Owner, "owner", "o", "", "GitHub organization to scope attestation lookup by") verifyCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format /") @@ -173,7 +174,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command } func runVerify(opts *Options) error { - artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm, opts.UseBundleFromRegistry) if err != nil { opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ Loading digest for %s failed\n"), opts.ArtifactPath) return err @@ -182,12 +183,13 @@ 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, + APIClient: opts.APIClient, + BundlePath: opts.BundlePath, + Digest: artifact.DigestWithAlg(), + Limit: opts.Limit, + Owner: opts.Owner, + Repo: opts.Repo, + AttestationsFromOCI: artifact.Attestations(), } attestations, err := verification.GetAttestations(c) if err != nil { @@ -198,25 +200,19 @@ func runVerify(opts *Options) error { if c.IsBundleProvided() { opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ Loading attestations from %s failed\n"), artifact.URL) + } else if opts.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")) } return err } - attestationsFromOCI := artifact.Attestations() - if len(attestationsFromOCI) > 0 { - attestations = append(attestations, attestationsFromOCI...) - - for _, attestation := range attestations { - // ja, err := attestation.Bundle.String() - opts.Logger.Printf("Loaded attestation from OCI registry: %s\n", attestation.Bundle.String()) - } - } - pluralAttestation := text.Pluralize(len(attestations), "attestation") if c.IsBundleProvided() { opts.Logger.Printf("Loaded %s from %s\n", pluralAttestation, opts.BundlePath) + } else if opts.UseBundleFromRegistry { + opts.Logger.Printf("Loaded %s from %s\n", pluralAttestation, opts.ArtifactPath) } else { opts.Logger.Printf("Loaded %s from GitHub API\n", pluralAttestation) } From 8ae4f1cfb9278530aa22e84ccb3dc789587af1ef Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Mon, 5 Aug 2024 12:53:43 -0700 Subject: [PATCH 003/301] add contain check --- pkg/cmd/attestation/artifact/oci/client.go | 94 ++++++++++++---------- 1 file changed, 52 insertions(+), 42 deletions(-) diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go index 9e53c6f2b..0171b4187 100644 --- a/pkg/cmd/attestation/artifact/oci/client.go +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -65,78 +65,88 @@ func (c LiveClient) GetImageDigest(name name.Reference) (*v1.Hash, error) { return &desc.Digest, nil } +// Ref: https://github.com/github/package-security/blob/main/garden/retrieve-sigstore-bundle-from-oci-registry.md func (c LiveClient) GetAttestations(name name.Reference, digest *v1.Hash) ([]*api.Attestation, error) { attestations := []*api.Attestation{} nameDegist := name.Context().Digest(digest.String()) - ref, err := remote.Referrers(nameDegist, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + imageIndex, err := remote.Referrers(nameDegist, remote.WithAuthFromKeychain(authn.DefaultKeychain)) if err != nil { return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } - indexManifest, err := ref.IndexManifest() + indexManifest, err := imageIndex.IndexManifest() if err != nil { return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } manifests := indexManifest.Manifests for _, m := range manifests { - allowedArtifactTypes := []string{"application/vnd.dev.sigstore.bundle.v0.3+json"} - for _, allowedType := range allowedArtifactTypes { - if allowedType == m.ArtifactType { - manifestDigest := m.Digest.String() + if containAllowedArtifactTypes(m.ArtifactType) { + manifestDigest := m.Digest.String() + + digest2 := nameDegist.Context().Digest(manifestDigest) + // TODO: replace to use GET for more correct type + // OR IS IT CORRECT TO USE type IMAGE? + img2, err := remote.Image(digest2, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) + } + + // Step 4: Get the layers + layers, err := img2.Layers() + if err != nil { + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) + + } + + // For simplicity, we'll just fetch the first layer + if len(layers) > 0 { + layer := layers[0] + + // Step 5: Read the blob (layer) content + rc, err := layer.Compressed() + if err != nil { + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) + } + defer rc.Close() + + layerBytes, err := io.ReadAll(rc) - digest2 := nameDegist.Context().Digest(manifestDigest) - // replace to use GET for more correc type - img2, err := remote.Image(digest2, remote.WithAuthFromKeychain(authn.DefaultKeychain)) if err != nil { return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } - // Step 4: Get the layers - layers, err := img2.Layers() + var bundle bundle.ProtobufBundle + bundle.Bundle = new(protobundle.Bundle) + err = bundle.UnmarshalJSON(layerBytes) + if err != nil { return attestations, fmt.Errorf("failed to fetch remote image: %v", err) - } - // For simplicity, we'll just fetch the first layer - if len(layers) > 0 { - layer := layers[0] + a := api.Attestation{Bundle: &bundle} + attestations = append(attestations, &a) - // Step 5: Read the blob (layer) content - rc, err := layer.Compressed() - if err != nil { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) - } - defer rc.Close() - - layerBytes, err := io.ReadAll(rc) - - if err != nil { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) - } - - var bundle bundle.ProtobufBundle - bundle.Bundle = new(protobundle.Bundle) - err = bundle.UnmarshalJSON(layerBytes) - - if err != nil { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) - } - - a := api.Attestation{Bundle: &bundle} - attestations = append(attestations, &a) - - } else { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) - } + } else { + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } } } return attestations, nil } +func containAllowedArtifactTypes(artifactType string) bool { + allowedArtifactTypes := []string{"application/vnd.dev.sigstore.bundle.v0.3+json"} + + for _, allowedType := range allowedArtifactTypes { + if allowedType == artifactType { + return true + } + } + return false +} + // Unlike other parts of this command set, we cannot pass a custom HTTP client // to the go-containerregistry library. This means we have limited visibility // into the HTTP requests being made to container registries. From bad127c342060eadfaa24b50604a9b8b88476706 Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Mon, 5 Aug 2024 12:56:35 -0700 Subject: [PATCH 004/301] clean naming --- pkg/cmd/attestation/artifact/oci/client.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go index 0171b4187..8fd57af4c 100644 --- a/pkg/cmd/attestation/artifact/oci/client.go +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -81,30 +81,26 @@ func (c LiveClient) GetAttestations(name name.Reference, digest *v1.Hash) ([]*ap manifests := indexManifest.Manifests for _, m := range manifests { - if containAllowedArtifactTypes(m.ArtifactType) { - manifestDigest := m.Digest.String() - - digest2 := nameDegist.Context().Digest(manifestDigest) + artifactManifestNameDigest := nameDegist.Context().Digest(m.Digest.String()) // TODO: replace to use GET for more correct type // OR IS IT CORRECT TO USE type IMAGE? - img2, err := remote.Image(digest2, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + artifactManifest, err := remote.Image(artifactManifestNameDigest, remote.WithAuthFromKeychain(authn.DefaultKeychain)) if err != nil { return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } // Step 4: Get the layers - layers, err := img2.Layers() + layers, err := artifactManifest.Layers() if err != nil { return attestations, fmt.Errorf("failed to fetch remote image: %v", err) - } // For simplicity, we'll just fetch the first layer if len(layers) > 0 { layer := layers[0] - // Step 5: Read the blob (layer) content + // Step 5: Read the layer content rc, err := layer.Compressed() if err != nil { return attestations, fmt.Errorf("failed to fetch remote image: %v", err) @@ -127,7 +123,6 @@ func (c LiveClient) GetAttestations(name name.Reference, digest *v1.Hash) ([]*ap a := api.Attestation{Bundle: &bundle} attestations = append(attestations, &a) - } else { return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } From 57aea664e5d95136c4b100adacb5eee75af000d3 Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Wed, 7 Aug 2024 10:10:59 -0700 Subject: [PATCH 005/301] added test --- pkg/cmd/attestation/artifact/artifact.go | 18 +-- pkg/cmd/attestation/artifact/image.go | 28 +--- pkg/cmd/attestation/artifact/oci/client.go | 135 +++++++++--------- .../attestation/artifact/oci/client_test.go | 14 +- .../attestation/artifact/oci/mock_client.go | 56 ++++++-- pkg/cmd/attestation/download/download.go | 2 +- pkg/cmd/attestation/inspect/inspect.go | 2 +- .../attestation/verification/attestation.go | 34 +++-- pkg/cmd/attestation/verify/verify.go | 22 +-- pkg/cmd/attestation/verify/verify_test.go | 27 ++++ 10 files changed, 204 insertions(+), 134 deletions(-) diff --git a/pkg/cmd/attestation/artifact/artifact.go b/pkg/cmd/attestation/artifact/artifact.go index e8e0ea095..131785166 100644 --- a/pkg/cmd/attestation/artifact/artifact.go +++ b/pkg/cmd/attestation/artifact/artifact.go @@ -7,7 +7,7 @@ import ( "path/filepath" "strings" - "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/google/go-containerregistry/pkg/name" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" ) @@ -21,10 +21,10 @@ const ( // DigestedArtifact abstracts the software artifact being verified type DigestedArtifact struct { - URL string - digest string - digestAlg string - attestations []*api.Attestation + URL string + digest string + digestAlg string + nameRef name.Reference } func normalizeReference(reference string, pathSeparator rune) (normalized string, artifactType artifactType, err error) { @@ -54,14 +54,14 @@ func normalizeReference(reference string, pathSeparator rune) (normalized string return filepath.Clean(reference), fileArtifactType, nil } -func NewDigestedArtifact(client oci.Client, reference, digestAlg string, useBundleFromRegistry bool) (artifact *DigestedArtifact, err error) { +func NewDigestedArtifact(client oci.Client, reference, digestAlg string) (artifact *DigestedArtifact, err error) { normalized, artifactType, err := normalizeReference(reference, os.PathSeparator) if err != nil { return nil, err } if artifactType == ociArtifactType { // TODO: should we allow custom digestAlg for OCI artifacts? - return digestContainerImageArtifact(normalized, client, useBundleFromRegistry) + return digestContainerImageArtifact(normalized, client) } return digestLocalFileArtifact(normalized, digestAlg) } @@ -81,6 +81,6 @@ func (a *DigestedArtifact) DigestWithAlg() string { return fmt.Sprintf("%s:%s", a.digestAlg, a.digest) } -func (a *DigestedArtifact) Attestations() []*api.Attestation { - return a.attestations +func (a *DigestedArtifact) NameRef() name.Reference { + return a.nameRef } diff --git a/pkg/cmd/attestation/artifact/image.go b/pkg/cmd/attestation/artifact/image.go index e1fba97bf..dda5f65db 100644 --- a/pkg/cmd/attestation/artifact/image.go +++ b/pkg/cmd/attestation/artifact/image.go @@ -7,7 +7,7 @@ import ( "github.com/distribution/reference" ) -func digestContainerImageArtifact(url string, client oci.Client, useBundleFromRegistry bool) (*DigestedArtifact, error) { +func digestContainerImageArtifact(url string, client oci.Client) (*DigestedArtifact, error) { // try to parse the url as a valid registry reference named, err := reference.Parse(url) if err != nil { @@ -15,30 +15,7 @@ func digestContainerImageArtifact(url string, client oci.Client, useBundleFromRe return nil, fmt.Errorf("artifact %s is not a valid registry reference: %v", url, err) } - name, err := client.ParseReference(named.String()) - if err != nil { - return nil, err - } - - digest, err := client.GetImageDigest(name) - - if err != nil { - return nil, err - } - if useBundleFromRegistry { - attestations, err := client.GetAttestations(name, digest) - - if err != nil { - return nil, err - } - - return &DigestedArtifact{ - URL: fmt.Sprintf("oci://%s", named.String()), - digest: digest.Hex, - digestAlg: digest.Algorithm, - attestations: attestations, - }, nil - } + digest, nameRef, err := client.GetImageDigest(named.String()) if err != nil { return nil, err @@ -48,5 +25,6 @@ func digestContainerImageArtifact(url string, client oci.Client, useBundleFromRe URL: fmt.Sprintf("oci://%s", named.String()), digest: digest.Hex, digestAlg: digest.Algorithm, + nameRef: nameRef, }, nil } diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go index 8fd57af4c..55d74fac0 100644 --- a/pkg/cmd/attestation/artifact/oci/client.go +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "io" + "net/http" + "strings" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -13,16 +15,14 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote/transport" - protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" ) var ErrDenied = errors.New("the provided token was denied access to the requested resource, please check the token's expiration and repository access") var ErrRegistryAuthz = errors.New("remote registry authorization failed, please authenticate with the registry and try again") type Client interface { - GetImageDigest(name name.Reference) (*v1.Hash, error) - GetAttestations(name name.Reference, digest *v1.Hash) ([]*api.Attestation, error) - ParseReference(ref string) (name.Reference, error) + GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) + GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) } func checkForUnauthorizedOrDeniedErr(err transport.Error) error { @@ -40,6 +40,7 @@ func checkForUnauthorizedOrDeniedErr(err transport.Error) error { type LiveClient struct { parseReference func(string, ...name.Option) (name.Reference, error) get func(name.Reference, ...remote.Option) (*remote.Descriptor, error) + referres func(d name.Digest, options ...remote.Option) (v1.ImageIndex, error) } func (c LiveClient) ParseReference(ref string) (name.Reference, error) { @@ -47,7 +48,11 @@ func (c LiveClient) ParseReference(ref string) (name.Reference, error) { } // where name is formed like ghcr.io/github/my-image-repo -func (c LiveClient) GetImageDigest(name name.Reference) (*v1.Hash, error) { +func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) { + name, err := c.parseReference(imgName) + if err != nil { + return nil, nil, fmt.Errorf("failed to create image tag: %v", err) + } // The user must already be authenticated with the container registry // The authn.DefaultKeychain argument indicates that Get should checks the // user's configuration for the registry credentials @@ -56,92 +61,92 @@ func (c LiveClient) GetImageDigest(name name.Reference) (*v1.Hash, error) { var transportErr *transport.Error if errors.As(err, &transportErr) { if accessErr := checkForUnauthorizedOrDeniedErr(*transportErr); accessErr != nil { - return nil, accessErr + return nil, nil, accessErr } } - return nil, fmt.Errorf("failed to fetch remote image: %v", err) + return nil, nil, fmt.Errorf("failed to fetch remote image: %v", err) } - return &desc.Digest, nil + return &desc.Digest, name, nil +} + +type noncompliantRegistryTransport struct{} + +// RoundTrip will check if a request and associated response fulfill the following: +// 1. The response returns a 406 status code +// 2. The request path contains /referrers/ +// If both conditions are met, the response's status code will be overwritten to 404 +// This is a temporary solution to handle non compliant registries that return +// an unexpected status code 406 when the go-containerregistry library used +// by this code attempts to make a request to the referrers API. +// The go-containerregistry library can handle 404 response but not a 406 response. +// See the related go-containerregistry issue: https://github.com/google/go-containerregistry/issues/1962 +func (a *noncompliantRegistryTransport) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := http.DefaultTransport.RoundTrip(req) + if resp.StatusCode == http.StatusNotAcceptable && strings.Contains(req.URL.Path, "/referrers/") { + resp.StatusCode = http.StatusNotFound + } + + return resp, err } // Ref: https://github.com/github/package-security/blob/main/garden/retrieve-sigstore-bundle-from-oci-registry.md -func (c LiveClient) GetAttestations(name name.Reference, digest *v1.Hash) ([]*api.Attestation, error) { - attestations := []*api.Attestation{} - nameDegist := name.Context().Digest(digest.String()) +func (c LiveClient) GetAttestations(ref name.Reference, digest string) ([]*api.Attestation, error) { + attestations := make([]*api.Attestation, 0) - imageIndex, err := remote.Referrers(nameDegist, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + transportOpts := []remote.Option{remote.WithTransport(&noncompliantRegistryTransport{}), remote.WithAuthFromKeychain(authn.DefaultKeychain)} + referrers, err := remote.Referrers(ref.Context().Digest(digest), transportOpts...) if err != nil { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) + return attestations, fmt.Errorf("error getting referrers: %w", err) } - indexManifest, err := imageIndex.IndexManifest() + refManifest, err := referrers.IndexManifest() if err != nil { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) + return attestations, fmt.Errorf("error getting referrers manifest: %w", err) } - manifests := indexManifest.Manifests - for _, m := range manifests { - if containAllowedArtifactTypes(m.ArtifactType) { - artifactManifestNameDigest := nameDegist.Context().Digest(m.Digest.String()) - // TODO: replace to use GET for more correct type - // OR IS IT CORRECT TO USE type IMAGE? - artifactManifest, err := remote.Image(artifactManifestNameDigest, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + for _, refDesc := range refManifest.Manifests { + if !strings.HasPrefix(refDesc.ArtifactType, "application/vnd.dev.sigstore.bundle") { + continue + } + + refImg, err := remote.Image(ref.Context().Digest(refDesc.Digest.String()), remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return attestations, fmt.Errorf("error getting referrer image: %w", err) + } + layers, err := refImg.Layers() + if err != nil { + return attestations, fmt.Errorf("error getting referrer image: %w", err) + } + + if len(layers) > 0 { + layer0, err := layers[0].Uncompressed() if err != nil { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) + return attestations, fmt.Errorf("error getting referrer image: %w", err) } + defer layer0.Close() + + bundleBytes, err := io.ReadAll(layer0) - // Step 4: Get the layers - layers, err := artifactManifest.Layers() if err != nil { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) + return attestations, fmt.Errorf("error getting referrer image: %w", err) } - // For simplicity, we'll just fetch the first layer - if len(layers) > 0 { - layer := layers[0] + b := &bundle.ProtobufBundle{} + err = b.UnmarshalJSON(bundleBytes) - // Step 5: Read the layer content - rc, err := layer.Compressed() - if err != nil { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) - } - defer rc.Close() - - layerBytes, err := io.ReadAll(rc) - - if err != nil { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) - } - - var bundle bundle.ProtobufBundle - bundle.Bundle = new(protobundle.Bundle) - err = bundle.UnmarshalJSON(layerBytes) - - if err != nil { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) - } - - a := api.Attestation{Bundle: &bundle} - attestations = append(attestations, &a) - } else { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) + if err != nil { + return attestations, fmt.Errorf("error unmarshalling bundle: %w", err) } + + a := api.Attestation{Bundle: b} + attestations = append(attestations, &a) + } else { + return attestations, fmt.Errorf("error getting referrer image: no layers found") } } return attestations, nil } -func containAllowedArtifactTypes(artifactType string) bool { - allowedArtifactTypes := []string{"application/vnd.dev.sigstore.bundle.v0.3+json"} - - for _, allowedType := range allowedArtifactTypes { - if allowedType == artifactType { - return true - } - } - return false -} - // Unlike other parts of this command set, we cannot pass a custom HTTP client // to the go-containerregistry library. This means we have limited visibility // into the HTTP requests being made to container registries. diff --git a/pkg/cmd/attestation/artifact/oci/client_test.go b/pkg/cmd/attestation/artifact/oci/client_test.go index 9aa415c47..a46533366 100644 --- a/pkg/cmd/attestation/artifact/oci/client_test.go +++ b/pkg/cmd/attestation/artifact/oci/client_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote/transport" @@ -30,9 +30,10 @@ func TestGetImageDigest_Success(t *testing.T) { }, } - digest, err := c.GetImageDigest("test") + digest, nameRef, err := c.GetImageDigest("test") require.NoError(t, err) require.Equal(t, &expectedDigest, digest) + require.Equal(t, name.Tag{}, nameRef) } func TestGetImageDigest_ReferenceFail(t *testing.T) { @@ -45,9 +46,10 @@ func TestGetImageDigest_ReferenceFail(t *testing.T) { }, } - digest, err := c.GetImageDigest("test") + digest, nameRef, err := c.GetImageDigest("test") require.Error(t, err) require.Nil(t, digest) + require.Nil(t, nameRef) } func TestGetImageDigest_AuthFail(t *testing.T) { @@ -60,10 +62,11 @@ func TestGetImageDigest_AuthFail(t *testing.T) { }, } - digest, err := c.GetImageDigest("test") + digest, nameRef, err := c.GetImageDigest("test") require.Error(t, err) require.ErrorIs(t, err, ErrRegistryAuthz) require.Nil(t, digest) + require.Nil(t, nameRef) } func TestGetImageDigest_Denied(t *testing.T) { @@ -76,8 +79,9 @@ func TestGetImageDigest_Denied(t *testing.T) { }, } - digest, err := c.GetImageDigest("test") + digest, nameRef, err := c.GetImageDigest("test") require.Error(t, err) require.ErrorIs(t, err, ErrDenied) require.Nil(t, digest) + require.Nil(t, nameRef) } diff --git a/pkg/cmd/attestation/artifact/oci/mock_client.go b/pkg/cmd/attestation/artifact/oci/mock_client.go index 24368dec8..1c9bd7876 100644 --- a/pkg/cmd/attestation/artifact/oci/mock_client.go +++ b/pkg/cmd/attestation/artifact/oci/mock_client.go @@ -3,32 +3,70 @@ package oci import ( "fmt" - "github.com/google/go-containerregistry/pkg/v1" + "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "github.com/cli/cli/v2/pkg/cmd/attestation/test/data" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" ) +func makeTestAttestation() api.Attestation { + return api.Attestation{Bundle: data.SigstoreBundle(nil)} +} + type MockClient struct{} -func (c MockClient) GetImageDigest(imgName string) (*v1.Hash, error) { +func (c MockClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) { return &v1.Hash{ Hex: "1234567890abcdef", Algorithm: "sha256", - }, nil + }, nil, nil +} + +func (c MockClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) { + att1 := makeTestAttestation() + att2 := makeTestAttestation() + return []*api.Attestation{&att1, &att2}, nil } type ReferenceFailClient struct{} -func (c ReferenceFailClient) GetImageDigest(imgName string) (*v1.Hash, error) { - return nil, fmt.Errorf("failed to parse reference") +func (c ReferenceFailClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) { + return nil, nil, fmt.Errorf("failed to parse reference") +} + +func (c ReferenceFailClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) { + return nil, nil } type AuthFailClient struct{} -func (c AuthFailClient) GetImageDigest(imgName string) (*v1.Hash, error) { - return nil, ErrRegistryAuthz +func (c AuthFailClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) { + return nil, nil, ErrRegistryAuthz +} + +func (c AuthFailClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) { + return nil, nil } type DeniedClient struct{} -func (c DeniedClient) GetImageDigest(imgName string) (*v1.Hash, error) { - return nil, ErrDenied +func (c DeniedClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) { + return nil, nil, ErrDenied +} + +func (c DeniedClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) { + return nil, nil +} + +type DeniedAttestationsClient struct{} + +func (c DeniedAttestationsClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) { + return &v1.Hash{ + Hex: "1234567890abcdef", + Algorithm: "sha256", + }, nil, nil +} + +func (c DeniedAttestationsClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) { + return nil, nil } diff --git a/pkg/cmd/attestation/download/download.go b/pkg/cmd/attestation/download/download.go index 6d74027ae..3af3b6200 100644 --- a/pkg/cmd/attestation/download/download.go +++ b/pkg/cmd/attestation/download/download.go @@ -111,7 +111,7 @@ func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Comman } func runDownload(opts *Options) error { - artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm, false) + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) if err != nil { return fmt.Errorf("failed to digest artifact: %v", err) } diff --git a/pkg/cmd/attestation/inspect/inspect.go b/pkg/cmd/attestation/inspect/inspect.go index f38181b77..2731ee7a4 100644 --- a/pkg/cmd/attestation/inspect/inspect.go +++ b/pkg/cmd/attestation/inspect/inspect.go @@ -97,7 +97,7 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command } func runInspect(opts *Options) error { - artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm, false) + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) if err != nil { return fmt.Errorf("failed to digest artifact: %s", err) } diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 902657d41..20e6a01bf 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -9,6 +9,8 @@ import ( "path/filepath" "github.com/cli/cli/v2/pkg/cmd/attestation/api" + "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" ) @@ -16,13 +18,15 @@ import ( var ErrUnrecognisedBundleExtension = errors.New("bundle file extension not supported, must be json or jsonl") type FetchAttestationsConfig struct { - APIClient api.Client - BundlePath string - Digest string - Limit int - Owner string - Repo string - AttestationsFromOCI []*api.Attestation + 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 { @@ -34,8 +38,8 @@ func GetAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) { return GetLocalAttestations(c.BundlePath) } - if len(c.AttestationsFromOCI) > 0 { - return c.AttestationsFromOCI, nil + if c.UseBundleFromRegistry { + return GetOCIAttestations(c) } return GetRemoteAttestations(c) @@ -121,6 +125,18 @@ 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) + if err != nil { + return nil, fmt.Errorf("failed to fetch OCI attestations: %w", err) + } + if len(attestations) == 0 { + return nil, fmt.Errorf("no OCI attestations found") + } + return attestations, nil +} + type IntotoStatement struct { PredicateType string `json:"predicateType"` } diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index ffa87a383..5f4e70fbc 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -174,7 +174,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command } func runVerify(opts *Options) error { - artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm, opts.UseBundleFromRegistry) + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) if err != nil { opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ Loading digest for %s failed\n"), opts.ArtifactPath) return err @@ -183,13 +183,15 @@ 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, - AttestationsFromOCI: artifact.Attestations(), + 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) if err != nil { @@ -200,7 +202,7 @@ func runVerify(opts *Options) error { if c.IsBundleProvided() { opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ Loading attestations from %s failed\n"), artifact.URL) - } else if opts.UseBundleFromRegistry { + } 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")) @@ -211,7 +213,7 @@ func runVerify(opts *Options) error { pluralAttestation := text.Pluralize(len(attestations), "attestation") if c.IsBundleProvided() { opts.Logger.Printf("Loaded %s from %s\n", pluralAttestation, opts.BundlePath) - } else if opts.UseBundleFromRegistry { + } 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) diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go index 8d06617f3..902fbc2d4 100644 --- a/pkg/cmd/attestation/verify/verify_test.go +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -463,4 +463,31 @@ func TestRunVerify(t *testing.T) { customOpts.BundlePath = "" require.Error(t, runVerify(&customOpts)) }) + + t.Run("with valid OCI artifact", func(t *testing.T) { + customOpts := publicGoodOpts + customOpts.ArtifactPath = "oci://ghcr.io/github/test" + customOpts.BundlePath = "" + + require.Nil(t, runVerify(&customOpts)) + }) + + t.Run("with valid OCI artifact with UseBundleFromRegistry flag", func(t *testing.T) { + customOpts := publicGoodOpts + customOpts.ArtifactPath = "oci://ghcr.io/github/test" + customOpts.BundlePath = "" + customOpts.UseBundleFromRegistry = true + + require.Nil(t, runVerify(&customOpts)) + }) + + t.Run("with valid OCI artifact with UseBundleFromRegistry flag but fail on fetching bundle from registry", func(t *testing.T) { + customOpts := publicGoodOpts + customOpts.ArtifactPath = "oci://ghcr.io/github/test" + customOpts.BundlePath = "" + customOpts.UseBundleFromRegistry = true + customOpts.OCIClient = oci.DeniedAttestationsClient{} + + require.ErrorContains(t, runVerify(&customOpts), "no OCI attestations found") + }) } From d1cd69c81c10bedf6e955e934ab8f326a3535236 Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Wed, 7 Aug 2024 10:16:30 -0700 Subject: [PATCH 006/301] minor fixed --- pkg/cmd/attestation/artifact/oci/client.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go index 55d74fac0..45c28cf7f 100644 --- a/pkg/cmd/attestation/artifact/oci/client.go +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -40,7 +40,6 @@ func checkForUnauthorizedOrDeniedErr(err transport.Error) error { type LiveClient struct { parseReference func(string, ...name.Option) (name.Reference, error) get func(name.Reference, ...remote.Option) (*remote.Descriptor, error) - referres func(d name.Digest, options ...remote.Option) (v1.ImageIndex, error) } func (c LiveClient) ParseReference(ref string) (name.Reference, error) { From 832a43072cf24aac5f8daaf118fc885527dda39b Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Wed, 7 Aug 2024 10:19:47 -0700 Subject: [PATCH 007/301] minor fixed --- pkg/cmd/attestation/artifact/oci/client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go index 45c28cf7f..046601637 100644 --- a/pkg/cmd/attestation/artifact/oci/client.go +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -82,6 +82,9 @@ type noncompliantRegistryTransport struct{} // See the related go-containerregistry issue: https://github.com/google/go-containerregistry/issues/1962 func (a *noncompliantRegistryTransport) RoundTrip(req *http.Request) (*http.Response, error) { resp, err := http.DefaultTransport.RoundTrip(req) + if err != nil { + return resp, err + } if resp.StatusCode == http.StatusNotAcceptable && strings.Contains(req.URL.Path, "/referrers/") { resp.StatusCode = http.StatusNotFound } From 5ae03d6e878fcce9743307e5b7a70f1f877e5e3d Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Mon, 12 Aug 2024 07:10:19 -0700 Subject: [PATCH 008/301] addded more test --- pkg/cmd/attestation/artifact/oci/client.go | 1 - .../attestation/artifact/oci/mock_client.go | 19 ++++++++++++++++--- .../attestation/verification/attestation.go | 1 - pkg/cmd/attestation/verify/verify_test.go | 12 +++++++++++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go index 046601637..5428fff2f 100644 --- a/pkg/cmd/attestation/artifact/oci/client.go +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -92,7 +92,6 @@ func (a *noncompliantRegistryTransport) RoundTrip(req *http.Request) (*http.Resp return resp, err } -// Ref: https://github.com/github/package-security/blob/main/garden/retrieve-sigstore-bundle-from-oci-registry.md func (c LiveClient) GetAttestations(ref name.Reference, digest string) ([]*api.Attestation, error) { attestations := make([]*api.Attestation, 0) diff --git a/pkg/cmd/attestation/artifact/oci/mock_client.go b/pkg/cmd/attestation/artifact/oci/mock_client.go index 1c9bd7876..b869c60a9 100644 --- a/pkg/cmd/attestation/artifact/oci/mock_client.go +++ b/pkg/cmd/attestation/artifact/oci/mock_client.go @@ -58,15 +58,28 @@ func (c DeniedClient) GetAttestations(name name.Reference, digest string) ([]*ap return nil, nil } -type DeniedAttestationsClient struct{} +type NoAttestationsClient struct{} -func (c DeniedAttestationsClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) { +func (c NoAttestationsClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) { return &v1.Hash{ Hex: "1234567890abcdef", Algorithm: "sha256", }, nil, nil } -func (c DeniedAttestationsClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) { +func (c NoAttestationsClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) { return nil, nil } + +type FailedToFetchAttestationsClient struct{} + +func (c FailedToFetchAttestationsClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) { + return &v1.Hash{ + Hex: "1234567890abcdef", + Algorithm: "sha256", + }, nil, nil +} + +func (c FailedToFetchAttestationsClient) GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) { + return nil, fmt.Errorf("failed to fetch attestations") +} diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 20e6a01bf..c1fb25b12 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -126,7 +126,6 @@ func GetRemoteAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error } func GetOCIAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) { - attestations, err := c.OCIClient.GetAttestations(c.NameRef, c.Digest) if err != nil { return nil, fmt.Errorf("failed to fetch OCI attestations: %w", err) diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go index 902fbc2d4..0f31d900c 100644 --- a/pkg/cmd/attestation/verify/verify_test.go +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -481,12 +481,22 @@ func TestRunVerify(t *testing.T) { require.Nil(t, runVerify(&customOpts)) }) + t.Run("with valid OCI artifact with UseBundleFromRegistry flag but no bundle return from registry", func(t *testing.T) { + customOpts := publicGoodOpts + customOpts.ArtifactPath = "oci://ghcr.io/github/test" + customOpts.BundlePath = "" + customOpts.UseBundleFromRegistry = true + customOpts.OCIClient = oci.NoAttestationsClient{} + + require.ErrorContains(t, runVerify(&customOpts), "no OCI attestations found") + }) + t.Run("with valid OCI artifact with UseBundleFromRegistry flag but fail on fetching bundle from registry", func(t *testing.T) { customOpts := publicGoodOpts customOpts.ArtifactPath = "oci://ghcr.io/github/test" customOpts.BundlePath = "" customOpts.UseBundleFromRegistry = true - customOpts.OCIClient = oci.DeniedAttestationsClient{} + customOpts.OCIClient = oci.NoAttestationsClient{} require.ErrorContains(t, runVerify(&customOpts), "no OCI attestations found") }) From 05891965d0c8827fcee6019bf28258c64a77a3fd Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Thu, 15 Aug 2024 11:56:28 -0400 Subject: [PATCH 009/301] udpate the options --- pkg/cmd/attestation/verify/options.go | 10 +++++ pkg/cmd/attestation/verify/options_test.go | 43 ++++++++++++++++++++++ pkg/cmd/attestation/verify/verify.go | 2 +- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index 902024b90..a21e27c58 100644 --- a/pkg/cmd/attestation/verify/options.go +++ b/pkg/cmd/attestation/verify/options.go @@ -84,6 +84,16 @@ func (opts *Options) AreFlagsValid() error { return fmt.Errorf("limit %d not allowed, must be between 1 and 1000", opts.Limit) } + // Check that the bundle-from-registry flag is only used with OCI artifact paths + if opts.UseBundleFromRegistry && !strings.HasPrefix(opts.ArtifactPath, "oci://") { + return fmt.Errorf("bundle-from-registry flag can only be used with OCI artifact paths") + } + + // Check that both the bundle-from-registry and bundle-path flags are not used together + if opts.UseBundleFromRegistry && opts.BundlePath != "" { + return fmt.Errorf("bundle-from-registry flag cannot be used with bundle-path flag") + } + return nil } diff --git a/pkg/cmd/attestation/verify/options_test.go b/pkg/cmd/attestation/verify/options_test.go index aea131b27..9fd319041 100644 --- a/pkg/cmd/attestation/verify/options_test.go +++ b/pkg/cmd/attestation/verify/options_test.go @@ -116,4 +116,47 @@ func TestSetPolicyFlags(t *testing.T) { require.Equal(t, "sigstore", opts.Owner) require.Equal(t, "^https://github/foo", opts.SANRegex) }) + + t.Run("returns error when UseBundleFromRegistry is true and ArtifactPath is not an OCI path", func(t *testing.T) { + opts := Options{ + ArtifactPath: publicGoodArtifactPath, + DigestAlgorithm: "sha512", + Owner: "sigstore", + UseBundleFromRegistry: true, + Limit: 1, + } + + err := opts.AreFlagsValid() + require.Error(t, err) + require.ErrorContains(t, err, "bundle-from-registry flag can only be used with OCI artifact paths") + }) + + t.Run("does not return error when UseBundleFromRegistry is true and ArtifactPath is an OCI path", func(t *testing.T) { + opts := Options{ + ArtifactPath: "oci://sigstore/sigstore-js:2.1.0", + DigestAlgorithm: "sha512", + OIDCIssuer: "some issuer", + Owner: "sigstore", + UseBundleFromRegistry: true, + Limit: 1, + } + + err := opts.AreFlagsValid() + require.NoError(t, err) + }) + + t.Run("returns error when UseBundleFromRegistry is true and BundlePath is provided", func(t *testing.T) { + opts := Options{ + ArtifactPath: "oci://sigstore/sigstore-js:2.1.0", + BundlePath: publicGoodBundlePath, + DigestAlgorithm: "sha512", + Owner: "sigstore", + UseBundleFromRegistry: true, + Limit: 1, + } + + err := opts.AreFlagsValid() + require.Error(t, err) + require.ErrorContains(t, err, "bundle-from-registry flag cannot be used with bundle-path flag") + }) } diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 5f4e70fbc..f77f5f822 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -150,7 +150,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command // general flags verifyCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles") cmdutil.DisableAuthCheckFlag(verifyCmd.Flags().Lookup("bundle")) - verifyCmd.Flags().BoolVarP(&opts.UseBundleFromRegistry, "bundle-from-registry", "", false, "Use the bundle from the OCI registry") + verifyCmd.Flags().BoolVarP(&opts.UseBundleFromRegistry, "bundle-from-registry", "", false, "Use the bundle from the OCI registry when artifact is an OCI image") cmdutil.StringEnumFlag(verifyCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact") verifyCmd.Flags().StringVarP(&opts.Owner, "owner", "o", "", "GitHub organization to scope attestation lookup by") verifyCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format /") From 009838a8db80f8c459be56bbc04792f58f5f72e4 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Thu, 15 Aug 2024 11:34:43 -0700 Subject: [PATCH 010/301] Always print URL scheme to stdout Fixes #9470 --- internal/text/text.go | 9 +++++++-- internal/text/text_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/internal/text/text.go b/internal/text/text.go index c14071c32..e5793cf04 100644 --- a/internal/text/text.go +++ b/internal/text/text.go @@ -65,14 +65,19 @@ func FuzzyAgoAbbr(a, b time.Time) string { return b.Format("Jan _2, 2006") } -// DisplayURL returns a copy of the string urlStr removing everything except the hostname and path. +// DisplayURL returns a copy of the string urlStr removing everything except the scheme, hostname, and path. +// If the scheme is not specified, "https" is assumed. // If there is an error parsing urlStr then urlStr is returned without modification. func DisplayURL(urlStr string) string { u, err := url.Parse(urlStr) if err != nil { return urlStr } - return u.Hostname() + u.Path + scheme := u.Scheme + if scheme == "" { + scheme = "https" + } + return scheme + "://" + u.Hostname() + u.Path } // RemoveDiacritics returns the input value without "diacritics", or accent marks diff --git a/internal/text/text_test.go b/internal/text/text_test.go index 46566bbf8..906a68762 100644 --- a/internal/text/text_test.go +++ b/internal/text/text_test.go @@ -146,3 +146,33 @@ func TestFormatSlice(t *testing.T) { }) } } + +func TestDisplayURL(t *testing.T) { + tests := []struct { + name string + url string + want string + }{ + { + name: "simple", + url: "https://github.com/cli/cli/issues/9470", + want: "https://github.com/cli/cli/issues/9470", + }, + { + name: "without scheme", + url: "github.com/cli/cli/issues/9470", + want: "https://github.com/cli/cli/issues/9470", + }, + { + name: "with query param and anchor", + url: "https://github.com/cli/cli/issues/9470?q=is:issue#issue-command", + want: "https://github.com/cli/cli/issues/9470", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, DisplayURL(tt.url)) + }) + } +} From 81f3526740085358cb1e2ccc75873f36ab85fe58 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Thu, 15 Aug 2024 11:44:26 -0700 Subject: [PATCH 011/301] Fix tests --- pkg/cmd/gist/create/create_test.go | 2 +- pkg/cmd/issue/comment/comment_test.go | 4 ++-- pkg/cmd/issue/create/create_test.go | 14 +++++++------- pkg/cmd/issue/list/list_test.go | 2 +- pkg/cmd/issue/view/view_test.go | 2 +- pkg/cmd/label/list_test.go | 2 +- pkg/cmd/pr/checks/checks_test.go | 2 +- pkg/cmd/pr/comment/comment_test.go | 4 ++-- pkg/cmd/pr/create/create_test.go | 4 ++-- pkg/cmd/pr/diff/diff_test.go | 2 +- pkg/cmd/pr/list/list_test.go | 2 +- pkg/cmd/pr/view/view_test.go | 2 +- pkg/cmd/repo/view/view_test.go | 2 +- pkg/cmd/ruleset/check/check_test.go | 4 ++-- pkg/cmd/ruleset/list/list_test.go | 4 ++-- pkg/cmd/ruleset/view/view_test.go | 4 ++-- pkg/cmd/run/view/view_test.go | 4 ++-- pkg/cmd/search/code/code_test.go | 2 +- pkg/cmd/search/commits/commits_test.go | 2 +- pkg/cmd/search/repos/repos_test.go | 2 +- pkg/cmd/search/shared/shared_test.go | 2 +- pkg/cmd/workflow/view/view_test.go | 6 +++--- 22 files changed, 37 insertions(+), 37 deletions(-) diff --git a/pkg/cmd/gist/create/create_test.go b/pkg/cmd/gist/create/create_test.go index 40c89c0d8..237486eb3 100644 --- a/pkg/cmd/gist/create/create_test.go +++ b/pkg/cmd/gist/create/create_test.go @@ -296,7 +296,7 @@ func Test_createRun(t *testing.T) { WebMode: true, Filenames: []string{fixtureFile}, }, - wantOut: "Opening gist.github.com/aa5a315d61ae9438b18d in your browser.\n", + wantOut: "Opening https://gist.github.com/aa5a315d61ae9438b18d in your browser.\n", wantStderr: "- Creating gist fixture.txt\n✓ Created secret gist fixture.txt\n", wantErr: false, wantBrowse: "https://gist.github.com/aa5a315d61ae9438b18d", diff --git a/pkg/cmd/issue/comment/comment_test.go b/pkg/cmd/issue/comment/comment_test.go index 6ab8df050..668d758e5 100644 --- a/pkg/cmd/issue/comment/comment_test.go +++ b/pkg/cmd/issue/comment/comment_test.go @@ -234,7 +234,7 @@ func Test_commentRun(t *testing.T) { OpenInBrowser: func(string) error { return nil }, }, - stderr: "Opening github.com/OWNER/REPO/issues/123 in your browser.\n", + stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n", }, { name: "non-interactive web with edit last", @@ -246,7 +246,7 @@ func Test_commentRun(t *testing.T) { OpenInBrowser: func(string) error { return nil }, }, - stderr: "Opening github.com/OWNER/REPO/issues/123 in your browser.\n", + stderr: "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n", }, { name: "non-interactive editor", diff --git a/pkg/cmd/issue/create/create_test.go b/pkg/cmd/issue/create/create_test.go index 9ae959c57..b95b5ef66 100644 --- a/pkg/cmd/issue/create/create_test.go +++ b/pkg/cmd/issue/create/create_test.go @@ -266,7 +266,7 @@ func Test_createRun(t *testing.T) { WebMode: true, }, wantsBrowse: "https://github.com/OWNER/REPO/issues/new", - wantsStderr: "Opening github.com/OWNER/REPO/issues/new in your browser.\n", + wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n", }, { name: "title and body", @@ -276,7 +276,7 @@ func Test_createRun(t *testing.T) { Body: "hello cli", }, wantsBrowse: "https://github.com/OWNER/REPO/issues/new?body=hello+cli&title=myissue", - wantsStderr: "Opening github.com/OWNER/REPO/issues/new in your browser.\n", + wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n", }, { name: "assignee", @@ -285,7 +285,7 @@ func Test_createRun(t *testing.T) { Assignees: []string{"monalisa"}, }, wantsBrowse: "https://github.com/OWNER/REPO/issues/new?assignees=monalisa&body=", - wantsStderr: "Opening github.com/OWNER/REPO/issues/new in your browser.\n", + wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n", }, { name: "@me", @@ -302,7 +302,7 @@ func Test_createRun(t *testing.T) { } }`)) }, wantsBrowse: "https://github.com/OWNER/REPO/issues/new?assignees=MonaLisa&body=", - wantsStderr: "Opening github.com/OWNER/REPO/issues/new in your browser.\n", + wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n", }, { name: "project", @@ -358,7 +358,7 @@ func Test_createRun(t *testing.T) { } } } }`)) }, wantsBrowse: "https://github.com/OWNER/REPO/issues/new?body=&projects=OWNER%2FREPO%2F1", - wantsStderr: "Opening github.com/OWNER/REPO/issues/new in your browser.\n", + wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new in your browser.\n", }, { name: "has templates", @@ -378,7 +378,7 @@ func Test_createRun(t *testing.T) { ) }, wantsBrowse: "https://github.com/OWNER/REPO/issues/new/choose", - wantsStderr: "Opening github.com/OWNER/REPO/issues/new/choose in your browser.\n", + wantsStderr: "Opening https://github.com/OWNER/REPO/issues/new/choose in your browser.\n", }, { name: "too long body", @@ -763,7 +763,7 @@ func TestIssueCreate_continueInBrowser(t *testing.T) { Creating issue in OWNER/REPO - Opening github.com/OWNER/REPO/issues/new in your browser. + Opening https://github.com/OWNER/REPO/issues/new in your browser. `), output.Stderr()) assert.Equal(t, "https://github.com/OWNER/REPO/issues/new?body=body&title=hello", output.BrowsedURL) } diff --git a/pkg/cmd/issue/list/list_test.go b/pkg/cmd/issue/list/list_test.go index e45fc76e4..852f0a46b 100644 --- a/pkg/cmd/issue/list/list_test.go +++ b/pkg/cmd/issue/list/list_test.go @@ -224,7 +224,7 @@ func TestIssueList_web(t *testing.T) { } assert.Equal(t, "", stdout.String()) - assert.Equal(t, "Opening github.com/OWNER/REPO/issues in your browser.\n", stderr.String()) + assert.Equal(t, "Opening https://github.com/OWNER/REPO/issues in your browser.\n", stderr.String()) browser.Verify(t, "https://github.com/OWNER/REPO/issues?q=assignee%3Apeter+author%3Ajohn+label%3Abug+label%3Adocs+mentions%3Afrank+milestone%3Av1.1+type%3Aissue") } diff --git a/pkg/cmd/issue/view/view_test.go b/pkg/cmd/issue/view/view_test.go index f77db9abc..e1798af9f 100644 --- a/pkg/cmd/issue/view/view_test.go +++ b/pkg/cmd/issue/view/view_test.go @@ -123,7 +123,7 @@ func TestIssueView_web(t *testing.T) { } assert.Equal(t, "", stdout.String()) - assert.Equal(t, "Opening github.com/OWNER/REPO/issues/123 in your browser.\n", stderr.String()) + assert.Equal(t, "Opening https://github.com/OWNER/REPO/issues/123 in your browser.\n", stderr.String()) browser.Verify(t, "https://github.com/OWNER/REPO/issues/123") } diff --git a/pkg/cmd/label/list_test.go b/pkg/cmd/label/list_test.go index 37f458439..6f066da32 100644 --- a/pkg/cmd/label/list_test.go +++ b/pkg/cmd/label/list_test.go @@ -296,7 +296,7 @@ func TestListRun(t *testing.T) { name: "web mode", tty: true, opts: &listOptions{WebMode: true}, - wantStderr: "Opening github.com/OWNER/REPO/labels in your browser.\n", + wantStderr: "Opening https://github.com/OWNER/REPO/labels in your browser.\n", }, { name: "order by name ascending", diff --git a/pkg/cmd/pr/checks/checks_test.go b/pkg/cmd/pr/checks/checks_test.go index cc21043ba..36ebf9b15 100644 --- a/pkg/cmd/pr/checks/checks_test.go +++ b/pkg/cmd/pr/checks/checks_test.go @@ -622,7 +622,7 @@ func TestChecksRun_web(t *testing.T) { { name: "tty", isTTY: true, - wantStderr: "Opening github.com/OWNER/REPO/pull/123/checks in your browser.\n", + wantStderr: "Opening https://github.com/OWNER/REPO/pull/123/checks in your browser.\n", wantStdout: "", wantBrowse: "https://github.com/OWNER/REPO/pull/123/checks", }, diff --git a/pkg/cmd/pr/comment/comment_test.go b/pkg/cmd/pr/comment/comment_test.go index 8de1be940..56cf58d21 100644 --- a/pkg/cmd/pr/comment/comment_test.go +++ b/pkg/cmd/pr/comment/comment_test.go @@ -254,7 +254,7 @@ func Test_commentRun(t *testing.T) { OpenInBrowser: func(string) error { return nil }, }, - stderr: "Opening github.com/OWNER/REPO/pull/123 in your browser.\n", + stderr: "Opening https://github.com/OWNER/REPO/pull/123 in your browser.\n", }, { name: "non-interactive web with edit last", @@ -266,7 +266,7 @@ func Test_commentRun(t *testing.T) { OpenInBrowser: func(string) error { return nil }, }, - stderr: "Opening github.com/OWNER/REPO/pull/123 in your browser.\n", + stderr: "Opening https://github.com/OWNER/REPO/pull/123 in your browser.\n", }, { name: "non-interactive editor", diff --git a/pkg/cmd/pr/create/create_test.go b/pkg/cmd/pr/create/create_test.go index e0347945c..cba552101 100644 --- a/pkg/cmd/pr/create/create_test.go +++ b/pkg/cmd/pr/create/create_test.go @@ -1080,7 +1080,7 @@ func Test_createRun(t *testing.T) { } } }, - expectedErrOut: "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", + expectedErrOut: "Opening https://github.com/OWNER/REPO/compare/master...feature in your browser.\n", expectedBrowse: "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1", }, { @@ -1113,7 +1113,7 @@ func Test_createRun(t *testing.T) { } } }, - expectedErrOut: "Opening github.com/OWNER/REPO/compare/master...feature in your browser.\n", + expectedErrOut: "Opening https://github.com/OWNER/REPO/compare/master...feature in your browser.\n", expectedBrowse: "https://github.com/OWNER/REPO/compare/master...feature?body=&expand=1&projects=ORG%2F1", }, { diff --git a/pkg/cmd/pr/diff/diff_test.go b/pkg/cmd/pr/diff/diff_test.go index be8c48428..28a83bfc4 100644 --- a/pkg/cmd/pr/diff/diff_test.go +++ b/pkg/cmd/pr/diff/diff_test.go @@ -218,7 +218,7 @@ func Test_diffRun(t *testing.T) { BrowserMode: true, }, wantFields: []string{"url"}, - wantStderr: "Opening github.com/OWNER/REPO/pull/123/files in your browser.\n", + wantStderr: "Opening https://github.com/OWNER/REPO/pull/123/files in your browser.\n", wantBrowsedURL: "https://github.com/OWNER/REPO/pull/123/files", }, } diff --git a/pkg/cmd/pr/list/list_test.go b/pkg/cmd/pr/list/list_test.go index 869db9c38..ecd0326b5 100644 --- a/pkg/cmd/pr/list/list_test.go +++ b/pkg/cmd/pr/list/list_test.go @@ -318,7 +318,7 @@ func TestPRList_web(t *testing.T) { } assert.Equal(t, "", output.String()) - assert.Equal(t, "Opening github.com/OWNER/REPO/pulls in your browser.\n", output.Stderr()) + assert.Equal(t, "Opening https://github.com/OWNER/REPO/pulls in your browser.\n", output.Stderr()) assert.Equal(t, test.expectedBrowserURL, output.BrowsedURL) }) } diff --git a/pkg/cmd/pr/view/view_test.go b/pkg/cmd/pr/view/view_test.go index 2b99df931..470e3bd27 100644 --- a/pkg/cmd/pr/view/view_test.go +++ b/pkg/cmd/pr/view/view_test.go @@ -640,7 +640,7 @@ func TestPRView_web_currentBranch(t *testing.T) { } assert.Equal(t, "", output.String()) - assert.Equal(t, "Opening github.com/OWNER/REPO/pull/10 in your browser.\n", output.Stderr()) + assert.Equal(t, "Opening https://github.com/OWNER/REPO/pull/10 in your browser.\n", output.Stderr()) assert.Equal(t, "https://github.com/OWNER/REPO/pull/10", output.BrowsedURL) } diff --git a/pkg/cmd/repo/view/view_test.go b/pkg/cmd/repo/view/view_test.go index a8361e9ef..53ec93d13 100644 --- a/pkg/cmd/repo/view/view_test.go +++ b/pkg/cmd/repo/view/view_test.go @@ -108,7 +108,7 @@ func Test_RepoView_Web(t *testing.T) { { name: "tty", stdoutTTY: true, - wantStderr: "Opening github.com/OWNER/REPO in your browser.\n", + wantStderr: "Opening https://github.com/OWNER/REPO in your browser.\n", wantBrowse: "https://github.com/OWNER/REPO", }, { diff --git a/pkg/cmd/ruleset/check/check_test.go b/pkg/cmd/ruleset/check/check_test.go index 2d9dffae4..b24e084c4 100644 --- a/pkg/cmd/ruleset/check/check_test.go +++ b/pkg/cmd/ruleset/check/check_test.go @@ -162,7 +162,7 @@ func Test_checkRun(t *testing.T) { Branch: "my-branch", WebMode: true, }, - wantStdout: "Opening github.com/my-org/repo-name/rules in your browser.\n", + wantStdout: "Opening https://github.com/my-org/repo-name/rules in your browser.\n", wantStderr: "", wantBrowse: "https://github.com/my-org/repo-name/rules?ref=refs%2Fheads%2Fmy-branch", }, @@ -173,7 +173,7 @@ func Test_checkRun(t *testing.T) { Branch: "my-feature/my-branch", WebMode: true, }, - wantStdout: "Opening github.com/my-org/repo-name/rules in your browser.\n", + wantStdout: "Opening https://github.com/my-org/repo-name/rules in your browser.\n", wantStderr: "", wantBrowse: "https://github.com/my-org/repo-name/rules?ref=refs%2Fheads%2Fmy-feature%2Fmy-branch", }, diff --git a/pkg/cmd/ruleset/list/list_test.go b/pkg/cmd/ruleset/list/list_test.go index 1c370fcfb..2b4675b2f 100644 --- a/pkg/cmd/ruleset/list/list_test.go +++ b/pkg/cmd/ruleset/list/list_test.go @@ -255,7 +255,7 @@ func Test_listRun(t *testing.T) { opts: ListOptions{ WebMode: true, }, - wantStdout: "Opening github.com/OWNER/REPO/rules in your browser.\n", + wantStdout: "Opening https://github.com/OWNER/REPO/rules in your browser.\n", wantStderr: "", wantBrowse: "https://github.com/OWNER/REPO/rules", }, @@ -266,7 +266,7 @@ func Test_listRun(t *testing.T) { WebMode: true, Organization: "my-org", }, - wantStdout: "Opening github.com/organizations/my-org/settings/rules in your browser.\n", + wantStdout: "Opening https://github.com/organizations/my-org/settings/rules in your browser.\n", wantStderr: "", wantBrowse: "https://github.com/organizations/my-org/settings/rules", }, diff --git a/pkg/cmd/ruleset/view/view_test.go b/pkg/cmd/ruleset/view/view_test.go index 5e2846441..e463a9ac4 100644 --- a/pkg/cmd/ruleset/view/view_test.go +++ b/pkg/cmd/ruleset/view/view_test.go @@ -317,7 +317,7 @@ func Test_viewRun(t *testing.T) { httpmock.FileResponse("./fixtures/rulesetViewRepo.json"), ) }, - wantStdout: "Opening github.com/my-owner/repo-name/rules/42 in your browser.\n", + wantStdout: "Opening https://github.com/my-owner/repo-name/rules/42 in your browser.\n", wantStderr: "", wantBrowse: "https://github.com/my-owner/repo-name/rules/42", }, @@ -352,7 +352,7 @@ func Test_viewRun(t *testing.T) { httpmock.FileResponse("./fixtures/rulesetViewOrg.json"), ) }, - wantStdout: "Opening github.com/organizations/my-owner/settings/rules/74 in your browser.\n", + wantStdout: "Opening https://github.com/organizations/my-owner/settings/rules/74 in your browser.\n", wantStderr: "", wantBrowse: "https://github.com/organizations/my-owner/settings/rules/74", }, diff --git a/pkg/cmd/run/view/view_test.go b/pkg/cmd/run/view/view_test.go index 44ca69fa6..60762c531 100644 --- a/pkg/cmd/run/view/view_test.go +++ b/pkg/cmd/run/view/view_test.go @@ -1183,7 +1183,7 @@ func TestViewRun(t *testing.T) { httpmock.JSONResponse(shared.TestWorkflow)) }, browsedURL: "https://github.com/runs/3", - wantOut: "Opening github.com/runs/3 in your browser.\n", + wantOut: "Opening https://github.com/runs/3 in your browser.\n", }, { name: "web job", @@ -1204,7 +1204,7 @@ func TestViewRun(t *testing.T) { httpmock.JSONResponse(shared.TestWorkflow)) }, browsedURL: "https://github.com/jobs/10?check_suite_focus=true", - wantOut: "Opening github.com/jobs/10 in your browser.\n", + wantOut: "Opening https://github.com/jobs/10 in your browser.\n", }, { name: "hide job header, failure", diff --git a/pkg/cmd/search/code/code_test.go b/pkg/cmd/search/code/code_test.go index 253bd2889..4b493c6d5 100644 --- a/pkg/cmd/search/code/code_test.go +++ b/pkg/cmd/search/code/code_test.go @@ -304,7 +304,7 @@ func TestCodeRun(t *testing.T) { WebMode: true, }, tty: true, - wantStderr: "Opening github.com/search in your browser.\n", + wantStderr: "Opening https://github.com/search in your browser.\n", }, { name: "opens browser for web mode notty", diff --git a/pkg/cmd/search/commits/commits_test.go b/pkg/cmd/search/commits/commits_test.go index efe022f32..d78599cbe 100644 --- a/pkg/cmd/search/commits/commits_test.go +++ b/pkg/cmd/search/commits/commits_test.go @@ -273,7 +273,7 @@ func TestCommitsRun(t *testing.T) { WebMode: true, }, tty: true, - wantStderr: "Opening github.com/search in your browser.\n", + wantStderr: "Opening https://github.com/search in your browser.\n", }, { name: "opens browser for web mode notty", diff --git a/pkg/cmd/search/repos/repos_test.go b/pkg/cmd/search/repos/repos_test.go index 85e2547f1..3b725b049 100644 --- a/pkg/cmd/search/repos/repos_test.go +++ b/pkg/cmd/search/repos/repos_test.go @@ -249,7 +249,7 @@ func TestReposRun(t *testing.T) { WebMode: true, }, tty: true, - wantStderr: "Opening github.com/search in your browser.\n", + wantStderr: "Opening https://github.com/search in your browser.\n", }, { name: "opens browser for web mode notty", diff --git a/pkg/cmd/search/shared/shared_test.go b/pkg/cmd/search/shared/shared_test.go index 689459d39..0700a688e 100644 --- a/pkg/cmd/search/shared/shared_test.go +++ b/pkg/cmd/search/shared/shared_test.go @@ -171,7 +171,7 @@ func TestSearchIssues(t *testing.T) { WebMode: true, }, tty: true, - wantStderr: "Opening github.com/search in your browser.\n", + wantStderr: "Opening https://github.com/search in your browser.\n", }, { name: "opens browser for web mode notty", diff --git a/pkg/cmd/workflow/view/view_test.go b/pkg/cmd/workflow/view/view_test.go index c706da48f..e5df52478 100644 --- a/pkg/cmd/workflow/view/view_test.go +++ b/pkg/cmd/workflow/view/view_test.go @@ -222,7 +222,7 @@ func TestViewRun(t *testing.T) { httpmock.JSONResponse(aWorkflow), ) }, - wantOut: "Opening github.com/OWNER/REPO/actions/workflows/flow.yml in your browser.\n", + wantOut: "Opening https://github.com/OWNER/REPO/actions/workflows/flow.yml in your browser.\n", }, { name: "web notty", @@ -257,7 +257,7 @@ func TestViewRun(t *testing.T) { httpmock.StringResponse(`{ "data": { "repository": { "defaultBranchRef": { "name": "trunk" } } } }`), ) }, - wantOut: "Opening github.com/OWNER/REPO/blob/trunk/.github/workflows/flow.yml in your browser.\n", + wantOut: "Opening https://github.com/OWNER/REPO/blob/trunk/.github/workflows/flow.yml in your browser.\n", }, { name: "web with yaml and ref", @@ -274,7 +274,7 @@ func TestViewRun(t *testing.T) { httpmock.JSONResponse(aWorkflow), ) }, - wantOut: "Opening github.com/OWNER/REPO/blob/base/.github/workflows/flow.yml in your browser.\n", + wantOut: "Opening https://github.com/OWNER/REPO/blob/base/.github/workflows/flow.yml in your browser.\n", }, { name: "workflow with yaml", From 6b9a0aa89fc0643308ad2f584265a5950cf5df65 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Thu, 15 Aug 2024 22:52:13 -0700 Subject: [PATCH 012/301] Check http scheme as well Co-authored-by: Andy Feller --- internal/text/text_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/text/text_test.go b/internal/text/text_test.go index 906a68762..cae9b37c1 100644 --- a/internal/text/text_test.go +++ b/internal/text/text_test.go @@ -168,6 +168,11 @@ func TestDisplayURL(t *testing.T) { url: "https://github.com/cli/cli/issues/9470?q=is:issue#issue-command", want: "https://github.com/cli/cli/issues/9470", }, + { + name: "preserve http protocol use despite insecure", + url: "http://github.com/cli/cli/issues/9470", + want: "http://github.com/cli/cli/issues/9470", + }, } for _, tt := range tests { From 64415df08d301e08a151f09337dce994e72beb07 Mon Sep 17 00:00:00 2001 From: benebsiny Date: Sun, 18 Aug 2024 16:57:19 +0800 Subject: [PATCH 013/301] fix the trimming of log filenames for `gh run view` --- pkg/cmd/run/view/fixtures/run_log.zip | Bin 6822 -> 6880 bytes pkg/cmd/run/view/view.go | 1 + pkg/cmd/run/view/view_test.go | 12 ++++++++++++ 3 files changed, 13 insertions(+) diff --git a/pkg/cmd/run/view/fixtures/run_log.zip b/pkg/cmd/run/view/fixtures/run_log.zip index 2422c3b2496a37b98c164713aee746f65ce150d1..757a6398307d835f12293e64b602d58ebc965308 100644 GIT binary patch delta 1524 zcmZ2x`oMI;d`5=Jfdb+aw=ZU8W)Wdvn9M6EzPXL@731UvW=~!Q1`dWRNi`AAzJ5&L zo_vith>4kHa-WdYAu~ZGtp23l{?jaBSKi24+ABQJ|{?IK*TFykRCPs&#^G6$YwM z1Y&iFipln(LI$T#`iGx5dB!``;7OBluYgao9s@!n$moeymdif@HL*xAFvx*5%GbZW zaWLzUgGlSc;^mPncX(n;XQ~U-G;8d7v5WtLRCJ*t%Mqu-sdM{tkKa+&$jV$;pnQ+N zEq+o@+SD@~4sP?`Gb=3Xx7>2$(V{tzeG**VfAw*(UW<63{I&dBvAp2{?Pn|epM}pz zYUTa)@`U)c^QWuAbYFLwTl*~DpZ)NeMfAC8^_!pOl(uAil6%(!Wc_{kGhl=2?Tpsv z%(G?BcIF(Mma|wiPW(pC*_$WCa!#5>*==c=&HqrJyIeykGS9(fd3pEdr&0W~cGUm( zU-$n1|M?)(dQu)w|G1*|WSrOzfeLGxABlnjU(O#|zWBA(c7cLzbN1#V1xge9{xI&2 z0~V%}6F9rsz=6y$nUU+cI0FX;k6j= zk_?69{G9weeS|}?L_2mZhVf}YLrOAI6_OH*s`N@KO5mx26P_$4&*R?&PW-!pB(I<% zW5i^A!DGyE%nXxfGiyygAjt+4*A!A@UJMj@z-&EPL5O4W1R*6xw#l1?On`DPgcKQl zCvyqAflLurWZK0tIZ#AwvJR^eNGU5Q??_I5BP7dMHTgMMk)DVm({om!A|Fk823%=f z1sWm(KzcbFIOPX&fkU2wfsp~2W`CHJotk`;@rzBqw#vs_j2GYKf$0EmMkaX%U=9Xa ziy2TbQx$;nw;-m1atn&d4V@=v@6fr<_&^l5$(R`k*<>SjkbC8I@wpe~ZjgILm_a4Z zGf_pR1P)-Z2v3#~b7TZ1Jux1jJHg57at)t0DDJ?)9{>t|rVif8uOwZ8kz^pH$ov(k#Gc;PAtLO3MbPwTWZu`N?}+C`h{UDLjr3ua!n?5^;og zyVL3S$gm?BQhA4IN>)fQIY_$i zUsRorb0Rd}6CVoau&{IRO!T^~MMlJMA4$Q#P$=B8_%|8&ag&+FykH^`ElfHZ?>)M&q*>&~Ah1c2ZV~O@e!BtNZT`d&iKw^|#?n+`qT}=lq6aFVD--XHH&;Z10-!rgh?}5AC<# z#<^+q+c3FxQuQa|2p(6&#+>UTcc9h6@wv@5ox!0MpelMC^omTJ6Ht2us=Xvkd6?AK?>VKc5bnA;i9OA7eNX$8yL$h zt;d3JND5(!A;YqWu+5xWZ5Wy@%gJtuR)l1Wf$4xKE2a9hVt_wqf(|OJOTlE-g?+aH zMo9E7PUInl7ll#X09n>?;UmqUx+rZ1UuacHjwe*r8RBFc>;0RdkEwoU(gmu(0U9XN zmY|u)>F3)OHd&9L7%!&vU=TFfWzCQtp4xxcU diff --git a/pkg/cmd/run/view/view.go b/pkg/cmd/run/view/view.go index 73ef7bb1e..c794cff9a 100644 --- a/pkg/cmd/run/view/view.go +++ b/pkg/cmd/run/view/view.go @@ -546,6 +546,7 @@ func logFilenameRegexp(job shared.Job, step shared.Step) *regexp.Regexp { // * Truncate long job names // sanitizedJobName := strings.ReplaceAll(job.Name, "/", "") + sanitizedJobName = strings.ReplaceAll(sanitizedJobName, ":", "") sanitizedJobName = truncateAsUTF16(sanitizedJobName, JOB_NAME_MAX_LENGTH) re := fmt.Sprintf(`^%s\/%d_.*\.txt`, regexp.QuoteMeta(sanitizedJobName), step.Number) return regexp.MustCompile(re) diff --git a/pkg/cmd/run/view/view_test.go b/pkg/cmd/run/view/view_test.go index 44ca69fa6..956f80cd5 100644 --- a/pkg/cmd/run/view/view_test.go +++ b/pkg/cmd/run/view/view_test.go @@ -1522,6 +1522,18 @@ func Test_attachRunLog(t *testing.T) { // not the double space in the dir name, as the slash has been removed wantFilename: "cool job with slash/1_fob the barz.txt", }, + { + name: "job name with colon matches dir with colon removed", + job: shared.Job{ + Name: "cool job : with colon", + Steps: []shared.Step{{ + Name: "fob the barz", + Number: 1, + }}, + }, + wantMatch: true, + wantFilename: "cool job with colon/1_fob the barz.txt", + }, { name: "Job name with really long name (over the ZIP limit)", job: shared.Job{ From 3fd309bdde0242dc46ff9a5a6473a5592361bce0 Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Mon, 19 Aug 2024 10:29:01 -0400 Subject: [PATCH 014/301] rename flag to bundle-from-oci --- pkg/cmd/attestation/verify/options.go | 8 ++++---- pkg/cmd/attestation/verify/options_test.go | 4 ++-- pkg/cmd/attestation/verify/verify.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index a21e27c58..3ec2d49f1 100644 --- a/pkg/cmd/attestation/verify/options.go +++ b/pkg/cmd/attestation/verify/options.go @@ -84,14 +84,14 @@ func (opts *Options) AreFlagsValid() error { return fmt.Errorf("limit %d not allowed, must be between 1 and 1000", opts.Limit) } - // Check that the bundle-from-registry flag is only used with OCI artifact paths + // Check that the bundle-from-oci flag is only used with OCI artifact paths if opts.UseBundleFromRegistry && !strings.HasPrefix(opts.ArtifactPath, "oci://") { - return fmt.Errorf("bundle-from-registry flag can only be used with OCI artifact paths") + return fmt.Errorf("bundle-from-oci flag can only be used with OCI artifact paths") } - // Check that both the bundle-from-registry and bundle-path flags are not used together + // Check that both the bundle-from-oci and bundle-path flags are not used together if opts.UseBundleFromRegistry && opts.BundlePath != "" { - return fmt.Errorf("bundle-from-registry flag cannot be used with bundle-path flag") + return fmt.Errorf("bundle-from-oci flag cannot be used with bundle-path flag") } return nil diff --git a/pkg/cmd/attestation/verify/options_test.go b/pkg/cmd/attestation/verify/options_test.go index 9fd319041..b9be054a5 100644 --- a/pkg/cmd/attestation/verify/options_test.go +++ b/pkg/cmd/attestation/verify/options_test.go @@ -128,7 +128,7 @@ func TestSetPolicyFlags(t *testing.T) { err := opts.AreFlagsValid() require.Error(t, err) - require.ErrorContains(t, err, "bundle-from-registry flag can only be used with OCI artifact paths") + require.ErrorContains(t, err, "bundle-from-oci flag can only be used with OCI artifact paths") }) t.Run("does not return error when UseBundleFromRegistry is true and ArtifactPath is an OCI path", func(t *testing.T) { @@ -157,6 +157,6 @@ func TestSetPolicyFlags(t *testing.T) { err := opts.AreFlagsValid() require.Error(t, err) - require.ErrorContains(t, err, "bundle-from-registry flag cannot be used with bundle-path flag") + require.ErrorContains(t, err, "bundle-from-oci flag cannot be used with bundle-path flag") }) } diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index f77f5f822..f053240de 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -150,7 +150,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command // general flags verifyCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles") cmdutil.DisableAuthCheckFlag(verifyCmd.Flags().Lookup("bundle")) - verifyCmd.Flags().BoolVarP(&opts.UseBundleFromRegistry, "bundle-from-registry", "", false, "Use the bundle from the OCI registry when artifact is an OCI image") + verifyCmd.Flags().BoolVarP(&opts.UseBundleFromRegistry, "bundle-from-oci", "", false, "When verifying an OCI image, fetch the attestation bundle from the OCI registry instead of from GitHub") cmdutil.StringEnumFlag(verifyCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact") verifyCmd.Flags().StringVarP(&opts.Owner, "owner", "o", "", "GitHub organization to scope attestation lookup by") verifyCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format /") From 47a8f4bbdd36e755dc5eba5bb548921329fc70a9 Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Tue, 20 Aug 2024 16:14:39 -0400 Subject: [PATCH 015/301] update error message --- pkg/cmd/attestation/verification/attestation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index c1fb25b12..5feca47ea 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -131,7 +131,7 @@ func GetOCIAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) { return nil, fmt.Errorf("failed to fetch OCI attestations: %w", err) } if len(attestations) == 0 { - return nil, fmt.Errorf("no OCI attestations found") + return nil, fmt.Errorf("no attestations found in the OCI registry. Retry the command without the --bundle-from-oci flag to check GitHub for the attestation") } return attestations, nil } From 0d38a2fd8e3a828deb8f92b7d5042463fff43709 Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Wed, 21 Aug 2024 10:52:42 -0400 Subject: [PATCH 016/301] fixed the test --- pkg/cmd/attestation/verify/verify_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go index 0f31d900c..89c5ae7c1 100644 --- a/pkg/cmd/attestation/verify/verify_test.go +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -488,7 +488,7 @@ func TestRunVerify(t *testing.T) { customOpts.UseBundleFromRegistry = true customOpts.OCIClient = oci.NoAttestationsClient{} - require.ErrorContains(t, runVerify(&customOpts), "no OCI attestations found") + require.ErrorContains(t, runVerify(&customOpts), "no attestations found in the OCI registry. Retry the command without the --bundle-from-oci flag to check GitHub for the attestation") }) t.Run("with valid OCI artifact with UseBundleFromRegistry flag but fail on fetching bundle from registry", func(t *testing.T) { @@ -498,6 +498,6 @@ func TestRunVerify(t *testing.T) { customOpts.UseBundleFromRegistry = true customOpts.OCIClient = oci.NoAttestationsClient{} - require.ErrorContains(t, runVerify(&customOpts), "no OCI attestations found") + require.ErrorContains(t, runVerify(&customOpts), "no attestations found in the OCI registry. Retry the command without the --bundle-from-oci flag to check GitHub for the attestation") }) } From c81ccab4b8bb1ef1c774f4ba9851e9dcb11fa5e7 Mon Sep 17 00:00:00 2001 From: Yukai Chou Date: Thu, 22 Aug 2024 03:20:45 +0800 Subject: [PATCH 017/301] Quote repo names consistently in `gh repo sync` stdout (#9491) * Quote repo names consistently in `gh repo sync` stdout * Update tests --- pkg/cmd/repo/sync/sync.go | 2 +- pkg/cmd/repo/sync/sync_test.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/repo/sync/sync.go b/pkg/cmd/repo/sync/sync.go index 87badc7a1..4418079b7 100644 --- a/pkg/cmd/repo/sync/sync.go +++ b/pkg/cmd/repo/sync/sync.go @@ -156,7 +156,7 @@ func syncLocalRepo(opts *SyncOptions) error { if opts.IO.IsStdoutTTY() { cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.Out, "%s Synced the \"%s\" branch from %s to local repository\n", + fmt.Fprintf(opts.IO.Out, "%s Synced the \"%s\" branch from \"%s\" to local repository\n", cs.SuccessIcon(), opts.Branch, ghrepo.FullName(srcRepo)) diff --git a/pkg/cmd/repo/sync/sync_test.go b/pkg/cmd/repo/sync/sync_test.go index 8b9e4352f..227e2a6a2 100644 --- a/pkg/cmd/repo/sync/sync_test.go +++ b/pkg/cmd/repo/sync/sync_test.go @@ -129,7 +129,7 @@ func Test_SyncRun(t *testing.T) { mgc.On("IsDirty").Return(false, nil).Once() mgc.On("MergeFastForward", "FETCH_HEAD").Return(nil).Once() }, - wantStdout: "✓ Synced the \"trunk\" branch from OWNER/REPO to local repository\n", + wantStdout: "✓ Synced the \"trunk\" branch from \"OWNER/REPO\" to local repository\n", }, { name: "sync local repo with parent - notty", @@ -162,7 +162,7 @@ func Test_SyncRun(t *testing.T) { mgc.On("IsDirty").Return(false, nil).Once() mgc.On("MergeFastForward", "FETCH_HEAD").Return(nil).Once() }, - wantStdout: "✓ Synced the \"trunk\" branch from OWNER2/REPO2 to local repository\n", + wantStdout: "✓ Synced the \"trunk\" branch from \"OWNER2/REPO2\" to local repository\n", }, { name: "sync local repo with parent and force specified", @@ -179,7 +179,7 @@ func Test_SyncRun(t *testing.T) { mgc.On("IsDirty").Return(false, nil).Once() mgc.On("ResetHard", "FETCH_HEAD").Return(nil).Once() }, - wantStdout: "✓ Synced the \"trunk\" branch from OWNER/REPO to local repository\n", + wantStdout: "✓ Synced the \"trunk\" branch from \"OWNER/REPO\" to local repository\n", }, { name: "sync local repo with specified source repo and force specified", @@ -197,7 +197,7 @@ func Test_SyncRun(t *testing.T) { mgc.On("IsDirty").Return(false, nil).Once() mgc.On("ResetHard", "FETCH_HEAD").Return(nil).Once() }, - wantStdout: "✓ Synced the \"trunk\" branch from OWNER2/REPO2 to local repository\n", + wantStdout: "✓ Synced the \"trunk\" branch from \"OWNER2/REPO2\" to local repository\n", }, { name: "sync local repo with parent and not fast forward merge", @@ -257,7 +257,7 @@ func Test_SyncRun(t *testing.T) { mgc.On("CurrentBranch").Return("test", nil).Once() mgc.On("UpdateBranch", "trunk", "FETCH_HEAD").Return(nil).Once() }, - wantStdout: "✓ Synced the \"trunk\" branch from OWNER/REPO to local repository\n", + wantStdout: "✓ Synced the \"trunk\" branch from \"OWNER/REPO\" to local repository\n", }, { name: "sync local repo with parent - create new branch", @@ -271,7 +271,7 @@ func Test_SyncRun(t *testing.T) { mgc.On("CurrentBranch").Return("test", nil).Once() mgc.On("CreateBranch", "trunk", "FETCH_HEAD", "origin/trunk").Return(nil).Once() }, - wantStdout: "✓ Synced the \"trunk\" branch from OWNER/REPO to local repository\n", + wantStdout: "✓ Synced the \"trunk\" branch from \"OWNER/REPO\" to local repository\n", }, { name: "sync remote fork with parent with new api - tty", From 2374e82633056dbe3bcfa49d21eaf91af9c670b2 Mon Sep 17 00:00:00 2001 From: Yukai Chou Date: Fri, 23 Aug 2024 00:12:09 +0800 Subject: [PATCH 018/301] Fix doc typo for `repo sync` --- pkg/cmd/repo/sync/sync.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/repo/sync/sync.go b/pkg/cmd/repo/sync/sync.go index 4418079b7..b025d9eec 100644 --- a/pkg/cmd/repo/sync/sync.go +++ b/pkg/cmd/repo/sync/sync.go @@ -50,7 +50,7 @@ func NewCmdSync(f *cmdutil.Factory, runF func(*SyncOptions) error) *cobra.Comman of the source repository to update the matching branch on the destination repository so they are equal. A fast forward update will be used except when the %[1]s--force%[1]s flag is specified, then the two branches will - by synced using a hard reset. + be synced using a hard reset. Without an argument, the local repository is selected as the destination repository. From 91eb34011c1904c16c0c9ac93471a7f0e81e32a8 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 22 Aug 2024 10:01:16 -0700 Subject: [PATCH 019/301] Remove `Internal` from `gh repo create` prompt when owner is not an org (#9465) * Remove `Internal` from `gh repo create` prompt when owner is not an org Closes #9464 Internal repos only exist for organizations, so when a user selects their personal namespace to create a repo using `gh repo create`, `Internal` should not be an option in the `Visibility` prompt. This should avoid the additional quirk where if the user selects `Internal` while creating a personal repo and then proceeds to add any of the README, .gitignore, or LICENSE files prompted for later, the repo will not error and instead get created as a `Public` repo. This has the potential for a user to unknowingly leak sensitive info intended to go into a non-public repo. * Refactor prompter with test coverage By extracting the repo visibility options to its own function, getRepoVisibilityOptions, we're able to directly test the behavior introduced with this change. This breaks the testing pattern established here thus far, but may be a good example of the direction we should explore for a future refactor. * Add failing tests to check for error with internal vis in non-org repos There is a bug in the code, currently, where a user repo can attempt to be created as with `--internal` visibility flag when that is not an option for non-org repos. It fails at the API level if the --gitignore, --license, or --add-readme flags are not included, but silently falls back to Public visibility if one of them is included. Because this bug already existed, this commit adds the tests to ensure that both scenarios described above are captured accurately by the test suite. A fix for the latter scenario will be coming in a future commit * Add Exclude to httpmock registry and implement in Test_repoCreate Upon attempting to make the previous commit pass, I realized that it was actually impossible to test what I wanted to. The tests in the previous commit were behaving as expected given the bug that commit described, but upon attempting to implement a solution I realized that the tests were only testing the mocks and not the code functionality itself. Essentially, when the code to fix the bug was implemented, the tests were failing because the mocks required to test the buggy behavior were no longer being called. To make the tests pass, I'd have to rewrite them, but were I to remove the bug fix, the tests would no longer fail. This pointed me to a gap in our httpmocks - the ability to intentionally exclude api calls. The behavior I'm trying to test, here, is that we stop executing when a certain condition is met, and therefore won't make any subsequent api calls down the chain. This implements the Exclude method on the registry such that it will fail if an excluded api pattern is called. I have refactored the tests in Test_repoCreate to use the Exclude mock for testing. * Add error if user attempts to create repo with --internal flag This was previously failing at either the API if no other flags were included or falling back to creating a public repo if one of gitignore, license, or add-readme were included. * Add testing for error messages in gh repo create In the previous commits, we've introduced a new error when a user tries to create an Internal repo not owned by an organization. This adds tests to verify that the error we are getting is, in fact, the one associated with this use case and not some random error. --- pkg/cmd/repo/create/create.go | 11 ++++++- pkg/cmd/repo/create/create_test.go | 24 +++++++++++++++ pkg/cmd/repo/create/http.go | 5 +++ pkg/cmd/repo/create/http_test.go | 49 ++++++++++++++++++++++++++++++ pkg/httpmock/registry.go | 18 ++++++++++- pkg/httpmock/stub.go | 1 + 6 files changed, 106 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 9074ea117..8b5dd515a 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -855,7 +855,7 @@ func interactiveRepoInfo(client *http.Client, hostname string, prompter iprompte return "", "", "", err } - visibilityOptions := []string{"Public", "Private", "Internal"} + visibilityOptions := getRepoVisibilityOptions(owner) selected, err := prompter.Select("Visibility", "Public", visibilityOptions) if err != nil { return "", "", "", err @@ -864,6 +864,15 @@ func interactiveRepoInfo(client *http.Client, hostname string, prompter iprompte return name, description, strings.ToUpper(visibilityOptions[selected]), nil } +func getRepoVisibilityOptions(owner string) []string { + visibilityOptions := []string{"Public", "Private"} + // orgs can also create internal repos + if owner != "" { + visibilityOptions = append(visibilityOptions, "Internal") + } + return visibilityOptions +} + func interactiveRepoNameAndOwner(client *http.Client, hostname string, prompter iprompter, defaultName string) (string, string, error) { name, err := prompter.Input("Repository name", defaultName) if err != nil { diff --git a/pkg/cmd/repo/create/create_test.go b/pkg/cmd/repo/create/create_test.go index e6576ebd0..cc0ec602a 100644 --- a/pkg/cmd/repo/create/create_test.go +++ b/pkg/cmd/repo/create/create_test.go @@ -866,3 +866,27 @@ func Test_createRun(t *testing.T) { }) } } + +func Test_getRepoVisibilityOptions(t *testing.T) { + tests := []struct { + name string + owner string + want []string + }{ + { + name: "user repo", + owner: "", + want: []string{"Public", "Private"}, + }, + { + name: "org repo", + owner: "fooOrg", + want: []string{"Public", "Private", "Internal"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, getRepoVisibilityOptions(tt.owner)) + }) + } +} diff --git a/pkg/cmd/repo/create/http.go b/pkg/cmd/repo/create/http.go index ffe9dbb6f..120683c08 100644 --- a/pkg/cmd/repo/create/http.go +++ b/pkg/cmd/repo/create/http.go @@ -99,6 +99,11 @@ func repoCreate(client *http.Client, hostname string, input repoCreateInput) (*a isOrg = owner.IsOrganization() } + isInternal := strings.ToLower(input.Visibility) == "internal" + if isInternal && !isOrg { + return nil, fmt.Errorf("internal repositories can only be created within an organization") + } + if input.TemplateRepositoryID != "" { var response struct { CloneTemplateRepository struct { diff --git a/pkg/cmd/repo/create/http_test.go b/pkg/cmd/repo/create/http_test.go index cfe14563a..ec39b3c50 100644 --- a/pkg/cmd/repo/create/http_test.go +++ b/pkg/cmd/repo/create/http_test.go @@ -16,6 +16,7 @@ func Test_repoCreate(t *testing.T) { input repoCreateInput stubs func(t *testing.T, r *httpmock.Registry) wantErr bool + errMsg string wantRepo string }{ { @@ -681,6 +682,51 @@ func Test_repoCreate(t *testing.T) { }, wantRepo: "https://github.com/snacks-inc/crisps", }, + { + name: "create personal repository but try to set it as 'internal'", + hostname: "github.com", + input: repoCreateInput{ + Name: "winter-foods", + Description: "roasted chestnuts", + HomepageURL: "http://example.com", + Visibility: "internal", + OwnerLogin: "OWNER", + }, + wantErr: true, + errMsg: "internal repositories can only be created within an organization", + stubs: func(t *testing.T, r *httpmock.Registry) { + r.Register( + httpmock.REST("GET", "users/OWNER"), + httpmock.StringResponse(`{ "node_id": "1234", "type": "Not-Org" }`)) + r.Exclude( + t, + httpmock.GraphQL(`mutation RepositoryCreate\b`), + ) + }, + }, + { + name: "create personal repository with README but try to set it as 'internal'", + hostname: "github.com", + input: repoCreateInput{ + Name: "winter-foods", + Description: "roasted chestnuts", + HomepageURL: "http://example.com", + Visibility: "internal", + OwnerLogin: "OWNER", + InitReadme: true, + }, + wantErr: true, + errMsg: "internal repositories can only be created within an organization", + stubs: func(t *testing.T, r *httpmock.Registry) { + r.Register( + httpmock.REST("GET", "users/OWNER"), + httpmock.StringResponse(`{ "node_id": "1234", "type": "Not-Org" }`)) + r.Exclude( + t, + httpmock.REST("POST", "user/repos"), + ) + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -691,6 +737,9 @@ func Test_repoCreate(t *testing.T) { r, err := repoCreate(httpClient, tt.hostname, tt.input) if tt.wantErr { assert.Error(t, err) + if tt.errMsg != "" { + assert.ErrorContains(t, err, tt.errMsg) + } return } else { assert.NoError(t, err) diff --git a/pkg/httpmock/registry.go b/pkg/httpmock/registry.go index e36f71a92..387d0fc95 100644 --- a/pkg/httpmock/registry.go +++ b/pkg/httpmock/registry.go @@ -4,6 +4,9 @@ import ( "fmt" "net/http" "sync" + "testing" + + "github.com/stretchr/testify/assert" ) // Replace http.Client transport layer with registry so all requests get @@ -25,6 +28,18 @@ func (r *Registry) Register(m Matcher, resp Responder) { }) } +func (r *Registry) Exclude(t *testing.T, m Matcher) { + excludedStub := &Stub{ + Matcher: m, + Responder: func(req *http.Request) (*http.Response, error) { + assert.FailNowf(t, "Exclude error", "API called when excluded: %v", req.URL) + return nil, nil + }, + exclude: true, + } + r.stubs = append(r.stubs, excludedStub) +} + type Testing interface { Errorf(string, ...interface{}) Helper() @@ -33,7 +48,7 @@ type Testing interface { func (r *Registry) Verify(t Testing) { n := 0 for _, s := range r.stubs { - if !s.matched { + if !s.matched && !s.exclude { n++ } } @@ -62,6 +77,7 @@ func (r *Registry) RoundTrip(req *http.Request) (*http.Response, error) { stub = s break // TODO: remove } + if stub != nil { stub.matched = true } diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index e7534d7f8..787cdcf9d 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -18,6 +18,7 @@ type Stub struct { matched bool Matcher Matcher Responder Responder + exclude bool } func MatchAny(*http.Request) bool { From 687a43fe89a6c52f419e48205062eb8f89d4f776 Mon Sep 17 00:00:00 2001 From: Yukai Chou Date: Wed, 21 Aug 2024 11:49:31 +0800 Subject: [PATCH 020/301] Drop surplus trailing space char in flag names in web Introduced by 92cb2cc7 (more closely match cobra default val display, 2023-12-05). --- internal/docs/markdown.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/docs/markdown.go b/internal/docs/markdown.go index 825db931c..7ae8c6862 100644 --- a/internal/docs/markdown.go +++ b/internal/docs/markdown.go @@ -103,7 +103,7 @@ type flagView struct { var flagsTemplate = `
{{ range . }}
{{ if .Shorthand }}-{{.Shorthand}}, {{ end }} - --{{.Name}}{{ if .Varname }} <{{.Varname}}>{{ end }}{{.DefValue}}
+ --{{.Name}}{{ if .Varname }} <{{.Varname}}>{{ end }}{{.DefValue}}
{{.Usage}}
{{ end }}
` From 1eecfd45301635e5a2f2f3375cd0445fc98a5668 Mon Sep 17 00:00:00 2001 From: Velmurugan Date: Sat, 24 Aug 2024 16:24:51 +0530 Subject: [PATCH 021/301] Added `--active` flag to the `gh auth status` command --- pkg/cmd/auth/status/status.go | 6 ++++++ pkg/cmd/auth/status/status_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 2139bda1e..73ee084e9 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -129,6 +129,7 @@ type StatusOptions struct { Hostname string ShowToken bool + Active bool } func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Command { @@ -163,6 +164,7 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "Check only a specific hostname's auth status") cmd.Flags().BoolVarP(&opts.ShowToken, "show-token", "t", false, "Display the auth token") + cmd.Flags().BoolVarP(&opts.Active, "active", "a", false, "Display the active account only") return cmd } @@ -224,6 +226,10 @@ func statusRun(opts *StatusOptions) error { err = cmdutil.SilentError } + if opts.Active { + continue + } + users := authCfg.UsersForHost(hostname) for _, username := range users { if username == activeUser { diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 03c26ca04..0f41ac8ab 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -44,6 +44,13 @@ func Test_NewCmdStatus(t *testing.T) { ShowToken: true, }, }, + { + name: "active", + cli: "--active", + wants: StatusOptions{ + Active: true, + }, + }, } for _, tt := range tests { @@ -433,6 +440,27 @@ func Test_statusRun(t *testing.T) { - To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe `), }, + { + name: "active", + opts: StatusOptions{ + Active: true, + }, + cfgStubs: func(t *testing.T, c gh.Config) { + login(t, c, "github.com", "monalisa", "gho_abc123", "https") + login(t, c, "github.com", "monalisa-2", "gho_abc123", "https") + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) + }, + wantOut: heredoc.Doc(` + github.com + ✓ Logged in to github.com account monalisa-2 (GH_CONFIG_DIR/hosts.yml) + - Active account: true + - Git operations protocol: https + - Token: gho_****** + - Token scopes: 'repo', 'read:org' + `), + }, } for _, tt := range tests { From 8305a49c3f7d5331d662c72e9956219fcfe37eae Mon Sep 17 00:00:00 2001 From: Aryan Bhosale <36108149+aryanbhosale@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:28:29 +0530 Subject: [PATCH 022/301] "offline" verification using the bundle of attestations without any additional handling of the file (#9523) --- pkg/cmd/attestation/verification/attestation.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 5feca47ea..c780e247c 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -2,6 +2,7 @@ package verification import ( "bufio" + "bytes" "encoding/json" "errors" "fmt" @@ -88,6 +89,10 @@ func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) { var line []byte line, err = reader.ReadBytes('\n') for err == nil { + if len(bytes.TrimSpace(line)) == 0 { + line, err = reader.ReadBytes('\n') + continue + } var bundle bundle.ProtobufBundle bundle.Bundle = new(protobundle.Bundle) err = bundle.UnmarshalJSON(line) From b8db372d71bd7dd6ca77ebd8e95f47cb033760ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:26:42 -0700 Subject: [PATCH 023/301] build(deps): bump actions/attest-build-provenance from 1.4.1 to 1.4.2 (#9518) Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 1.4.1 to 1.4.2. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/310b0a4a3b0b78ef57ecda988ee04b132db73ef8...6149ea5740be74af77f260b9db67e633f6b0a9a1) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tyler McGoffin --- .github/workflows/deployment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 51b47e6d8..bc9442d9d 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -299,7 +299,7 @@ jobs: rpmsign --addsign dist/*.rpm - name: Attest release artifacts if: inputs.environment == 'production' - uses: actions/attest-build-provenance@310b0a4a3b0b78ef57ecda988ee04b132db73ef8 # v1.4.1 + uses: actions/attest-build-provenance@6149ea5740be74af77f260b9db67e633f6b0a9a1 # v1.4.2 with: subject-path: "dist/gh_*" - name: Run createrepo From 192f57ef429b4e89937c87dcbd330520730d4a3a Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Wed, 28 Aug 2024 14:00:46 -0400 Subject: [PATCH 024/301] Improve the help message for -F (#9525) Changing to `release-notes.md` from `changelog.md` may help users better contextually understand usage. Co-authored-by: Tyler McGoffin --- pkg/cmd/release/create/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/release/create/create.go b/pkg/cmd/release/create/create.go index 7a72f7f90..0b7c99b23 100644 --- a/pkg/cmd/release/create/create.go +++ b/pkg/cmd/release/create/create.go @@ -114,7 +114,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co $ gh release create v1.2.3 --generate-notes Use release notes from a file - $ gh release create v1.2.3 -F changelog.md + $ gh release create v1.2.3 -F release-notes.md Use annotated tag notes $ gh release create v1.2.3 --notes-from-tag From 03c34e71f96891cb5b944bde959be1b873643528 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 30 Aug 2024 10:57:51 -0400 Subject: [PATCH 025/301] Expand active test cases --- pkg/cmd/auth/status/status_test.go | 73 ++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 0f41ac8ab..3f16baf46 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -389,14 +389,14 @@ func Test_statusRun(t *testing.T) { { name: "multiple hosts with multiple accounts with environment tokens and with errors", opts: StatusOptions{}, - env: map[string]string{"GH_ENTERPRISE_TOKEN": "gho_abc123"}, + env: map[string]string{"GH_ENTERPRISE_TOKEN": "gho_abc123"}, // monalisa-ghe-2 cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "gho_def456", "https") login(t, c, "github.com", "monalisa-2", "gho_ghi789", "https") login(t, c, "ghe.io", "monalisa-ghe", "gho_xyz123", "ssh") }, httpStubs: func(reg *httpmock.Registry) { - // Get scopes for monalia-2 + // Get scopes for monalisa-2 reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) // Get scopes for monalisa reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo")) @@ -441,7 +441,7 @@ func Test_statusRun(t *testing.T) { `), }, { - name: "active", + name: "multiple accounts on a host, only active users", opts: StatusOptions{ Active: true, }, @@ -461,6 +461,73 @@ func Test_statusRun(t *testing.T) { - Token scopes: 'repo', 'read:org' `), }, + { + name: "multiple hosts with multiple accounts, only active users", + opts: StatusOptions{ + Active: true, + }, + cfgStubs: func(t *testing.T, c gh.Config) { + login(t, c, "github.com", "monalisa", "gho_abc123", "https") + login(t, c, "github.com", "monalisa-2", "gho_abc123", "https") + login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "ssh") + login(t, c, "ghe.io", "monalisa-ghe-2", "gho_abc123", "ssh") + }, + httpStubs: func(reg *httpmock.Registry) { + // Get scopes for monalisa-2 + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) + // Get scopes for monalisa-ghe-2 + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org")) + }, + wantOut: heredoc.Doc(` + github.com + ✓ Logged in to github.com account monalisa-2 (GH_CONFIG_DIR/hosts.yml) + - Active account: true + - Git operations protocol: https + - Token: gho_****** + - Token scopes: 'repo', 'read:org' + + ghe.io + ✓ Logged in to ghe.io account monalisa-ghe-2 (GH_CONFIG_DIR/hosts.yml) + - Active account: true + - Git operations protocol: ssh + - Token: gho_****** + - Token scopes: 'repo', 'read:org' + `), + }, + { + name: "multiple hosts with multiple accounts, only active users with errors", + opts: StatusOptions{ + Active: true, + }, + cfgStubs: func(t *testing.T, c gh.Config) { + login(t, c, "github.com", "monalisa", "gho_abc123", "https") + login(t, c, "github.com", "monalisa-2", "gho_abc123", "https") + login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "ssh") + login(t, c, "ghe.io", "monalisa-ghe-2", "gho_abc123", "ssh") + }, + httpStubs: func(reg *httpmock.Registry) { + // Get scopes for monalisa-2 + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) + // Error getting scopes for monalisa-ghe-2 + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(404, "{}")) + }, + wantErr: cmdutil.SilentError, + wantErrOut: heredoc.Doc(` + github.com + ✓ Logged in to github.com account monalisa-2 (GH_CONFIG_DIR/hosts.yml) + - Active account: true + - Git operations protocol: https + - Token: gho_****** + - Token scopes: 'repo', 'read:org' + + ghe.io + X Failed to log in to ghe.io account monalisa-ghe-2 (GH_CONFIG_DIR/hosts.yml) + - Active account: true + - The token in GH_CONFIG_DIR/hosts.yml is invalid. + - To re-authenticate, run: gh auth login -h ghe.io + - To forget about this account, run: gh auth logout -h ghe.io -u monalisa-ghe-2 + `), + }, } for _, tt := range tests { From 5562c1489f90c2886d28d2044664bfa6735d40df Mon Sep 17 00:00:00 2001 From: crystalstall Date: Mon, 2 Sep 2024 15:18:42 +0800 Subject: [PATCH 026/301] chore: fix some function names Signed-off-by: crystalstall --- pkg/cmd/issue/shared/lookup.go | 2 +- pkg/markdown/markdown.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/issue/shared/lookup.go b/pkg/cmd/issue/shared/lookup.go index f93b0867d..be79f9a73 100644 --- a/pkg/cmd/issue/shared/lookup.go +++ b/pkg/cmd/issue/shared/lookup.go @@ -36,7 +36,7 @@ func IssueFromArgWithFields(httpClient *http.Client, baseRepoFn func() (ghrepo.I return issue, baseRepo, err } -// IssuesFromArgWithFields loads 1 or more issues or pull requests with the specified fields. If some of the fields +// IssuesFromArgsWithFields loads 1 or more issues or pull requests with the specified fields. If some of the fields // could not be fetched by GraphQL, this returns non-nil issues and a *PartialLoadError. func IssuesFromArgsWithFields(httpClient *http.Client, baseRepoFn func() (ghrepo.Interface, error), args []string, fields []string) ([]*api.Issue, ghrepo.Interface, error) { var issuesRepo ghrepo.Interface diff --git a/pkg/markdown/markdown.go b/pkg/markdown/markdown.go index 1aab0bcd3..f3f4951af 100644 --- a/pkg/markdown/markdown.go +++ b/pkg/markdown/markdown.go @@ -9,7 +9,7 @@ func WithoutIndentation() glamour.TermRendererOption { return ghMarkdown.WithoutIndentation() } -// WithoutWrap is a rendering option that set the character limit for soft +// WithWrap is a rendering option that set the character limit for soft // wrapping the markdown rendering. There is a max limit of 120 characters. // If 0 is passed then wrapping is disabled. func WithWrap(w int) glamour.TermRendererOption { From 9a0a7d427e8c2bd6f9bb785bc92b01b3d1045796 Mon Sep 17 00:00:00 2001 From: Aryan Bhosale <36108149+aryanbhosale@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:27:56 +0530 Subject: [PATCH 027/301] verify 2nd artifact without swapping order (#9532) * verify 2nd artifact without swapping order possible solution to https://github.com/cli/cli/issues/9521#issuecomment-2310686619? * copy the mentioned test file and adds some extra lines * rm unnecessary import * Update pkg/cmd/attestation/verification/attestation_test.go Co-authored-by: Meredith Lancaster * gofmt --------- Co-authored-by: Meredith Lancaster --- .../attestation/verification/attestation.go | 21 +++---------- .../verification/attestation_test.go | 31 ++++++++++++++++--- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index c780e247c..4d96196da 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -1,7 +1,6 @@ package verification import ( - "bufio" "bytes" "encoding/json" "errors" @@ -76,33 +75,23 @@ func loadBundleFromJSONFile(path string) ([]*api.Attestation, error) { } func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) { - file, err := os.Open(path) + fileContent, err := os.ReadFile(path) if err != nil { - return nil, fmt.Errorf("could not open file: %v", err) + return nil, fmt.Errorf("could not read file: %v", err) } - defer file.Close() attestations := []*api.Attestation{} - reader := bufio.NewReader(file) + decoder := json.NewDecoder(bytes.NewReader(fileContent)) - var line []byte - line, err = reader.ReadBytes('\n') - for err == nil { - if len(bytes.TrimSpace(line)) == 0 { - line, err = reader.ReadBytes('\n') - continue - } + for decoder.More() { var bundle bundle.ProtobufBundle bundle.Bundle = new(protobundle.Bundle) - err = bundle.UnmarshalJSON(line) - if err != nil { + if err := decoder.Decode(&bundle); err != nil { return nil, fmt.Errorf("failed to unmarshal bundle from JSON: %v", err) } a := api.Attestation{Bundle: &bundle} attestations = append(attestations, &a) - - line, err = reader.ReadBytes('\n') } return attestations, nil diff --git a/pkg/cmd/attestation/verification/attestation_test.go b/pkg/cmd/attestation/verification/attestation_test.go index 87a91cea9..ba530e55d 100644 --- a/pkg/cmd/attestation/verification/attestation_test.go +++ b/pkg/cmd/attestation/verification/attestation_test.go @@ -1,6 +1,8 @@ package verification import ( + "os" + "path/filepath" "testing" protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" @@ -12,11 +14,32 @@ import ( ) func TestLoadBundlesFromJSONLinesFile(t *testing.T) { - path := "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl" - attestations, err := loadBundlesFromJSONLinesFile(path) + t.Run("with original file", func(t *testing.T) { + path := "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl" + attestations, err := loadBundlesFromJSONLinesFile(path) + require.NoError(t, err) + require.Len(t, attestations, 2) + }) - require.NoError(t, err) - require.Len(t, attestations, 2) + t.Run("with extra lines", func(t *testing.T) { + // Create a temporary file with extra lines + tempDir := t.TempDir() + tempFile := filepath.Join(tempDir, "test_with_extra_lines.jsonl") + + originalContent, err := os.ReadFile("../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl") + require.NoError(t, err) + + extraLines := []byte("\n\n") + newContent := append(originalContent, extraLines...) + + err = os.WriteFile(tempFile, newContent, 0644) + require.NoError(t, err) + + // Test the function with the new file + attestations, err := loadBundlesFromJSONLinesFile(tempFile) + require.NoError(t, err) + require.Len(t, attestations, 2, "Should still load 2 valid attestations") + }) } func TestLoadBundleFromJSONFile(t *testing.T) { From 34d7ef7a0efb07b5be163d971c97df0c64655abd Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 4 Sep 2024 10:31:41 -0600 Subject: [PATCH 028/301] `gh attestation verify` handles empty JSONL files (#9541) * handle empty jsonl files Signed-off-by: Meredith Lancaster * check processed attestations slice length Signed-off-by: Meredith Lancaster * update err name and message Signed-off-by: Meredith Lancaster --------- Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/attestation.go | 5 +++++ .../attestation/verification/attestation_test.go | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 4d96196da..3a2d7456f 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -16,6 +16,7 @@ import ( ) 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 { APIClient api.Client @@ -94,6 +95,10 @@ func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) { attestations = append(attestations, &a) } + if len(attestations) == 0 { + return nil, ErrEmptyBundleFile + } + return attestations, nil } diff --git a/pkg/cmd/attestation/verification/attestation_test.go b/pkg/cmd/attestation/verification/attestation_test.go index ba530e55d..66b337ad7 100644 --- a/pkg/cmd/attestation/verification/attestation_test.go +++ b/pkg/cmd/attestation/verification/attestation_test.go @@ -42,6 +42,19 @@ func TestLoadBundlesFromJSONLinesFile(t *testing.T) { }) } +func TestLoadBundlesFromJSONLinesFile_RejectEmptyJSONLFile(t *testing.T) { + // Create a temporary file + emptyJSONL, err := os.CreateTemp("", "empty.jsonl") + require.NoError(t, err) + err = emptyJSONL.Close() + require.NoError(t, err) + + attestations, err := loadBundlesFromJSONLinesFile(emptyJSONL.Name()) + + require.ErrorIs(t, err, ErrEmptyBundleFile) + require.Nil(t, attestations) +} + func TestLoadBundleFromJSONFile(t *testing.T) { path := "../test/data/sigstore-js-2.1.0-bundle.json" attestations, err := loadBundleFromJSONFile(path) From 1b67b354a9845439a725a1eb280c71d41838c822 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Wed, 4 Sep 2024 13:30:30 -0600 Subject: [PATCH 029/301] update bundle file parsing err messages Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/attestation.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 3a2d7456f..0f52adec5 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -53,13 +53,13 @@ func GetLocalAttestations(path string) ([]*api.Attestation, error) { case ".json": attestations, err := loadBundleFromJSONFile(path) if err != nil { - return nil, fmt.Errorf("bundle could not be loaded from JSON file: %v", err) + return nil, fmt.Errorf("bundle could not be loaded from JSON file at %s", path) } return attestations, nil case ".jsonl": attestations, err := loadBundlesFromJSONLinesFile(path) if err != nil { - return nil, fmt.Errorf("bundles could not be loaded from JSON lines file: %v", err) + return nil, fmt.Errorf("bundles could not be loaded from JSON lines file at %s", path) } return attestations, nil } From 84460796563adbc0a71e3c1a9d4e3649729489e9 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Wed, 4 Sep 2024 16:38:13 -0400 Subject: [PATCH 030/301] Upgrade to sigstore-go v0.6.1 Signed-off-by: Cody Soyland --- go.mod | 23 +++++------ go.sum | 38 ++++++++++--------- .../attestation/verification/mock_verifier.go | 2 +- .../verification/sigstore_integration_test.go | 26 ++----------- 4 files changed, 37 insertions(+), 52 deletions(-) diff --git a/go.mod b/go.mod index 68d87c034..50278d733 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/cli/cli/v2 -go 1.22.0 +go 1.22.5 -toolchain go1.22.5 +toolchain go1.22.6 require ( github.com/AlecAivazis/survey/v2 v2.3.7 @@ -26,7 +26,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.3.0 github.com/henvic/httpretty v0.1.3 - github.com/in-toto/in-toto-golang v0.9.0 + github.com/in-toto/attestation v1.1.0 github.com/joho/godotenv v1.5.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-colorable v0.1.13 @@ -38,15 +38,15 @@ require ( github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc github.com/sigstore/protobuf-specs v0.3.2 - github.com/sigstore/sigstore-go v0.5.1 + github.com/sigstore/sigstore-go v0.6.1 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 github.com/zalando/go-keyring v0.2.5 - golang.org/x/crypto v0.25.0 - golang.org/x/sync v0.7.0 - golang.org/x/term v0.22.0 - golang.org/x/text v0.16.0 + golang.org/x/crypto v0.26.0 + golang.org/x/sync v0.8.0 + golang.org/x/term v0.23.0 + golang.org/x/text v0.17.0 google.golang.org/grpc v1.64.1 google.golang.org/protobuf v1.34.2 gopkg.in/h2non/gock.v1 v1.1.2 @@ -99,6 +99,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/in-toto/in-toto-golang v0.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/gojq v0.12.15 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect @@ -132,7 +133,7 @@ require ( github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect github.com/sigstore/rekor v1.3.6 // indirect - github.com/sigstore/sigstore v1.8.7 // indirect + github.com/sigstore/sigstore v1.8.9 // indirect github.com/sigstore/timestamp-authority v1.2.2 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -156,9 +157,9 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect - golang.org/x/mod v0.19.0 // indirect + golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.27.0 // indirect - golang.org/x/sys v0.22.0 // indirect + golang.org/x/sys v0.23.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 1ed19f12c..407739dfe 100644 --- a/go.sum +++ b/go.sum @@ -259,6 +259,8 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= +github.com/in-toto/attestation v1.1.0 h1:oRWzfmZPDSctChD0VaQV7MJrywKOzyNrtpENQFq//2Q= +github.com/in-toto/attestation v1.1.0/go.mod h1:DB59ytd3z7cIHgXxwpSX2SABrU6WJUKg/grpdgHVgVs= github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -391,10 +393,10 @@ github.com/sigstore/protobuf-specs v0.3.2 h1:nCVARCN+fHjlNCk3ThNXwrZRqIommIeNKWw github.com/sigstore/protobuf-specs v0.3.2/go.mod h1:RZ0uOdJR4OB3tLQeAyWoJFbNCBFrPQdcokntde4zRBA= github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8= github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc= -github.com/sigstore/sigstore v1.8.7 h1:L7/zKauHTg0d0Hukx7qlR4nifh6T6O6UIt9JBwAmTIg= -github.com/sigstore/sigstore v1.8.7/go.mod h1:MPiQ/NIV034Fc3Kk2IX9/XmBQdK60wfmpvgK9Z1UjRA= -github.com/sigstore/sigstore-go v0.5.1 h1:5IhKvtjlQBeLnjKkzMELNG4tIBf+xXQkDzhLV77+/8Y= -github.com/sigstore/sigstore-go v0.5.1/go.mod h1:TuOfV7THHqiDaUHuJ5+QN23RP/YoKmsbwJpY+aaYPN0= +github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk= +github.com/sigstore/sigstore v1.8.9/go.mod h1:d9ZAbNDs8JJfxJrYmulaTazU3Pwr8uLL9+mii4BNR3w= +github.com/sigstore/sigstore-go v0.6.1 h1:tGkkv1oDIER+QYU5MrjqlttQOVDWfSkmYwMqkJhB/cg= +github.com/sigstore/sigstore-go v0.6.1/go.mod h1:Xe5GHmUeACRFbomUWzVkf/xYCn8xVifb9DgqJrV2dIw= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3 h1:LTfPadUAo+PDRUbbdqbeSl2OuoFQwUFTnJ4stu+nwWw= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3/go.mod h1:QV/Lxlxm0POyhfyBtIbTWxNeF18clMlkkyL9mu45y18= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3 h1:xgbPRCr2npmmsuVVteJqi/ERw9+I13Wou7kq0Yk4D8g= @@ -483,24 +485,24 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -512,19 +514,19 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/pkg/cmd/attestation/verification/mock_verifier.go b/pkg/cmd/attestation/verification/mock_verifier.go index dcbfb1ba2..9943e6a97 100644 --- a/pkg/cmd/attestation/verification/mock_verifier.go +++ b/pkg/cmd/attestation/verification/mock_verifier.go @@ -8,7 +8,7 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/test/data" "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" - "github.com/in-toto/in-toto-golang/in_toto" + in_toto "github.com/in-toto/attestation/go/v1" "github.com/sigstore/sigstore-go/pkg/verify" ) diff --git a/pkg/cmd/attestation/verification/sigstore_integration_test.go b/pkg/cmd/attestation/verification/sigstore_integration_test.go index 48d1f4b49..e56e7d1ed 100644 --- a/pkg/cmd/attestation/verification/sigstore_integration_test.go +++ b/pkg/cmd/attestation/verification/sigstore_integration_test.go @@ -43,31 +43,13 @@ func TestLiveSigstoreVerifier(t *testing.T) { }) t.Run("with missing verification material", func(t *testing.T) { - attestations := getAttestationsFor(t, "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle-missing-verification-material.jsonl") - require.NotNil(t, attestations) - - verifier := NewLiveSigstoreVerifier(SigstoreConfig{ - Logger: io.NewTestHandler(), - }) - - res := verifier.Verify(attestations, publicGoodPolicy(t)) - require.Error(t, res.Error) - require.ErrorContains(t, res.Error, "failed to get bundle verification content") - require.Nil(t, res.VerifyResults) + _, err := GetLocalAttestations("../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle-missing-verification-material.jsonl") + require.ErrorContains(t, err, "missing verification material") }) t.Run("with missing verification certificate", func(t *testing.T) { - attestations := getAttestationsFor(t, "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle-missing-cert.jsonl") - require.NotNil(t, attestations) - - verifier := NewLiveSigstoreVerifier(SigstoreConfig{ - Logger: io.NewTestHandler(), - }) - - res := verifier.Verify(attestations, publicGoodPolicy(t)) - require.Error(t, res.Error) - require.ErrorContains(t, res.Error, "leaf cert not found") - require.Nil(t, res.VerifyResults) + _, err := GetLocalAttestations("../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle-missing-cert.jsonl") + require.ErrorContains(t, err, "missing bundle content") }) t.Run("with GitHub Sigstore artifact", func(t *testing.T) { From ea1a3da1eb9cf83667aa34a56fc8541369045f34 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Wed, 4 Sep 2024 16:45:02 -0400 Subject: [PATCH 031/301] Rename ProtobufBundle to Bundle Signed-off-by: Cody Soyland --- pkg/cmd/attestation/api/attestation.go | 2 +- pkg/cmd/attestation/artifact/oci/client.go | 2 +- pkg/cmd/attestation/test/data/data.go | 2 +- pkg/cmd/attestation/verification/attestation.go | 2 +- pkg/cmd/attestation/verification/attestation_test.go | 6 +++--- pkg/cmd/attestation/verification/sigstore.go | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/attestation/api/attestation.go b/pkg/cmd/attestation/api/attestation.go index 2b96a51fc..ea055b293 100644 --- a/pkg/cmd/attestation/api/attestation.go +++ b/pkg/cmd/attestation/api/attestation.go @@ -25,7 +25,7 @@ func newErrNoAttestations(name, digest string) ErrNoAttestations { } type Attestation struct { - Bundle *bundle.ProtobufBundle `json:"bundle"` + Bundle *bundle.Bundle `json:"bundle"` } type AttestationsResponse struct { diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go index 5428fff2f..bda114708 100644 --- a/pkg/cmd/attestation/artifact/oci/client.go +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -132,7 +132,7 @@ func (c LiveClient) GetAttestations(ref name.Reference, digest string) ([]*api.A return attestations, fmt.Errorf("error getting referrer image: %w", err) } - b := &bundle.ProtobufBundle{} + b := &bundle.Bundle{} err = b.UnmarshalJSON(bundleBytes) if err != nil { diff --git a/pkg/cmd/attestation/test/data/data.go b/pkg/cmd/attestation/test/data/data.go index 77f07e60c..b33efaa28 100644 --- a/pkg/cmd/attestation/test/data/data.go +++ b/pkg/cmd/attestation/test/data/data.go @@ -12,6 +12,6 @@ import ( var SigstoreBundleRaw []byte // SigstoreBundle returns a test *sigstore.Bundle -func SigstoreBundle(t *testing.T) *bundle.ProtobufBundle { +func SigstoreBundle(t *testing.T) *bundle.Bundle { return sgData.TestBundle(t, SigstoreBundleRaw) } diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 3a2d7456f..4b2545f62 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -86,7 +86,7 @@ func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) { decoder := json.NewDecoder(bytes.NewReader(fileContent)) for decoder.More() { - var bundle bundle.ProtobufBundle + var bundle bundle.Bundle bundle.Bundle = new(protobundle.Bundle) if err := decoder.Decode(&bundle); err != nil { return nil, fmt.Errorf("failed to unmarshal bundle from JSON: %v", err) diff --git a/pkg/cmd/attestation/verification/attestation_test.go b/pkg/cmd/attestation/verification/attestation_test.go index 66b337ad7..a3f444572 100644 --- a/pkg/cmd/attestation/verification/attestation_test.go +++ b/pkg/cmd/attestation/verification/attestation_test.go @@ -92,7 +92,7 @@ func TestGetLocalAttestations(t *testing.T) { func TestFilterAttestations(t *testing.T) { attestations := []*api.Attestation{ { - Bundle: &bundle.ProtobufBundle{ + Bundle: &bundle.Bundle{ Bundle: &protobundle.Bundle{ Content: &protobundle.Bundle_DsseEnvelope{ DsseEnvelope: &dsse.Envelope{ @@ -104,7 +104,7 @@ func TestFilterAttestations(t *testing.T) { }, }, { - Bundle: &bundle.ProtobufBundle{ + Bundle: &bundle.Bundle{ Bundle: &protobundle.Bundle{ Content: &protobundle.Bundle_DsseEnvelope{ DsseEnvelope: &dsse.Envelope{ @@ -116,7 +116,7 @@ func TestFilterAttestations(t *testing.T) { }, }, { - Bundle: &bundle.ProtobufBundle{ + Bundle: &bundle.Bundle{ Bundle: &protobundle.Bundle{ Content: &protobundle.Bundle_DsseEnvelope{ DsseEnvelope: &dsse.Envelope{ diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go index d86a709b5..9a4ac5194 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -55,7 +55,7 @@ func NewLiveSigstoreVerifier(config SigstoreConfig) *LiveSigstoreVerifier { } } -func (v *LiveSigstoreVerifier) chooseVerifier(b *bundle.ProtobufBundle) (*verify.SignedEntityVerifier, string, error) { +func (v *LiveSigstoreVerifier) chooseVerifier(b *bundle.Bundle) (*verify.SignedEntityVerifier, string, error) { if !b.MinVersion("0.2") { return nil, "", fmt.Errorf("unsupported bundle version: %s", b.MediaType) } From 7c405e8b6ece71d48be910fdee81aed281a8aee3 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 5 Sep 2024 08:16:34 -0600 Subject: [PATCH 032/301] dont print err content 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 d86a709b5..f4aad8c22 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -200,7 +200,7 @@ func (v *LiveSigstoreVerifier) Verify(attestations []*api.Attestation, policy ve )) return &SigstoreResults{ - Error: fmt.Errorf("verifying with issuer \"%s\": %v", issuer, err), + Error: fmt.Errorf("verifying with issuer \"%s\"", issuer), } } From 57b20291bd7274e2c4b099dbf2aff35634ab9312 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 5 Sep 2024 13:20:13 -0600 Subject: [PATCH 033/301] check for os.PathError Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/attestation.go | 12 ++++++++++-- pkg/cmd/attestation/verification/attestation_test.go | 8 ++++++++ pkg/cmd/attestation/verification/sigstore.go | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 0f52adec5..d1659feef 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -53,13 +53,21 @@ func GetLocalAttestations(path string) ([]*api.Attestation, error) { case ".json": attestations, err := loadBundleFromJSONFile(path) if err != nil { - return nil, fmt.Errorf("bundle could not be loaded from JSON file at %s", path) + var pathErr *os.PathError + if errors.As(err, &pathErr) { + return nil, fmt.Errorf("bundle could not be loaded from JSON file at %s", path) + } + return nil, fmt.Errorf("bundle content could not be parsed") } return attestations, nil case ".jsonl": attestations, err := loadBundlesFromJSONLinesFile(path) if err != nil { - return nil, fmt.Errorf("bundles could not be loaded from JSON lines file at %s", path) + var pathErr *os.PathError + if errors.As(err, &pathErr) { + return nil, fmt.Errorf("bundles could not be loaded from JSON lines file at %s", path) + } + return nil, fmt.Errorf("bundle content could not be parsed") } return attestations, nil } diff --git a/pkg/cmd/attestation/verification/attestation_test.go b/pkg/cmd/attestation/verification/attestation_test.go index 66b337ad7..2db62d56d 100644 --- a/pkg/cmd/attestation/verification/attestation_test.go +++ b/pkg/cmd/attestation/verification/attestation_test.go @@ -87,6 +87,14 @@ func TestGetLocalAttestations(t *testing.T) { require.ErrorIs(t, err, ErrUnrecognisedBundleExtension) require.Nil(t, attestations) }) + + t.Run("with non-existent bundle file", func(t *testing.T) { + path := "../test/data/not-found-bundle.json" + attestations, err := GetLocalAttestations(path) + + require.ErrorContains(t, err, "bundle could not be loaded from JSON file") + require.Nil(t, attestations) + }) } func TestFilterAttestations(t *testing.T) { diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go index f4aad8c22..822e7b621 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -186,7 +186,7 @@ func (v *LiveSigstoreVerifier) Verify(attestations []*api.Attestation, policy ve verifier, issuer, err := v.chooseVerifier(apr.Attestation.Bundle) if err != nil { return &SigstoreResults{ - Error: fmt.Errorf("failed to find recognized issuer from bundle content: %v", err), + Error: fmt.Errorf("failed to find recognized issuer from bundle content"), } } From 668706ccf513278a41be59df9b502a6839a89c6a Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Thu, 5 Sep 2024 13:29:22 -0600 Subject: [PATCH 034/301] print verify err 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 822e7b621..f4aad8c22 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -186,7 +186,7 @@ func (v *LiveSigstoreVerifier) Verify(attestations []*api.Attestation, policy ve verifier, issuer, err := v.chooseVerifier(apr.Attestation.Bundle) if err != nil { return &SigstoreResults{ - Error: fmt.Errorf("failed to find recognized issuer from bundle content"), + Error: fmt.Errorf("failed to find recognized issuer from bundle content: %v", err), } } From 043bdbedb9963368eadbf86769b12fc6d8c86b60 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 6 Sep 2024 08:36:04 -0400 Subject: [PATCH 035/301] Remove note explaining 2 year old GPG ID change Relates #9569 Having been 2 years since the GitHub CLI changed GPG keys used to sign our releases, it no longer seems relevant to keep these notes in our installation docs as they are confusing to the uninitiated. --- docs/install_linux.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/install_linux.md b/docs/install_linux.md index 15619fe96..e5b263fac 100644 --- a/docs/install_linux.md +++ b/docs/install_linux.md @@ -23,9 +23,6 @@ Install: && sudo apt install gh -y ``` -> **Note** -> We were recently forced to change our GPG signing key. If you've previously downloaded the `githubcli-archive-keyring.gpg` file, you should re-download it again per above instructions. If you are using a keyserver to download the key, the ID of the new key is `23F3D4EA75716059`. - Upgrade: ```bash @@ -65,9 +62,6 @@ sudo yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.re sudo yum install gh ``` -> **Note** -> We were recently forced to change our GPG signing key. If you've added the repository previously and now you're getting a GPG signing key error, disable the repository first with `sudo yum-config-manager --disable gh-cli` and add it again with `sudo yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo`. - Upgrade: ```bash From 78fa57dff7b92a6acbe3da1c7b9df74a56f43a0b Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 6 Sep 2024 08:41:59 -0400 Subject: [PATCH 036/301] Revert "Remove note explaining 2 year old GPG ID change" This reverts commit 043bdbedb9963368eadbf86769b12fc6d8c86b60. --- docs/install_linux.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/install_linux.md b/docs/install_linux.md index e5b263fac..15619fe96 100644 --- a/docs/install_linux.md +++ b/docs/install_linux.md @@ -23,6 +23,9 @@ Install: && sudo apt install gh -y ``` +> **Note** +> We were recently forced to change our GPG signing key. If you've previously downloaded the `githubcli-archive-keyring.gpg` file, you should re-download it again per above instructions. If you are using a keyserver to download the key, the ID of the new key is `23F3D4EA75716059`. + Upgrade: ```bash @@ -62,6 +65,9 @@ sudo yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.re sudo yum install gh ``` +> **Note** +> We were recently forced to change our GPG signing key. If you've added the repository previously and now you're getting a GPG signing key error, disable the repository first with `sudo yum-config-manager --disable gh-cli` and add it again with `sudo yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo`. + Upgrade: ```bash From 5a7cdff9db891f6fd55a75ed74fe5519bd114905 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 6 Sep 2024 08:45:24 -0400 Subject: [PATCH 037/301] Update linux install to point to GPG troubleshoot Relates #9569 Updates notes from older 2 year GPG ID change to redirect users in case of GPG errors to recent issue. --- docs/install_linux.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/install_linux.md b/docs/install_linux.md index 15619fe96..fabaa19aa 100644 --- a/docs/install_linux.md +++ b/docs/install_linux.md @@ -15,17 +15,14 @@ Install: ```bash (type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \ -&& sudo mkdir -p -m 755 /etc/apt/keyrings \ -&& wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ -&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ -&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ -&& sudo apt update \ -&& sudo apt install gh -y + && sudo mkdir -p -m 755 /etc/apt/keyrings \ + && wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ + && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && sudo apt update \ + && sudo apt install gh -y ``` -> **Note** -> We were recently forced to change our GPG signing key. If you've previously downloaded the `githubcli-archive-keyring.gpg` file, you should re-download it again per above instructions. If you are using a keyserver to download the key, the ID of the new key is `23F3D4EA75716059`. - Upgrade: ```bash @@ -33,6 +30,9 @@ sudo apt update sudo apt install gh ``` +> [!NOTE] +> If errors regarding GPG signatures occur, see [cli/cli#9569](https://github.com/cli/cli/issues/9569) for steps to fix this. + ### Fedora, CentOS, Red Hat Enterprise Linux (dnf) Install from our package repository for immediate access to latest releases: @@ -65,15 +65,15 @@ sudo yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.re sudo yum install gh ``` -> **Note** -> We were recently forced to change our GPG signing key. If you've added the repository previously and now you're getting a GPG signing key error, disable the repository first with `sudo yum-config-manager --disable gh-cli` and add it again with `sudo yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo`. - Upgrade: ```bash sudo yum update gh ``` +> [!NOTE] +> If errors regarding GPG signatures occur, see [cli/cli#9569](https://github.com/cli/cli/issues/9569) for steps to fix this. + ### openSUSE/SUSE Linux (zypper) Install: From a21e78bf0d3c5117846950d6b9a82e61c00f051c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 14:31:25 +0000 Subject: [PATCH 038/301] build(deps): bump actions/attest-build-provenance from 1.4.2 to 1.4.3 Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 1.4.2 to 1.4.3. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/6149ea5740be74af77f260b9db67e633f6b0a9a1...1c608d11d69870c2092266b3f9a6f3abbf17002c) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/deployment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index bc9442d9d..82966ced4 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -299,7 +299,7 @@ jobs: rpmsign --addsign dist/*.rpm - name: Attest release artifacts if: inputs.environment == 'production' - uses: actions/attest-build-provenance@6149ea5740be74af77f260b9db67e633f6b0a9a1 # v1.4.2 + uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 with: subject-path: "dist/gh_*" - name: Run createrepo From 500b619a5e7b388af89370c0f1fbba338aa5383f Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 6 Sep 2024 13:55:25 -0400 Subject: [PATCH 039/301] Move non-integration test to different test file Signed-off-by: Cody Soyland --- pkg/cmd/attestation/verification/attestation_test.go | 12 ++++++++++++ .../verification/sigstore_integration_test.go | 10 ---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/attestation/verification/attestation_test.go b/pkg/cmd/attestation/verification/attestation_test.go index a3f444572..0795f053c 100644 --- a/pkg/cmd/attestation/verification/attestation_test.go +++ b/pkg/cmd/attestation/verification/attestation_test.go @@ -87,6 +87,18 @@ func TestGetLocalAttestations(t *testing.T) { require.ErrorIs(t, err, ErrUnrecognisedBundleExtension) require.Nil(t, attestations) }) + + t.Run("with missing verification material", func(t *testing.T) { + path := "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle-missing-verification-material.jsonl" + _, err := GetLocalAttestations(path) + require.ErrorContains(t, err, "missing verification material") + }) + + t.Run("with missing verification certificate", func(t *testing.T) { + path := "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle-missing-cert.jsonl" + _, err := GetLocalAttestations(path) + require.ErrorContains(t, err, "missing bundle content") + }) } func TestFilterAttestations(t *testing.T) { diff --git a/pkg/cmd/attestation/verification/sigstore_integration_test.go b/pkg/cmd/attestation/verification/sigstore_integration_test.go index e56e7d1ed..97b44581e 100644 --- a/pkg/cmd/attestation/verification/sigstore_integration_test.go +++ b/pkg/cmd/attestation/verification/sigstore_integration_test.go @@ -42,16 +42,6 @@ func TestLiveSigstoreVerifier(t *testing.T) { require.NoError(t, res.Error) }) - t.Run("with missing verification material", func(t *testing.T) { - _, err := GetLocalAttestations("../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle-missing-verification-material.jsonl") - require.ErrorContains(t, err, "missing verification material") - }) - - t.Run("with missing verification certificate", func(t *testing.T) { - _, err := GetLocalAttestations("../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle-missing-cert.jsonl") - require.ErrorContains(t, err, "missing bundle content") - }) - t.Run("with GitHub Sigstore artifact", func(t *testing.T) { githubArtifactPath := test.NormalizeRelativePath("../test/data/github_provenance_demo-0.0.12-py3-none-any.whl") githubArtifact, err := artifact.NewDigestedArtifact(nil, githubArtifactPath, "sha256") From b14e430441f1d78d7fe9c44a6e97d8603d736b89 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 6 Sep 2024 15:22:43 -0400 Subject: [PATCH 040/301] Check for nil values to prevent nil dereference panic Signed-off-by: Cody Soyland --- pkg/cmd/attestation/verify/verify.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index f053240de..055636ad5 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -307,11 +307,19 @@ func buildTableVerifyContent(results []*verification.AttestationProcessingResult content := make([][]string, len(results)) for i, res := range results { + if res.VerificationResult == nil || + res.VerificationResult.Signature == nil || + res.VerificationResult.Signature.Certificate == nil { + return nil, fmt.Errorf("bundle missing verification result fields") + } builderSignerURI := res.VerificationResult.Signature.Certificate.Extensions.BuildSignerURI repoAndOrg, workflow, err := extractAttestationDetail(builderSignerURI) if err != nil { return nil, err } + if res.VerificationResult.Statement == nil { + return nil, fmt.Errorf("bundle missing attestation statement (bundle must originate from GitHub Artifact Attestations)") + } predicateType := res.VerificationResult.Statement.PredicateType content[i] = []string{repoAndOrg, predicateType, workflow} } From bbefc5b24feeac275596d006c8bffdcb77209f77 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Mon, 9 Sep 2024 08:53:11 -0600 Subject: [PATCH 041/301] handle os.PathError in GetLocalAttestations Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/attestation.go | 2 +- pkg/cmd/attestation/verification/attestation_test.go | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 6107ccf4e..d6e7d9e15 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -86,7 +86,7 @@ func loadBundleFromJSONFile(path string) ([]*api.Attestation, error) { func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) { fileContent, err := os.ReadFile(path) if err != nil { - return nil, fmt.Errorf("could not read file: %v", err) + return nil, err } attestations := []*api.Attestation{} diff --git a/pkg/cmd/attestation/verification/attestation_test.go b/pkg/cmd/attestation/verification/attestation_test.go index e321ffd87..68914afad 100644 --- a/pkg/cmd/attestation/verification/attestation_test.go +++ b/pkg/cmd/attestation/verification/attestation_test.go @@ -88,13 +88,21 @@ func TestGetLocalAttestations(t *testing.T) { require.Nil(t, attestations) }) - t.Run("with non-existent bundle file", func(t *testing.T) { + t.Run("with non-existent bundle file and JSON file", func(t *testing.T) { path := "../test/data/not-found-bundle.json" attestations, err := GetLocalAttestations(path) require.ErrorContains(t, err, "bundle could not be loaded from JSON file") require.Nil(t, attestations) }) + + t.Run("with non-existent bundle file and JSON lines file", func(t *testing.T) { + path := "../test/data/not-found-bundle.jsonl" + attestations, err := GetLocalAttestations(path) + + require.ErrorContains(t, err, "bundles could not be loaded from JSON lines file") + require.Nil(t, attestations) + }) } func TestFilterAttestations(t *testing.T) { From 70e935ba9535a34fef49a0287a2573d0c1d553c0 Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 10 Sep 2024 07:54:33 -0600 Subject: [PATCH 042/301] get latest sigstore-go commit Signed-off-by: Meredith Lancaster --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 50278d733..0f10c32c8 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc github.com/sigstore/protobuf-specs v0.3.2 - github.com/sigstore/sigstore-go v0.6.1 + github.com/sigstore/sigstore-go v0.6.2-0.20240909222714-8c0e75bb6206 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 407739dfe..adadcf6b5 100644 --- a/go.sum +++ b/go.sum @@ -397,6 +397,8 @@ github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFa github.com/sigstore/sigstore v1.8.9/go.mod h1:d9ZAbNDs8JJfxJrYmulaTazU3Pwr8uLL9+mii4BNR3w= github.com/sigstore/sigstore-go v0.6.1 h1:tGkkv1oDIER+QYU5MrjqlttQOVDWfSkmYwMqkJhB/cg= github.com/sigstore/sigstore-go v0.6.1/go.mod h1:Xe5GHmUeACRFbomUWzVkf/xYCn8xVifb9DgqJrV2dIw= +github.com/sigstore/sigstore-go v0.6.2-0.20240909222714-8c0e75bb6206 h1:V2F8VZ/oJJheQfmYA2hq+6uE7baNdHXSrCG1Ydmz+Ig= +github.com/sigstore/sigstore-go v0.6.2-0.20240909222714-8c0e75bb6206/go.mod h1:Xe5GHmUeACRFbomUWzVkf/xYCn8xVifb9DgqJrV2dIw= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3 h1:LTfPadUAo+PDRUbbdqbeSl2OuoFQwUFTnJ4stu+nwWw= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3/go.mod h1:QV/Lxlxm0POyhfyBtIbTWxNeF18clMlkkyL9mu45y18= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3 h1:xgbPRCr2npmmsuVVteJqi/ERw9+I13Wou7kq0Yk4D8g= From 83519e4e9258fb1732e66f2c001ebce7bb4106be Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 10 Sep 2024 07:54:45 -0600 Subject: [PATCH 043/301] check for sigstore-go validation errs Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/attestation.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index d6e7d9e15..14936e9f1 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -94,12 +94,15 @@ func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) { decoder := json.NewDecoder(bytes.NewReader(fileContent)) for decoder.More() { - var bundle bundle.Bundle - bundle.Bundle = new(protobundle.Bundle) - if err := decoder.Decode(&bundle); err != nil { - return nil, fmt.Errorf("failed to unmarshal bundle from JSON: %v", err) + var b bundle.Bundle + b.Bundle = new(protobundle.Bundle) + if err := decoder.Decode(&b); err != nil { + if errors.Is(err, bundle.ErrValidation) { + return nil, err + } + return nil, fmt.Errorf("failed to unmarshal bundle from JSON") } - a := api.Attestation{Bundle: &bundle} + a := api.Attestation{Bundle: &b} attestations = append(attestations, &a) } From 442111075c8bc04c2bdfbe52b983c124ca6ff1db Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 10 Sep 2024 08:04:51 -0600 Subject: [PATCH 044/301] go mod tidy Signed-off-by: Meredith Lancaster --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index adadcf6b5..fe198bec9 100644 --- a/go.sum +++ b/go.sum @@ -395,8 +395,6 @@ github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8= github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc= github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk= github.com/sigstore/sigstore v1.8.9/go.mod h1:d9ZAbNDs8JJfxJrYmulaTazU3Pwr8uLL9+mii4BNR3w= -github.com/sigstore/sigstore-go v0.6.1 h1:tGkkv1oDIER+QYU5MrjqlttQOVDWfSkmYwMqkJhB/cg= -github.com/sigstore/sigstore-go v0.6.1/go.mod h1:Xe5GHmUeACRFbomUWzVkf/xYCn8xVifb9DgqJrV2dIw= github.com/sigstore/sigstore-go v0.6.2-0.20240909222714-8c0e75bb6206 h1:V2F8VZ/oJJheQfmYA2hq+6uE7baNdHXSrCG1Ydmz+Ig= github.com/sigstore/sigstore-go v0.6.2-0.20240909222714-8c0e75bb6206/go.mod h1:Xe5GHmUeACRFbomUWzVkf/xYCn8xVifb9DgqJrV2dIw= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3 h1:LTfPadUAo+PDRUbbdqbeSl2OuoFQwUFTnJ4stu+nwWw= From 3814e82f9b858daa7bf35449a68060241487c0aa Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 10 Sep 2024 10:32:46 -0600 Subject: [PATCH 045/301] check err in GetLocalAttestations Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/attestation.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 14936e9f1..50542a6b3 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -56,6 +56,8 @@ func GetLocalAttestations(path string) ([]*api.Attestation, error) { var pathErr *os.PathError if errors.As(err, &pathErr) { return nil, fmt.Errorf("bundle could not be loaded from JSON file at %s", path) + } else if errors.Is(err, bundle.ErrValidation) { + return nil, err } return nil, fmt.Errorf("bundle content could not be parsed") } @@ -66,6 +68,8 @@ func GetLocalAttestations(path string) ([]*api.Attestation, error) { var pathErr *os.PathError if errors.As(err, &pathErr) { return nil, fmt.Errorf("bundles could not be loaded from JSON lines file at %s", path) + } else if errors.Is(err, bundle.ErrValidation) { + return nil, err } return nil, fmt.Errorf("bundle content could not be parsed") } @@ -97,10 +101,7 @@ func loadBundlesFromJSONLinesFile(path string) ([]*api.Attestation, error) { var b bundle.Bundle b.Bundle = new(protobundle.Bundle) if err := decoder.Decode(&b); err != nil { - if errors.Is(err, bundle.ErrValidation) { - return nil, err - } - return nil, fmt.Errorf("failed to unmarshal bundle from JSON") + return nil, err } a := api.Attestation{Bundle: &b} attestations = append(attestations, &a) From 50d335566d71763b7f841876dddef935aedee05a Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 10 Sep 2024 10:47:06 -0600 Subject: [PATCH 046/301] check specific err Signed-off-by: Meredith Lancaster --- pkg/cmd/attestation/verification/attestation_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/attestation/verification/attestation_test.go b/pkg/cmd/attestation/verification/attestation_test.go index b49a179a5..08f0ccfef 100644 --- a/pkg/cmd/attestation/verification/attestation_test.go +++ b/pkg/cmd/attestation/verification/attestation_test.go @@ -107,13 +107,13 @@ func TestGetLocalAttestations(t *testing.T) { t.Run("with missing verification material", func(t *testing.T) { path := "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle-missing-verification-material.jsonl" _, err := GetLocalAttestations(path) - require.ErrorContains(t, err, "missing verification material") + require.ErrorIs(t, err, bundle.ErrMissingVerificationMaterial) }) t.Run("with missing verification certificate", func(t *testing.T) { path := "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle-missing-cert.jsonl" _, err := GetLocalAttestations(path) - require.ErrorContains(t, err, "missing bundle content") + require.ErrorIs(t, err, bundle.ErrMissingBundleContent) }) } From e2c33e5cc1affb05e07cc698047beb54a14aaedd Mon Sep 17 00:00:00 2001 From: Meredith Lancaster Date: Tue, 10 Sep 2024 13:07:52 -0600 Subject: [PATCH 047/301] use sigstore-go v0.6.2 Signed-off-by: Meredith Lancaster --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 0f10c32c8..78ceb7f70 100644 --- a/go.mod +++ b/go.mod @@ -38,15 +38,15 @@ require ( github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc github.com/sigstore/protobuf-specs v0.3.2 - github.com/sigstore/sigstore-go v0.6.2-0.20240909222714-8c0e75bb6206 + github.com/sigstore/sigstore-go v0.6.2 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 github.com/zalando/go-keyring v0.2.5 - golang.org/x/crypto v0.26.0 + golang.org/x/crypto v0.27.0 golang.org/x/sync v0.8.0 - golang.org/x/term v0.23.0 - golang.org/x/text v0.17.0 + golang.org/x/term v0.24.0 + golang.org/x/text v0.18.0 google.golang.org/grpc v1.64.1 google.golang.org/protobuf v1.34.2 gopkg.in/h2non/gock.v1 v1.1.2 @@ -159,7 +159,7 @@ require ( golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.27.0 // indirect - golang.org/x/sys v0.23.0 // indirect + golang.org/x/sys v0.25.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index fe198bec9..37c40407d 100644 --- a/go.sum +++ b/go.sum @@ -395,8 +395,8 @@ github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8= github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc= github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk= github.com/sigstore/sigstore v1.8.9/go.mod h1:d9ZAbNDs8JJfxJrYmulaTazU3Pwr8uLL9+mii4BNR3w= -github.com/sigstore/sigstore-go v0.6.2-0.20240909222714-8c0e75bb6206 h1:V2F8VZ/oJJheQfmYA2hq+6uE7baNdHXSrCG1Ydmz+Ig= -github.com/sigstore/sigstore-go v0.6.2-0.20240909222714-8c0e75bb6206/go.mod h1:Xe5GHmUeACRFbomUWzVkf/xYCn8xVifb9DgqJrV2dIw= +github.com/sigstore/sigstore-go v0.6.2 h1:8uiywjt73vzfrGfWYVwVsiB1E1Qmwmpgr1kVpl4fs6A= +github.com/sigstore/sigstore-go v0.6.2/go.mod h1:pOIUH7Jx+ctwMICo+2zNrViOJJN5sGaQgwX4yAVJkA0= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3 h1:LTfPadUAo+PDRUbbdqbeSl2OuoFQwUFTnJ4stu+nwWw= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3/go.mod h1:QV/Lxlxm0POyhfyBtIbTWxNeF18clMlkkyL9mu45y18= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3 h1:xgbPRCr2npmmsuVVteJqi/ERw9+I13Wou7kq0Yk4D8g= @@ -485,8 +485,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -514,19 +514,19 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 1b59ec8ad07c07173df8da120b3ea82966a94ddc Mon Sep 17 00:00:00 2001 From: Fredrik Skogman Date: Thu, 29 Aug 2024 13:42:26 +0200 Subject: [PATCH 048/301] This commit introduces tenancy aware attestation policy building. This is done by inspecting the current hostname to determine if tenancy is enabled. The attestation commands also accepts a --hostname parameter, that is used to pick the current host, similar to how the GH_HOST variable can be used. Signed-off-by: Fredrik Skogman --- .gitignore | 3 + pkg/cmd/attestation/api/client.go | 24 ++++- pkg/cmd/attestation/api/client_test.go | 32 +++++++ .../attestation/api/mock_apiClient_test.go | 28 ++++++ pkg/cmd/attestation/api/mock_client.go | 5 ++ pkg/cmd/attestation/api/trust_domain.go | 15 ++++ pkg/cmd/attestation/auth/host.go | 5 +- pkg/cmd/attestation/auth/host_test.go | 5 +- pkg/cmd/attestation/download/download.go | 16 ++-- pkg/cmd/attestation/download/options.go | 1 + pkg/cmd/attestation/inspect/bundle.go | 30 ++++--- pkg/cmd/attestation/inspect/bundle_test.go | 14 ++- pkg/cmd/attestation/inspect/inspect.go | 33 ++++++- pkg/cmd/attestation/inspect/options.go | 2 + .../attestation/trustedroot/trustedroot.go | 88 ++++++++++++++----- .../attestation/verification/extensions.go | 54 ++++++++---- .../verification/extensions_test.go | 60 ++++++++++++- pkg/cmd/attestation/verification/sigstore.go | 20 ++++- pkg/cmd/attestation/verify/options.go | 15 +++- pkg/cmd/attestation/verify/policy.go | 50 +++-------- pkg/cmd/attestation/verify/policy_test.go | 47 ++++++---- pkg/cmd/attestation/verify/verify.go | 73 +++++++++++---- .../verify/verify_integration_test.go | 16 +++- pkg/cmd/attestation/verify/verify_test.go | 67 +++++++++++++- 24 files changed, 550 insertions(+), 153 deletions(-) create mode 100644 pkg/cmd/attestation/api/trust_domain.go diff --git a/.gitignore b/.gitignore index b2b66aaf7..272b7703d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,7 @@ # vim *.swp +# Emacs +*~ + vendor/ diff --git a/pkg/cmd/attestation/api/client.go b/pkg/cmd/attestation/api/client.go index b0cafa6e9..460ae3aad 100644 --- a/pkg/cmd/attestation/api/client.go +++ b/pkg/cmd/attestation/api/client.go @@ -8,7 +8,6 @@ import ( "github.com/cli/cli/v2/api" ioconfig "github.com/cli/cli/v2/pkg/cmd/attestation/io" - "github.com/cli/go-gh/v2/pkg/auth" ) const ( @@ -18,12 +17,14 @@ const ( ) type apiClient interface { + REST(hostname, method, p string, body io.Reader, data interface{}) error RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error) } type Client interface { GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) + GetTrustDomain() (string, error) } type LiveClient struct { @@ -32,9 +33,7 @@ type LiveClient struct { logger *ioconfig.Handler } -func NewLiveClient(hc *http.Client, l *ioconfig.Handler) *LiveClient { - host, _ := auth.DefaultHost() - +func NewLiveClient(hc *http.Client, host string, l *ioconfig.Handler) *LiveClient { return &LiveClient{ api: api.NewClientFromHTTP(hc), host: strings.TrimSuffix(host, "/"), @@ -64,6 +63,12 @@ func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*At return c.getAttestations(url, owner, digest, limit) } +// GetTrustDomain returns the current trust domain. If the default is used +// the empty string is returned +func (c *LiveClient) GetTrustDomain() (string, error) { + return c.getTrustDomain(MetaPath) +} + func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*Attestation, error) { c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest) @@ -102,3 +107,14 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At return attestations, nil } + +func (c *LiveClient) getTrustDomain(url string) (string, error) { + var resp MetaResponse + + err := c.api.REST(c.host, http.MethodGet, url, nil, &resp) + if err != nil { + return "", err + } + + return resp.Domains.ArtifactAttestations.TrustDomain, nil +} diff --git a/pkg/cmd/attestation/api/client_test.go b/pkg/cmd/attestation/api/client_test.go index 693e3266e..bfcb40f5a 100644 --- a/pkg/cmd/attestation/api/client_test.go +++ b/pkg/cmd/attestation/api/client_test.go @@ -172,3 +172,35 @@ func TestGetByDigest_Error(t *testing.T) { require.Error(t, err) require.Nil(t, attestations) } + +func TestGetTrustDomain(t *testing.T) { + fetcher := mockMetaGenerator{ + TrustDomain: "foo", + } + + t.Run("with returned trust domain", func(t *testing.T) { + c := LiveClient{ + api: mockAPIClient{ + OnREST: fetcher.OnREST, + }, + logger: io.NewTestHandler(), + } + td, err := c.GetTrustDomain() + require.Nil(t, err) + require.Equal(t, "foo", td) + + }) + + t.Run("with error", func(t *testing.T) { + c := LiveClient{ + api: mockAPIClient{ + OnREST: fetcher.OnRESTError, + }, + logger: io.NewTestHandler(), + } + td, err := c.GetTrustDomain() + require.Equal(t, "", td) + require.ErrorContains(t, err, "test error") + }) + +} diff --git a/pkg/cmd/attestation/api/mock_apiClient_test.go b/pkg/cmd/attestation/api/mock_apiClient_test.go index 1d4f61cd9..d58cdbc79 100644 --- a/pkg/cmd/attestation/api/mock_apiClient_test.go +++ b/pkg/cmd/attestation/api/mock_apiClient_test.go @@ -10,12 +10,17 @@ import ( type mockAPIClient struct { OnRESTWithNext func(hostname, method, p string, body io.Reader, data interface{}) (string, error) + OnREST func(hostname, method, p string, body io.Reader, data interface{}) error } func (m mockAPIClient) RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error) { return m.OnRESTWithNext(hostname, method, p, body, data) } +func (m mockAPIClient) REST(hostname, method, p string, body io.Reader, data interface{}) error { + return m.OnREST(hostname, method, p, body, data) +} + type mockDataGenerator struct { NumAttestations int } @@ -87,3 +92,26 @@ func (m mockDataGenerator) OnRESTWithNextNoAttestations(hostname, method, p stri func (m mockDataGenerator) OnRESTWithNextError(hostname, method, p string, body io.Reader, data interface{}) (string, error) { return "", errors.New("failed to get attestations") } + +type mockMetaGenerator struct { + TrustDomain string +} + +func (m mockMetaGenerator) OnREST(hostname, method, p string, body io.Reader, data interface{}) error { + var template = ` +{ + "domains": { + "artifact_attestations": { + "trust_domain": "%s" + } + } +} +` + var jsonString = fmt.Sprintf(template, m.TrustDomain) + return json.Unmarshal([]byte(jsonString), &data) + +} + +func (m mockMetaGenerator) OnRESTError(hostname, method, p string, body io.Reader, data interface{}) error { + return errors.New("test error") +} diff --git a/pkg/cmd/attestation/api/mock_client.go b/pkg/cmd/attestation/api/mock_client.go index edb51ee6e..bcb51c414 100644 --- a/pkg/cmd/attestation/api/mock_client.go +++ b/pkg/cmd/attestation/api/mock_client.go @@ -9,6 +9,7 @@ import ( type MockClient struct { OnGetByRepoAndDigest func(repo, digest string, limit int) ([]*Attestation, error) OnGetByOwnerAndDigest func(owner, digest string, limit int) ([]*Attestation, error) + OnGetTrustDomain func() (string, error) } func (m MockClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) { @@ -19,6 +20,10 @@ func (m MockClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Att return m.OnGetByOwnerAndDigest(owner, digest, limit) } +func (m MockClient) GetTrustDomain() (string, error) { + return m.OnGetTrustDomain() +} + func makeTestAttestation() Attestation { return Attestation{Bundle: data.SigstoreBundle(nil)} } diff --git a/pkg/cmd/attestation/api/trust_domain.go b/pkg/cmd/attestation/api/trust_domain.go new file mode 100644 index 000000000..5cf9309ae --- /dev/null +++ b/pkg/cmd/attestation/api/trust_domain.go @@ -0,0 +1,15 @@ +package api + +const MetaPath = "meta" + +type ArtifactAttestations struct { + TrustDomain string `json:"trust_domain"` +} + +type Domain struct { + ArtifactAttestations ArtifactAttestations `json:"artifact_attestations"` +} + +type MetaResponse struct { + Domains Domain `json:"domains"` +} diff --git a/pkg/cmd/attestation/auth/host.go b/pkg/cmd/attestation/auth/host.go index 1b5a344ea..a40df8335 100644 --- a/pkg/cmd/attestation/auth/host.go +++ b/pkg/cmd/attestation/auth/host.go @@ -4,14 +4,11 @@ import ( "errors" "github.com/cli/cli/v2/internal/ghinstance" - "github.com/cli/go-gh/v2/pkg/auth" ) var ErrUnsupportedHost = errors.New("An unsupported host was detected. Note that gh attestation does not currently support GHES") -func IsHostSupported() error { - host, _ := auth.DefaultHost() - +func IsHostSupported(host string) error { // Note that this check is slightly redundant as Tenancy should not be considered Enterprise // but the ghinstance package has not been updated to reflect this yet. if ghinstance.IsEnterprise(host) && !ghinstance.IsTenancy(host) { diff --git a/pkg/cmd/attestation/auth/host_test.go b/pkg/cmd/attestation/auth/host_test.go index 1d84888c4..5d905bd04 100644 --- a/pkg/cmd/attestation/auth/host_test.go +++ b/pkg/cmd/attestation/auth/host_test.go @@ -3,6 +3,8 @@ package auth import ( "testing" + ghauth "github.com/cli/go-gh/v2/pkg/auth" + "github.com/stretchr/testify/require" ) @@ -38,7 +40,8 @@ func TestIsHostSupported(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Setenv("GH_HOST", tc.host) - err := IsHostSupported() + host, _ := ghauth.DefaultHost() + err := IsHostSupported(host) if tc.expectedErr { require.ErrorIs(t, err, ErrUnsupportedHost) } else { diff --git a/pkg/cmd/attestation/download/download.go b/pkg/cmd/attestation/download/download.go index 3af3b6200..770fa442b 100644 --- a/pkg/cmd/attestation/download/download.go +++ b/pkg/cmd/attestation/download/download.go @@ -11,6 +11,7 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/io" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" "github.com/cli/cli/v2/pkg/cmdutil" + ghauth "github.com/cli/go-gh/v2/pkg/auth" "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" @@ -78,16 +79,18 @@ func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Comman if err != nil { return err } - opts.APIClient = api.NewLiveClient(hc, opts.Logger) - opts.OCIClient = oci.NewLiveClient() - - opts.Store = NewLiveStore("") - - if err := auth.IsHostSupported(); err != nil { + if opts.Hostname == "" { + opts.Hostname, _ = ghauth.DefaultHost() + } + if err := auth.IsHostSupported(opts.Hostname); err != nil { return err } + opts.APIClient = api.NewLiveClient(hc, opts.Hostname, opts.Logger) + opts.OCIClient = oci.NewLiveClient() + opts.Store = NewLiveStore("") + if runF != nil { return runF(opts) } @@ -106,6 +109,7 @@ func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Comman downloadCmd.Flags().StringVarP(&opts.PredicateType, "predicate-type", "", "", "Filter attestations by provided predicate type") cmdutil.StringEnumFlag(downloadCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact") downloadCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch") + downloadCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use") return downloadCmd } diff --git a/pkg/cmd/attestation/download/options.go b/pkg/cmd/attestation/download/options.go index cc1c4d3b5..91f724853 100644 --- a/pkg/cmd/attestation/download/options.go +++ b/pkg/cmd/attestation/download/options.go @@ -24,6 +24,7 @@ type Options struct { Owner string PredicateType string Repo string + Hostname string } func (opts *Options) AreFlagsValid() error { diff --git a/pkg/cmd/attestation/inspect/bundle.go b/pkg/cmd/attestation/inspect/bundle.go index 283b2c14e..d6dc5a4bb 100644 --- a/pkg/cmd/attestation/inspect/bundle.go +++ b/pkg/cmd/attestation/inspect/bundle.go @@ -55,17 +55,27 @@ type AttestationDetail struct { WorkflowID string `json:"workflowId"` } -func getOrgAndRepo(repoURL string) (string, string, error) { - after, found := strings.CutPrefix(repoURL, "https://github.com/") - if !found { - return "", "", fmt.Errorf("failed to get org and repo from %s", repoURL) +func getOrgAndRepo(tenant, repoURL string) (string, string, error) { + var after string + var found bool + if tenant == "" { + after, found = strings.CutPrefix(repoURL, "https://github.com/") + if !found { + return "", "", fmt.Errorf("failed to get org and repo from %s", repoURL) + } + } else { + after, found = strings.CutPrefix(repoURL, + fmt.Sprintf("https://%s.ghe.com/", tenant)) + if !found { + return "", "", fmt.Errorf("failed to get org and repo from %s", repoURL) + } } parts := strings.Split(after, "/") return parts[0], parts[1], nil } -func getAttestationDetail(attr api.Attestation) (AttestationDetail, error) { +func getAttestationDetail(tenant string, attr api.Attestation) (AttestationDetail, error) { envelope, err := attr.Bundle.Envelope() if err != nil { return AttestationDetail{}, fmt.Errorf("failed to get envelope from bundle: %v", err) @@ -87,7 +97,7 @@ func getAttestationDetail(attr api.Attestation) (AttestationDetail, error) { return AttestationDetail{}, fmt.Errorf("failed to unmarshal predicate: %v", err) } - org, repo, err := getOrgAndRepo(predicate.BuildDefinition.ExternalParameters.Workflow.Repository) + org, repo, err := getOrgAndRepo(tenant, predicate.BuildDefinition.ExternalParameters.Workflow.Repository) if err != nil { return AttestationDetail{}, fmt.Errorf("failed to parse attestation content: %v", err) } @@ -101,11 +111,11 @@ func getAttestationDetail(attr api.Attestation) (AttestationDetail, error) { }, nil } -func getDetailsAsSlice(results []*verification.AttestationProcessingResult) ([][]string, error) { +func getDetailsAsSlice(tenant string, results []*verification.AttestationProcessingResult) ([][]string, error) { details := make([][]string, len(results)) for i, result := range results { - detail, err := getAttestationDetail(*result.Attestation) + detail, err := getAttestationDetail(tenant, *result.Attestation) if err != nil { return nil, fmt.Errorf("failed to get attestation detail: %v", err) } @@ -114,11 +124,11 @@ func getDetailsAsSlice(results []*verification.AttestationProcessingResult) ([][ return details, nil } -func getAttestationDetails(results []*verification.AttestationProcessingResult) ([]AttestationDetail, error) { +func getAttestationDetails(tenant string, results []*verification.AttestationProcessingResult) ([]AttestationDetail, error) { details := make([]AttestationDetail, len(results)) for i, result := range results { - detail, err := getAttestationDetail(*result.Attestation) + detail, err := getAttestationDetail(tenant, *result.Attestation) if err != nil { return nil, fmt.Errorf("failed to get attestation detail: %v", err) } diff --git a/pkg/cmd/attestation/inspect/bundle_test.go b/pkg/cmd/attestation/inspect/bundle_test.go index d7e9babd8..61b8d7bfc 100644 --- a/pkg/cmd/attestation/inspect/bundle_test.go +++ b/pkg/cmd/attestation/inspect/bundle_test.go @@ -12,7 +12,7 @@ import ( func TestGetOrgAndRepo(t *testing.T) { t.Run("with valid source URL", func(t *testing.T) { sourceURL := "https://github.com/github/gh-attestation" - org, repo, err := getOrgAndRepo(sourceURL) + org, repo, err := getOrgAndRepo("", sourceURL) require.Nil(t, err) require.Equal(t, "github", org) require.Equal(t, "gh-attestation", repo) @@ -20,11 +20,19 @@ func TestGetOrgAndRepo(t *testing.T) { t.Run("with invalid source URL", func(t *testing.T) { sourceURL := "hub.com/github/gh-attestation" - org, repo, err := getOrgAndRepo(sourceURL) + org, repo, err := getOrgAndRepo("", sourceURL) require.Error(t, err) require.Zero(t, org) require.Zero(t, repo) }) + + t.Run("with valid source tenant URL", func(t *testing.T) { + sourceURL := "https://foo.ghe.com/github/gh-attestation" + org, repo, err := getOrgAndRepo("foo", sourceURL) + require.Nil(t, err) + require.Equal(t, "github", org) + require.Equal(t, "gh-attestation", repo) + }) } func TestGetAttestationDetail(t *testing.T) { @@ -35,7 +43,7 @@ func TestGetAttestationDetail(t *testing.T) { require.NoError(t, err) attestation := attestations[0] - detail, err := getAttestationDetail(*attestation) + detail, err := getAttestationDetail("", *attestation) require.NoError(t, err) require.Equal(t, "sigstore", detail.OrgName) diff --git a/pkg/cmd/attestation/inspect/inspect.go b/pkg/cmd/attestation/inspect/inspect.go index 2731ee7a4..867ff8c1b 100644 --- a/pkg/cmd/attestation/inspect/inspect.go +++ b/pkg/cmd/attestation/inspect/inspect.go @@ -3,13 +3,16 @@ package inspect import ( "fmt" + "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/internal/tableprinter" + "github.com/cli/cli/v2/pkg/cmd/attestation/api" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" "github.com/cli/cli/v2/pkg/cmd/attestation/auth" "github.com/cli/cli/v2/pkg/cmd/attestation/io" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" "github.com/cli/cli/v2/pkg/cmdutil" + ghauth "github.com/cli/go-gh/v2/pkg/auth" "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" @@ -66,8 +69,11 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command }, RunE: func(cmd *cobra.Command, args []string) error { opts.OCIClient = oci.NewLiveClient() + if opts.Hostname == "" { + opts.Hostname, _ = ghauth.DefaultHost() + } - if err := auth.IsHostSupported(); err != nil { + if err := auth.IsHostSupported(opts.Hostname); err != nil { return err } @@ -78,6 +84,26 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command config := verification.SigstoreConfig{ Logger: opts.Logger, } + // Prepare for tenancy if detected + if ghinstance.IsTenancy(opts.Hostname) { + hc, err := f.HttpClient() + if err != nil { + return err + } + apiClient := api.NewLiveClient(hc, opts.Hostname, opts.Logger) + td, err := apiClient.GetTrustDomain() + if err != nil { + return err + } + tenant, found := ghinstance.TenantName(opts.Hostname) + if !found { + return fmt.Errorf("Invalid hostname provided: '%s'", + opts.Hostname) + } + + config.TrustDomain = td + opts.Tenant = tenant + } opts.SigstoreVerifier = verification.NewLiveSigstoreVerifier(config) @@ -90,6 +116,7 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command inspectCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles") inspectCmd.MarkFlagRequired("bundle") //nolint:errcheck + inspectCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use") cmdutil.StringEnumFlag(inspectCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact") cmdutil.AddFormatFlags(inspectCmd, &opts.exporter) @@ -125,7 +152,7 @@ func runInspect(opts *Options) error { // If the user provides the --format=json flag, print the results in JSON format if opts.exporter != nil { - details, err := getAttestationDetails(res.VerifyResults) + details, err := getAttestationDetails(opts.Tenant, res.VerifyResults) if err != nil { return fmt.Errorf("failed to get attestation detail: %v", err) } @@ -138,7 +165,7 @@ func runInspect(opts *Options) error { } // otherwise, print results in a table - details, err := getDetailsAsSlice(res.VerifyResults) + details, err := getDetailsAsSlice(opts.Tenant, res.VerifyResults) if err != nil { return fmt.Errorf("failed to parse attestation details: %v", err) } diff --git a/pkg/cmd/attestation/inspect/options.go b/pkg/cmd/attestation/inspect/options.go index b9c8819c4..1a5a1b937 100644 --- a/pkg/cmd/attestation/inspect/options.go +++ b/pkg/cmd/attestation/inspect/options.go @@ -18,6 +18,8 @@ type Options struct { OCIClient oci.Client SigstoreVerifier verification.SigstoreVerifier exporter cmdutil.Exporter + Hostname string + Tenant string } // Clean cleans the file path option values diff --git a/pkg/cmd/attestation/trustedroot/trustedroot.go b/pkg/cmd/attestation/trustedroot/trustedroot.go index e28ceab62..c9c3fdb04 100644 --- a/pkg/cmd/attestation/trustedroot/trustedroot.go +++ b/pkg/cmd/attestation/trustedroot/trustedroot.go @@ -6,9 +6,13 @@ import ( "fmt" "os" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/pkg/cmd/attestation/api" "github.com/cli/cli/v2/pkg/cmd/attestation/auth" + "github.com/cli/cli/v2/pkg/cmd/attestation/io" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" "github.com/cli/cli/v2/pkg/cmdutil" + ghauth "github.com/cli/go-gh/v2/pkg/auth" "github.com/MakeNowJust/heredoc" "github.com/sigstore/sigstore-go/pkg/tuf" @@ -19,6 +23,8 @@ type Options struct { TufUrl string TufRootPath string VerifyOnly bool + Hostname string + TrustDomain string } type tufClientInstantiator func(o *tuf.Options) (*tuf.Client, error) @@ -54,10 +60,28 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com gh attestation trusted-root `), RunE: func(cmd *cobra.Command, args []string) error { - if err := auth.IsHostSupported(); err != nil { + if opts.Hostname == "" { + opts.Hostname, _ = ghauth.DefaultHost() + } + + if err := auth.IsHostSupported(opts.Hostname); err != nil { return err } + if ghinstance.IsTenancy(opts.Hostname) { + hc, err := f.HttpClient() + if err != nil { + return err + } + logger := io.NewHandler(f.IOStreams) + apiClient := api.NewLiveClient(hc, opts.Hostname, logger) + td, err := apiClient.GetTrustDomain() + if err != nil { + return err + } + opts.TrustDomain = td + } + if runF != nil { return runF(opts) } @@ -74,12 +98,19 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com trustedRootCmd.Flags().StringVarP(&opts.TufRootPath, "tuf-root", "", "", "Path to the TUF root.json file on disk") trustedRootCmd.MarkFlagsRequiredTogether("tuf-url", "tuf-root") trustedRootCmd.Flags().BoolVarP(&opts.VerifyOnly, "verify-only", "", false, "Don't output trusted_root.jsonl contents") + trustedRootCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use") return &trustedRootCmd } +type tufConfig struct { + tufOptions *tuf.Options + targets []string +} + func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error { - var tufOptions []*tuf.Options + var tufOptions []tufConfig + var defaultTR = "trusted_root.json" tufOpt := verification.DefaultOptionsWithCacheSetting() // Disable local caching, so we get up-to-date response from TUF repository @@ -93,37 +124,54 @@ func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error { tufOpt.Root = tufRoot tufOpt.RepositoryBaseURL = opts.TufUrl - tufOptions = append(tufOptions, tufOpt) + tufOptions = append(tufOptions, tufConfig{ + tufOptions: tufOpt, + targets: []string{defaultTR}, + }) } else { // Get from both Sigstore public good and GitHub private instance - tufOptions = append(tufOptions, tufOpt) + tufOptions = append(tufOptions, tufConfig{ + tufOptions: tufOpt, + targets: []string{defaultTR}, + }) tufOpt = verification.GitHubTUFOptions() tufOpt.CacheValidity = 0 - tufOptions = append(tufOptions, tufOpt) + targets := []string{defaultTR} + if opts.TrustDomain != "" { + targets = append(targets, fmt.Sprintf("%s.%s", + opts.TrustDomain, defaultTR)) + } + tufOptions = append(tufOptions, tufConfig{ + tufOptions: tufOpt, + targets: targets, + }) } - for _, tufOpt = range tufOptions { - tufClient, err := makeTUF(tufOpt) + for _, tufOpt := range tufOptions { + tufClient, err := makeTUF(tufOpt.tufOptions) if err != nil { return fmt.Errorf("failed to create TUF client: %v", err) } - t, err := tufClient.GetTarget("trusted_root.json") - if err != nil { - return err - } + for _, target := range tufOpt.targets { + t, err := tufClient.GetTarget(target) + if err != nil { + return fmt.Errorf("failed to retrieve trusted root %s via TUF: %w", + target, err) + } - output := new(bytes.Buffer) - err = json.Compact(output, t) - if err != nil { - return err - } + output := new(bytes.Buffer) + err = json.Compact(output, t) + if err != nil { + return err + } - if !opts.VerifyOnly { - fmt.Println(output) - } else { - fmt.Printf("Local TUF repository for %s updated and verified\n", tufOpt.RepositoryBaseURL) + if !opts.VerifyOnly { + fmt.Println(output) + } else { + fmt.Printf("Local TUF repository for %s updated and verified\n", tufOpt.tufOptions.RepositoryBaseURL) + } } } diff --git a/pkg/cmd/attestation/verification/extensions.go b/pkg/cmd/attestation/verification/extensions.go index 1915152dc..727feb72c 100644 --- a/pkg/cmd/attestation/verification/extensions.go +++ b/pkg/cmd/attestation/verification/extensions.go @@ -1,28 +1,50 @@ package verification import ( + "errors" "fmt" "strings" ) -func VerifyCertExtensions(results []*AttestationProcessingResult, owner string, repo string) error { - for _, attestation := range results { - // TODO: handle proxima prefix - expectedSourceRepositoryOwnerURI := fmt.Sprintf("https://github.com/%s", owner) - sourceRepositoryOwnerURI := attestation.VerificationResult.Signature.Certificate.Extensions.SourceRepositoryOwnerURI - if !strings.EqualFold(expectedSourceRepositoryOwnerURI, sourceRepositoryOwnerURI) { - return fmt.Errorf("expected SourceRepositoryOwnerURI to be %s, got %s", expectedSourceRepositoryOwnerURI, sourceRepositoryOwnerURI) - } +func VerifyCertExtensions(results []*AttestationProcessingResult, tenant, owner, repo string) error { + if len(results) == 0 { + return errors.New("no attestations proccessing results") + } - // if repo is set, check the SourceRepositoryURI field - if repo != "" { - // TODO: handle proxima prefix - expectedSourceRepositoryURI := fmt.Sprintf("https://github.com/%s", repo) - sourceRepositoryURI := attestation.VerificationResult.Signature.Certificate.Extensions.SourceRepositoryURI - if !strings.EqualFold(expectedSourceRepositoryURI, sourceRepositoryURI) { - return fmt.Errorf("expected SourceRepositoryURI to be %s, got %s", expectedSourceRepositoryURI, sourceRepositoryURI) - } + for _, attestation := range results { + if err := verifyCertExtensions(attestation, tenant, owner, repo); err != nil { + return err } } return nil } + +func verifyCertExtensions(attestation *AttestationProcessingResult, tenant, owner, repo string) error { + var want string + + if tenant == "" { + want = fmt.Sprintf("https://github.com/%s", owner) + } else { + want = fmt.Sprintf("https://%s.ghe.com/%s", tenant, owner) + } + sourceRepositoryOwnerURI := attestation.VerificationResult.Signature.Certificate.Extensions.SourceRepositoryOwnerURI + if !strings.EqualFold(want, sourceRepositoryOwnerURI) { + return fmt.Errorf("expected SourceRepositoryOwnerURI to be %s, got %s", want, sourceRepositoryOwnerURI) + } + + // if repo is set, check the SourceRepositoryURI field + if repo != "" { + if tenant == "" { + want = fmt.Sprintf("https://github.com/%s", repo) + } else { + want = fmt.Sprintf("https://%s.ghe.com/%s", tenant, repo) + } + + sourceRepositoryURI := attestation.VerificationResult.Signature.Certificate.Extensions.SourceRepositoryURI + if !strings.EqualFold(want, sourceRepositoryURI) { + return fmt.Errorf("expected SourceRepositoryURI to be %s, got %s", want, sourceRepositoryURI) + } + } + + return nil +} diff --git a/pkg/cmd/attestation/verification/extensions_test.go b/pkg/cmd/attestation/verification/extensions_test.go index 7aec4ec45..c86f2030a 100644 --- a/pkg/cmd/attestation/verification/extensions_test.go +++ b/pkg/cmd/attestation/verification/extensions_test.go @@ -25,22 +25,74 @@ func TestVerifyCertExtensions(t *testing.T) { } t.Run("VerifyCertExtensions with owner and repo", func(t *testing.T) { - err := VerifyCertExtensions(results, "owner", "owner/repo") + err := VerifyCertExtensions(results, "", "owner", "owner/repo") require.NoError(t, err) }) + t.Run("VerifyCertExtensions with owner and repo, but wrong tenant", func(t *testing.T) { + err := VerifyCertExtensions(results, "foo", "owner", "owner/repo") + require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://foo.ghe.com/owner, got https://github.com/owner") + }) + t.Run("VerifyCertExtensions with owner", func(t *testing.T) { - err := VerifyCertExtensions(results, "owner", "") + err := VerifyCertExtensions(results, "", "owner", "") require.NoError(t, err) }) t.Run("VerifyCertExtensions with wrong owner", func(t *testing.T) { - err := VerifyCertExtensions(results, "wrong", "") + err := VerifyCertExtensions(results, "", "wrong", "") require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://github.com/wrong, got https://github.com/owner") }) t.Run("VerifyCertExtensions with wrong repo", func(t *testing.T) { - err := VerifyCertExtensions(results, "owner", "wrong") + err := VerifyCertExtensions(results, "", "owner", "wrong") require.ErrorContains(t, err, "expected SourceRepositoryURI to be https://github.com/wrong, got https://github.com/owner/repo") }) } + +func TestVerifyTenancyCertExtensions(t *testing.T) { + results := []*AttestationProcessingResult{ + { + VerificationResult: &verify.VerificationResult{ + Signature: &verify.SignatureVerificationResult{ + Certificate: &certificate.Summary{ + Extensions: certificate.Extensions{ + SourceRepositoryOwnerURI: "https://foo.ghe.com/owner", + SourceRepositoryURI: "https://foo.ghe.com/owner/repo", + }, + }, + }, + }, + }, + } + + t.Run("VerifyCertExtensions with owner and repo", func(t *testing.T) { + err := VerifyCertExtensions(results, "foo", "owner", "owner/repo") + require.NoError(t, err) + }) + + t.Run("VerifyCertExtensions with owner and repo, no tenant", func(t *testing.T) { + err := VerifyCertExtensions(results, "", "owner", "owner/repo") + require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://github.com/owner, got https://foo.ghe.com/owner") + }) + + t.Run("VerifyCertExtensions with owner and repo, wrong tenant", func(t *testing.T) { + err := VerifyCertExtensions(results, "bar", "owner", "owner/repo") + require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://bar.ghe.com/owner, got https://foo.ghe.com/owner") + }) + + t.Run("VerifyCertExtensions with owner", func(t *testing.T) { + err := VerifyCertExtensions(results, "foo", "owner", "") + require.NoError(t, err) + }) + + t.Run("VerifyCertExtensions with wrong owner", func(t *testing.T) { + err := VerifyCertExtensions(results, "foo", "wrong", "") + require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://foo.ghe.com/wrong, got https://foo.ghe.com/owner") + }) + + t.Run("VerifyCertExtensions with wrong repo", func(t *testing.T) { + err := VerifyCertExtensions(results, "foo", "owner", "wrong") + require.ErrorContains(t, err, "expected SourceRepositoryURI to be https://foo.ghe.com/wrong, got https://foo.ghe.com/owner/repo") + }) +} diff --git a/pkg/cmd/attestation/verification/sigstore.go b/pkg/cmd/attestation/verification/sigstore.go index 9a4ac5194..927eacf49 100644 --- a/pkg/cmd/attestation/verification/sigstore.go +++ b/pkg/cmd/attestation/verification/sigstore.go @@ -36,6 +36,8 @@ type SigstoreConfig struct { TrustedRoot string Logger *io.Handler NoPublicGood bool + // If tenancy mode is not used, trust domain is empty + TrustDomain string } type SigstoreVerifier interface { @@ -144,7 +146,7 @@ func (v *LiveSigstoreVerifier) chooseVerifier(b *bundle.Bundle) (*verify.SignedE return publicGoodVerifier, issuer, nil } else if leafCert.Issuer.Organization[0] == GitHubIssuerOrg || v.config.NoPublicGood { - ghVerifier, err := newGitHubVerifier() + ghVerifier, err := newGitHubVerifier(v.config.TrustDomain) if err != nil { return nil, "", fmt.Errorf("failed to create GitHub Sigstore verifier: %v", err) } @@ -240,13 +242,25 @@ func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.SignedEntityVerif return gv, nil } -func newGitHubVerifier() (*verify.SignedEntityVerifier, error) { +func newGitHubVerifier(trustDomain string) (*verify.SignedEntityVerifier, error) { + var tr string + opts := GitHubTUFOptions() client, err := tuf.New(opts) if err != nil { return nil, fmt.Errorf("failed to create TUF client: %v", err) } - trustedRoot, err := root.GetTrustedRoot(client) + + if trustDomain == "" { + tr = "trusted_root.json" + } else { + tr = fmt.Sprintf("%s.trusted_root.json", trustDomain) + } + jsonBytes, err := client.GetTarget(tr) + if err != nil { + return nil, err + } + trustedRoot, err := root.NewTrustedRootFromJSON(jsonBytes) if err != nil { return nil, err } diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index 3ec2d49f1..126159023 100644 --- a/pkg/cmd/attestation/verify/options.go +++ b/pkg/cmd/attestation/verify/options.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/ghinstance" "github.com/cli/cli/v2/pkg/cmd/attestation/api" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" "github.com/cli/cli/v2/pkg/cmd/attestation/io" @@ -37,6 +38,9 @@ type Options struct { OCIClient oci.Client SigstoreVerifier verification.SigstoreVerifier exporter cmdutil.Exporter + Hostname string + // Tenant is only set when tenancy is used + Tenant string } // Clean cleans the file path option values @@ -57,12 +61,12 @@ func (opts *Options) SetPolicyFlags() { opts.Owner = splitRepo[0] if !isSignerIdentityProvided(opts) { - opts.SANRegex = expandToGitHubURL(opts.Repo) + opts.SANRegex = expandToGitHubURL(opts.Tenant, opts.Repo) } return } if !isSignerIdentityProvided(opts) { - opts.SANRegex = expandToGitHubURL(opts.Owner) + opts.SANRegex = expandToGitHubURL(opts.Tenant, opts.Owner) } } @@ -94,6 +98,13 @@ func (opts *Options) AreFlagsValid() error { return fmt.Errorf("bundle-from-oci flag cannot be used with bundle-path flag") } + // Verify provided hostname + if opts.Hostname != "" { + if err := ghinstance.HostnameValidator(opts.Hostname); err != nil { + return fmt.Errorf("error parsing hostname: %w", err) + } + } + return nil } diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index 4d850ddcc..0c7a686c2 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -1,8 +1,8 @@ package verify import ( + "errors" "fmt" - "os" "regexp" "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" @@ -13,21 +13,23 @@ import ( ) const ( - GitHubOIDCIssuer = "https://token.actions.githubusercontent.com" + GitHubOIDCIssuer = "https://token.actions.githubusercontent.com" + GitHubTenantOIDCIssuer = "https://token.actions.%s.ghe.com" // represents the GitHub hosted runner in the certificate RunnerEnvironment extension GitHubRunner = "github-hosted" - githubHost = "github.com" hostRegex = `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*$` ) -func expandToGitHubURL(ownerOrRepo string) string { - // TODO: handle proxima prefix - return fmt.Sprintf("(?i)^https://github.com/%s/", ownerOrRepo) +func expandToGitHubURL(tenant, ownerOrRepo string) string { + if tenant == "" { + return fmt.Sprintf("(?i)^https://github.com/%s/", ownerOrRepo) + } + return fmt.Sprintf("(?i)^https://%s.ghe.com/%s/", tenant, ownerOrRepo) } func buildSANMatcher(opts *Options) (verify.SubjectAlternativeNameMatcher, error) { if opts.SignerRepo != "" { - signedRepoRegex := expandToGitHubURL(opts.SignerRepo) + signedRepoRegex := expandToGitHubURL(opts.Tenant, opts.SignerRepo) return verify.NewSANMatcher("", signedRepoRegex) } else if opts.SignerWorkflow != "" { validatedWorkflowRegex, err := validateSignerWorkflow(opts) @@ -118,37 +120,9 @@ func validateSignerWorkflow(opts *Options) (string, error) { return addSchemeToRegex(opts.SignerWorkflow), nil } - // if the provided workflow does not contain a host, check for a host - // and prepend it to the workflow - host, err := chooseHost(opts) - if err != nil { - return "", err + if opts.Hostname == "" { + return "", errors.New("unknown host") } - return addSchemeToRegex(fmt.Sprintf("%s/%s", host, opts.SignerWorkflow)), nil -} - -// if a host was not provided as part of a flag argument choose a host based -// on gh cli configuration -func chooseHost(opts *Options) (string, error) { - // check if GH_HOST is set and use that host if it is - host := os.Getenv("GH_HOST") - if host != "" { - return host, nil - } - - // check if the CLI is authenticated with any hosts - cfg, err := opts.Config() - if err != nil { - return "", err - } - - // if authenticated, return the authenticated host - authCfg := cfg.Authentication() - if host, _ := authCfg.DefaultHost(); host != "" { - return host, nil - } - - // if not authenticated, return the default host github.com - return githubHost, nil + return addSchemeToRegex(fmt.Sprintf("%s/%s", opts.Hostname, opts.SignerWorkflow)), nil } diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go index 40435d19e..89f989b45 100644 --- a/pkg/cmd/attestation/verify/policy_test.go +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -1,7 +1,6 @@ package verify import ( - "os" "testing" "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" @@ -32,13 +31,12 @@ func TestBuildPolicy(t *testing.T) { require.NoError(t, err) } -func ValidateSignerWorkflow(t *testing.T) { +func TestValidateSignerWorkflow(t *testing.T) { type testcase struct { name string providedSignerWorkflow string expectedWorkflowRegex string - ghHost string - authHost string + host string } testcases := []testcase{ @@ -56,13 +54,19 @@ func ValidateSignerWorkflow(t *testing.T) { name: "workflow with GH_HOST set", providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml", expectedWorkflowRegex: "^https://myhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", - ghHost: "myhost.github.com", + host: "myhost.github.com", }, { name: "workflow with authenticated host", providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml", expectedWorkflowRegex: "^https://authedhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", - authHost: "authedhost.github.com", + host: "authedhost.github.com", + }, + { + name: "workflow with authenticated host", + providedSignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml", + expectedWorkflowRegex: "^https://authedhost.github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml", + host: "authedhost.github.com", }, } @@ -74,22 +78,29 @@ func ValidateSignerWorkflow(t *testing.T) { SignerWorkflow: tc.providedSignerWorkflow, } - if tc.ghHost != "" { - err := os.Setenv("GH_HOST", tc.ghHost) - require.NoError(t, err) - } - - if tc.authHost != "" { - cfg, err := opts.Config() - require.NoError(t, err) - - // if authenticated, return the authenticated host - authCfg := cfg.Authentication() - authCfg.SetDefaultHost(tc.authHost, "") + // All host resolution is done verify.go:RunE + if tc.host == "" { + // Set to default host + tc.host = "github.com" } + opts.Hostname = tc.host workflowRegex, err := validateSignerWorkflow(opts) require.NoError(t, err) require.Equal(t, tc.expectedWorkflowRegex, workflowRegex) + } } + +func TestValidateSignerWorkflowNoHost(t *testing.T) { + cmdFactory := factory.New("test") + opts := &Options{ + Config: cmdFactory.Config, + SignerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml", + } + + workflowRegex, err := validateSignerWorkflow(opts) + require.Error(t, err) + require.ErrorContains(t, err, "unknown host") + require.Equal(t, "", workflowRegex) +} diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 055636ad5..9e508e994 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -5,6 +5,7 @@ import ( "fmt" "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" @@ -13,6 +14,7 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/io" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" "github.com/cli/cli/v2/pkg/cmdutil" + ghauth "github.com/cli/go-gh/v2/pkg/auth" "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" @@ -109,9 +111,6 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command // Clean file path options opts.Clean() - // set policy flags based on what has been provided - opts.SetPolicyFlags() - return nil }, RunE: func(cmd *cobra.Command, args []string) error { @@ -119,17 +118,18 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command if err != nil { return err } - opts.APIClient = api.NewLiveClient(hc, opts.Logger) opts.OCIClient = oci.NewLiveClient() - if err := auth.IsHostSupported(); err != nil { + if opts.Hostname == "" { + opts.Hostname, _ = ghauth.DefaultHost() + } + err = auth.IsHostSupported(opts.Hostname) + if err != nil { return err } - if runF != nil { - return runF(opts) - } + opts.APIClient = api.NewLiveClient(hc, opts.Hostname, opts.Logger) config := verification.SigstoreConfig{ TrustedRoot: opts.TrustedRoot, @@ -137,6 +137,30 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command NoPublicGood: opts.NoPublicGood, } + // Prepare for tenancy if detected + if ghinstance.IsTenancy(opts.Hostname) { + td, err := opts.APIClient.GetTrustDomain() + if err != nil { + return fmt.Errorf("error getting trust domain, make sure you are authenticated against the host: %w", err) + } + + tenant, found := ghinstance.TenantName(opts.Hostname) + if !found { + return fmt.Errorf("invalid hostname provided: '%s'", + opts.Hostname) + } + config.TrustDomain = td + opts.Tenant = tenant + opts.OIDCIssuer = fmt.Sprintf(GitHubTenantOIDCIssuer, tenant) + } + + // set policy flags based on what has been provided + opts.SetPolicyFlags() + + if runF != nil { + return runF(opts) + } + opts.SigstoreVerifier = verification.NewLiveSigstoreVerifier(config) opts.Config = f.Config @@ -169,6 +193,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]////") verifyCmd.MarkFlagsMutuallyExclusive("cert-identity", "cert-identity-regex", "signer-repo", "signer-workflow") verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", GitHubOIDCIssuer, "Issuer of the OIDC token") + verifyCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use") return verifyCmd } @@ -244,7 +269,7 @@ func runVerify(opts *Options) error { } // Verify extensions - if err := verification.VerifyCertExtensions(sigstoreRes.VerifyResults, opts.Owner, opts.Repo); err != nil { + if err := verification.VerifyCertExtensions(sigstoreRes.VerifyResults, opts.Tenant, opts.Owner, opts.Repo); err != nil { opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Verification failed")) return err } @@ -264,7 +289,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(sigstoreRes.VerifyResults) + tableContent, err := buildTableVerifyContent(opts.Tenant, sigstoreRes.VerifyResults) if err != nil { opts.Logger.Println(opts.Logger.ColorScheme.Red("failed to parse results")) return err @@ -280,30 +305,44 @@ func runVerify(opts *Options) error { return nil } -func extractAttestationDetail(builderSignerURI string) (string, string, error) { +func extractAttestationDetail(tenant, builderSignerURI string) (string, string, error) { // If given a build signer URI like // https://github.com/foo/bar/.github/workflows/release.yml@refs/heads/main // We want to extract: // * foo/bar // * .github/workflows/release.yml@refs/heads/main - orgAndRepoRegexp := regexp.MustCompile(`https://github\.com/([^/]+/[^/]+)/`) + var orgAndRepoRegexp *regexp.Regexp + var workflowRegexp *regexp.Regexp + + if tenant == "" { + orgAndRepoRegexp = regexp.MustCompile(`https://github\.com/([^/]+/[^/]+)/`) + workflowRegexp = regexp.MustCompile(`https://github\.com/[^/]+/[^/]+/(.+)`) + } else { + var tr = regexp.QuoteMeta(tenant) + orgAndRepoRegexp = regexp.MustCompile(fmt.Sprintf( + `https://%s\.ghe\.com/([^/]+/[^/]+)/`, + tr)) + workflowRegexp = regexp.MustCompile(fmt.Sprintf( + `https://%s\.ghe\.com/[^/]+/[^/]+/(.+)`, + tr)) + } + match := orgAndRepoRegexp.FindStringSubmatch(builderSignerURI) if len(match) < 2 { return "", "", fmt.Errorf("no match found for org and repo") } - repoAndOrg := match[1] + orgAndRepo := match[1] - workflowRegexp := regexp.MustCompile(`https://github\.com/[^/]+/[^/]+/(.+)`) match = workflowRegexp.FindStringSubmatch(builderSignerURI) if len(match) < 2 { return "", "", fmt.Errorf("no match found for workflow") } workflow := match[1] - return repoAndOrg, workflow, nil + return orgAndRepo, workflow, nil } -func buildTableVerifyContent(results []*verification.AttestationProcessingResult) ([][]string, error) { +func buildTableVerifyContent(tenant string, results []*verification.AttestationProcessingResult) ([][]string, error) { content := make([][]string, len(results)) for i, res := range results { @@ -313,7 +352,7 @@ func buildTableVerifyContent(results []*verification.AttestationProcessingResult return nil, fmt.Errorf("bundle missing verification result fields") } builderSignerURI := res.VerificationResult.Signature.Certificate.Extensions.BuildSignerURI - repoAndOrg, workflow, err := extractAttestationDetail(builderSignerURI) + repoAndOrg, workflow, err := extractAttestationDetail(tenant, builderSignerURI) if err != nil { return nil, err } diff --git a/pkg/cmd/attestation/verify/verify_integration_test.go b/pkg/cmd/attestation/verify/verify_integration_test.go index 9ad7a87a3..611b1d89f 100644 --- a/pkg/cmd/attestation/verify/verify_integration_test.go +++ b/pkg/cmd/attestation/verify/verify_integration_test.go @@ -11,6 +11,7 @@ import ( "github.com/cli/cli/v2/pkg/cmd/attestation/test" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" "github.com/cli/cli/v2/pkg/cmd/factory" + "github.com/cli/go-gh/v2/pkg/auth" "github.com/stretchr/testify/require" ) @@ -28,8 +29,10 @@ func TestVerifyIntegration(t *testing.T) { t.Fatal(err) } + host, _ := auth.DefaultHost() + publicGoodOpts := Options{ - APIClient: api.NewLiveClient(hc, logger), + APIClient: api.NewLiveClient(hc, host, logger), ArtifactPath: artifactPath, BundlePath: bundlePath, DigestAlgorithm: "sha512", @@ -99,8 +102,10 @@ func TestVerifyIntegrationReusableWorkflow(t *testing.T) { t.Fatal(err) } + host, _ := auth.DefaultHost() + baseOpts := Options{ - APIClient: api.NewLiveClient(hc, logger), + APIClient: api.NewLiveClient(hc, host, logger), ArtifactPath: artifactPath, BundlePath: bundlePath, DigestAlgorithm: "sha256", @@ -185,8 +190,10 @@ func TestVerifyIntegrationReusableWorkflowSignerWorkflow(t *testing.T) { t.Fatal(err) } + host, _ := auth.DefaultHost() + baseOpts := Options{ - APIClient: api.NewLiveClient(hc, logger), + APIClient: api.NewLiveClient(hc, host, logger), ArtifactPath: artifactPath, BundlePath: bundlePath, Config: cmdFactory.Config, @@ -203,6 +210,7 @@ func TestVerifyIntegrationReusableWorkflowSignerWorkflow(t *testing.T) { name string signerWorkflow string expectErr bool + host string } testcases := []testcase{ @@ -220,12 +228,14 @@ func TestVerifyIntegrationReusableWorkflowSignerWorkflow(t *testing.T) { name: "valid signer workflow without host (defaults to github.com)", signerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml", expectErr: false, + host: "github.com", }, } for _, tc := range testcases { opts := baseOpts opts.SignerWorkflow = tc.signerWorkflow + opts.Hostname = tc.host err := runVerify(&opts) if tc.expectErr { diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go index 89c5ae7c1..6f71b537f 100644 --- a/pkg/cmd/attestation/verify/verify_test.go +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -35,10 +35,21 @@ var ( func TestNewVerifyCmd(t *testing.T) { testIO, _, _, _ := iostreams.Test() + var testReg httpmock.Registry + var metaResp = api.MetaResponse{ + Domains: api.Domain{ + ArtifactAttestations: api.ArtifactAttestations{ + TrustDomain: "foo", + }, + }, + } + testReg.Register(httpmock.REST(http.MethodGet, "api/v3/meta"), + httpmock.StatusJSONResponse(200, &metaResp)) + f := &cmdutil.Factory{ IOStreams: testIO, HttpClient: func() (*http.Client, error) { - reg := &httpmock.Registry{} + reg := &testReg client := &http.Client{} httpmock.ReplaceTripper(client, reg) return client, nil @@ -63,6 +74,7 @@ func TestNewVerifyCmd(t *testing.T) { OIDCIssuer: GitHubOIDCIssuer, Owner: "sigstore", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + Hostname: "github.com", }, wantsErr: true, }, @@ -78,9 +90,42 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", SANRegex: "(?i)^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + Hostname: "github.com", }, wantsErr: false, }, + { + name: "Custom host", + cli: fmt.Sprintf("%s --bundle %s --owner sigstore --hostname foo.ghe.com", artifactPath, bundlePath), + wants: Options{ + ArtifactPath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"), + BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"), + DigestAlgorithm: "sha256", + Limit: 30, + OIDCIssuer: "https://token.actions.foo.ghe.com", + Owner: "sigstore", + SANRegex: "(?i)^https://foo.ghe.com/sigstore/", + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + Hostname: "foo.ghe.com", + }, + wantsErr: false, + }, + { + name: "Invalid custom host", + cli: fmt.Sprintf("%s --bundle %s --owner sigstore --hostname foo.bar.com", artifactPath, bundlePath), + wants: Options{ + ArtifactPath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"), + BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"), + DigestAlgorithm: "sha256", + Limit: 30, + OIDCIssuer: GitHubOIDCIssuer, + Owner: "sigstore", + SANRegex: "(?i)^https://github.com/sigstore/", + SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + Hostname: "foo.ghe.com", + }, + wantsErr: true, + }, { name: "Use custom digest-alg value", cli: fmt.Sprintf("%s --bundle %s --owner sigstore --digest-alg sha512", artifactPath, bundlePath), @@ -93,6 +138,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", SANRegex: "(?i)^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + Hostname: "github.com", }, wantsErr: false, }, @@ -107,6 +153,7 @@ func TestNewVerifyCmd(t *testing.T) { Limit: 30, SANRegex: "(?i)^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + Hostname: "github.com", }, wantsErr: true, }, @@ -121,6 +168,7 @@ func TestNewVerifyCmd(t *testing.T) { Repo: "sigstore/sigstore-js", Limit: 30, SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + Hostname: "github.com", }, wantsErr: true, }, @@ -135,6 +183,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", SANRegex: "(?i)^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + Hostname: "github.com", }, wantsErr: false, }, @@ -149,6 +198,7 @@ func TestNewVerifyCmd(t *testing.T) { Limit: 101, SANRegex: "(?i)^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + Hostname: "github.com", }, wantsErr: false, }, @@ -163,6 +213,7 @@ func TestNewVerifyCmd(t *testing.T) { Limit: 0, SANRegex: "(?i)^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + Hostname: "github.com", }, wantsErr: true, }, @@ -178,6 +229,7 @@ func TestNewVerifyCmd(t *testing.T) { SAN: "https://github.com/sigstore/", SANRegex: "(?i)^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + Hostname: "github.com", }, wantsErr: true, }, @@ -193,6 +245,7 @@ func TestNewVerifyCmd(t *testing.T) { Owner: "sigstore", SANRegex: "(?i)^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), + Hostname: "github.com", }, wantsExporter: true, }, @@ -230,6 +283,7 @@ func TestNewVerifyCmd(t *testing.T) { assert.Equal(t, tc.wants.Repo, opts.Repo) assert.Equal(t, tc.wants.SAN, opts.SAN) assert.Equal(t, tc.wants.SANRegex, opts.SANRegex) + assert.Equal(t, tc.wants.Hostname, opts.Hostname) assert.NotNil(t, opts.APIClient) assert.NotNil(t, opts.Logger) assert.NotNil(t, opts.OCIClient) @@ -357,6 +411,17 @@ func TestRunVerify(t *testing.T) { require.Nil(t, runVerify(&opts)) }) + // Test with bad tenancy + t.Run("with bad tenancy", func(t *testing.T) { + opts := publicGoodOpts + opts.BundlePath = "" + opts.Repo = "sigstore/sigstore-js" + opts.Tenant = "foo" + + err := runVerify(&opts) + require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://foo.ghe.com/sigstore, got https://github.com/sigstore") + }) + t.Run("with repo which not matches SourceRepositoryURI", func(t *testing.T) { opts := publicGoodOpts opts.BundlePath = "" From a0a25673542e669561551376431b5029ae5484d4 Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Wed, 11 Sep 2024 11:50:05 +0100 Subject: [PATCH 049/301] Suggest installing Rosetta when extension installation fails due to missing `darwin-arm64` binary, but a `darwin-amd64` binary is available When installing an extension, the CLI must to select the correct binary to download for the machine (see the [`installBin` function](https://github.com/cli/cli/blob/78c1d00eccac1b2ae82ac0bfeea3e2292c98056a/pkg/cmd/extension/manager.go#L240)). By default, the CLI will download a binary matching the current machine's architecture. However, to provide better support for Macs running on Apple Silicon, it will [fall back](https://github.com/cli/cli/blob/78c1d00eccac1b2ae82ac0bfeea3e2292c98056a/pkg/cmd/extension/manager.go#L267-L274) from `darwin-arm64` to `darwin-amd64` if [Rosetta](https://support.apple.com/en-gb/102527) (Apple's compatibility layer) is installed. If Rosetta isn't installed, this fallback doesn't happen, which can lead to surprising and confusing results when one Mac has Rosetta and another doesn't, because the extension will install on one machine but not another. In the situation where a `darwin-arm64` binary isn't available but the CLI can't fall back to `amd64` because Rosetta isn't installed, this updates our error message to suggest installing Rosetta. Closes https://github.com/cli/cli/issues/9592. --- pkg/cmd/extension/manager.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 0ab429c2e..60fe8d239 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -263,12 +263,18 @@ func (m *Manager) installBin(repo ghrepo.Interface, target string) error { } } - // if an arm64 binary is unavailable, fall back to amd64 if it can be executed through Rosetta 2 - if asset == nil && isMacARM && hasRosetta() { + // if using an ARM-based Mac and an arm64 binary is unavailable, fall back to amd64 if a relevant binary is available and Rosetta 2 is installed + if asset == nil && isMacARM { for _, a := range r.Assets { if strings.HasSuffix(a.Name, "darwin-amd64") { - asset = &a - break + if hasRosetta() { + asset = &a + break + } else { + return fmt.Errorf( + "%[1]s unsupported for %[2]s. Install Rosetta with `softwareupdate --install-rosetta` to use the available darwin-amd64 binary, or open an issue: `gh issue create -R %[3]s/%[1]s -t'Support %[2]s'`", + repo.RepoName(), platform, repo.RepoOwner()) + } } } } From fbdf0ccb7576055c1a44ee23192b919e07199083 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:39:44 +0000 Subject: [PATCH 050/301] build(deps): bump github.com/sigstore/sigstore-go from 0.6.1 to 0.6.2 Bumps [github.com/sigstore/sigstore-go](https://github.com/sigstore/sigstore-go) from 0.6.1 to 0.6.2. - [Release notes](https://github.com/sigstore/sigstore-go/releases) - [Commits](https://github.com/sigstore/sigstore-go/compare/v0.6.1...v0.6.2) --- updated-dependencies: - dependency-name: github.com/sigstore/sigstore-go dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 50278d733..78ceb7f70 100644 --- a/go.mod +++ b/go.mod @@ -38,15 +38,15 @@ require ( github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc github.com/sigstore/protobuf-specs v0.3.2 - github.com/sigstore/sigstore-go v0.6.1 + github.com/sigstore/sigstore-go v0.6.2 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 github.com/zalando/go-keyring v0.2.5 - golang.org/x/crypto v0.26.0 + golang.org/x/crypto v0.27.0 golang.org/x/sync v0.8.0 - golang.org/x/term v0.23.0 - golang.org/x/text v0.17.0 + golang.org/x/term v0.24.0 + golang.org/x/text v0.18.0 google.golang.org/grpc v1.64.1 google.golang.org/protobuf v1.34.2 gopkg.in/h2non/gock.v1 v1.1.2 @@ -159,7 +159,7 @@ require ( golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.27.0 // indirect - golang.org/x/sys v0.23.0 // indirect + golang.org/x/sys v0.25.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 407739dfe..37c40407d 100644 --- a/go.sum +++ b/go.sum @@ -395,8 +395,8 @@ github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8= github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc= github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk= github.com/sigstore/sigstore v1.8.9/go.mod h1:d9ZAbNDs8JJfxJrYmulaTazU3Pwr8uLL9+mii4BNR3w= -github.com/sigstore/sigstore-go v0.6.1 h1:tGkkv1oDIER+QYU5MrjqlttQOVDWfSkmYwMqkJhB/cg= -github.com/sigstore/sigstore-go v0.6.1/go.mod h1:Xe5GHmUeACRFbomUWzVkf/xYCn8xVifb9DgqJrV2dIw= +github.com/sigstore/sigstore-go v0.6.2 h1:8uiywjt73vzfrGfWYVwVsiB1E1Qmwmpgr1kVpl4fs6A= +github.com/sigstore/sigstore-go v0.6.2/go.mod h1:pOIUH7Jx+ctwMICo+2zNrViOJJN5sGaQgwX4yAVJkA0= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3 h1:LTfPadUAo+PDRUbbdqbeSl2OuoFQwUFTnJ4stu+nwWw= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.3/go.mod h1:QV/Lxlxm0POyhfyBtIbTWxNeF18clMlkkyL9mu45y18= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.3 h1:xgbPRCr2npmmsuVVteJqi/ERw9+I13Wou7kq0Yk4D8g= @@ -485,8 +485,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -514,19 +514,19 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From f128ae8349950cbaeb4c68b7635955f1b6200f19 Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Tue, 10 Sep 2024 14:37:38 -0700 Subject: [PATCH 051/301] add att verify test for custom OIDC issuer Signed-off-by: Brian DeHamer --- .../test/data/custom-issuer-artifact | 1 + .../test/data/custom-issuer.sigstore.json | 61 ++++++++++++++++ .../verify/verify_integration_test.go | 69 +++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 pkg/cmd/attestation/test/data/custom-issuer-artifact create mode 100644 pkg/cmd/attestation/test/data/custom-issuer.sigstore.json diff --git a/pkg/cmd/attestation/test/data/custom-issuer-artifact b/pkg/cmd/attestation/test/data/custom-issuer-artifact new file mode 100644 index 000000000..bdd51cc27 --- /dev/null +++ b/pkg/cmd/attestation/test/data/custom-issuer-artifact @@ -0,0 +1 @@ +hello-world \ No newline at end of file diff --git a/pkg/cmd/attestation/test/data/custom-issuer.sigstore.json b/pkg/cmd/attestation/test/data/custom-issuer.sigstore.json new file mode 100644 index 000000000..ad47e2478 --- /dev/null +++ b/pkg/cmd/attestation/test/data/custom-issuer.sigstore.json @@ -0,0 +1,61 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "tlogEntries": [ + { + "logIndex": "129601213", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "integratedTime": "1726082343", + "inclusionPromise": { + "signedEntryTimestamp": "MEQCICL0FAIR4ISP9CZJERTDWm0ZWQXmBfk1n2rNaKcThjFnAiAOpJMbjiwKD+Nt32VgodKh3whOZWERIerwtuTGChcKVg==" + }, + "inclusionProof": { + "logIndex": "7696951", + "rootHash": "jFHZ9WG6TKsPs3sSueywIxZ8kCLggGmqg2toWJ8seXk=", + "treeSize": "7696953", + "hashes": [ + "CYHKf/bh3CxW39mRO4FlajMmrzH8KleobYBryPGMjhQ=", + "kAIZZLHLd1KnJQ3CNShHaxG5wQjuF0wG49oq5AC4vXQ=", + "f9/xH+QDA5+muxMr2QouK2OLOLkI+jPM2lUX7diPaOA=", + "mlhYVIwuxUw07ewtU3um0c8IkYPf55EhyXwuOlzwJbs=", + "K2QMn27+dp+8+2utA7P0W1+pFT18nvdFMIlz3qXBC/0=", + "+5kLbgrjmfzkYQ0V+vofM18LsqyNpLa5oRr/24gOH+s=", + "kNWva6L6IlKsmCkDx0cdNtZJztdunXsjWqzwn/k9moQ=", + "W8NjV+EXoTQRJYFsLhEueUiT6vxbPXYoSIONJIJmCvM=", + "8tdMgSRLWN3UxGVxNBjKm/4Sjivq1EMAAomCJVhscmU=", + "hPmHSU/WMp+ST2P+1mEnh/wjLLY9KbulaYu+ELcIJ2o=", + "KYw9/y5e7chXWKn9xKSkwIm0ZV/niE9MccszZ/yMVH8=", + "52g33BcJumS4u9qvM95+2WQcPJoG3zKFTsDQU/yGT/Q=", + "57ZnG4cTkj/dfCv8Vz7kMnUbcY3NL1PkfzMA2cgdg0c=", + "uRsmea7eVXshBNN6huh/owmfaAy9Rx4Cq2M2vFb2Ntk=", + "NeHKGVl6KVXfx3+wnQrIrxra4Pr9Fa7YDpTlf86mlTc=" + ], + "checkpoint": { + "envelope": "rekor.sigstore.dev - 1193050959916656506\n7696953\njFHZ9WG6TKsPs3sSueywIxZ8kCLggGmqg2toWJ8seXk=\n\n— rekor.sigstore.dev wNI9ajBFAiEA4cRIk3KpKhPAmONZTnKJ84MWoy/uylIgvcQ5hZsQdsQCIFrXcNcJfpQQAXlhca0jAsz/4vqXvuFdHTT12JDyXhjW\n" + } + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiNTM0ZDM4OWFmY2ZiMGYyOGE1MzE2MDEzNmRhOTNmODQyNGEwOGMzMzZhMTQ5YzcxNjg5NWFiY2EyZDlhMzAxMSJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjhkOWMxNzg0NjA5ZGJkZmQxMjhhMjFlMzdiNzJhY2QwZDVmY2RhNjBlMzRjZjQwZGI2ZGYwMDQyODJmMGFjMDQifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVZQ0lRRG82SGN1ZkQxWDVXK0FMcUFzK0d0VXZvcEZYQlFGNUpDRFJXODlWTndOV1FJaEFKdjRTSjhINlJPM3JwV3Zib3VUWTdrNGJzUWN1NWNDa2l1aXRRSjlkMEgvIiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VjNGFrTkRRbTV0WjBGM1NVSkJaMGxWVDBwd05EVmpPVTExVGxKU1JISk5XSEpMZG5sSWJHc3JVbFpKZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwUmQwOVVSWGhOVkd0NFQxUkJlbGRvWTA1TmFsRjNUMVJGZUUxVWEzbFBWRUY2VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVlpUbVZEZFVsMUsySnRhRzR4UkdsdmRHRnVNakZpUjFsTGVGcHdSVFpOV2paUFlTOEtWMjh4WTNsNU5raFhNVVZJVEZsQ00wbG5XRE56Y1RkdFNFSklaMWQyY1dwMlFWVkdVelZZTUZaVFZGWktkR0Z4TkV0UFEwSmFaM2RuWjFkVlRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVXJkVkkxQ2tSMFRHeDRWelJQTnpCMWFWcGtXVXBZTWlzd01EUnpkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMWwzV1VSV1VqQlNRVkZJTDBKR2EzZFdORnBXWVVoU01HTklUVFpNZVRsdVlWaFNiMlJYU1hWWk1qbDBURE5TZG1KNU1YTmFWMlJ3WkVNNWFBcGtTRkpzWXpOUmRreHRaSEJrUjJneFdXazVNMkl6U25KYWJYaDJaRE5OZG1GWE5UQmFWMlI1V1ZoU2NHSXlOSFZsVnpGelVVaEtiRnB1VFhaaFIxWm9DbHBJVFhaaVYwWndZbXBDUmtKbmIzSkNaMFZGUVZsUEwwMUJSVUpDUkdSdlpFaFNkMk42YjNaTU0xSjJZVEpXZFV4dFJtcGtSMngyWW01TmRWb3liREFLWVVoV2FXUllUbXhqYlU1MlltNVNiR0p1VVhWWk1qbDBUREpvYUdKWE1XeGphVEV3WVZjeGJFMUNPRWREYVhOSFFWRlJRbWMzT0hkQlVVbEZSVmhrZGdwamJYUnRZa2M1TTFneVVuQmpNMEpvWkVkT2IwMUVXVWREYVhOSFFWRlJRbWMzT0hkQlVVMUZTMFJKTkUweVNtMVBWRmt6V20xRmVsbDZWVFZOVkd0NUNrNVhVbXROYWxac1RtMUZNbHBVYUd4T2VtZDRUbFJSZVUxVVJUTlpiVmwzUzJkWlMwdDNXVUpDUVVkRWRucEJRa0pCVVdOUldGSXdXbGhPTUZsWVVuQUtZakkwWjFOWE5UQmFWMlI1V1ZoU2NHSXlOR2RXUjFaNlpFUkJaVUpuYjNKQ1owVkZRVmxQTDAxQlJVWkNRa0l3WWpJNGRHSkhWbTVoV0ZGMldWaFNNQXBhV0U0d1RVSXdSME5wYzBkQlVWRkNaemM0ZDBGUldVVkVNMHBzV201TmRtRkhWbWhhU0UxMllsZEdjR0pxUWtoQ1oyOXlRbWRGUlVGWlR5OU5RVVZKQ2tKRWEwMU9NbWd3WkVoQ2VrOXBPSFprUnpseVdsYzBkVmxYVGpCaFZ6bDFZM2sxYm1GWVVtOWtWMG94WXpKV2VWa3lPWFZrUjFaMVpFTTFhbUl5TUhZS1lVZEdkR0pYVm5sTVdGSndZbGRWZDFwUldVdExkMWxDUWtGSFJIWjZRVUpEVVZKWVJFWldiMlJJVW5kamVtOTJUREprY0dSSGFERlphVFZxWWpJd2RncGtSemwyVEZkNGJGb3liREJNTWtZd1pFZFdlbVJET0hWYU1td3dZVWhXYVV3elpIWmpiWFJ0WWtjNU0yTjVPWEJpYmxKc1dqTkthR1JIYkhaaWFUVTFDbUpYZUVGamJWWnRZM2s1YjFwWFJtdGplVGwwV1Zkc2RVMUVaMGREYVhOSFFWRlJRbWMzT0hkQlVXOUZTMmQzYjAxcVozcFpiVmsxVG1wa2JWbFVUbW9LVGxScmVFOVVTVEZhUjFGNVRsZFZNbGxVV214UFIxVXpUMFJGTVU1RVNYaE5WR1JwV21wQlpFSm5iM0pDWjBWRlFWbFBMMDFCUlV4Q1FUaE5SRmRrY0Fwa1IyZ3hXV2t4YjJJelRqQmFWMUYzVFhkWlMwdDNXVUpDUVVkRWRucEJRa1JCVVd4RVEwNXZaRWhTZDJONmIzWk1NbVJ3WkVkb01WbHBOV3BpTWpCMkNtUkhPWFpNVjNoc1dqSnNNRXd5UmpCa1IxWjZaRVJCTkVKbmIzSkNaMFZGUVZsUEwwMUJSVTVDUTI5TlMwUkpORTB5U20xUFZGa3pXbTFGZWxsNlZUVUtUVlJyZVU1WFVtdE5hbFpzVG0xRk1scFVhR3hPZW1kNFRsUlJlVTFVUlROWmJWbDNTSGRaUzB0M1dVSkNRVWRFZG5wQlFrUm5VVkpFUVRsNVdsZGFlZ3BNTW1oc1dWZFNla3d5TVdoaFZ6UjNSMUZaUzB0M1dVSkNRVWRFZG5wQlFrUjNVVXhFUVdzMFRsUkZORTlVVVRSTmFsRjNURUZaUzB0M1dVSkNRVWRFQ25aNlFVSkZRVkZsUkVKNGIyUklVbmRqZW05MlRESmtjR1JIYURGWmFUVnFZakl3ZG1SSE9YWk1WM2hzV2pKc01FMUNhMGREYVhOSFFWRlJRbWMzT0hjS1FWSkZSVU4zZDBwTlZHZDNUWHBSTkUxRVVUSk5SMVZIUTJselIwRlJVVUpuTnpoM1FWSkpSVlozZUZaaFNGSXdZMGhOTmt4NU9XNWhXRkp2WkZkSmRRcFpNamwwVEROU2RtSjVNWE5hVjJSd1pFTTVhR1JJVW14ak0xRjJURzFrY0dSSGFERlphVGt6WWpOS2NscHRlSFprTTAxMllWYzFNRnBYWkhsWldGSndDbUl5TkhWbFZ6RnpVVWhLYkZwdVRYWmhSMVpvV2toTmRtSlhSbkJpYWtFMFFtZHZja0puUlVWQldVOHZUVUZGVkVKRGIwMUxSRWswVFRKS2JVOVVXVE1LV20xRmVsbDZWVFZOVkd0NVRsZFNhMDFxVm14T2JVVXlXbFJvYkU1NlozaE9WRkY1VFZSRk0xbHRXWGRKVVZsTFMzZFpRa0pCUjBSMmVrRkNSa0ZSVkFwRVFrWXpZak5LY2xwdGVIWmtNVGxyWVZoT2QxbFlVbXBoUkVKWVFtZHZja0puUlVWQldVOHZUVUZGVmtKRmEwMVNNbWd3WkVoQ2VrOXBPSFphTW13d0NtRklWbWxNYlU1MllsTTVNR0l5T0hSaVIxWnVZVmhSZGxsWVVqQmFXRTR3VERKR2FtUkhiSFppYmsxMlkyNVdkV041T0hoTlJHZDRUMFJKZVU1cVJYa0tUbE01YUdSSVVteGlXRUl3WTNrNGVFMUNXVWREYVhOSFFWRlJRbWMzT0hkQlVsbEZRMEYzUjJOSVZtbGlSMnhxVFVsSFMwSm5iM0pDWjBWRlFXUmFOUXBCWjFGRFFraDNSV1ZuUWpSQlNGbEJNMVF3ZDJGellraEZWRXBxUjFJMFkyMVhZek5CY1VwTFdISnFaVkJMTXk5b05IQjVaME00Y0Rkdk5FRkJRVWRTQ2pSdldtbFpRVUZCUWtGTlFWSjZRa1pCYVVGamJHNDNUR1JRUkZOS1lXZzVSRzFDTVdwT2EwMXlRMVZVYVU5WEwxbzNTMkpJZWtoNFZWQjZNek4zU1dnS1FVdGFhVmhwTDFjelVsSTVjbXh2ZVdWV1JsTlFOemc0U1VsdVprcERiekJPY21acWIybFhNRWh4TkhkTlFXOUhRME54UjFOTk5EbENRVTFFUVRKalFRcE5SMUZEVFVFd2RFWTFObmRDYlZCSWNtdzBVQ3RWTUVGaGNuRnNWbGg1VVV4eloxQkphVFU0UmxWeFlqVjNkMVZLZUZwQmRFOU1kbFJMYTI1dWNrVmxDakpNU3pCWlFVbDNVVTFtVUZsa2NHeHlWUzlWVUdaWlJtWlZOSFV2YlhGV05HdFBOVWh6WXpoUGFGcG9lVTE1WjBJNWJVSnhSMnBpYlRkVlFrNTRlakFLWXpNMVZXMUhRbWNLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUW89In1dfX0=" + } + ], + "timestampVerificationData": { + }, + "certificate": { + "rawBytes": "MIIG8jCCBnmgAwIBAgIUOJp45c9MuNRRDrMXrKvyHlk+RVIwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwOTExMTkxOTAzWhcNMjQwOTExMTkyOTAzWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYNeCuIu+bmhn1Diotan21bGYKxZpE6MZ6Oa/Wo1cyy6HW1EHLYB3IgX3sq7mHBHgWvqjvAUFS5X0VSTVJtaq4KOCBZgwggWUMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU+uR5DtLlxW4O70uiZdYJX2+004swHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wYwYDVR0RAQH/BFkwV4ZVaHR0cHM6Ly9naXRodWIuY29tL3Rvby1sZWdpdC9hdHRlc3QvLmdpdGh1Yi93b3JrZmxvd3MvaW50ZWdyYXRpb24ueW1sQHJlZnMvaGVhZHMvbWFpbjBFBgorBgEEAYO/MAEBBDdodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2hhbW1lci10aW1lMB8GCisGAQQBg78wAQIEEXdvcmtmbG93X2Rpc3BhdGNoMDYGCisGAQQBg78wAQMEKDI4M2JmOTY3ZmEzYzU5MTkyNWRkMjVlNmE2ZThlNzgxNTQyMTE3YmYwKgYKKwYBBAGDvzABBAQcQXR0ZXN0YXRpb24gSW50ZWdyYXRpb24gVGVzdDAeBgorBgEEAYO/MAEFBBB0b28tbGVnaXQvYXR0ZXN0MB0GCisGAQQBg78wAQYED3JlZnMvaGVhZHMvbWFpbjBHBgorBgEEAYO/MAEIBDkMN2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20vaGFtbWVyLXRpbWUwZQYKKwYBBAGDvzABCQRXDFVodHRwczovL2dpdGh1Yi5jb20vdG9vLWxlZ2l0L2F0dGVzdC8uZ2l0aHViL3dvcmtmbG93cy9pbnRlZ3JhdGlvbi55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wAQoEKgwoMjgzYmY5NjdmYTNjNTkxOTI1ZGQyNWU2YTZlOGU3ODE1NDIxMTdiZjAdBgorBgEEAYO/MAELBA8MDWdpdGh1Yi1ob3N0ZWQwMwYKKwYBBAGDvzABDAQlDCNodHRwczovL2dpdGh1Yi5jb20vdG9vLWxlZ2l0L2F0dGVzdDA4BgorBgEEAYO/MAENBCoMKDI4M2JmOTY3ZmEzYzU5MTkyNWRkMjVlNmE2ZThlNzgxNTQyMTE3YmYwHwYKKwYBBAGDvzABDgQRDA9yZWZzL2hlYWRzL21haW4wGQYKKwYBBAGDvzABDwQLDAk4NTE4OTQ4MjQwLAYKKwYBBAGDvzABEAQeDBxodHRwczovL2dpdGh1Yi5jb20vdG9vLWxlZ2l0MBkGCisGAQQBg78wAREECwwJMTgwMzQ4MDQ2MGUGCisGAQQBg78wARIEVwxVaHR0cHM6Ly9naXRodWIuY29tL3Rvby1sZWdpdC9hdHRlc3QvLmdpdGh1Yi93b3JrZmxvd3MvaW50ZWdyYXRpb24ueW1sQHJlZnMvaGVhZHMvbWFpbjA4BgorBgEEAYO/MAETBCoMKDI4M2JmOTY3ZmEzYzU5MTkyNWRkMjVlNmE2ZThlNzgxNTQyMTE3YmYwIQYKKwYBBAGDvzABFAQTDBF3b3JrZmxvd19kaXNwYXRjaDBXBgorBgEEAYO/MAEVBEkMR2h0dHBzOi8vZ2l0aHViLmNvbS90b28tbGVnaXQvYXR0ZXN0L2FjdGlvbnMvcnVucy8xMDgxODIyNjEyNS9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwGcHVibGljMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGR4oZiYAAABAMARzBFAiAcln7LdPDSJah9DmB1jNkMrCUTiOW/Z7KbHzHxUPz33wIhAKZiXi/W3RR9rloyeVFSP788IInfJCo0NrfjoiW0Hq4wMAoGCCqGSM49BAMDA2cAMGQCMA0tF56wBmPHrl4P+U0AarqlVXyQLsgPIi58FUqb5wwUJxZAtOLvTKknnrEe2LK0YAIwQMfPYdplrU/UPfYFfU4u/mqV4kO5Hsc8OhZhyMygB9mBqGjbm7UBNxz0c35UmGBg" + } + }, + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiYXJ0aWZhY3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWZhMjdiNDRkNDNiMDJhOWZlYTQxZDEzY2VkYzJlNDAxNmNmY2Y4N2M1ZGJmOTkwZTU5MzY2OWFhOGNlMjg2ZCJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vYWN0aW9ucy5naXRodWIuaW8vYnVpbGR0eXBlcy93b3JrZmxvdy92MSIsImV4dGVybmFsUGFyYW1ldGVycyI6eyJ3b3JrZmxvdyI6eyJyZWYiOiJyZWZzL2hlYWRzL21haW4iLCJyZXBvc2l0b3J5IjoiaHR0cHM6Ly9naXRodWIuY29tL3Rvby1sZWdpdC9hdHRlc3QiLCJwYXRoIjoiLmdpdGh1Yi93b3JrZmxvd3MvaW50ZWdyYXRpb24ueW1sIn19LCJpbnRlcm5hbFBhcmFtZXRlcnMiOnsiZ2l0aHViIjp7ImV2ZW50X25hbWUiOiJ3b3JrZmxvd19kaXNwYXRjaCIsInJlcG9zaXRvcnlfaWQiOiI4NTE4OTQ4MjQiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiMTgwMzQ4MDQ2IiwicnVubmVyX2Vudmlyb25tZW50IjoiZ2l0aHViLWhvc3RlZCJ9fSwicmVzb2x2ZWREZXBlbmRlbmNpZXMiOlt7InVyaSI6ImdpdCtodHRwczovL2dpdGh1Yi5jb20vdG9vLWxlZ2l0L2F0dGVzdEByZWZzL2hlYWRzL21haW4iLCJkaWdlc3QiOnsiZ2l0Q29tbWl0IjoiMjgzYmY5NjdmYTNjNTkxOTI1ZGQyNWU2YTZlOGU3ODE1NDIxMTdiZiJ9fV19LCJydW5EZXRhaWxzIjp7ImJ1aWxkZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vdG9vLWxlZ2l0L2F0dGVzdC8uZ2l0aHViL3dvcmtmbG93cy9pbnRlZ3JhdGlvbi55bWxAcmVmcy9oZWFkcy9tYWluIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS90b28tbGVnaXQvYXR0ZXN0L2FjdGlvbnMvcnVucy8xMDgxODIyNjEyNS9hdHRlbXB0cy8xIn19fX0=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEYCIQDo6HcufD1X5W+ALqAs+GtUvopFXBQF5JCDRW89VNwNWQIhAJv4SJ8H6RO3rpWvbouTY7k4bsQcu5cCkiuitQJ9d0H/" + } + ] + } +} \ No newline at end of file diff --git a/pkg/cmd/attestation/verify/verify_integration_test.go b/pkg/cmd/attestation/verify/verify_integration_test.go index 611b1d89f..df24b31c4 100644 --- a/pkg/cmd/attestation/verify/verify_integration_test.go +++ b/pkg/cmd/attestation/verify/verify_integration_test.go @@ -85,6 +85,75 @@ func TestVerifyIntegration(t *testing.T) { }) } +func TestVerifyIntegrationCustomIssuer(t *testing.T) { + artifactPath := test.NormalizeRelativePath("../test/data/custom-issuer-artifact") + bundlePath := test.NormalizeRelativePath("../test/data/custom-issuer.sigstore.json") + + logger := io.NewTestHandler() + + sigstoreConfig := verification.SigstoreConfig{ + Logger: logger, + } + + cmdFactory := factory.New("test") + + hc, err := cmdFactory.HttpClient() + if err != nil { + t.Fatal(err) + } + + host, _ := auth.DefaultHost() + + baseOpts := Options{ + APIClient: api.NewLiveClient(hc, host, logger), + ArtifactPath: artifactPath, + BundlePath: bundlePath, + DigestAlgorithm: "sha256", + Logger: logger, + OCIClient: oci.NewLiveClient(), + OIDCIssuer: "https://token.actions.githubusercontent.com/hammer-time", + SigstoreVerifier: verification.NewLiveSigstoreVerifier(sigstoreConfig), + } + + t.Run("with owner and valid workflow SAN", func(t *testing.T) { + opts := baseOpts + opts.Owner = "too-legit" + opts.SAN = "https://github.com/too-legit/attest/.github/workflows/integration.yml@refs/heads/main" + + err := runVerify(&opts) + require.NoError(t, err) + }) + + t.Run("with owner and valid workflow SAN regex", func(t *testing.T) { + opts := baseOpts + opts.Owner = "too-legit" + opts.SANRegex = "^https://github.com/too-legit/attest" + + err := runVerify(&opts) + require.NoError(t, err) + }) + + t.Run("with repo and valid workflow SAN", func(t *testing.T) { + opts := baseOpts + opts.Owner = "too-legit" + opts.Repo = "too-legit/attest" + opts.SAN = "https://github.com/too-legit/attest/.github/workflows/integration.yml@refs/heads/main" + + err := runVerify(&opts) + require.NoError(t, err) + }) + + t.Run("with repo and valid workflow SAN regex", func(t *testing.T) { + opts := baseOpts + opts.Owner = "too-legit" + opts.Repo = "too-legit/attest" + opts.SANRegex = "^https://github.com/too-legit/attest" + + err := runVerify(&opts) + require.NoError(t, err) + }) +} + func TestVerifyIntegrationReusableWorkflow(t *testing.T) { artifactPath := test.NormalizeRelativePath("../test/data/reusable-workflow-artifact") bundlePath := test.NormalizeRelativePath("../test/data/reusable-workflow-attestation.sigstore.json") From 677edbac777fd2da8fe863a3b49172a93cf50bb2 Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Thu, 12 Sep 2024 09:40:47 -0700 Subject: [PATCH 052/301] suppress att verify output when no tty Signed-off-by: Brian DeHamer --- pkg/cmd/attestation/io/handler.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/attestation/io/handler.go b/pkg/cmd/attestation/io/handler.go index 07be3d1df..9664c7f65 100644 --- a/pkg/cmd/attestation/io/handler.go +++ b/pkg/cmd/attestation/io/handler.go @@ -62,6 +62,10 @@ func (h *Handler) VerbosePrintf(f string, v ...interface{}) (int, error) { } func (h *Handler) PrintTable(headers []string, rows [][]string) error { + if !h.IO.IsStdoutTTY() { + return nil + } + t := tableprinter.New(h.IO, tableprinter.WithHeader(headers...)) for _, row := range rows { From 5296d608323f2c48a6a8e588df007b22f4617010 Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Thu, 12 Sep 2024 18:53:24 +0100 Subject: [PATCH 053/301] Refactor conditions Co-authored-by: Andy Feller --- pkg/cmd/extension/manager.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 60fe8d239..cf5c44bcd 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -267,14 +267,14 @@ func (m *Manager) installBin(repo ghrepo.Interface, target string) error { if asset == nil && isMacARM { for _, a := range r.Assets { if strings.HasSuffix(a.Name, "darwin-amd64") { - if hasRosetta() { - asset = &a - break - } else { + if !hasRosetta() { return fmt.Errorf( "%[1]s unsupported for %[2]s. Install Rosetta with `softwareupdate --install-rosetta` to use the available darwin-amd64 binary, or open an issue: `gh issue create -R %[3]s/%[1]s -t'Support %[2]s'`", repo.RepoName(), platform, repo.RepoOwner()) } + + asset = &a + break } } } From e0fb793b44d7060fdff429500382badf030ae880 Mon Sep 17 00:00:00 2001 From: William Martin Date: Thu, 12 Sep 2024 20:29:59 +0200 Subject: [PATCH 054/301] Stub hasRosetta for tests --- pkg/cmd/extension/manager.go | 2 +- pkg/cmd/extension/manager_test.go | 52 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index cf5c44bcd..45186d689 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -771,7 +771,7 @@ func possibleDists() []string { } } -func hasRosetta() bool { +var hasRosetta = func() bool { _, err := os.Stat("/Library/Apple/usr/libexec/oah/libRosettaRuntime") return err == nil } diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index 6d9b63124..f03cc7f73 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -912,6 +912,58 @@ func TestManager_Install_binary_unsupported(t *testing.T) { assert.Equal(t, "", stderr.String()) } +func TestManager_Install_rosetta_fallback_not_found(t *testing.T) { + repo := ghrepo.NewWithHost("owner", "gh-bin-ext", "example.com") + + reg := httpmock.Registry{} + defer reg.Verify(t) + client := http.Client{Transport: ®} + + reg.Register( + httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), + httpmock.JSONResponse( + release{ + Assets: []releaseAsset{ + { + Name: "gh-bin-ext-darwin-amd64", + APIURL: "https://example.com/release/cool", + }, + }, + })) + reg.Register( + httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), + httpmock.JSONResponse( + release{ + Tag: "v1.0.1", + Assets: []releaseAsset{ + { + Name: "gh-bin-ext-darwin-amd64", + APIURL: "https://example.com/release/cool", + }, + }, + })) + + ios, _, stdout, stderr := iostreams.Test() + tempDir := t.TempDir() + + m := newTestManager(tempDir, &client, nil, ios) + m.platform = func() (string, string) { + return "darwin-arm64", "" + } + + originalHasRosetta := hasRosetta + t.Cleanup(func() { hasRosetta = originalHasRosetta }) + hasRosetta = func() bool { + return false + } + + err := m.Install(repo, "") + assert.EqualError(t, err, "gh-bin-ext unsupported for darwin-arm64. Install Rosetta with `softwareupdate --install-rosetta` to use the available darwin-amd64 binary, or open an issue: `gh issue create -R owner/gh-bin-ext -t'Support darwin-arm64'`") + + assert.Equal(t, "", stdout.String()) + assert.Equal(t, "", stderr.String()) +} + func TestManager_Install_binary(t *testing.T) { repo := ghrepo.NewWithHost("owner", "gh-bin-ext", "example.com") From 99b789e745d260d314c05a8d6f68ddabaa9cb882 Mon Sep 17 00:00:00 2001 From: William Martin Date: Fri, 13 Sep 2024 11:59:54 +0200 Subject: [PATCH 055/301] Add test for extension install fallback to amd64 on darwin --- pkg/cmd/extension/manager_test.go | 74 +++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index f03cc7f73..5de4e15cb 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -1028,6 +1028,80 @@ func TestManager_Install_binary(t *testing.T) { assert.Equal(t, "", stderr.String()) } +func TestManager_Install_amd64_when_supported(t *testing.T) { + repo := ghrepo.NewWithHost("owner", "gh-bin-ext", "example.com") + + reg := httpmock.Registry{} + defer reg.Verify(t) + client := http.Client{Transport: ®} + + reg.Register( + httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), + httpmock.JSONResponse( + release{ + Assets: []releaseAsset{ + { + Name: "gh-bin-ext-darwin-amd64", + APIURL: "https://example.com/release/cool", + }, + }, + })) + reg.Register( + httpmock.REST("GET", "api/v3/repos/owner/gh-bin-ext/releases/latest"), + httpmock.JSONResponse( + release{ + Tag: "v1.0.1", + Assets: []releaseAsset{ + { + Name: "gh-bin-ext-darwin-amd64", + APIURL: "https://example.com/release/cool", + }, + }, + })) + reg.Register( + httpmock.REST("GET", "release/cool"), + httpmock.StringResponse("FAKE BINARY")) + + ios, _, stdout, stderr := iostreams.Test() + tempDir := t.TempDir() + + m := newTestManager(tempDir, &client, nil, ios) + m.platform = func() (string, string) { + return "darwin-arm64", "" + } + + originalHasRosetta := hasRosetta + t.Cleanup(func() { hasRosetta = originalHasRosetta }) + hasRosetta = func() bool { + return true + } + + err := m.Install(repo, "") + assert.NoError(t, err) + + manifest, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext", manifestName)) + assert.NoError(t, err) + + var bm binManifest + err = yaml.Unmarshal(manifest, &bm) + assert.NoError(t, err) + + assert.Equal(t, binManifest{ + Name: "gh-bin-ext", + Owner: "owner", + Host: "example.com", + Tag: "v1.0.1", + Path: filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext"), + }, bm) + + fakeBin, err := os.ReadFile(filepath.Join(tempDir, "extensions/gh-bin-ext/gh-bin-ext")) + assert.NoError(t, err) + assert.Equal(t, "FAKE BINARY", string(fakeBin)) + + assert.Equal(t, "", stdout.String()) + assert.Equal(t, "", stderr.String()) +} + func TestManager_repo_not_found(t *testing.T) { repo := ghrepo.NewWithHost("owner", "gh-bin-ext", "example.com") From 352737cb6026fcf6ce0f5a203bc5e4604bac6b98 Mon Sep 17 00:00:00 2001 From: William Martin Date: Fri, 13 Sep 2024 14:22:34 +0200 Subject: [PATCH 056/301] Use api subdomains for commands using ghinstance package --- internal/ghinstance/host.go | 14 ++++++++++++++ internal/ghinstance/host_test.go | 8 ++++++++ pkg/cmd/attestation/verify/verify_test.go | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/internal/ghinstance/host.go b/internal/ghinstance/host.go index 729d47f31..f80bfef57 100644 --- a/internal/ghinstance/host.go +++ b/internal/ghinstance/host.go @@ -73,6 +73,13 @@ func GraphQLEndpoint(hostname string) string { if isGarage(hostname) { return fmt.Sprintf("https://%s/api/graphql", hostname) } + // Once we change Tenancy to no longer be treated as Enterprise, this + // conditional can be removed as the flow will fall through to the bottom. + // However, we can't do that until we've investigated all places in which + // Tenancy is currently treated as Enterprise. + if IsTenancy(hostname) { + return fmt.Sprintf("https://api.%s/graphql", hostname) + } if IsEnterprise(hostname) { return fmt.Sprintf("https://%s/api/graphql", hostname) } @@ -86,6 +93,13 @@ func RESTPrefix(hostname string) string { if isGarage(hostname) { return fmt.Sprintf("https://%s/api/v3/", hostname) } + // Once we change Tenancy to no longer be treated as Enterprise, this + // conditional can be removed as the flow will fall through to the bottom. + // However, we can't do that until we've investigated all places in which + // Tenancy is currently treated as Enterprise. + if IsTenancy(hostname) { + return fmt.Sprintf("https://api.%s/", hostname) + } if IsEnterprise(hostname) { return fmt.Sprintf("https://%s/api/v3/", hostname) } diff --git a/internal/ghinstance/host_test.go b/internal/ghinstance/host_test.go index 673fad24c..e9fb97008 100644 --- a/internal/ghinstance/host_test.go +++ b/internal/ghinstance/host_test.go @@ -254,6 +254,10 @@ func TestGraphQLEndpoint(t *testing.T) { host: "ghe.io", want: "https://ghe.io/api/graphql", }, + { + host: "tenant.ghe.com", + want: "https://api.tenant.ghe.com/graphql", + }, } for _, tt := range tests { t.Run(tt.host, func(t *testing.T) { @@ -285,6 +289,10 @@ func TestRESTPrefix(t *testing.T) { host: "ghe.io", want: "https://ghe.io/api/v3/", }, + { + host: "tenant.ghe.com", + want: "https://api.tenant.ghe.com/", + }, } for _, tt := range tests { t.Run(tt.host, func(t *testing.T) { diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go index 6f71b537f..3ff309922 100644 --- a/pkg/cmd/attestation/verify/verify_test.go +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -43,7 +43,7 @@ func TestNewVerifyCmd(t *testing.T) { }, }, } - testReg.Register(httpmock.REST(http.MethodGet, "api/v3/meta"), + testReg.Register(httpmock.REST(http.MethodGet, "meta"), httpmock.StatusJSONResponse(200, &metaResp)) f := &cmdutil.Factory{ From 420d80b4980f1cffd39b0b0b6006c4a9ab1de254 Mon Sep 17 00:00:00 2001 From: William Martin Date: Fri, 13 Sep 2024 14:48:55 +0200 Subject: [PATCH 057/301] Update go-gh to use api subdomains --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 78ceb7f70..54d02ec8e 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.9.0 + github.com/cli/go-gh/v2 v2.10.0 github.com/cli/oauth v1.0.1 github.com/cli/safeexec v1.0.1 github.com/cpuguy83/go-md2man/v2 v2.0.4 diff --git a/go.sum b/go.sum index 37c40407d..416f2ee59 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.9.0 h1:D3lTjEneMYl54M+WjZ+kRPrR5CEJ5BHS05isBPOV3LI= -github.com/cli/go-gh/v2 v2.9.0/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE= +github.com/cli/go-gh/v2 v2.10.0 h1:GMflBKoErBXlLvN2euxzL+p7JaM8erlSmw0cT7uZr7M= +github.com/cli/go-gh/v2 v2.10.0/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE= github.com/cli/oauth v1.0.1 h1:pXnTFl/qUegXHK531Dv0LNjW4mLx626eS42gnzfXJPA= github.com/cli/oauth v1.0.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= From 24336f1344f57af4f6c41c4d50481a11ec742239 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 16 Sep 2024 13:42:51 -0400 Subject: [PATCH 058/301] Enhance gh repo create docs, fix random cmd link Relates #8946 - updates the documentation within `gh repo create` to include links to lookup .gitignore templates and licenses - fixes link markup within `gh auth setup-git` so link is formatted correctly on https://cli.github.com --- pkg/cmd/auth/setupgit/setupgit.go | 2 +- pkg/cmd/repo/create/create.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/auth/setupgit/setupgit.go b/pkg/cmd/auth/setupgit/setupgit.go index c1200b475..0ff7b6903 100644 --- a/pkg/cmd/auth/setupgit/setupgit.go +++ b/pkg/cmd/auth/setupgit/setupgit.go @@ -36,7 +36,7 @@ func NewCmdSetupGit(f *cmdutil.Factory, runF func(*SetupGitOptions) error) *cobr Long: heredoc.Docf(` This command configures %[1]sgit%[1]s to use GitHub CLI as a credential helper. For more information on git credential helpers please reference: - https://git-scm.com/docs/gitcredentials. + . By default, GitHub CLI will be set as the credential helper for all authenticated hosts. If there is no authenticated hosts the command fails with an error. diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 8b5dd515a..28ca0d1e8 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -93,7 +93,12 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co To create a remote repository from an existing local repository, specify the source directory with %[1]s--source%[1]s. By default, the remote repository name will be the name of the source directory. + Pass %[1]s--push%[1]s to push any local commits to the new repository. + + For language or platform .gitignore templates to use with %[1]s--gitignore%[1]s, . + + For license keywords to use with %[1]s--license%[1]s, . `, "`"), Example: heredoc.Doc(` # create a repository interactively From 8c8423aa3dfa147495644867d1031432fdb6cb45 Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Thu, 12 Sep 2024 15:04:02 -0700 Subject: [PATCH 059/301] better error for att verify custom issuer mismatch Signed-off-by: Brian DeHamer Co-authored-by: Zach Steindler Co-authored-by: Phill MV --- .../attestation/verification/extensions.go | 32 +++++++- .../verification/extensions_test.go | 74 ++++++++++++++++--- .../attestation/verification/mock_verifier.go | 1 + pkg/cmd/attestation/verify/policy.go | 5 +- pkg/cmd/attestation/verify/policy_test.go | 1 - pkg/cmd/attestation/verify/verify.go | 5 +- .../verify/verify_integration_test.go | 6 +- pkg/cmd/attestation/verify/verify_test.go | 30 ++++---- 8 files changed, 115 insertions(+), 39 deletions(-) diff --git a/pkg/cmd/attestation/verification/extensions.go b/pkg/cmd/attestation/verification/extensions.go index 727feb72c..2ffb11a9d 100644 --- a/pkg/cmd/attestation/verification/extensions.go +++ b/pkg/cmd/attestation/verification/extensions.go @@ -6,20 +6,25 @@ import ( "strings" ) -func VerifyCertExtensions(results []*AttestationProcessingResult, tenant, owner, repo string) error { +var ( + GitHubOIDCIssuer = "https://token.actions.githubusercontent.com" + GitHubTenantOIDCIssuer = "https://token.actions.%s.ghe.com" +) + +func VerifyCertExtensions(results []*AttestationProcessingResult, tenant, owner, repo, issuer string) error { if len(results) == 0 { return errors.New("no attestations proccessing results") } for _, attestation := range results { - if err := verifyCertExtensions(attestation, tenant, owner, repo); err != nil { + if err := verifyCertExtensions(attestation, tenant, owner, repo, issuer); err != nil { return err } } return nil } -func verifyCertExtensions(attestation *AttestationProcessingResult, tenant, owner, repo string) error { +func verifyCertExtensions(attestation *AttestationProcessingResult, tenant, owner, repo, issuer string) error { var want string if tenant == "" { @@ -46,5 +51,26 @@ func verifyCertExtensions(attestation *AttestationProcessingResult, tenant, owne } } + // if issuer is anything other than the default, use the user-provided value; + // otherwise, select the appropriate default based on the tenant + if issuer != GitHubOIDCIssuer { + want = issuer + } else { + if tenant != "" { + want = fmt.Sprintf(GitHubTenantOIDCIssuer, tenant) + } else { + want = GitHubOIDCIssuer + } + } + + certIssuer := attestation.VerificationResult.Signature.Certificate.Extensions.Issuer + if !strings.EqualFold(want, certIssuer) { + if strings.Index(certIssuer, want+"/") == 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", want, certIssuer) + } else { + return fmt.Errorf("expected Issuer to be %s, got %s", want, certIssuer) + } + } + return nil } diff --git a/pkg/cmd/attestation/verification/extensions_test.go b/pkg/cmd/attestation/verification/extensions_test.go index c86f2030a..5eb28829d 100644 --- a/pkg/cmd/attestation/verification/extensions_test.go +++ b/pkg/cmd/attestation/verification/extensions_test.go @@ -17,6 +17,7 @@ func TestVerifyCertExtensions(t *testing.T) { Extensions: certificate.Extensions{ SourceRepositoryOwnerURI: "https://github.com/owner", SourceRepositoryURI: "https://github.com/owner/repo", + Issuer: "https://token.actions.githubusercontent.com", }, }, }, @@ -25,32 +26,72 @@ func TestVerifyCertExtensions(t *testing.T) { } t.Run("VerifyCertExtensions with owner and repo", func(t *testing.T) { - err := VerifyCertExtensions(results, "", "owner", "owner/repo") + err := VerifyCertExtensions(results, "", "owner", "owner/repo", GitHubOIDCIssuer) require.NoError(t, err) }) t.Run("VerifyCertExtensions with owner and repo, but wrong tenant", func(t *testing.T) { - err := VerifyCertExtensions(results, "foo", "owner", "owner/repo") + err := VerifyCertExtensions(results, "foo", "owner", "owner/repo", GitHubOIDCIssuer) require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://foo.ghe.com/owner, got https://github.com/owner") }) t.Run("VerifyCertExtensions with owner", func(t *testing.T) { - err := VerifyCertExtensions(results, "", "owner", "") + err := VerifyCertExtensions(results, "", "owner", "", GitHubOIDCIssuer) require.NoError(t, err) }) t.Run("VerifyCertExtensions with wrong owner", func(t *testing.T) { - err := VerifyCertExtensions(results, "", "wrong", "") + err := VerifyCertExtensions(results, "", "wrong", "", GitHubOIDCIssuer) require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://github.com/wrong, got https://github.com/owner") }) t.Run("VerifyCertExtensions with wrong repo", func(t *testing.T) { - err := VerifyCertExtensions(results, "", "owner", "wrong") + err := VerifyCertExtensions(results, "", "owner", "wrong", GitHubOIDCIssuer) require.ErrorContains(t, err, "expected SourceRepositoryURI to be https://github.com/wrong, got https://github.com/owner/repo") }) + + t.Run("VerifyCertExtensions with wrong issuer", func(t *testing.T) { + err := VerifyCertExtensions(results, "", "owner", "", "wrong") + require.ErrorContains(t, err, "expected Issuer to be wrong, got https://token.actions.githubusercontent.com") + }) +} + +func TestVerifyCertExtensionsCustomizedIssuer(t *testing.T) { + results := []*AttestationProcessingResult{ + { + VerificationResult: &verify.VerificationResult{ + Signature: &verify.SignatureVerificationResult{ + Certificate: &certificate.Summary{ + Extensions: certificate.Extensions{ + SourceRepositoryOwnerURI: "https://github.com/owner", + SourceRepositoryURI: "https://github.com/owner/repo", + Issuer: "https://token.actions.githubusercontent.com/foo-bar", + }, + }, + }, + }, + }, + } + + t.Run("VerifyCertExtensions with exact issuer match", func(t *testing.T) { + err := VerifyCertExtensions(results, "", "owner", "owner/repo", "https://token.actions.githubusercontent.com/foo-bar") + require.NoError(t, err) + }) + + t.Run("VerifyCertExtensions with partial issuer match", func(t *testing.T) { + err := VerifyCertExtensions(results, "", "owner", "owner/repo", "https://token.actions.githubusercontent.com") + 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") + }) + + t.Run("VerifyCertExtensions with wrong issuer", func(t *testing.T) { + err := VerifyCertExtensions(results, "", "owner", "", "wrong") + require.ErrorContains(t, err, "expected Issuer to be wrong, got https://token.actions.githubusercontent.com/foo-bar") + }) } func TestVerifyTenancyCertExtensions(t *testing.T) { + defaultIssuer := GitHubOIDCIssuer + results := []*AttestationProcessingResult{ { VerificationResult: &verify.VerificationResult{ @@ -59,6 +100,7 @@ func TestVerifyTenancyCertExtensions(t *testing.T) { Extensions: certificate.Extensions{ SourceRepositoryOwnerURI: "https://foo.ghe.com/owner", SourceRepositoryURI: "https://foo.ghe.com/owner/repo", + Issuer: "https://token.actions.foo.ghe.com", }, }, }, @@ -67,32 +109,42 @@ func TestVerifyTenancyCertExtensions(t *testing.T) { } t.Run("VerifyCertExtensions with owner and repo", func(t *testing.T) { - err := VerifyCertExtensions(results, "foo", "owner", "owner/repo") + err := VerifyCertExtensions(results, "foo", "owner", "owner/repo", defaultIssuer) require.NoError(t, err) }) t.Run("VerifyCertExtensions with owner and repo, no tenant", func(t *testing.T) { - err := VerifyCertExtensions(results, "", "owner", "owner/repo") + err := VerifyCertExtensions(results, "", "owner", "owner/repo", defaultIssuer) require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://github.com/owner, got https://foo.ghe.com/owner") }) t.Run("VerifyCertExtensions with owner and repo, wrong tenant", func(t *testing.T) { - err := VerifyCertExtensions(results, "bar", "owner", "owner/repo") + err := VerifyCertExtensions(results, "bar", "owner", "owner/repo", defaultIssuer) require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://bar.ghe.com/owner, got https://foo.ghe.com/owner") }) t.Run("VerifyCertExtensions with owner", func(t *testing.T) { - err := VerifyCertExtensions(results, "foo", "owner", "") + err := VerifyCertExtensions(results, "foo", "owner", "", defaultIssuer) require.NoError(t, err) }) t.Run("VerifyCertExtensions with wrong owner", func(t *testing.T) { - err := VerifyCertExtensions(results, "foo", "wrong", "") + err := VerifyCertExtensions(results, "foo", "wrong", "", defaultIssuer) require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://foo.ghe.com/wrong, got https://foo.ghe.com/owner") }) t.Run("VerifyCertExtensions with wrong repo", func(t *testing.T) { - err := VerifyCertExtensions(results, "foo", "owner", "wrong") + err := VerifyCertExtensions(results, "foo", "owner", "wrong", defaultIssuer) require.ErrorContains(t, err, "expected SourceRepositoryURI to be https://foo.ghe.com/wrong, got https://foo.ghe.com/owner/repo") }) + + t.Run("VerifyCertExtensions with correct, non-default issuer", func(t *testing.T) { + err := VerifyCertExtensions(results, "foo", "owner", "owner/repo", "https://token.actions.foo.ghe.com") + require.NoError(t, err) + }) + + t.Run("VerifyCertExtensions with wrong issuer", func(t *testing.T) { + err := VerifyCertExtensions(results, "foo", "owner", "owner/repo", "wrong") + require.ErrorContains(t, err, "expected Issuer to be wrong, got https://token.actions.foo.ghe.com") + }) } diff --git a/pkg/cmd/attestation/verification/mock_verifier.go b/pkg/cmd/attestation/verification/mock_verifier.go index 9943e6a97..cb3a4c061 100644 --- a/pkg/cmd/attestation/verification/mock_verifier.go +++ b/pkg/cmd/attestation/verification/mock_verifier.go @@ -34,6 +34,7 @@ func (v *MockSigstoreVerifier) Verify(attestations []*api.Attestation, policy ve BuildSignerURI: "https://github.com/github/example/.github/workflows/release.yml@refs/heads/main", SourceRepositoryOwnerURI: "https://github.com/sigstore", SourceRepositoryURI: "https://github.com/sigstore/sigstore-js", + Issuer: "https://token.actions.githubusercontent.com", }, }, }, diff --git a/pkg/cmd/attestation/verify/policy.go b/pkg/cmd/attestation/verify/policy.go index 0c7a686c2..f41b2f66b 100644 --- a/pkg/cmd/attestation/verify/policy.go +++ b/pkg/cmd/attestation/verify/policy.go @@ -13,8 +13,6 @@ import ( ) const ( - GitHubOIDCIssuer = "https://token.actions.githubusercontent.com" - GitHubTenantOIDCIssuer = "https://token.actions.%s.ghe.com" // represents the GitHub hosted runner in the certificate RunnerEnvironment extension GitHubRunner = "github-hosted" hostRegex = `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+.*$` @@ -51,7 +49,8 @@ func buildCertificateIdentityOption(opts *Options, runnerEnv string) (verify.Pol return nil, err } - issuerMatcher, err := verify.NewIssuerMatcher(opts.OIDCIssuer, "") + // Accept any issuer, we will verify the issuer as part of the extension verification + issuerMatcher, err := verify.NewIssuerMatcher("", ".*") if err != nil { return nil, err } diff --git a/pkg/cmd/attestation/verify/policy_test.go b/pkg/cmd/attestation/verify/policy_test.go index 89f989b45..ae1e52955 100644 --- a/pkg/cmd/attestation/verify/policy_test.go +++ b/pkg/cmd/attestation/verify/policy_test.go @@ -22,7 +22,6 @@ func TestBuildPolicy(t *testing.T) { opts := &Options{ ArtifactPath: artifactPath, - OIDCIssuer: GitHubOIDCIssuer, Owner: "sigstore", SANRegex: "^https://github.com/sigstore/", } diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 9e508e994..17b8a84d1 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -151,7 +151,6 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command } config.TrustDomain = td opts.Tenant = tenant - opts.OIDCIssuer = fmt.Sprintf(GitHubTenantOIDCIssuer, tenant) } // set policy flags based on what has been provided @@ -192,7 +191,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command verifyCmd.Flags().StringVarP(&opts.SignerRepo, "signer-repo", "", "", "Repository of reusable workflow that signed attestation in the format /") verifyCmd.Flags().StringVarP(&opts.SignerWorkflow, "signer-workflow", "", "", "Workflow that signed attestation in the format [host/]////") verifyCmd.MarkFlagsMutuallyExclusive("cert-identity", "cert-identity-regex", "signer-repo", "signer-workflow") - verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", GitHubOIDCIssuer, "Issuer of the OIDC token") + verifyCmd.Flags().StringVarP(&opts.OIDCIssuer, "cert-oidc-issuer", "", verification.GitHubOIDCIssuer, "Issuer of the OIDC token") verifyCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use") return verifyCmd @@ -269,7 +268,7 @@ func runVerify(opts *Options) error { } // Verify extensions - if err := verification.VerifyCertExtensions(sigstoreRes.VerifyResults, opts.Tenant, opts.Owner, opts.Repo); err != nil { + if err := verification.VerifyCertExtensions(sigstoreRes.VerifyResults, opts.Tenant, opts.Owner, opts.Repo, opts.OIDCIssuer); err != nil { opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Verification failed")) return err } diff --git a/pkg/cmd/attestation/verify/verify_integration_test.go b/pkg/cmd/attestation/verify/verify_integration_test.go index df24b31c4..4b0f0adfb 100644 --- a/pkg/cmd/attestation/verify/verify_integration_test.go +++ b/pkg/cmd/attestation/verify/verify_integration_test.go @@ -38,7 +38,7 @@ func TestVerifyIntegration(t *testing.T) { DigestAlgorithm: "sha512", Logger: logger, OCIClient: oci.NewLiveClient(), - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", SANRegex: "^https://github.com/sigstore/", SigstoreVerifier: verification.NewLiveSigstoreVerifier(sigstoreConfig), @@ -180,7 +180,7 @@ func TestVerifyIntegrationReusableWorkflow(t *testing.T) { DigestAlgorithm: "sha256", Logger: logger, OCIClient: oci.NewLiveClient(), - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, SigstoreVerifier: verification.NewLiveSigstoreVerifier(sigstoreConfig), } @@ -269,7 +269,7 @@ func TestVerifyIntegrationReusableWorkflowSignerWorkflow(t *testing.T) { DigestAlgorithm: "sha256", Logger: logger, OCIClient: oci.NewLiveClient(), - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "malancas", Repo: "malancas/attest-demo", SigstoreVerifier: verification.NewLiveSigstoreVerifier(sigstoreConfig), diff --git a/pkg/cmd/attestation/verify/verify_test.go b/pkg/cmd/attestation/verify/verify_test.go index 6f71b537f..8b211fe4c 100644 --- a/pkg/cmd/attestation/verify/verify_test.go +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -71,7 +71,7 @@ func TestNewVerifyCmd(t *testing.T) { BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"), DigestAlgorithm: "sha384", Limit: 30, - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), Hostname: "github.com", @@ -86,7 +86,7 @@ func TestNewVerifyCmd(t *testing.T) { BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"), DigestAlgorithm: "sha256", Limit: 30, - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", SANRegex: "(?i)^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), @@ -102,7 +102,7 @@ func TestNewVerifyCmd(t *testing.T) { BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"), DigestAlgorithm: "sha256", Limit: 30, - OIDCIssuer: "https://token.actions.foo.ghe.com", + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", SANRegex: "(?i)^https://foo.ghe.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), @@ -118,7 +118,7 @@ func TestNewVerifyCmd(t *testing.T) { BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"), DigestAlgorithm: "sha256", Limit: 30, - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", SANRegex: "(?i)^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), @@ -134,7 +134,7 @@ func TestNewVerifyCmd(t *testing.T) { BundlePath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"), DigestAlgorithm: "sha512", Limit: 30, - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", SANRegex: "(?i)^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), @@ -148,7 +148,7 @@ func TestNewVerifyCmd(t *testing.T) { wants: Options{ ArtifactPath: test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"), DigestAlgorithm: "sha256", - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", Limit: 30, SANRegex: "(?i)^https://github.com/sigstore/", @@ -163,7 +163,7 @@ func TestNewVerifyCmd(t *testing.T) { wants: Options{ ArtifactPath: artifactPath, DigestAlgorithm: "sha256", - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", Repo: "sigstore/sigstore-js", Limit: 30, @@ -179,7 +179,7 @@ func TestNewVerifyCmd(t *testing.T) { ArtifactPath: artifactPath, DigestAlgorithm: "sha256", Limit: 30, - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", SANRegex: "(?i)^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), @@ -193,7 +193,7 @@ func TestNewVerifyCmd(t *testing.T) { wants: Options{ ArtifactPath: artifactPath, DigestAlgorithm: "sha256", - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", Limit: 101, SANRegex: "(?i)^https://github.com/sigstore/", @@ -208,7 +208,7 @@ func TestNewVerifyCmd(t *testing.T) { wants: Options{ ArtifactPath: artifactPath, DigestAlgorithm: "sha256", - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", Limit: 0, SANRegex: "(?i)^https://github.com/sigstore/", @@ -224,7 +224,7 @@ func TestNewVerifyCmd(t *testing.T) { ArtifactPath: artifactPath, DigestAlgorithm: "sha256", Limit: 30, - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", SAN: "https://github.com/sigstore/", SANRegex: "(?i)^https://github.com/sigstore/", @@ -241,7 +241,7 @@ func TestNewVerifyCmd(t *testing.T) { BundlePath: bundlePath, DigestAlgorithm: "sha256", Limit: 30, - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", SANRegex: "(?i)^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), @@ -331,7 +331,7 @@ func TestJSONOutput(t *testing.T) { APIClient: api.NewTestClient(), Logger: io.NewHandler(testIO), OCIClient: oci.MockClient{}, - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", SANRegex: "^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), @@ -354,7 +354,7 @@ func TestRunVerify(t *testing.T) { APIClient: api.NewTestClient(), Logger: logger, OCIClient: oci.MockClient{}, - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", SANRegex: "^https://github.com/sigstore/", SigstoreVerifier: verification.NewMockSigstoreVerifier(t), @@ -474,7 +474,7 @@ func TestRunVerify(t *testing.T) { APIClient: api.NewTestClient(), DigestAlgorithm: "sha512", Logger: logger, - OIDCIssuer: GitHubOIDCIssuer, + OIDCIssuer: verification.GitHubOIDCIssuer, Owner: "sigstore", SAN: SigstoreSanValue, SigstoreVerifier: verification.NewMockSigstoreVerifier(t), From 8e8fc696f13ce3396eabb92471bc4ae71a86274e Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Thu, 12 Sep 2024 09:20:02 -0700 Subject: [PATCH 060/301] disable auth check for att trusted-root cmd Signed-off-by: Brian DeHamer --- pkg/cmd/attestation/trustedroot/trustedroot.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cmd/attestation/trustedroot/trustedroot.go b/pkg/cmd/attestation/trustedroot/trustedroot.go index c9c3fdb04..a79c32ddb 100644 --- a/pkg/cmd/attestation/trustedroot/trustedroot.go +++ b/pkg/cmd/attestation/trustedroot/trustedroot.go @@ -94,6 +94,7 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com }, } + cmdutil.DisableAuthCheck(&trustedRootCmd) trustedRootCmd.Flags().StringVarP(&opts.TufUrl, "tuf-url", "", "", "URL to the TUF repository mirror") trustedRootCmd.Flags().StringVarP(&opts.TufRootPath, "tuf-root", "", "", "Path to the TUF root.json file on disk") trustedRootCmd.MarkFlagsRequiredTogether("tuf-url", "tuf-root") From cbe85253214d1ff3ce3c4fe707d55e99fe2adfaf Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Fri, 13 Sep 2024 15:26:14 -0700 Subject: [PATCH 061/301] enforce auth for tenancy Signed-off-by: Brian DeHamer --- .../attestation/trustedroot/trustedroot.go | 9 ++ .../trustedroot/trustedroot_test.go | 97 +++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/pkg/cmd/attestation/trustedroot/trustedroot.go b/pkg/cmd/attestation/trustedroot/trustedroot.go index a79c32ddb..7dba916eb 100644 --- a/pkg/cmd/attestation/trustedroot/trustedroot.go +++ b/pkg/cmd/attestation/trustedroot/trustedroot.go @@ -69,6 +69,15 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com } if ghinstance.IsTenancy(opts.Hostname) { + c, err := f.Config() + if err != nil { + return err + } + + if token, _ := c.Authentication().ActiveToken(opts.Hostname); token == "" { + return fmt.Errorf("not authenticated with %s", opts.Hostname) + } + hc, err := f.HttpClient() if err != nil { return err diff --git a/pkg/cmd/attestation/trustedroot/trustedroot_test.go b/pkg/cmd/attestation/trustedroot/trustedroot_test.go index 70b5ae2a1..b7d5f6c2f 100644 --- a/pkg/cmd/attestation/trustedroot/trustedroot_test.go +++ b/pkg/cmd/attestation/trustedroot/trustedroot_test.go @@ -3,6 +3,7 @@ package trustedroot import ( "bytes" "fmt" + "net/http" "strings" "testing" @@ -10,8 +11,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/gh" + ghmock "github.com/cli/cli/v2/internal/gh/mock" + "github.com/cli/cli/v2/pkg/cmd/attestation/api" "github.com/cli/cli/v2/pkg/cmd/attestation/test" "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" ) @@ -19,6 +25,9 @@ func TestNewTrustedRootCmd(t *testing.T) { testIO, _, _, _ := iostreams.Test() f := &cmdutil.Factory{ IOStreams: testIO, + Config: func() (gh.Config, error) { + return &ghmock.ConfigMock{}, nil + }, } testcases := []struct { @@ -72,6 +81,83 @@ func TestNewTrustedRootCmd(t *testing.T) { } } +func TestNewTrustedRootWithTenancy(t *testing.T) { + testIO, _, _, _ := iostreams.Test() + var testReg httpmock.Registry + var metaResp = api.MetaResponse{ + Domains: api.Domain{ + ArtifactAttestations: api.ArtifactAttestations{ + TrustDomain: "foo", + }, + }, + } + testReg.Register(httpmock.REST(http.MethodGet, "meta"), + httpmock.StatusJSONResponse(200, &metaResp)) + + httpClientFunc := func() (*http.Client, error) { + reg := &testReg + client := &http.Client{} + httpmock.ReplaceTripper(client, reg) + return client, nil + } + + cli := "--hostname foo-bar.ghe.com" + + t.Run("Host with NO auth configured", func(t *testing.T) { + f := &cmdutil.Factory{ + IOStreams: testIO, + Config: func() (gh.Config, error) { + return &ghmock.ConfigMock{ + AuthenticationFunc: func() gh.AuthConfig { + return &MockAuthConfig{Token: ""} + }, + }, nil + }, + } + + cmd := NewTrustedRootCmd(f, func(_ *Options) error { + return nil + }) + + argv := strings.Split(cli, " ") + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + _, err := cmd.ExecuteC() + + assert.Error(t, err) + assert.ErrorContains(t, err, "not authenticated") + }) + + t.Run("Host wth auth configured", func(t *testing.T) { + f := &cmdutil.Factory{ + IOStreams: testIO, + Config: func() (gh.Config, error) { + return &ghmock.ConfigMock{ + AuthenticationFunc: func() gh.AuthConfig { + return &MockAuthConfig{Token: "TOKEN"} + }, + }, nil + }, + HttpClient: httpClientFunc, + } + + cmd := NewTrustedRootCmd(f, func(_ *Options) error { + return nil + }) + + argv := strings.Split(cli, " ") + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + + _, err := cmd.ExecuteC() + assert.NoError(t, err) + }) +} + var newTUFErrClient tufClientInstantiator = func(o *tuf.Options) (*tuf.Client, error) { return nil, fmt.Errorf("failed to create TUF client") } @@ -99,3 +185,14 @@ func TestGetTrustedRoot(t *testing.T) { }) } + +type MockAuthConfig struct { + config.AuthConfig + Token string +} + +var _ gh.AuthConfig = (*MockAuthConfig)(nil) + +func (c *MockAuthConfig) ActiveToken(host string) (string, string) { + return c.Token, "" +} From 1511c9f225f5a7b2c884f0d35f5cdebe9cdfc858 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:28:48 +0000 Subject: [PATCH 062/301] build(deps): bump github.com/cpuguy83/go-md2man/v2 from 2.0.4 to 2.0.5 Bumps [github.com/cpuguy83/go-md2man/v2](https://github.com/cpuguy83/go-md2man) from 2.0.4 to 2.0.5. - [Release notes](https://github.com/cpuguy83/go-md2man/releases) - [Commits](https://github.com/cpuguy83/go-md2man/compare/v2.0.4...v2.0.5) --- updated-dependencies: - dependency-name: github.com/cpuguy83/go-md2man/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 54d02ec8e..51083f997 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/cli/go-gh/v2 v2.10.0 github.com/cli/oauth v1.0.1 github.com/cli/safeexec v1.0.1 - github.com/cpuguy83/go-md2man/v2 v2.0.4 + github.com/cpuguy83/go-md2man/v2 v2.0.5 github.com/creack/pty v1.1.23 github.com/distribution/reference v0.5.0 github.com/gabriel-vasile/mimetype v1.4.5 diff --git a/go.sum b/go.sum index 416f2ee59..f0e05ee81 100644 --- a/go.sum +++ b/go.sum @@ -109,8 +109,9 @@ github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AX github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= From 3bcedfe7f071904af24b0d9096980e53844ca42a Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Tue, 17 Sep 2024 14:18:00 -0700 Subject: [PATCH 063/301] Update pkg/cmd/attestation/trustedroot/trustedroot_test.go Co-authored-by: Fredrik Skogman --- pkg/cmd/attestation/trustedroot/trustedroot_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/attestation/trustedroot/trustedroot_test.go b/pkg/cmd/attestation/trustedroot/trustedroot_test.go index b7d5f6c2f..c3c1818dd 100644 --- a/pkg/cmd/attestation/trustedroot/trustedroot_test.go +++ b/pkg/cmd/attestation/trustedroot/trustedroot_test.go @@ -130,7 +130,7 @@ func TestNewTrustedRootWithTenancy(t *testing.T) { assert.ErrorContains(t, err, "not authenticated") }) - t.Run("Host wth auth configured", func(t *testing.T) { + t.Run("Host with auth configured", func(t *testing.T) { f := &cmdutil.Factory{ IOStreams: testIO, Config: func() (gh.Config, error) { From dded0391254781d1222b1050885a5c658b4461b0 Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Thu, 12 Sep 2024 10:48:12 +0100 Subject: [PATCH 064/301] Improve the suggested command for creating an issue when an extension doesn't have a binary for your platform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When installing an extension, the CLI must to select the correct binary to download for the machine (see the [`installBin` function](https://github.com/cli/cli/blob/78c1d00eccac1b2ae82ac0bfeea3e2292c98056a/pkg/cmd/extension/manager.go#L240)). By default, the CLI will download a binary matching the current machine's architecture. If a suitable binary isn't available, the CLI [outputs an error](https://github.com/cli/cli/blob/78c1d00eccac1b2ae82ac0bfeea3e2292c98056a/pkg/cmd/extension/manager.go#L278), directing the user to create an issue on the extension's repository: ```go if asset == nil { return fmt.Errorf( "%[1]s unsupported for %[2]s. Open an issue: `gh issue create -R %[3]s/%[1]s -t'Support %[2]s'`", repo.RepoName(), platform, repo.RepoOwner()) } ``` The issue this creates isn't very clear or helpful. It isn't obvious where it is coming from, or what you need to do. This improves the suggested command, adding a better title to the issue and an explanatory body. To test this, try installing my `timrogers/gh-extension-without-binary` extension, which only has an esoteric `linux-ppc64` binary 😸: ```bash gh extension install timrogers/gh-extension-without-binary ``` You'll get a nice output like this: ``` gh-extension-without-binary unsupported for darwin-arm64. Open an issue: `gh issue create -R timrogers/gh-extension-without-binary --title "Add support for the darwin-arm64 architecture" --body "This extension does not support the darwin-arm64 architecture. I tried to install it on a darwin-arm64 machine, and it failed due to the lack of an available binary. Would you be able to update the extension's build and release process to include the relevant binary? For more details, see ."` ``` ...which produces an issue like https://github.com/timrogers/gh-extension-without-binary/issues/4. I have tested the resulting command on macOS and Windows, so I am confident that it has *at least reasonable* cross-platform support. Fixes https://github.com/cli/cli/issues/9600. --- pkg/cmd/extension/manager.go | 15 +++++++++++++-- pkg/cmd/extension/manager_test.go | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 45186d689..37140ee0f 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -280,9 +280,11 @@ func (m *Manager) installBin(repo ghrepo.Interface, target string) error { } if asset == nil { + issueCreateCommand := generateMissingBinaryIssueCreateCommand(repo.RepoOwner(), repo.RepoName(), platform) + return fmt.Errorf( - "%[1]s unsupported for %[2]s. Open an issue: `gh issue create -R %[3]s/%[1]s -t'Support %[2]s'`", - repo.RepoName(), platform, repo.RepoOwner()) + "%[1]s unsupported for %[2]s. Open an issue: `%[3]s`", + repo.RepoName(), platform, issueCreateCommand) } name := repo.RepoName() @@ -334,6 +336,15 @@ func (m *Manager) installBin(repo ghrepo.Interface, target string) error { return nil } +func generateMissingBinaryIssueCreateCommand(repoOwner string, repoName string, currentPlatform string) string { + issueBody := generateMissingBinaryIssueBody(currentPlatform) + return fmt.Sprintf("gh issue create -R %[1]s/%[2]s --title \"Add support for the %[3]s architecture\" --body \"%[4]s\"", repoOwner, repoName, currentPlatform, issueBody) +} + +func generateMissingBinaryIssueBody(currentPlatform string) string { + return fmt.Sprintf("This extension does not support the %[1]s architecture. I tried to install it on a %[1]s machine, and it failed due to the lack of an available binary. Would you be able to update the extension's build and release process to include the relevant binary? For more details, see .", currentPlatform) +} + func writeManifest(dir, name string, data []byte) (writeErr error) { path := filepath.Join(dir, name) var f *os.File diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index 5de4e15cb..78fd40978 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -906,7 +906,7 @@ func TestManager_Install_binary_unsupported(t *testing.T) { m := newTestManager(tempDir, &client, nil, ios) err := m.Install(repo, "") - assert.EqualError(t, err, "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext -t'Support windows-amd64'`") + assert.EqualError(t, err, "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext --title \"Add support for the windows-amd64 architecture\" --body \"This extension does not support the windows-amd64 architecture. I tried to install it on a windows-amd64 machine, and it failed due to the lack of an available binary. Would you be able to update the extension's build and release process to include the relevant binary? For more details, see .\"`") assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) From 81d57a0643ff5856a96c0c5735184a571985f3df Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Thu, 12 Sep 2024 23:18:37 +0100 Subject: [PATCH 065/301] Improve error presentation --- pkg/cmd/extension/manager.go | 6 ++++-- pkg/cmd/extension/manager_test.go | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 37140ee0f..6bed532d3 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -280,11 +280,13 @@ func (m *Manager) installBin(repo ghrepo.Interface, target string) error { } if asset == nil { + cs := m.io.ColorScheme() + errorMessageInRed := fmt.Sprintf(cs.Red("%[1]s unsupported for %[2]s."), repo.RepoName(), platform) issueCreateCommand := generateMissingBinaryIssueCreateCommand(repo.RepoOwner(), repo.RepoName(), platform) return fmt.Errorf( - "%[1]s unsupported for %[2]s. Open an issue: `%[3]s`", - repo.RepoName(), platform, issueCreateCommand) + "%[1]s\n\nOpen an issue on the extension's repo by running the following command:\n\n `%[2]s`", + errorMessageInRed, issueCreateCommand) } name := repo.RepoName() diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index 78fd40978..ae3a9a469 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -906,7 +906,7 @@ func TestManager_Install_binary_unsupported(t *testing.T) { m := newTestManager(tempDir, &client, nil, ios) err := m.Install(repo, "") - assert.EqualError(t, err, "gh-bin-ext unsupported for windows-amd64. Open an issue: `gh issue create -R owner/gh-bin-ext --title \"Add support for the windows-amd64 architecture\" --body \"This extension does not support the windows-amd64 architecture. I tried to install it on a windows-amd64 machine, and it failed due to the lack of an available binary. Would you be able to update the extension's build and release process to include the relevant binary? For more details, see .\"`") + assert.EqualError(t, err, "gh-bin-ext unsupported for windows-amd64.\n\nOpen an issue on the extension's repo by running the following command:\n\n\t`gh issue create -R owner/gh-bin-ext --title \"Add support for the windows-amd64 architecture\" --body \"This extension does not support the windows-amd64 architecture. I tried to install it on a windows-amd64 machine, and it failed due to the lack of an available binary. Would you be able to update the extension's build and release process to include the relevant binary? For more details, see .\"") assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) From e7403b89d0c0be36dacae29f61cb040525ba7246 Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Wed, 18 Sep 2024 08:42:19 -0600 Subject: [PATCH 066/301] Add HasActiveToken to AuthConfig. Co-authored-by: William Martin --- internal/config/auth_config_test.go | 26 +++++++++++++++++++++++++- internal/config/config.go | 7 +++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/internal/config/auth_config_test.go b/internal/config/auth_config_test.go index ed000ff18..61245c650 100644 --- a/internal/config/auth_config_test.go +++ b/internal/config/auth_config_test.go @@ -52,8 +52,32 @@ func TestTokenFromKeyringForUserErrorsIfUsernameIsBlank(t *testing.T) { require.ErrorContains(t, err, "username cannot be blank") } +func TestHasActiveToken(t *testing.T) { + // Given the user has logged in for a host + authCfg := newTestAuthConfig(t) + _, err := authCfg.Login("github.com", "test-user", "test-token", "", false) + require.NoError(t, err) + + // When we check if that host has an active token + hasActiveToken := authCfg.HasActiveToken("github.com") + + // Then there is an active token + require.True(t, hasActiveToken, "expected there to be an active token") +} + +func TestHasNoActiveToken(t *testing.T) { + // Given there are no users logged in for a host + authCfg := newTestAuthConfig(t) + + // When we check if any host has an active token + hasActiveToken := authCfg.HasActiveToken("github.com") + + // Then there is no active token + require.False(t, hasActiveToken, "expected there to be no active token") +} + func TestTokenStoredInConfig(t *testing.T) { - // When the user has logged in insecurely + // Given the user has logged in insecurely authCfg := newTestAuthConfig(t) _, err := authCfg.Login("github.com", "test-user", "test-token", "", false) require.NoError(t, err) diff --git a/internal/config/config.go b/internal/config/config.go index 29b66b73b..f7c949a46 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -217,6 +217,13 @@ func (c *AuthConfig) ActiveToken(hostname string) (string, string) { return token, source } +// HasActiveToken returns true when a token for the hostname is +// present. +func (c *AuthConfig) HasActiveToken(hostname string) bool { + token, _ := c.ActiveToken(hostname) + return token != "" +} + // HasEnvToken returns true when a token has been specified in an // environment variable, else returns false. func (c *AuthConfig) HasEnvToken() bool { From 88d48f23654838ea7428ed47ed28f64f65405f6a Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:32:58 -0600 Subject: [PATCH 067/301] Add HasActiveToken method to AuthConfig interface --- internal/gh/gh.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/gh/gh.go b/internal/gh/gh.go index c39734075..e4431fdab 100644 --- a/internal/gh/gh.go +++ b/internal/gh/gh.go @@ -93,6 +93,9 @@ type Migration interface { // with knowledge on how to access encrypted storage when neccesarry. // Behavior is scoped to authentication specific tasks. type AuthConfig interface { + // HasActiveToken returns true when a token for the hostname is present. + HasActiveToken(hostname string) bool + // ActiveToken will retrieve the active auth token for the given hostname, searching environment variables, // general configuration, and finally encrypted storage. ActiveToken(hostname string) (token string, source string) From d8e77d256fe83791cf675e0ff539e1390ffd98d5 Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:35:11 -0600 Subject: [PATCH 068/301] Use new HasActiveToken method in trustedroot.go --- pkg/cmd/attestation/trustedroot/trustedroot.go | 2 +- .../attestation/trustedroot/trustedroot_test.go | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/attestation/trustedroot/trustedroot.go b/pkg/cmd/attestation/trustedroot/trustedroot.go index 7dba916eb..6f741dcd4 100644 --- a/pkg/cmd/attestation/trustedroot/trustedroot.go +++ b/pkg/cmd/attestation/trustedroot/trustedroot.go @@ -74,7 +74,7 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com return err } - if token, _ := c.Authentication().ActiveToken(opts.Hostname); token == "" { + if !c.Authentication().HasActiveToken(opts.Hostname) { return fmt.Errorf("not authenticated with %s", opts.Hostname) } diff --git a/pkg/cmd/attestation/trustedroot/trustedroot_test.go b/pkg/cmd/attestation/trustedroot/trustedroot_test.go index c3c1818dd..c4a259436 100644 --- a/pkg/cmd/attestation/trustedroot/trustedroot_test.go +++ b/pkg/cmd/attestation/trustedroot/trustedroot_test.go @@ -109,7 +109,7 @@ func TestNewTrustedRootWithTenancy(t *testing.T) { Config: func() (gh.Config, error) { return &ghmock.ConfigMock{ AuthenticationFunc: func() gh.AuthConfig { - return &MockAuthConfig{Token: ""} + return &stubAuthConfig{hasActiveToken: false} }, }, nil }, @@ -136,7 +136,7 @@ func TestNewTrustedRootWithTenancy(t *testing.T) { Config: func() (gh.Config, error) { return &ghmock.ConfigMock{ AuthenticationFunc: func() gh.AuthConfig { - return &MockAuthConfig{Token: "TOKEN"} + return &stubAuthConfig{hasActiveToken: true} }, }, nil }, @@ -186,13 +186,13 @@ func TestGetTrustedRoot(t *testing.T) { } -type MockAuthConfig struct { +type stubAuthConfig struct { config.AuthConfig - Token string + hasActiveToken bool } -var _ gh.AuthConfig = (*MockAuthConfig)(nil) +var _ gh.AuthConfig = (*stubAuthConfig)(nil) -func (c *MockAuthConfig) ActiveToken(host string) (string, string) { - return c.Token, "" +func (c *stubAuthConfig) HasActiveToken(host string) bool { + return c.hasActiveToken } From d24dfbeacfdc87ea108835ee79e9f766fd75f639 Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:35:35 -0600 Subject: [PATCH 069/301] Update comment formatting --- internal/config/config.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index f7c949a46..1b56d30b2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -217,8 +217,7 @@ func (c *AuthConfig) ActiveToken(hostname string) (string, string) { return token, source } -// HasActiveToken returns true when a token for the hostname is -// present. +// HasActiveToken returns true when a token for the hostname is present. func (c *AuthConfig) HasActiveToken(hostname string) bool { token, _ := c.ActiveToken(hostname) return token != "" From 195188cee8751247d94c0196ac5ab20371210591 Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Wed, 18 Sep 2024 18:55:20 +0100 Subject: [PATCH 070/301] Update pkg/cmd/extension/manager.go Co-authored-by: Tyler McGoffin --- pkg/cmd/extension/manager.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 6bed532d3..410d4c224 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -285,8 +285,8 @@ func (m *Manager) installBin(repo ghrepo.Interface, target string) error { issueCreateCommand := generateMissingBinaryIssueCreateCommand(repo.RepoOwner(), repo.RepoName(), platform) return fmt.Errorf( - "%[1]s\n\nOpen an issue on the extension's repo by running the following command:\n\n `%[2]s`", - errorMessageInRed, issueCreateCommand) + "%[1]s\n\nTo request support for %[2]s, open an issue on the extension's repo by running the following command:\n\n `%[3]s`", + errorMessageInRed, platform, issueCreateCommand) } name := repo.RepoName() From 665e41566fe0856fd8d4c2991f81535b50b7da61 Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Wed, 18 Sep 2024 19:07:32 +0100 Subject: [PATCH 071/301] Fix test --- pkg/cmd/extension/manager_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index ae3a9a469..0ad8e991e 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -906,7 +906,7 @@ func TestManager_Install_binary_unsupported(t *testing.T) { m := newTestManager(tempDir, &client, nil, ios) err := m.Install(repo, "") - assert.EqualError(t, err, "gh-bin-ext unsupported for windows-amd64.\n\nOpen an issue on the extension's repo by running the following command:\n\n\t`gh issue create -R owner/gh-bin-ext --title \"Add support for the windows-amd64 architecture\" --body \"This extension does not support the windows-amd64 architecture. I tried to install it on a windows-amd64 machine, and it failed due to the lack of an available binary. Would you be able to update the extension's build and release process to include the relevant binary? For more details, see .\"") + assert.EqualError(t, err, "gh-bin-ext unsupported for windows-amd64.\n\nTo request support for windows-amd64, open an issue on the extension's repo by running the following command:\n\n\t`gh issue create -R owner/gh-bin-ext --title \"Add support for the windows-amd64 architecture\" --body \"This extension does not support the windows-amd64 architecture. I tried to install it on a windows-amd64 machine, and it failed due to the lack of an available binary. Would you be able to update the extension's build and release process to include the relevant binary? For more details, see .\"`") assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) From 8123de9722c698c2ebbb21bdd32516ca7bcad1bf Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Wed, 18 Sep 2024 14:05:04 -0700 Subject: [PATCH 072/301] fix tenant-awareness for trusted-root command Signed-off-by: Brian DeHamer --- pkg/cmd/attestation/trustedroot/trustedroot.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/cmd/attestation/trustedroot/trustedroot.go b/pkg/cmd/attestation/trustedroot/trustedroot.go index c9c3fdb04..c52bdac15 100644 --- a/pkg/cmd/attestation/trustedroot/trustedroot.go +++ b/pkg/cmd/attestation/trustedroot/trustedroot.go @@ -116,6 +116,12 @@ func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error { // Disable local caching, so we get up-to-date response from TUF repository tufOpt.CacheValidity = 0 + // Target will be either the default trusted root, or the trust domain-qualified one + ghTR := defaultTR + if opts.TrustDomain != "" { + ghTR = fmt.Sprintf("%s.%s", opts.TrustDomain, defaultTR) + } + if opts.TufUrl != "" && opts.TufRootPath != "" { tufRoot, err := os.ReadFile(opts.TufRootPath) if err != nil { @@ -126,7 +132,7 @@ func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error { tufOpt.RepositoryBaseURL = opts.TufUrl tufOptions = append(tufOptions, tufConfig{ tufOptions: tufOpt, - targets: []string{defaultTR}, + targets: []string{ghTR}, }) } else { // Get from both Sigstore public good and GitHub private instance @@ -137,14 +143,9 @@ func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error { tufOpt = verification.GitHubTUFOptions() tufOpt.CacheValidity = 0 - targets := []string{defaultTR} - if opts.TrustDomain != "" { - targets = append(targets, fmt.Sprintf("%s.%s", - opts.TrustDomain, defaultTR)) - } tufOptions = append(tufOptions, tufConfig{ tufOptions: tufOpt, - targets: targets, + targets: []string{ghTR}, }) } From 5dd52463071d0eb053d675796eca1ff47bb20550 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 19 Sep 2024 14:52:16 -0700 Subject: [PATCH 073/301] Replace "GitHub Enterprise Server" with "other" in `gh auth login` prompt This change is meant to better support the login flow for other customers besides GitHub Enterprise Server customers that use the same login flow as GHES. --- pkg/cmd/auth/login/login.go | 2 +- pkg/cmd/auth/login/login_test.go | 52 ++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 9e27f28ac..2eded806e 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -222,7 +222,7 @@ func loginRun(opts *LoginOptions) error { } func promptForHostname(opts *LoginOptions) (string, error) { - options := []string{"GitHub.com", "GitHub Enterprise Server"} + options := []string{"GitHub.com", "other"} hostType, err := opts.Prompter.Select( "What account do you want to log into?", options[0], diff --git a/pkg/cmd/auth/login/login_test.go b/pkg/cmd/auth/login/login_test.go index d53dd3f0b..555606d56 100644 --- a/pkg/cmd/auth/login/login_test.go +++ b/pkg/cmd/auth/login/login_test.go @@ -2,6 +2,7 @@ package login import ( "bytes" + "fmt" "net/http" "regexp" "runtime" @@ -546,7 +547,7 @@ func Test_loginRun_Survey(t *testing.T) { wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://rebecca.chambers/settings/tokens"), }, { - name: "choose enterprise", + name: "choose other", wantHosts: heredoc.Doc(` brad.vickers: users: @@ -564,7 +565,7 @@ func Test_loginRun_Survey(t *testing.T) { pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) { switch prompt { case "What account do you want to log into?": - return prompter.IndexFor(opts, "GitHub Enterprise Server") + return prompter.IndexFor(opts, "other") case "What is your preferred protocol for Git operations on this host?": return prompter.IndexFor(opts, "HTTPS") case "How would you like to authenticate GitHub CLI?": @@ -803,3 +804,50 @@ func Test_loginRun_Survey(t *testing.T) { }) } } + +func Test_promptForHostname(t *testing.T) { + tests := []struct { + name string + options []string + selectedIndex int + // This is so we can test that the options in the function don't change + expectedSelection string + inputHostname string + expect string + }{ + { + name: "select GitHub.com", + selectedIndex: 0, + expectedSelection: "GitHub.com", + expect: "github.com", + }, + { + name: "select other", + selectedIndex: 1, + expectedSelection: "other", + inputHostname: "github.enterprise.com", + expect: "github.enterprise.com", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + promptMock := &prompter.PrompterMock{ + SelectFunc: func(_ string, _ string, options []string) (int, error) { + if options[tt.selectedIndex] != tt.expectedSelection { + return 0, fmt.Errorf("expected %s at index %d, but got %s", tt.expectedSelection, tt.selectedIndex, options[tt.selectedIndex]) + } + return tt.selectedIndex, nil + }, + InputHostnameFunc: func() (string, error) { + return tt.inputHostname, nil + }, + } + opts := &LoginOptions{ + Prompter: promptMock, + } + hostname, err := promptForHostname(opts) + require.NoError(t, err) + require.Equal(t, tt.expect, hostname) + }) + } +} From 46790977bb69023ae51a47469b39f6855d9c90af Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Thu, 19 Sep 2024 15:11:02 -0700 Subject: [PATCH 074/301] Add documentation explaining how to use `hostname` for `gh auth login` --- pkg/cmd/auth/login/login.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 2eded806e..613555dd7 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -59,6 +59,10 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm Long: heredoc.Docf(` Authenticate with a GitHub host. + Your %[1]shostname%[1]s is the domain where the GitHub instance you are logging into is hosted. + For example, "github.com" is the default hostname for GitHub.com. To authenticate with + a different hostname like github.enterprise.com, use that hostname instead. + The default authentication mode is a web-based browser flow. After completion, an authentication token will be stored securely in the system credential store. If a credential store is not found or there is an issue using it gh will fallback From 1ef71409f3605709e01beab4e7a227a9bc44272f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:11:28 +0000 Subject: [PATCH 075/301] build(deps): bump github.com/henvic/httpretty from 0.1.3 to 0.1.4 Bumps [github.com/henvic/httpretty](https://github.com/henvic/httpretty) from 0.1.3 to 0.1.4. - [Commits](https://github.com/henvic/httpretty/compare/v0.1.3...v0.1.4) --- updated-dependencies: - dependency-name: github.com/henvic/httpretty dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 54d02ec8e..9bf6be8e4 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.3.0 - github.com/henvic/httpretty v0.1.3 + github.com/henvic/httpretty v0.1.4 github.com/in-toto/attestation v1.1.0 github.com/joho/godotenv v1.5.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 diff --git a/go.sum b/go.sum index 416f2ee59..cdde6c210 100644 --- a/go.sum +++ b/go.sum @@ -251,8 +251,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/vault/api v1.12.2 h1:7YkCTE5Ni90TcmYHDBExdt4WGJxhpzaHqR6uGbQb/rE= github.com/hashicorp/vault/api v1.12.2/go.mod h1:LSGf1NGT1BnvFFnKVtnvcaLBM2Lz+gJdpL6HUYed8KE= -github.com/henvic/httpretty v0.1.3 h1:4A6vigjz6Q/+yAfTD4wqipCv+Px69C7Th/NhT0ApuU8= -github.com/henvic/httpretty v0.1.3/go.mod h1:UUEv7c2kHZ5SPQ51uS3wBpzPDibg2U3Y+IaXyHy5GBg= +github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU= +github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= From bc587f6576dc7a939545e791ee69ef969bcf5815 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 20 Sep 2024 13:55:20 -0700 Subject: [PATCH 076/301] Sentence case 'Other' option in hostname prompt --- pkg/cmd/auth/login/login.go | 2 +- pkg/cmd/auth/login/login_test.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 613555dd7..9066abd5d 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -226,7 +226,7 @@ func loginRun(opts *LoginOptions) error { } func promptForHostname(opts *LoginOptions) (string, error) { - options := []string{"GitHub.com", "other"} + options := []string{"GitHub.com", "Other"} hostType, err := opts.Prompter.Select( "What account do you want to log into?", options[0], diff --git a/pkg/cmd/auth/login/login_test.go b/pkg/cmd/auth/login/login_test.go index 555606d56..1cd955b1f 100644 --- a/pkg/cmd/auth/login/login_test.go +++ b/pkg/cmd/auth/login/login_test.go @@ -547,7 +547,7 @@ func Test_loginRun_Survey(t *testing.T) { wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://rebecca.chambers/settings/tokens"), }, { - name: "choose other", + name: "choose Other", wantHosts: heredoc.Doc(` brad.vickers: users: @@ -565,7 +565,7 @@ func Test_loginRun_Survey(t *testing.T) { pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) { switch prompt { case "What account do you want to log into?": - return prompter.IndexFor(opts, "other") + return prompter.IndexFor(opts, "Other") case "What is your preferred protocol for Git operations on this host?": return prompter.IndexFor(opts, "HTTPS") case "How would you like to authenticate GitHub CLI?": @@ -816,15 +816,15 @@ func Test_promptForHostname(t *testing.T) { expect string }{ { - name: "select GitHub.com", + name: "select 'GitHub.com'", selectedIndex: 0, expectedSelection: "GitHub.com", expect: "github.com", }, { - name: "select other", + name: "select 'Other'", selectedIndex: 1, - expectedSelection: "other", + expectedSelection: "Other", inputHostname: "github.enterprise.com", expect: "github.enterprise.com", }, From 13e916bcfb22366d2d5db10b486ae066ce54b494 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 20 Sep 2024 14:10:05 -0700 Subject: [PATCH 077/301] Change prompts for `gh auth login` to reflect change from GHE to Other --- internal/prompter/prompter.go | 2 +- pkg/cmd/auth/login/login.go | 2 +- pkg/cmd/auth/login/login_test.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go index 44579b189..1d4b11cbc 100644 --- a/internal/prompter/prompter.go +++ b/internal/prompter/prompter.go @@ -92,7 +92,7 @@ func (p *surveyPrompter) InputHostname() (string, error) { var result string err := p.ask( &survey.Input{ - Message: "GHE hostname:", + Message: "Hostname:", }, &result, survey.WithValidator(func(v interface{}) error { return ghinstance.HostnameValidator(v.(string)) })) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 9066abd5d..4d4db3259 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -228,7 +228,7 @@ func loginRun(opts *LoginOptions) error { func promptForHostname(opts *LoginOptions) (string, error) { options := []string{"GitHub.com", "Other"} hostType, err := opts.Prompter.Select( - "What account do you want to log into?", + "Where do you use GitHub?", options[0], options) if err != nil { diff --git a/pkg/cmd/auth/login/login_test.go b/pkg/cmd/auth/login/login_test.go index 1cd955b1f..3264ed91c 100644 --- a/pkg/cmd/auth/login/login_test.go +++ b/pkg/cmd/auth/login/login_test.go @@ -564,7 +564,7 @@ func Test_loginRun_Survey(t *testing.T) { prompterStubs: func(pm *prompter.PrompterMock) { pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) { switch prompt { - case "What account do you want to log into?": + case "Where do you use GitHub?": return prompter.IndexFor(opts, "Other") case "What is your preferred protocol for Git operations on this host?": return prompter.IndexFor(opts, "HTTPS") @@ -607,7 +607,7 @@ func Test_loginRun_Survey(t *testing.T) { prompterStubs: func(pm *prompter.PrompterMock) { pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) { switch prompt { - case "What account do you want to log into?": + case "Where do you use GitHub?": return prompter.IndexFor(opts, "GitHub.com") case "What is your preferred protocol for Git operations on this host?": return prompter.IndexFor(opts, "HTTPS") @@ -641,7 +641,7 @@ func Test_loginRun_Survey(t *testing.T) { prompterStubs: func(pm *prompter.PrompterMock) { pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) { switch prompt { - case "What account do you want to log into?": + case "Where do you use GitHub?": return prompter.IndexFor(opts, "GitHub.com") case "What is your preferred protocol for Git operations on this host?": return prompter.IndexFor(opts, "SSH") From 2059c630a27443df0d4c2185b26e22c592aaf3ef Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 20 Sep 2024 14:12:13 -0700 Subject: [PATCH 078/301] Update language on docstring for `gh auth login` --- pkg/cmd/auth/login/login.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 4d4db3259..a5e601960 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -59,8 +59,8 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm Long: heredoc.Docf(` Authenticate with a GitHub host. - Your %[1]shostname%[1]s is the domain where the GitHub instance you are logging into is hosted. - For example, "github.com" is the default hostname for GitHub.com. To authenticate with + The %[1]shostname%[1]s is the domain where the GitHub instance you are logging into is hosted. + For example, "github.com" is the hostname for GitHub.com. To authenticate with a different hostname like github.enterprise.com, use that hostname instead. The default authentication mode is a web-based browser flow. After completion, an From ccb830ca3674dc480cae08f026f3e298662e35e1 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Fri, 20 Sep 2024 15:51:07 -0700 Subject: [PATCH 079/301] Shorten language on Authenticate with a GitHub host. The default authentication mode is a web-based browser flow. After completion, an authentication token will be stored securely in the system credential store. If a credential store is not found or there is an issue using it gh will fallback to writing the token to a plain text file. See `gh auth status` for its stored location. Alternatively, use `--with-token` to pass in a token on standard input. The minimum required scopes for the token are: `repo`, `read:org`, and `gist`. Alternatively, gh will use the authentication token found in environment variables. This method is most suitable for "headless" use of gh such as in automation. See `gh help environment` for more info. To use gh in GitHub Actions, add `GH_TOKEN: ${{ github.token }}` to `env`. The git protocol to use for git operations on this host can be set with `--git-protocol`, or during the interactive prompting. Although login is for a single account on a host, setting the git protocol will take effect for all users on the host. Specifying `ssh` for the git protocol will detect existing SSH keys to upload, prompting to create and upload a new key if one is not found. This can be skipped with `--skip-ssh-key` flag. USAGE gh auth login [flags] FLAGS -p, --git-protocol string The protocol to use for git operations on this host: {ssh|https} -h, --hostname string The hostname of the GitHub instance to authenticate with --insecure-storage Save authentication credentials in plain text instead of credential store -s, --scopes strings Additional authentication scopes to request --skip-ssh-key Skip generate/upload SSH key prompt -w, --web Open a browser to authenticate --with-token Read token from standard input INHERITED FLAGS --help Show help for command EXAMPLES # Start interactive setup $ gh auth login # Authenticate against github.com by reading the token from a file $ gh auth login --with-token < mytoken.txt # Authenticate with specific host $ gh auth login --hostname enterprise.internal LEARN MORE Use `gh --help` for more information about a command. Read the manual at https://cli.github.com/manual Learn about exit codes using `gh help exit-codes` around Tylers-GitHub-MacBook.local --- pkg/cmd/auth/login/login.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index a5e601960..2a4f118cc 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -59,9 +59,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm Long: heredoc.Docf(` Authenticate with a GitHub host. - The %[1]shostname%[1]s is the domain where the GitHub instance you are logging into is hosted. - For example, "github.com" is the hostname for GitHub.com. To authenticate with - a different hostname like github.enterprise.com, use that hostname instead. + The %[1]shostname%[1]s is where you log in to GitHub. The default hostname is %[1]sgithub.com%[1]s. The default authentication mode is a web-based browser flow. After completion, an authentication token will be stored securely in the system credential store. From 1eda1b41a7af430a13d1c17f6c5d2fbbe1e6c3da Mon Sep 17 00:00:00 2001 From: Tim Rogers Date: Sat, 21 Sep 2024 14:56:43 -0500 Subject: [PATCH 080/301] Emit a log message when extension installation falls back to a darwin-amd64 binary on an Apple Silicon macOS machine --- pkg/cmd/extension/manager.go | 11 ++++++++--- pkg/cmd/extension/manager_test.go | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/extension/manager.go b/pkg/cmd/extension/manager.go index 410d4c224..2431c6b83 100644 --- a/pkg/cmd/extension/manager.go +++ b/pkg/cmd/extension/manager.go @@ -29,6 +29,8 @@ import ( // ErrInitialCommitFailed indicates the initial commit when making a new extension failed. var ErrInitialCommitFailed = errors.New("initial commit failed") +const darwinAmd64 = "darwin-amd64" + type Manager struct { dataDir func() string lookPath func(string) (string, error) @@ -266,13 +268,16 @@ func (m *Manager) installBin(repo ghrepo.Interface, target string) error { // if using an ARM-based Mac and an arm64 binary is unavailable, fall back to amd64 if a relevant binary is available and Rosetta 2 is installed if asset == nil && isMacARM { for _, a := range r.Assets { - if strings.HasSuffix(a.Name, "darwin-amd64") { + if strings.HasSuffix(a.Name, darwinAmd64) { if !hasRosetta() { return fmt.Errorf( - "%[1]s unsupported for %[2]s. Install Rosetta with `softwareupdate --install-rosetta` to use the available darwin-amd64 binary, or open an issue: `gh issue create -R %[3]s/%[1]s -t'Support %[2]s'`", - repo.RepoName(), platform, repo.RepoOwner()) + "%[1]s unsupported for %[2]s. Install Rosetta with `softwareupdate --install-rosetta` to use the available %[3]s binary, or open an issue: `gh issue create -R %[4]s/%[1]s -t'Support %[2]s'`", + repo.RepoName(), platform, darwinAmd64, repo.RepoOwner()) } + fallbackMessage := fmt.Sprintf("%[1]s not available for %[2]s. Falling back to compatible %[3]s binary", repo.RepoName(), platform, darwinAmd64) + fmt.Fprintln(m.io.Out, fallbackMessage) + asset = &a break } diff --git a/pkg/cmd/extension/manager_test.go b/pkg/cmd/extension/manager_test.go index 0ad8e991e..e25bc1496 100644 --- a/pkg/cmd/extension/manager_test.go +++ b/pkg/cmd/extension/manager_test.go @@ -1098,7 +1098,7 @@ func TestManager_Install_amd64_when_supported(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "FAKE BINARY", string(fakeBin)) - assert.Equal(t, "", stdout.String()) + assert.Equal(t, "gh-bin-ext not available for darwin-arm64. Falling back to compatible darwin-amd64 binary\n", stdout.String()) assert.Equal(t, "", stderr.String()) } From 029dc8ffc128361ac94b04414675089f3207f4f1 Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Mon, 23 Sep 2024 09:04:03 -0700 Subject: [PATCH 081/301] Change conditional in promptForHostname to better reflect prompter changes The old isEnterprise check no longer makes sense, given the prompter is providing 'other', not 'GitHub Enterprise Server' as its non-GitHub.com option. Additionally, there was an opportunity for cleaning up the code via early returns and the removal of the default hostname lookup if we don't need it. --- pkg/cmd/auth/login/login.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 2a4f118cc..a25bd9f85 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -233,12 +233,10 @@ func promptForHostname(opts *LoginOptions) (string, error) { return "", err } - isEnterprise := hostType == 1 - - hostname := ghinstance.Default() - if isEnterprise { - hostname, err = opts.Prompter.InputHostname() + isGitHubDotCom := hostType == 0 + if isGitHubDotCom { + return ghinstance.Default(), nil } - return hostname, err + return opts.Prompter.InputHostname() } From f4af65d9dfb246ce8c3cbbd09f247917e34de3bd Mon Sep 17 00:00:00 2001 From: Tyler McGoffin Date: Mon, 23 Sep 2024 09:35:57 -0700 Subject: [PATCH 082/301] Update docs language to remove possible confusion around 'where you log in' --- pkg/cmd/auth/login/login.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index a25bd9f85..1480a29a0 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -59,7 +59,8 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm Long: heredoc.Docf(` Authenticate with a GitHub host. - The %[1]shostname%[1]s is where you log in to GitHub. The default hostname is %[1]sgithub.com%[1]s. + The default hostname is %[1]sgithub.com%[1]s. This can be overridden using the %[1]s--hostname%[1]s + flag. The default authentication mode is a web-based browser flow. After completion, an authentication token will be stored securely in the system credential store. From fd8c4633e334f7bb5788fbb73fa9e0c02ebad7c9 Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:30:02 -0600 Subject: [PATCH 083/301] First pass at implementing `gh repo license list` --- pkg/cmd/repo/create/create.go | 4 +- pkg/cmd/repo/create/http.go | 11 -- pkg/cmd/repo/license/license.go | 18 ++ pkg/cmd/repo/license/list/list.go | 96 +++++++++ pkg/cmd/repo/license/list/list_test.go | 259 +++++++++++++++++++++++++ pkg/cmd/repo/repo.go | 2 + pkg/cmd/repo/shared/repo.go | 14 ++ 7 files changed, 391 insertions(+), 13 deletions(-) create mode 100644 pkg/cmd/repo/license/license.go create mode 100644 pkg/cmd/repo/license/list/list.go create mode 100644 pkg/cmd/repo/license/list/list_test.go diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 28ca0d1e8..4b8b18c70 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -244,7 +244,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co return nil, cobra.ShellCompDirectiveError } hostname, _ := cfg.Authentication().DefaultHost() - licenses, err := listLicenseTemplates(httpClient, hostname) + licenses, err := shared.ListLicenseTemplates(httpClient, hostname) if err != nil { return nil, cobra.ShellCompDirectiveError } @@ -830,7 +830,7 @@ func interactiveLicense(client *http.Client, hostname string, prompter iprompter return "", nil } - licenses, err := listLicenseTemplates(client, hostname) + licenses, err := shared.ListLicenseTemplates(client, hostname) if err != nil { return "", err } diff --git a/pkg/cmd/repo/create/http.go b/pkg/cmd/repo/create/http.go index 120683c08..ba99c3e4c 100644 --- a/pkg/cmd/repo/create/http.go +++ b/pkg/cmd/repo/create/http.go @@ -350,17 +350,6 @@ func listGitIgnoreTemplates(httpClient *http.Client, hostname string) ([]string, return gitIgnoreTemplates, nil } -// listLicenseTemplates uses API v3 here because license template isn't supported by GraphQL yet. -func listLicenseTemplates(httpClient *http.Client, hostname string) ([]api.License, error) { - var licenseTemplates []api.License - client := api.NewClientFromHTTP(httpClient) - err := client.REST(hostname, "GET", "licenses", nil, &licenseTemplates) - if err != nil { - return nil, err - } - return licenseTemplates, nil -} - // Returns the current username and any orgs that user is a member of. func userAndOrgs(httpClient *http.Client, hostname string) (string, []string, error) { client := api.NewClientFromHTTP(httpClient) diff --git a/pkg/cmd/repo/license/license.go b/pkg/cmd/repo/license/license.go new file mode 100644 index 000000000..84896f2cd --- /dev/null +++ b/pkg/cmd/repo/license/license.go @@ -0,0 +1,18 @@ +package license + +import ( + cmdList "github.com/cli/cli/v2/pkg/cmd/repo/license/list" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/spf13/cobra" +) + +func NewCmdLicense(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "license ", + Short: "View available repository license options", + } + + cmd.AddCommand(cmdList.NewCmdList(f, nil)) + + return cmd +} diff --git a/pkg/cmd/repo/license/list/list.go b/pkg/cmd/repo/license/list/list.go new file mode 100644 index 000000000..48f8a74d9 --- /dev/null +++ b/pkg/cmd/repo/license/list/list.go @@ -0,0 +1,96 @@ +package list + +import ( + "net/http" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/tableprinter" + "github.com/cli/cli/v2/pkg/cmd/repo/shared" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +type LicenseRenderer interface { + Render([]api.License, ListOptions) error +} + +type TableLicenseRenderer struct{} + +var licenseFields = []string{ + "key", + "name", +} + +type ListOptions struct { + IO *iostreams.IOStreams + HTTPClient func() (*http.Client, error) + Exporter cmdutil.Exporter + Config func() (gh.Config, error) +} + +func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { + opts := &ListOptions{ + IO: f.IOStreams, + HTTPClient: f.HttpClient, + Config: f.Config, + } + + cmd := &cobra.Command{ + Use: "list", + Short: "List available repository license options", + Aliases: []string{"ls"}, + Args: cmdutil.ExactArgs(0, "gh repo license list takes no arguments"), + RunE: func(cmd *cobra.Command, args []string) error { + + if runF != nil { + return runF(opts) + } + return listRun(opts) + }, + } + // cmdutil.AddFormatFlags(cmd, &opts.Exporter) + cmdutil.AddJSONFlags(cmd, &opts.Exporter, licenseFields) + return cmd +} + +func listRun(opts *ListOptions) error { + client, err := opts.HTTPClient() + if err != nil { + return err + } + + cfg, err := opts.Config() + if err != nil { + return err + } + + hostname, _ := cfg.Authentication().DefaultHost() + licenses, err := shared.ListLicenseTemplates(client, hostname) + if err != nil { + return err + } + + if len(licenses) == 0 { + return cmdutil.NewNoResultsError("no licenses found") + } + + if opts.Exporter != nil { + return opts.Exporter.Write(opts.IO, licenses) + } + + r := &TableLicenseRenderer{} + return r.Render(licenses, opts) +} + +func (r *TableLicenseRenderer) Render(licenses []api.License, opts *ListOptions) error { + t := tableprinter.New(opts.IO, tableprinter.WithHeader("KEY", "NAME")) + for _, l := range licenses { + t.AddField(l.Key) + t.AddField(l.Name) + t.EndRow() + } + + return t.Render() +} diff --git a/pkg/cmd/repo/license/list/list_test.go b/pkg/cmd/repo/license/list/list_test.go new file mode 100644 index 000000000..c84e3fff8 --- /dev/null +++ b/pkg/cmd/repo/license/list/list_test.go @@ -0,0 +1,259 @@ +package list + +import ( + "bytes" + "net/http" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewCmdList(t *testing.T) { + tests := []struct { + name string + args []string + wantErr bool + tty bool + }{ + { + name: "no arguments", + args: []string{}, + wantErr: false, + tty: false, + }, + { + name: "too many arguments", + args: []string{"foo", "bar"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + ios.SetStdinTTY(tt.tty) + ios.SetStdoutTTY(tt.tty) + ios.SetStderrTTY(tt.tty) + + f := &cmdutil.Factory{ + IOStreams: ios, + } + cmd := NewCmdList(f, func(*ListOptions) error { + return nil + }) + cmd.SetArgs(tt.args) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + + _, err := cmd.ExecuteC() + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestTableLicenseRenderer(t *testing.T) { + tests := []struct { + name string + opts ListOptions + isTTY bool + wantStdout string + wantErr bool + licenses []api.License + }{ + { + name: "licenses + tty", + opts: ListOptions{}, + isTTY: true, + wantStdout: heredoc.Doc(` + KEY NAME + mit MIT License + lgpl-3.0 GNU Lesser General Public License v3.0 + `), + wantErr: false, + licenses: []api.License{ + { + Key: "mit", + Name: "MIT License", + }, + { + Key: "lgpl-3.0", + Name: "GNU Lesser General Public License v3.0", + }, + }, + }, + { + name: "no licenses + tty", + opts: ListOptions{}, + isTTY: true, + wantStdout: heredoc.Doc(` + KEY NAME + `), + wantErr: false, + licenses: []api.License{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, stdout, _ := iostreams.Test() + ios.SetStdoutTTY(tt.isTTY) + ios.SetStdinTTY(tt.isTTY) + ios.SetStderrTTY(tt.isTTY) + tt.opts.IO = ios + + r := &TableLicenseRenderer{} + err := r.Render(tt.licenses, &tt.opts) + + if !tt.wantErr { + assert.NoError(t, err, "Expected no error while rendering table") + } + + assert.Equal(t, tt.wantStdout, stdout.String(), "Rendered table differs from expected") + + }) + } +} + +func TestListRun(t *testing.T) { + tests := []struct { + name string + opts *ListOptions + isTTY bool + httpStubs func(t *testing.T, reg *httpmock.Registry) + wantStdout string + wantStderr string + wantErr bool + errMsg string + }{ + { + name: "license list tty", + isTTY: true, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "licenses"), + httpmock.StringResponse(`[ + { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + { + "key": "lgpl-3.0", + "name": "GNU Lesser General Public License v3.0", + "spdx_id": "LGPL-3.0", + "url": "https://api.github.com/licenses/lgpl-3.0", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + { + "key": "mpl-2.0", + "name": "Mozilla Public License 2.0", + "spdx_id": "MPL-2.0", + "url": "https://api.github.com/licenses/mpl-2.0", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + { + "key": "agpl-3.0", + "name": "GNU Affero General Public License v3.0", + "spdx_id": "AGPL-3.0", + "url": "https://api.github.com/licenses/agpl-3.0", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + { + "key": "unlicense", + "name": "The Unlicense", + "spdx_id": "Unlicense", + "url": "https://api.github.com/licenses/unlicense", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + { + "key": "apache-2.0", + "name": "Apache License 2.0", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + { + "key": "gpl-3.0", + "name": "GNU General Public License v3.0", + "spdx_id": "GPL-3.0", + "url": "https://api.github.com/licenses/gpl-3.0", + "node_id": "MDc6TGljZW5zZW1pdA==" + } + ]`, + )) + }, + wantStdout: heredoc.Doc(` + KEY NAME + mit MIT License + lgpl-3.0 GNU Lesser General Public License v3.0 + mpl-2.0 Mozilla Public License 2.0 + agpl-3.0 GNU Affero General Public License v3.0 + unlicense The Unlicense + apache-2.0 Apache License 2.0 + gpl-3.0 GNU General Public License v3.0 + `), + wantStderr: "", + opts: &ListOptions{}, + }, + { + name: "license list no licenses tty", + isTTY: true, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "licenses"), + httpmock.StringResponse(`[]`), + ) + }, + wantStdout: "", + wantStderr: "", + wantErr: true, + errMsg: "no licenses found", + opts: &ListOptions{}, + }, + } + + for _, tt := range tests { + reg := &httpmock.Registry{} + if tt.httpStubs != nil { + tt.httpStubs(t, reg) + } + tt.opts.Config = func() (gh.Config, error) { + return config.NewBlankConfig(), nil + } + tt.opts.HTTPClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + ios, _, stdout, stderr := iostreams.Test() + ios.SetStdoutTTY(tt.isTTY) + ios.SetStdinTTY(tt.isTTY) + ios.SetStderrTTY(tt.isTTY) + tt.opts.IO = ios + + t.Run(tt.name, func(t *testing.T) { + defer reg.Verify(t) + err := listRun(tt.opts) + + if tt.wantErr { + assert.Error(t, err) + assert.Equal(t, tt.errMsg, err.Error()) + return + } + + assert.Equal(t, tt.wantStdout, stdout.String(), "Stdout differs from expected") + assert.Equal(t, tt.wantStderr, stderr.String(), "Stderr differs from expected") + }) + } +} diff --git a/pkg/cmd/repo/repo.go b/pkg/cmd/repo/repo.go index baaa0e10f..611ef49ea 100644 --- a/pkg/cmd/repo/repo.go +++ b/pkg/cmd/repo/repo.go @@ -11,6 +11,7 @@ import ( repoEditCmd "github.com/cli/cli/v2/pkg/cmd/repo/edit" repoForkCmd "github.com/cli/cli/v2/pkg/cmd/repo/fork" gardenCmd "github.com/cli/cli/v2/pkg/cmd/repo/garden" + licenseCmd "github.com/cli/cli/v2/pkg/cmd/repo/license" repoListCmd "github.com/cli/cli/v2/pkg/cmd/repo/list" repoRenameCmd "github.com/cli/cli/v2/pkg/cmd/repo/rename" repoDefaultCmd "github.com/cli/cli/v2/pkg/cmd/repo/setdefault" @@ -54,6 +55,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command { repoSyncCmd.NewCmdSync(f, nil), repoEditCmd.NewCmdEdit(f, nil), deployKeyCmd.NewCmdDeployKey(f), + licenseCmd.NewCmdLicense(f), repoRenameCmd.NewCmdRename(f, nil), repoArchiveCmd.NewCmdArchive(f, nil), repoUnarchiveCmd.NewCmdUnarchive(f, nil), diff --git a/pkg/cmd/repo/shared/repo.go b/pkg/cmd/repo/shared/repo.go index b980098b8..b2633573e 100644 --- a/pkg/cmd/repo/shared/repo.go +++ b/pkg/cmd/repo/shared/repo.go @@ -1,8 +1,11 @@ package shared import ( + "net/http" "regexp" "strings" + + "github.com/cli/cli/v2/api" ) var invalidCharactersRE = regexp.MustCompile(`[^\w._-]+`) @@ -12,3 +15,14 @@ func NormalizeRepoName(repoName string) string { newName := invalidCharactersRE.ReplaceAllString(repoName, "-") return strings.TrimSuffix(newName, ".git") } + +// ListLicenseTemplates uses API v3 here because license template isn't supported by GraphQL yet. +func ListLicenseTemplates(httpClient *http.Client, hostname string) ([]api.License, error) { + var licenseTemplates []api.License + client := api.NewClientFromHTTP(httpClient) + err := client.REST(hostname, "GET", "licenses", nil, &licenseTemplates) + if err != nil { + return nil, err + } + return licenseTemplates, nil +} From c843dddf0d7d5665fe01789cfa262c8efb80b653 Mon Sep 17 00:00:00 2001 From: Miroma <136986257+its-miroma@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:49:20 +0200 Subject: [PATCH 084/301] Add GPG key instructions to appropriate sections --- docs/install_linux.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/install_linux.md b/docs/install_linux.md index fabaa19aa..882ee3976 100644 --- a/docs/install_linux.md +++ b/docs/install_linux.md @@ -49,6 +49,9 @@ Alternatively, install from the [community repository](https://packages.fedorapr sudo dnf install gh ``` +> [!NOTE] +> If errors regarding GPG signatures occur, see [cli/cli#9569](https://github.com/cli/cli/issues/9569) for steps to fix this. + Upgrade: ```bash @@ -91,6 +94,9 @@ sudo zypper ref sudo zypper update gh ``` +> [!NOTE] +> If errors regarding GPG signatures occur, see [cli/cli#9569](https://github.com/cli/cli/issues/9569) for steps to fix this. + ## Manual installation * [Download release binaries][releases page] that match your platform; or From 4de63ac75b99e42ac6d2bd6504056c26335f5a2b Mon Sep 17 00:00:00 2001 From: Miroma <136986257+its-miroma@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:49:35 +0200 Subject: [PATCH 085/301] Include `dnf5` commands --- docs/install_linux.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/install_linux.md b/docs/install_linux.md index 882ee3976..9be6aed9b 100644 --- a/docs/install_linux.md +++ b/docs/install_linux.md @@ -43,6 +43,20 @@ sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.re sudo dnf install gh --repo gh-cli ``` +
+Show dnf5 commands + +If you're using `dnf5`, commands will vary slightly: + +```bash +sudo dnf5 install dnf5-plugins +sudo dnf5 config-manager addrepo --from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo +sudo dnf5 install gh --repo gh-cli +``` + +For more details, check out the [`dnf5 config-manager` documentation](https://dnf5.readthedocs.io/en/latest/dnf5_plugins/config-manager.8.html). +
+ Alternatively, install from the [community repository](https://packages.fedoraproject.org/pkgs/gh/gh/): ```bash From 21f0d9466ecb23b3e76585f8b9e93452d35ff296 Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Sun, 29 Sep 2024 10:40:22 -0600 Subject: [PATCH 086/301] Divide shared repo package and add queries tests --- pkg/cmd/repo/create/create.go | 15 +- pkg/cmd/repo/fork/fork.go | 4 +- pkg/cmd/repo/license/list/list.go | 4 +- pkg/cmd/repo/shared/format/format.go | 14 ++ .../{repo_test.go => format/format_test.go} | 2 +- pkg/cmd/repo/shared/queries/queries.go | 19 +++ pkg/cmd/repo/shared/queries/queries_test.go | 136 ++++++++++++++++++ pkg/cmd/repo/shared/repo.go | 28 ---- 8 files changed, 182 insertions(+), 40 deletions(-) create mode 100644 pkg/cmd/repo/shared/format/format.go rename pkg/cmd/repo/shared/{repo_test.go => format/format_test.go} (98%) create mode 100644 pkg/cmd/repo/shared/queries/queries.go create mode 100644 pkg/cmd/repo/shared/queries/queries_test.go delete mode 100644 pkg/cmd/repo/shared/repo.go diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index 4b8b18c70..a1ff22863 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -19,7 +19,8 @@ import ( "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/shared" + "github.com/cli/cli/v2/pkg/cmd/repo/shared/format" + "github.com/cli/cli/v2/pkg/cmd/repo/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" @@ -244,7 +245,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co return nil, cobra.ShellCompDirectiveError } hostname, _ := cfg.Authentication().DefaultHost() - licenses, err := shared.ListLicenseTemplates(httpClient, hostname) + licenses, err := queries.ListLicenseTemplates(httpClient, hostname) if err != nil { return nil, cobra.ShellCompDirectiveError } @@ -317,9 +318,9 @@ func createFromScratch(opts *CreateOptions) error { return err } - targetRepo := shared.NormalizeRepoName(opts.Name) + targetRepo := format.NormalizeRepoName(opts.Name) if idx := strings.IndexRune(opts.Name, '/'); idx > 0 { - targetRepo = opts.Name[0:idx+1] + shared.NormalizeRepoName(opts.Name[idx+1:]) + targetRepo = opts.Name[0:idx+1] + format.NormalizeRepoName(opts.Name[idx+1:]) } confirmed, err := opts.Prompter.Confirm(fmt.Sprintf(`This will create "%s" as a %s repository on GitHub. Continue?`, targetRepo, strings.ToLower(opts.Visibility)), true) if err != nil { @@ -476,9 +477,9 @@ func createFromTemplate(opts *CreateOptions) error { } templateRepoMainBranch := templateRepo.DefaultBranchRef.Name - targetRepo := shared.NormalizeRepoName(opts.Name) + targetRepo := format.NormalizeRepoName(opts.Name) if idx := strings.IndexRune(opts.Name, '/'); idx > 0 { - targetRepo = opts.Name[0:idx+1] + shared.NormalizeRepoName(opts.Name[idx+1:]) + targetRepo = opts.Name[0:idx+1] + format.NormalizeRepoName(opts.Name[idx+1:]) } confirmed, err := opts.Prompter.Confirm(fmt.Sprintf(`This will create "%s" as a %s repository on GitHub. Continue?`, targetRepo, strings.ToLower(opts.Visibility)), true) if err != nil { @@ -830,7 +831,7 @@ func interactiveLicense(client *http.Client, hostname string, prompter iprompter return "", nil } - licenses, err := shared.ListLicenseTemplates(client, hostname) + licenses, err := queries.ListLicenseTemplates(client, hostname) if err != nil { return "", err } diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go index a49f5d567..274af3083 100644 --- a/pkg/cmd/repo/fork/fork.go +++ b/pkg/cmd/repo/fork/fork.go @@ -16,7 +16,7 @@ import ( "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" - "github.com/cli/cli/v2/pkg/cmd/repo/shared" + "github.com/cli/cli/v2/pkg/cmd/repo/shared/format" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" @@ -225,7 +225,7 @@ func forkRun(opts *ForkOptions) error { } // Rename the new repo if necessary - if opts.ForkName != "" && !strings.EqualFold(forkedRepo.RepoName(), shared.NormalizeRepoName(opts.ForkName)) { + if opts.ForkName != "" && !strings.EqualFold(forkedRepo.RepoName(), format.NormalizeRepoName(opts.ForkName)) { forkedRepo, err = api.RenameRepo(apiClient, forkedRepo, opts.ForkName) if err != nil { return fmt.Errorf("could not rename fork: %w", err) diff --git a/pkg/cmd/repo/license/list/list.go b/pkg/cmd/repo/license/list/list.go index 48f8a74d9..4e52a5c6c 100644 --- a/pkg/cmd/repo/license/list/list.go +++ b/pkg/cmd/repo/license/list/list.go @@ -6,7 +6,7 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/tableprinter" - "github.com/cli/cli/v2/pkg/cmd/repo/shared" + "github.com/cli/cli/v2/pkg/cmd/repo/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/spf13/cobra" @@ -67,7 +67,7 @@ func listRun(opts *ListOptions) error { } hostname, _ := cfg.Authentication().DefaultHost() - licenses, err := shared.ListLicenseTemplates(client, hostname) + licenses, err := queries.ListLicenseTemplates(client, hostname) if err != nil { return err } diff --git a/pkg/cmd/repo/shared/format/format.go b/pkg/cmd/repo/shared/format/format.go new file mode 100644 index 000000000..5d29201ec --- /dev/null +++ b/pkg/cmd/repo/shared/format/format.go @@ -0,0 +1,14 @@ +package format + +import ( + "regexp" + "strings" +) + +var invalidCharactersRE = regexp.MustCompile(`[^\w._-]+`) + +// NormalizeRepoName takes in the repo name the user inputted and normalizes it using the same logic as GitHub (GitHub.com/new) +func NormalizeRepoName(repoName string) string { + newName := invalidCharactersRE.ReplaceAllString(repoName, "-") + return strings.TrimSuffix(newName, ".git") +} diff --git a/pkg/cmd/repo/shared/repo_test.go b/pkg/cmd/repo/shared/format/format_test.go similarity index 98% rename from pkg/cmd/repo/shared/repo_test.go rename to pkg/cmd/repo/shared/format/format_test.go index 2e9251840..6a781ac02 100644 --- a/pkg/cmd/repo/shared/repo_test.go +++ b/pkg/cmd/repo/shared/format/format_test.go @@ -1,4 +1,4 @@ -package shared +package format import ( "testing" diff --git a/pkg/cmd/repo/shared/queries/queries.go b/pkg/cmd/repo/shared/queries/queries.go new file mode 100644 index 000000000..27616bc00 --- /dev/null +++ b/pkg/cmd/repo/shared/queries/queries.go @@ -0,0 +1,19 @@ +package queries + +import ( + "net/http" + + "github.com/cli/cli/v2/api" +) + +// ListLicenseTemplates fetches available repository templates. +// It uses API v3 because license template isn't supported by GraphQL. +func ListLicenseTemplates(httpClient *http.Client, hostname string) ([]api.License, error) { + var licenseTemplates []api.License + client := api.NewClientFromHTTP(httpClient) + err := client.REST(hostname, "GET", "licenses", nil, &licenseTemplates) + if err != nil { + return nil, err + } + return licenseTemplates, nil +} diff --git a/pkg/cmd/repo/shared/queries/queries_test.go b/pkg/cmd/repo/shared/queries/queries_test.go new file mode 100644 index 000000000..7babbb264 --- /dev/null +++ b/pkg/cmd/repo/shared/queries/queries_test.go @@ -0,0 +1,136 @@ +package queries + +import ( + "net/http" + "testing" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/stretchr/testify/assert" +) + +func TestListLicenseTemplates(t *testing.T) { + tests := []struct { + name string + httpStubs func(t *testing.T, reg *httpmock.Registry) + hostname string + wantLicenses []api.License + wantErr bool + wantErrMsg string + httpClient func() (*http.Client, error) + }{ + { + name: "happy path", + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "licenses"), + httpmock.StringResponse(`[ + { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + { + "key": "lgpl-3.0", + "name": "GNU Lesser General Public License v3.0", + "spdx_id": "LGPL-3.0", + "url": "https://api.github.com/licenses/lgpl-3.0", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + { + "key": "mpl-2.0", + "name": "Mozilla Public License 2.0", + "spdx_id": "MPL-2.0", + "url": "https://api.github.com/licenses/mpl-2.0", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + { + "key": "agpl-3.0", + "name": "GNU Affero General Public License v3.0", + "spdx_id": "AGPL-3.0", + "url": "https://api.github.com/licenses/agpl-3.0", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + { + "key": "unlicense", + "name": "The Unlicense", + "spdx_id": "Unlicense", + "url": "https://api.github.com/licenses/unlicense", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + { + "key": "apache-2.0", + "name": "Apache License 2.0", + "spdx_id": "Apache-2.0", + "url": "https://api.github.com/licenses/apache-2.0", + "node_id": "MDc6TGljZW5zZW1pdA==" + }, + { + "key": "gpl-3.0", + "name": "GNU General Public License v3.0", + "spdx_id": "GPL-3.0", + "url": "https://api.github.com/licenses/gpl-3.0", + "node_id": "MDc6TGljZW5zZW1pdA==" + } + ]`, + )) + }, + hostname: "api.github.com", + wantLicenses: []api.License{ + { + Key: "mit", + Name: "MIT License", + }, + { + Key: "lgpl-3.0", + Name: "GNU Lesser General Public License v3.0", + }, + { + Key: "mpl-2.0", + Name: "Mozilla Public License 2.0", + }, + { + Key: "agpl-3.0", + Name: "GNU Affero General Public License v3.0", + }, + { + Key: "unlicense", + Name: "The Unlicense", + }, + { + Key: "apache-2.0", + Name: "Apache License 2.0", + }, + { + Key: "gpl-3.0", + Name: "GNU General Public License v3.0", + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + reg := &httpmock.Registry{} + if tt.httpStubs != nil { + tt.httpStubs(t, reg) + } + tt.httpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + client, _ := tt.httpClient() + t.Run(tt.name, func(t *testing.T) { + defer reg.Verify(t) + gotLicenses, err := ListLicenseTemplates(client, tt.hostname) + if !tt.wantErr { + assert.NoError(t, err, "Expected no error while fetching /licenses") + } + if tt.wantErr { + assert.Error(t, err, "Expected error while fetching /licenses") + } + assert.Equal(t, tt.wantLicenses, gotLicenses, "Licenses fetched is not as expected") + }) + } +} diff --git a/pkg/cmd/repo/shared/repo.go b/pkg/cmd/repo/shared/repo.go deleted file mode 100644 index b2633573e..000000000 --- a/pkg/cmd/repo/shared/repo.go +++ /dev/null @@ -1,28 +0,0 @@ -package shared - -import ( - "net/http" - "regexp" - "strings" - - "github.com/cli/cli/v2/api" -) - -var invalidCharactersRE = regexp.MustCompile(`[^\w._-]+`) - -// NormalizeRepoName takes in the repo name the user inputted and normalizes it using the same logic as GitHub (GitHub.com/new) -func NormalizeRepoName(repoName string) string { - newName := invalidCharactersRE.ReplaceAllString(repoName, "-") - return strings.TrimSuffix(newName, ".git") -} - -// ListLicenseTemplates uses API v3 here because license template isn't supported by GraphQL yet. -func ListLicenseTemplates(httpClient *http.Client, hostname string) ([]api.License, error) { - var licenseTemplates []api.License - client := api.NewClientFromHTTP(httpClient) - err := client.REST(hostname, "GET", "licenses", nil, &licenseTemplates) - if err != nil { - return nil, err - } - return licenseTemplates, nil -} From 1aa2a824bac6b549ab6e07aff3ae87ec1cd686aa Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Sun, 29 Sep 2024 10:42:54 -0600 Subject: [PATCH 087/301] Remove json output option Because the API query uses REST, the JSON exporter doesn't work as expected. --- pkg/cmd/repo/license/list/list.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pkg/cmd/repo/license/list/list.go b/pkg/cmd/repo/license/list/list.go index 4e52a5c6c..8d369c4ee 100644 --- a/pkg/cmd/repo/license/list/list.go +++ b/pkg/cmd/repo/license/list/list.go @@ -18,15 +18,9 @@ type LicenseRenderer interface { type TableLicenseRenderer struct{} -var licenseFields = []string{ - "key", - "name", -} - type ListOptions struct { IO *iostreams.IOStreams HTTPClient func() (*http.Client, error) - Exporter cmdutil.Exporter Config func() (gh.Config, error) } @@ -50,8 +44,6 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman return listRun(opts) }, } - // cmdutil.AddFormatFlags(cmd, &opts.Exporter) - cmdutil.AddJSONFlags(cmd, &opts.Exporter, licenseFields) return cmd } @@ -76,10 +68,6 @@ func listRun(opts *ListOptions) error { return cmdutil.NewNoResultsError("no licenses found") } - if opts.Exporter != nil { - return opts.Exporter.Write(opts.IO, licenses) - } - r := &TableLicenseRenderer{} return r.Render(licenses, opts) } From 2f608e3772118d14c926a8778dd5acc1fc2b358f Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Sun, 29 Sep 2024 11:46:37 -0600 Subject: [PATCH 088/301] Cleanup rendering and tests --- pkg/cmd/repo/license/list/list.go | 11 +---- pkg/cmd/repo/license/list/list_test.go | 67 +------------------------- 2 files changed, 4 insertions(+), 74 deletions(-) diff --git a/pkg/cmd/repo/license/list/list.go b/pkg/cmd/repo/license/list/list.go index 8d369c4ee..7b931811f 100644 --- a/pkg/cmd/repo/license/list/list.go +++ b/pkg/cmd/repo/license/list/list.go @@ -12,12 +12,6 @@ import ( "github.com/spf13/cobra" ) -type LicenseRenderer interface { - Render([]api.License, ListOptions) error -} - -type TableLicenseRenderer struct{} - type ListOptions struct { IO *iostreams.IOStreams HTTPClient func() (*http.Client, error) @@ -68,11 +62,10 @@ func listRun(opts *ListOptions) error { return cmdutil.NewNoResultsError("no licenses found") } - r := &TableLicenseRenderer{} - return r.Render(licenses, opts) + return renderLicenseTemplatesTable(licenses, opts) } -func (r *TableLicenseRenderer) Render(licenses []api.License, opts *ListOptions) error { +func renderLicenseTemplatesTable(licenses []api.License, opts *ListOptions) error { t := tableprinter.New(opts.IO, tableprinter.WithHeader("KEY", "NAME")) for _, l := range licenses { t.AddField(l.Key) diff --git a/pkg/cmd/repo/license/list/list_test.go b/pkg/cmd/repo/license/list/list_test.go index c84e3fff8..43d47c2df 100644 --- a/pkg/cmd/repo/license/list/list_test.go +++ b/pkg/cmd/repo/license/list/list_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/pkg/cmdutil" @@ -24,14 +23,14 @@ func TestNewCmdList(t *testing.T) { tty bool }{ { - name: "no arguments", + name: "happy path no arguments", args: []string{}, wantErr: false, tty: false, }, { name: "too many arguments", - args: []string{"foo", "bar"}, + args: []string{"foo"}, wantErr: true, }, } @@ -63,68 +62,6 @@ func TestNewCmdList(t *testing.T) { } } -func TestTableLicenseRenderer(t *testing.T) { - tests := []struct { - name string - opts ListOptions - isTTY bool - wantStdout string - wantErr bool - licenses []api.License - }{ - { - name: "licenses + tty", - opts: ListOptions{}, - isTTY: true, - wantStdout: heredoc.Doc(` - KEY NAME - mit MIT License - lgpl-3.0 GNU Lesser General Public License v3.0 - `), - wantErr: false, - licenses: []api.License{ - { - Key: "mit", - Name: "MIT License", - }, - { - Key: "lgpl-3.0", - Name: "GNU Lesser General Public License v3.0", - }, - }, - }, - { - name: "no licenses + tty", - opts: ListOptions{}, - isTTY: true, - wantStdout: heredoc.Doc(` - KEY NAME - `), - wantErr: false, - licenses: []api.License{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(tt.isTTY) - ios.SetStdinTTY(tt.isTTY) - ios.SetStderrTTY(tt.isTTY) - tt.opts.IO = ios - - r := &TableLicenseRenderer{} - err := r.Render(tt.licenses, &tt.opts) - - if !tt.wantErr { - assert.NoError(t, err, "Expected no error while rendering table") - } - - assert.Equal(t, tt.wantStdout, stdout.String(), "Rendered table differs from expected") - - }) - } -} - func TestListRun(t *testing.T) { tests := []struct { name string From ac779ba82a43df8ba601e7222417b84f675dc69b Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Sun, 29 Sep 2024 11:48:48 -0600 Subject: [PATCH 089/301] fix output capitalization --- pkg/cmd/repo/license/list/list.go | 2 +- pkg/cmd/repo/license/list/list_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/repo/license/list/list.go b/pkg/cmd/repo/license/list/list.go index 7b931811f..573bd4e29 100644 --- a/pkg/cmd/repo/license/list/list.go +++ b/pkg/cmd/repo/license/list/list.go @@ -59,7 +59,7 @@ func listRun(opts *ListOptions) error { } if len(licenses) == 0 { - return cmdutil.NewNoResultsError("no licenses found") + return cmdutil.NewNoResultsError("No licenses found") } return renderLicenseTemplatesTable(licenses, opts) diff --git a/pkg/cmd/repo/license/list/list_test.go b/pkg/cmd/repo/license/list/list_test.go index 43d47c2df..a740b0f83 100644 --- a/pkg/cmd/repo/license/list/list_test.go +++ b/pkg/cmd/repo/license/list/list_test.go @@ -157,7 +157,7 @@ func TestListRun(t *testing.T) { wantStdout: "", wantStderr: "", wantErr: true, - errMsg: "no licenses found", + errMsg: "No licenses found", opts: &ListOptions{}, }, } From 2b4464a3af1dc9d15fe4520d1cec89e5b70586b6 Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Sun, 29 Sep 2024 11:49:09 -0600 Subject: [PATCH 090/301] Fix ListLicenseTemplates doc --- pkg/cmd/repo/shared/queries/queries.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/repo/shared/queries/queries.go b/pkg/cmd/repo/shared/queries/queries.go index 27616bc00..fdb238fb4 100644 --- a/pkg/cmd/repo/shared/queries/queries.go +++ b/pkg/cmd/repo/shared/queries/queries.go @@ -6,7 +6,7 @@ import ( "github.com/cli/cli/v2/api" ) -// ListLicenseTemplates fetches available repository templates. +// ListLicenseTemplates fetches available repository license templates. // It uses API v3 because license template isn't supported by GraphQL. func ListLicenseTemplates(httpClient *http.Client, hostname string) ([]api.License, error) { var licenseTemplates []api.License From 25cdce2cecf7aef00f1d0e768d68b4aff1120094 Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Sun, 29 Sep 2024 11:59:15 -0600 Subject: [PATCH 091/301] Update license table headings and tests --- pkg/cmd/repo/license/list/list.go | 2 +- pkg/cmd/repo/license/list/list_test.go | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/repo/license/list/list.go b/pkg/cmd/repo/license/list/list.go index 573bd4e29..b1d2b3578 100644 --- a/pkg/cmd/repo/license/list/list.go +++ b/pkg/cmd/repo/license/list/list.go @@ -66,7 +66,7 @@ func listRun(opts *ListOptions) error { } func renderLicenseTemplatesTable(licenses []api.License, opts *ListOptions) error { - t := tableprinter.New(opts.IO, tableprinter.WithHeader("KEY", "NAME")) + t := tableprinter.New(opts.IO, tableprinter.WithHeader("LICENSE KEY", "LICENSE NAME")) for _, l := range licenses { t.AddField(l.Key) t.AddField(l.Name) diff --git a/pkg/cmd/repo/license/list/list_test.go b/pkg/cmd/repo/license/list/list_test.go index a740b0f83..711a7e728 100644 --- a/pkg/cmd/repo/license/list/list_test.go +++ b/pkg/cmd/repo/license/list/list_test.go @@ -133,14 +133,14 @@ func TestListRun(t *testing.T) { )) }, wantStdout: heredoc.Doc(` - KEY NAME - mit MIT License - lgpl-3.0 GNU Lesser General Public License v3.0 - mpl-2.0 Mozilla Public License 2.0 - agpl-3.0 GNU Affero General Public License v3.0 - unlicense The Unlicense - apache-2.0 Apache License 2.0 - gpl-3.0 GNU General Public License v3.0 + LICENSE KEY LICENSE NAME + mit MIT License + lgpl-3.0 GNU Lesser General Public License v3.0 + mpl-2.0 Mozilla Public License 2.0 + agpl-3.0 GNU Affero General Public License v3.0 + unlicense The Unlicense + apache-2.0 Apache License 2.0 + gpl-3.0 GNU General Public License v3.0 `), wantStderr: "", opts: &ListOptions{}, @@ -189,8 +189,8 @@ func TestListRun(t *testing.T) { return } - assert.Equal(t, tt.wantStdout, stdout.String(), "Stdout differs from expected") - assert.Equal(t, tt.wantStderr, stderr.String(), "Stderr differs from expected") + assert.Equal(t, tt.wantStdout, stdout.String()) + assert.Equal(t, tt.wantStderr, stderr.String()) }) } } From 038b57c866ba0f0599df7317d54a2ef4b0e171c3 Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Sun, 29 Sep 2024 12:29:56 -0600 Subject: [PATCH 092/301] implement gitignore list --- pkg/cmd/repo/create/create.go | 4 +- pkg/cmd/repo/create/http.go | 11 -- pkg/cmd/repo/gitignore/gitignore.go | 18 ++ pkg/cmd/repo/gitignore/list/list.go | 75 ++++++++ pkg/cmd/repo/gitignore/list/list_test.go | 190 ++++++++++++++++++++ pkg/cmd/repo/license/license.go | 2 +- pkg/cmd/repo/license/list/list.go | 14 +- pkg/cmd/repo/license/list/list_test.go | 4 +- pkg/cmd/repo/repo.go | 2 + pkg/cmd/repo/shared/queries/queries.go | 12 ++ pkg/cmd/repo/shared/queries/queries_test.go | 97 ++++++++++ 11 files changed, 406 insertions(+), 23 deletions(-) create mode 100644 pkg/cmd/repo/gitignore/gitignore.go create mode 100644 pkg/cmd/repo/gitignore/list/list.go create mode 100644 pkg/cmd/repo/gitignore/list/list_test.go diff --git a/pkg/cmd/repo/create/create.go b/pkg/cmd/repo/create/create.go index a1ff22863..760757928 100644 --- a/pkg/cmd/repo/create/create.go +++ b/pkg/cmd/repo/create/create.go @@ -228,7 +228,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co return nil, cobra.ShellCompDirectiveError } hostname, _ := cfg.Authentication().DefaultHost() - results, err := listGitIgnoreTemplates(httpClient, hostname) + results, err := queries.ListGitIgnoreTemplates(httpClient, hostname) if err != nil { return nil, cobra.ShellCompDirectiveError } @@ -812,7 +812,7 @@ func interactiveGitIgnore(client *http.Client, hostname string, prompter iprompt return "", nil } - templates, err := listGitIgnoreTemplates(client, hostname) + templates, err := queries.ListGitIgnoreTemplates(client, hostname) if err != nil { return "", err } diff --git a/pkg/cmd/repo/create/http.go b/pkg/cmd/repo/create/http.go index ba99c3e4c..725fc48c5 100644 --- a/pkg/cmd/repo/create/http.go +++ b/pkg/cmd/repo/create/http.go @@ -339,17 +339,6 @@ func listTemplateRepositories(client *http.Client, hostname, owner string) ([]ap return templateRepositories, nil } -// listGitIgnoreTemplates uses API v3 here because gitignore template isn't supported by GraphQL yet. -func listGitIgnoreTemplates(httpClient *http.Client, hostname string) ([]string, error) { - var gitIgnoreTemplates []string - client := api.NewClientFromHTTP(httpClient) - err := client.REST(hostname, "GET", "gitignore/templates", nil, &gitIgnoreTemplates) - if err != nil { - return []string{}, err - } - return gitIgnoreTemplates, nil -} - // Returns the current username and any orgs that user is a member of. func userAndOrgs(httpClient *http.Client, hostname string) (string, []string, error) { client := api.NewClientFromHTTP(httpClient) diff --git a/pkg/cmd/repo/gitignore/gitignore.go b/pkg/cmd/repo/gitignore/gitignore.go new file mode 100644 index 000000000..260b506c5 --- /dev/null +++ b/pkg/cmd/repo/gitignore/gitignore.go @@ -0,0 +1,18 @@ +package gitignore + +import ( + cmdList "github.com/cli/cli/v2/pkg/cmd/repo/gitignore/list" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/spf13/cobra" +) + +func NewCmdGitIgnore(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "gitignore ", + Short: "View available repository .gitignore template", + } + + cmd.AddCommand(cmdList.NewCmdList(f, nil)) + + return cmd +} diff --git a/pkg/cmd/repo/gitignore/list/list.go b/pkg/cmd/repo/gitignore/list/list.go new file mode 100644 index 000000000..7de608566 --- /dev/null +++ b/pkg/cmd/repo/gitignore/list/list.go @@ -0,0 +1,75 @@ +package list + +import ( + "net/http" + + "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/internal/tableprinter" + "github.com/cli/cli/v2/pkg/cmd/repo/shared/queries" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +type ListOptions struct { + IO *iostreams.IOStreams + HTTPClient func() (*http.Client, error) + Config func() (gh.Config, error) +} + +func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { + opts := &ListOptions{ + IO: f.IOStreams, + HTTPClient: f.HttpClient, + Config: f.Config, + } + + cmd := &cobra.Command{ + Use: "list", + Short: "List available repository .gitignore templates", + Aliases: []string{"ls"}, + Args: cmdutil.ExactArgs(0, "gh repo gitignore list takes no arguments"), + RunE: func(cmd *cobra.Command, args []string) error { + + if runF != nil { + return runF(opts) + } + return listRun(opts) + }, + } + return cmd +} + +func listRun(opts *ListOptions) error { + client, err := opts.HTTPClient() + if err != nil { + return err + } + + cfg, err := opts.Config() + if err != nil { + return err + } + + hostname, _ := cfg.Authentication().DefaultHost() + gitIgnoreTemplates, err := queries.ListGitIgnoreTemplates(client, hostname) + if err != nil { + return err + } + + if len(gitIgnoreTemplates) == 0 { + return cmdutil.NewNoResultsError("No .gitignore templates found") + } + + return renderGitIgnoreTemplatesTable(gitIgnoreTemplates, opts) +} + +func renderGitIgnoreTemplatesTable(gitIgnoreTemplates []string, opts *ListOptions) error { + t := tableprinter.New(opts.IO, tableprinter.WithHeader("GITIGNORE")) + for _, gt := range gitIgnoreTemplates { + t.AddField(gt) + t.EndRow() + } + + return t.Render() +} diff --git a/pkg/cmd/repo/gitignore/list/list_test.go b/pkg/cmd/repo/gitignore/list/list_test.go new file mode 100644 index 000000000..d7a0ef9e2 --- /dev/null +++ b/pkg/cmd/repo/gitignore/list/list_test.go @@ -0,0 +1,190 @@ +package list + +import ( + "bytes" + "net/http" + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/httpmock" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewCmdList(t *testing.T) { + tests := []struct { + name string + args []string + wantErr bool + tty bool + }{ + { + name: "happy path no arguments", + args: []string{}, + wantErr: false, + tty: false, + }, + { + name: "too many arguments", + args: []string{"foo"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + ios.SetStdinTTY(tt.tty) + ios.SetStdoutTTY(tt.tty) + ios.SetStderrTTY(tt.tty) + + f := &cmdutil.Factory{ + IOStreams: ios, + } + cmd := NewCmdList(f, func(*ListOptions) error { + return nil + }) + cmd.SetArgs(tt.args) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + + _, err := cmd.ExecuteC() + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestListRun(t *testing.T) { + tests := []struct { + name string + opts *ListOptions + isTTY bool + httpStubs func(t *testing.T, reg *httpmock.Registry) + wantStdout string + wantStderr string + wantErr bool + errMsg string + }{ + { + name: "gitignore list tty", + isTTY: true, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "gitignore/templates"), + httpmock.StringResponse(`[ + "AL", + "Actionscript", + "Ada", + "Agda", + "Android", + "AppEngine", + "AppceleratorTitanium", + "ArchLinuxPackages", + "Autotools", + "Ballerina", + "C", + "C++", + "CFWheels", + "CMake", + "CUDA", + "CakePHP", + "ChefCookbook", + "Clojure", + "CodeIgniter", + "CommonLisp", + "Composer", + "Concrete5", + "Coq", + "CraftCMS", + "D" + ]`, + )) + }, + wantStdout: heredoc.Doc(` + GITIGNORE + AL + Actionscript + Ada + Agda + Android + AppEngine + AppceleratorTitanium + ArchLinuxPackages + Autotools + Ballerina + C + C++ + CFWheels + CMake + CUDA + CakePHP + ChefCookbook + Clojure + CodeIgniter + CommonLisp + Composer + Concrete5 + Coq + CraftCMS + D + `), + wantStderr: "", + opts: &ListOptions{}, + }, + { + name: "gitignore list no .gitignore templates tty", + isTTY: true, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "gitignore/templates"), + httpmock.StringResponse(`[]`), + ) + }, + wantStdout: "", + wantStderr: "", + wantErr: true, + errMsg: "No .gitignore templates found", + opts: &ListOptions{}, + }, + } + + for _, tt := range tests { + reg := &httpmock.Registry{} + if tt.httpStubs != nil { + tt.httpStubs(t, reg) + } + tt.opts.Config = func() (gh.Config, error) { + return config.NewBlankConfig(), nil + } + tt.opts.HTTPClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + ios, _, stdout, stderr := iostreams.Test() + ios.SetStdoutTTY(tt.isTTY) + ios.SetStdinTTY(tt.isTTY) + ios.SetStderrTTY(tt.isTTY) + tt.opts.IO = ios + + t.Run(tt.name, func(t *testing.T) { + defer reg.Verify(t) + err := listRun(tt.opts) + + if tt.wantErr { + assert.Error(t, err) + assert.Equal(t, tt.errMsg, err.Error()) + return + } + + assert.Equal(t, tt.wantStdout, stdout.String()) + assert.Equal(t, tt.wantStderr, stderr.String()) + }) + } +} diff --git a/pkg/cmd/repo/license/license.go b/pkg/cmd/repo/license/license.go index 84896f2cd..a28b14f0b 100644 --- a/pkg/cmd/repo/license/license.go +++ b/pkg/cmd/repo/license/license.go @@ -9,7 +9,7 @@ import ( func NewCmdLicense(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "license ", - Short: "View available repository license options", + Short: "View available repository license templates", } cmd.AddCommand(cmdList.NewCmdList(f, nil)) diff --git a/pkg/cmd/repo/license/list/list.go b/pkg/cmd/repo/license/list/list.go index b1d2b3578..2ef9e7204 100644 --- a/pkg/cmd/repo/license/list/list.go +++ b/pkg/cmd/repo/license/list/list.go @@ -27,7 +27,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman cmd := &cobra.Command{ Use: "list", - Short: "List available repository license options", + Short: "List available repository license templates", Aliases: []string{"ls"}, Args: cmdutil.ExactArgs(0, "gh repo license list takes no arguments"), RunE: func(cmd *cobra.Command, args []string) error { @@ -53,21 +53,21 @@ func listRun(opts *ListOptions) error { } hostname, _ := cfg.Authentication().DefaultHost() - licenses, err := queries.ListLicenseTemplates(client, hostname) + licenseTemplates, err := queries.ListLicenseTemplates(client, hostname) if err != nil { return err } - if len(licenses) == 0 { - return cmdutil.NewNoResultsError("No licenses found") + if len(licenseTemplates) == 0 { + return cmdutil.NewNoResultsError("No repository license templates found") } - return renderLicenseTemplatesTable(licenses, opts) + return renderLicenseTemplatesTable(licenseTemplates, opts) } -func renderLicenseTemplatesTable(licenses []api.License, opts *ListOptions) error { +func renderLicenseTemplatesTable(licenseTemplates []api.License, opts *ListOptions) error { t := tableprinter.New(opts.IO, tableprinter.WithHeader("LICENSE KEY", "LICENSE NAME")) - for _, l := range licenses { + for _, l := range licenseTemplates { t.AddField(l.Key) t.AddField(l.Name) t.EndRow() diff --git a/pkg/cmd/repo/license/list/list_test.go b/pkg/cmd/repo/license/list/list_test.go index 711a7e728..b01d64d9a 100644 --- a/pkg/cmd/repo/license/list/list_test.go +++ b/pkg/cmd/repo/license/list/list_test.go @@ -146,7 +146,7 @@ func TestListRun(t *testing.T) { opts: &ListOptions{}, }, { - name: "license list no licenses tty", + name: "license list no license templates tty", isTTY: true, httpStubs: func(t *testing.T, reg *httpmock.Registry) { reg.Register( @@ -157,7 +157,7 @@ func TestListRun(t *testing.T) { wantStdout: "", wantStderr: "", wantErr: true, - errMsg: "No licenses found", + errMsg: "No repository license templates found", opts: &ListOptions{}, }, } diff --git a/pkg/cmd/repo/repo.go b/pkg/cmd/repo/repo.go index 611ef49ea..14a4bf49c 100644 --- a/pkg/cmd/repo/repo.go +++ b/pkg/cmd/repo/repo.go @@ -11,6 +11,7 @@ import ( repoEditCmd "github.com/cli/cli/v2/pkg/cmd/repo/edit" repoForkCmd "github.com/cli/cli/v2/pkg/cmd/repo/fork" gardenCmd "github.com/cli/cli/v2/pkg/cmd/repo/garden" + gitIgnoreCmd "github.com/cli/cli/v2/pkg/cmd/repo/gitignore" licenseCmd "github.com/cli/cli/v2/pkg/cmd/repo/license" repoListCmd "github.com/cli/cli/v2/pkg/cmd/repo/list" repoRenameCmd "github.com/cli/cli/v2/pkg/cmd/repo/rename" @@ -56,6 +57,7 @@ func NewCmdRepo(f *cmdutil.Factory) *cobra.Command { repoEditCmd.NewCmdEdit(f, nil), deployKeyCmd.NewCmdDeployKey(f), licenseCmd.NewCmdLicense(f), + gitIgnoreCmd.NewCmdGitIgnore(f), repoRenameCmd.NewCmdRename(f, nil), repoArchiveCmd.NewCmdArchive(f, nil), repoUnarchiveCmd.NewCmdUnarchive(f, nil), diff --git a/pkg/cmd/repo/shared/queries/queries.go b/pkg/cmd/repo/shared/queries/queries.go index fdb238fb4..dea02de6c 100644 --- a/pkg/cmd/repo/shared/queries/queries.go +++ b/pkg/cmd/repo/shared/queries/queries.go @@ -17,3 +17,15 @@ func ListLicenseTemplates(httpClient *http.Client, hostname string) ([]api.Licen } return licenseTemplates, nil } + +// ListGitIgnoreTemplates fetches available repository gitignore templates. +// It uses API v3 here because gitignore template isn't supported by GraphQL. +func ListGitIgnoreTemplates(httpClient *http.Client, hostname string) ([]string, error) { + var gitIgnoreTemplates []string + client := api.NewClientFromHTTP(httpClient) + err := client.REST(hostname, "GET", "gitignore/templates", nil, &gitIgnoreTemplates) + if err != nil { + return []string{}, err + } + return gitIgnoreTemplates, nil +} diff --git a/pkg/cmd/repo/shared/queries/queries_test.go b/pkg/cmd/repo/shared/queries/queries_test.go index 7babbb264..097467cfa 100644 --- a/pkg/cmd/repo/shared/queries/queries_test.go +++ b/pkg/cmd/repo/shared/queries/queries_test.go @@ -134,3 +134,100 @@ func TestListLicenseTemplates(t *testing.T) { }) } } + +func TestListGitIgnoreTemplates(t *testing.T) { + tests := []struct { + name string + httpStubs func(t *testing.T, reg *httpmock.Registry) + wantGitIgnoreTemplates []string + wantErr bool + wantErrMsg string + httpClient func() (*http.Client, error) + }{ + { + name: "happy path", + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "gitignore/templates"), + httpmock.StringResponse(`[ + "AL", + "Actionscript", + "Ada", + "Agda", + "Android", + "AppEngine", + "AppceleratorTitanium", + "ArchLinuxPackages", + "Autotools", + "Ballerina", + "C", + "C++", + "CFWheels", + "CMake", + "CUDA", + "CakePHP", + "ChefCookbook", + "Clojure", + "CodeIgniter", + "CommonLisp", + "Composer", + "Concrete5", + "Coq", + "CraftCMS", + "D" + ]`, + )) + }, + wantGitIgnoreTemplates: []string{ + "AL", + "Actionscript", + "Ada", + "Agda", + "Android", + "AppEngine", + "AppceleratorTitanium", + "ArchLinuxPackages", + "Autotools", + "Ballerina", + "C", + "C++", + "CFWheels", + "CMake", + "CUDA", + "CakePHP", + "ChefCookbook", + "Clojure", + "CodeIgniter", + "CommonLisp", + "Composer", + "Concrete5", + "Coq", + "CraftCMS", + "D", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + reg := &httpmock.Registry{} + if tt.httpStubs != nil { + tt.httpStubs(t, reg) + } + tt.httpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + client, _ := tt.httpClient() + t.Run(tt.name, func(t *testing.T) { + defer reg.Verify(t) + gotGitIgnoreTemplates, err := ListGitIgnoreTemplates(client, "api.github.com") + if !tt.wantErr { + assert.NoError(t, err, "Expected no error while fetching /gitignore/templates") + } + if tt.wantErr { + assert.Error(t, err, "Expected error while fetching /gitignore/templates") + } + assert.Equal(t, tt.wantGitIgnoreTemplates, gotGitIgnoreTemplates, "GitIgnore templates fetched is not as expected") + }) + } +} From c76acb6aff9c100b73edfe5ca1b9c6c9f599e06b Mon Sep 17 00:00:00 2001 From: bagtoad <47394200+BagToad@users.noreply.github.com> Date: Sun, 29 Sep 2024 17:26:04 -0600 Subject: [PATCH 093/301] Implement gitignore view --- api/queries_repo.go | 5 + pkg/cmd/repo/gitignore/gitignore.go | 4 +- pkg/cmd/repo/gitignore/list/list.go | 11 +- pkg/cmd/repo/gitignore/list/list_test.go | 2 +- pkg/cmd/repo/gitignore/view/view.go | 102 +++++++++++ pkg/cmd/repo/gitignore/view/view_test.go | 186 ++++++++++++++++++++ pkg/cmd/repo/license/list/list.go | 7 + pkg/cmd/repo/shared/queries/queries.go | 13 ++ pkg/cmd/repo/shared/queries/queries_test.go | 54 ++++++ 9 files changed, 380 insertions(+), 4 deletions(-) create mode 100644 pkg/cmd/repo/gitignore/view/view.go create mode 100644 pkg/cmd/repo/gitignore/view/view_test.go diff --git a/api/queries_repo.go b/api/queries_repo.go index 32cb5ff3b..2e2f711ca 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -211,6 +211,11 @@ type License struct { Name string `json:"name"` } +type GitIgnore struct { + Name string `json:"name"` + Source string `json:"source"` +} + // RepoOwner is the login name of the owner func (r Repository) RepoOwner() string { return r.Owner.Login diff --git a/pkg/cmd/repo/gitignore/gitignore.go b/pkg/cmd/repo/gitignore/gitignore.go index 260b506c5..8a9b8468e 100644 --- a/pkg/cmd/repo/gitignore/gitignore.go +++ b/pkg/cmd/repo/gitignore/gitignore.go @@ -2,6 +2,7 @@ package gitignore import ( cmdList "github.com/cli/cli/v2/pkg/cmd/repo/gitignore/list" + cmdView "github.com/cli/cli/v2/pkg/cmd/repo/gitignore/view" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/spf13/cobra" ) @@ -9,10 +10,11 @@ import ( func NewCmdGitIgnore(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "gitignore ", - Short: "View available repository .gitignore template", + Short: "List and view available repository gitignore templates", } cmd.AddCommand(cmdList.NewCmdList(f, nil)) + cmd.AddCommand(cmdView.NewCmdView(f, nil)) return cmd } diff --git a/pkg/cmd/repo/gitignore/list/list.go b/pkg/cmd/repo/gitignore/list/list.go index 7de608566..cddaeea83 100644 --- a/pkg/cmd/repo/gitignore/list/list.go +++ b/pkg/cmd/repo/gitignore/list/list.go @@ -1,6 +1,7 @@ package list import ( + "fmt" "net/http" "github.com/cli/cli/v2/internal/gh" @@ -26,7 +27,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman cmd := &cobra.Command{ Use: "list", - Short: "List available repository .gitignore templates", + Short: "List available repository gitignore templates", Aliases: []string{"ls"}, Args: cmdutil.ExactArgs(0, "gh repo gitignore list takes no arguments"), RunE: func(cmd *cobra.Command, args []string) error { @@ -51,6 +52,12 @@ func listRun(opts *ListOptions) error { return err } + if err := opts.IO.StartPager(); err == nil { + defer opts.IO.StopPager() + } else { + fmt.Fprintf(opts.IO.ErrOut, "failed to start pager: %v\n", err) + } + hostname, _ := cfg.Authentication().DefaultHost() gitIgnoreTemplates, err := queries.ListGitIgnoreTemplates(client, hostname) if err != nil { @@ -58,7 +65,7 @@ func listRun(opts *ListOptions) error { } if len(gitIgnoreTemplates) == 0 { - return cmdutil.NewNoResultsError("No .gitignore templates found") + return cmdutil.NewNoResultsError("No gitignore templates found") } return renderGitIgnoreTemplatesTable(gitIgnoreTemplates, opts) diff --git a/pkg/cmd/repo/gitignore/list/list_test.go b/pkg/cmd/repo/gitignore/list/list_test.go index d7a0ef9e2..3a68ab511 100644 --- a/pkg/cmd/repo/gitignore/list/list_test.go +++ b/pkg/cmd/repo/gitignore/list/list_test.go @@ -151,7 +151,7 @@ func TestListRun(t *testing.T) { wantStdout: "", wantStderr: "", wantErr: true, - errMsg: "No .gitignore templates found", + errMsg: "No gitignore templates found", opts: &ListOptions{}, }, } diff --git a/pkg/cmd/repo/gitignore/view/view.go b/pkg/cmd/repo/gitignore/view/view.go new file mode 100644 index 000000000..917a66481 --- /dev/null +++ b/pkg/cmd/repo/gitignore/view/view.go @@ -0,0 +1,102 @@ +package view + +import ( + "errors" + "fmt" + "net/http" + "strings" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/gh" + "github.com/cli/cli/v2/pkg/cmd/repo/shared/queries" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +type ViewOptions struct { + IO *iostreams.IOStreams + HTTPClient func() (*http.Client, error) + Config func() (gh.Config, error) + Template string +} + +func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command { + opts := &ViewOptions{ + IO: f.IOStreams, + HTTPClient: f.HttpClient, + Config: f.Config, + Template: "", + } + + cmd := &cobra.Command{ + Use: "view