From 20d39314279502ec3bc1bba40f4a4614f9d218c0 Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Mon, 5 Aug 2024 09:11:25 -0700 Subject: [PATCH 01/33] 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 de354b9477f..f9302b43919 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 2af13e72357..45e3104c29c 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 5b8d8cf7a00..e0a497e4093 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, attestations, fmt.Errorf("failed to fetch remote image: %v", err) + } + + 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.") + } } } - return nil, fmt.Errorf("failed to fetch remote image: %v", err) } - return &desc.Digest, nil + // 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 e1a5b1c50ce..990de3985eb 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 02/33] 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 f9302b43919..e8e0ea095f1 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 45e3104c29c..e1fba97bfa6 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 e0a497e4093..9e53c6f2bae 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) } -// 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) 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(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) + } + + 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) } - // Now you have the decompressed blob content in 'decompressed' buffer - // fmt.Println("Blob content:", decompressed.String()) + 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 3af3b62005c..6d74027aebc 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 2731ee7a410..f38181b77c8 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 52a8ff02547..902657d41ce 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 da2c7bb4e88..902024b90d3 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 990de3985eb..ffa87a383e8 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 03/33] add contain check --- pkg/cmd/attestation/artifact/oci/client.go | 90 ++++++++++++---------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go index 9e53c6f2bae..0171b4187f4 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) - // replace to use GET for more correc type - img2, err := remote.Image(digest2, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + 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) - // 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) - - 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) - 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 { + 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) } } } 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 04/33] 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 0171b4187f4..8fd57af4cdb 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 05/33] added test --- pkg/cmd/attestation/artifact/artifact.go | 18 +-- pkg/cmd/attestation/artifact/image.go | 28 +--- pkg/cmd/attestation/artifact/oci/client.go | 137 +++++++++--------- .../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, 205 insertions(+), 135 deletions(-) diff --git a/pkg/cmd/attestation/artifact/artifact.go b/pkg/cmd/attestation/artifact/artifact.go index e8e0ea095f1..13178516636 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 e1fba97bfa6..dda5f65dbf0 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 8fd57af4cdb..55d74fac059 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,90 +61,90 @@ 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)) - if err != nil { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) - } - - // Step 4: Get the 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] + for _, refDesc := range refManifest.Manifests { + if !strings.HasPrefix(refDesc.ArtifactType, "application/vnd.dev.sigstore.bundle") { + continue + } - // 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() + 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) + } - layerBytes, err := io.ReadAll(rc) + if len(layers) > 0 { + layer0, err := layers[0].Uncompressed() + if err != nil { + return attestations, fmt.Errorf("error getting referrer image: %w", err) + } + defer layer0.Close() - if err != nil { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) - } + bundleBytes, err := io.ReadAll(layer0) - var bundle bundle.ProtobufBundle - bundle.Bundle = new(protobundle.Bundle) - err = bundle.UnmarshalJSON(layerBytes) + if err != nil { + return attestations, fmt.Errorf("error getting referrer image: %w", err) + } - if err != nil { - return attestations, fmt.Errorf("failed to fetch remote image: %v", err) - } + b := &bundle.ProtobufBundle{} + err = b.UnmarshalJSON(bundleBytes) - 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) } - } - } - 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 + a := api.Attestation{Bundle: b} + attestations = append(attestations, &a) + } else { + return attestations, fmt.Errorf("error getting referrer image: no layers found") } } - return false + return attestations, nil } // Unlike other parts of this command set, we cannot pass a custom HTTP client diff --git a/pkg/cmd/attestation/artifact/oci/client_test.go b/pkg/cmd/attestation/artifact/oci/client_test.go index 9aa415c47c8..a465333666b 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 24368dec8cc..1c9bd787639 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 6d74027aebc..3af3b62005c 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 f38181b77c8..2731ee7a410 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 902657d41ce..20e6a01bf66 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 ffa87a383e8..5f4e70fbc9a 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 8d06617f30f..902fbc2d44d 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 06/33] 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 55d74fac059..45c28cf7f1d 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 07/33] 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 45c28cf7f1d..046601637ae 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 08/33] 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 046601637ae..5428fff2fcc 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 1c9bd787639..b869c60a931 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 20e6a01bf66..c1fb25b12ba 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 902fbc2d44d..0f31d900cbd 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 09/33] 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 902024b90d3..a21e27c588c 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 aea131b279f..9fd3190416a 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 5f4e70fbc9a..f77f5f8221a 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 10/33] 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 c14071c320a..e5793cf04b4 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 46566bbf895..906a687625f 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 11/33] 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 40c89c0d8d1..237486eb3c5 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 6ab8df050d4..668d758e58c 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 9ae959c5797..b95b5ef6621 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 e45fc76e47b..852f0a46b2e 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 f77db9abc52..e1798af9f83 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 37f4584391a..6f066da32cf 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 cc21043ba01..36ebf9b159d 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 8de1be94089..56cf58d2190 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 e0347945c38..cba5521011d 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 be8c48428b3..28a83bfc42a 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 869db9c3872..ecd0326b508 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 2b99df9315a..470e3bd27f9 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 a8361e9ef69..53ec93d138e 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 2d9dffae47c..b24e084c4d9 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 1c370fcfbac..2b4675b2f16 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 5e2846441ff..e463a9ac40b 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 44ca69fa689..60762c531af 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 253bd288960..4b493c6d5c4 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 efe022f32f4..d78599cbe0b 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 85e2547f183..3b725b04901 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 689459d399d..0700a688e9b 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 c706da48f3c..e5df52478be 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 12/33] 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 906a687625f..cae9b37c18e 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 13/33] 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 73ef7bb1ed7..c794cff9af2 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 44ca69fa689..956f80cd5dc 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 14/33] 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 a21e27c588c..3ec2d49f1c8 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 9fd3190416a..b9be054a512 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 f77f5f8221a..f053240deb6 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 15/33] 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 c1fb25b12ba..5feca47eac7 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 16/33] 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 0f31d900cbd..89c5ae7c1a7 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 17/33] 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 87badc7a154..4418079b7bd 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 8b9e4352fe7..227e2a6a264 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 18/33] 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 4418079b7bd..b025d9eec26 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 19/33] 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 9074ea117f9..8b5dd515a4a 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 e6576ebd067..cc0ec602a52 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 ffe9dbb6fd2..120683c08b6 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 cfe14563a0c..ec39b3c5048 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 e36f71a9210..387d0fc9560 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 e7534d7f807..787cdcf9dc6 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 20/33] 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 825db931cb7..7ae8c6862b4 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 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 21/33] "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 5feca47eac7..c780e247c2b 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 22/33] 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 51b47e6d8b5..bc9442d9d68 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 23/33] 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 7a72f7f9086..0b7c99b235b 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 5562c1489f90c2886d28d2044664bfa6735d40df Mon Sep 17 00:00:00 2001 From: crystalstall Date: Mon, 2 Sep 2024 15:18:42 +0800 Subject: [PATCH 24/33] 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 f93b0867d4e..be79f9a73e6 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 1aab0bcd36a..f3f4951af7b 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 25/33] 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 c780e247c2b..4d96196da01 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 87a91cea99a..ba530e55d32 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 26/33] `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 4d96196da01..3a2d7456ff8 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 ba530e55d32..66b337ad700 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 84460796563adbc0a71e3c1a9d4e3649729489e9 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Wed, 4 Sep 2024 16:38:13 -0400 Subject: [PATCH 27/33] 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 68d87c03411..50278d7334f 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 1ed19f12cb7..407739dfe2f 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 dcbfb1ba2df..9943e6a9729 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 48d1f4b4994..e56e7d1edd8 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 28/33] 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 2b96a51fc15..ea055b293b6 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 5428fff2fcc..bda11470814 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 77f07e60c92..b33efaa28d9 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 3a2d7456ff8..4b2545f62b4 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 66b337ad700..a3f444572d3 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 d86a709b55a..9a4ac51945f 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 043bdbedb9963368eadbf86769b12fc6d8c86b60 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Fri, 6 Sep 2024 08:36:04 -0400 Subject: [PATCH 29/33] 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 15619fe967f..e5b263fac22 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 30/33] 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 e5b263fac22..15619fe967f 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 31/33] 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 15619fe967f..fabaa19aa13 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 32/33] 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 bc9442d9d68..82966ced496 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 b14e430441f1d78d7fe9c44a6e97d8603d736b89 Mon Sep 17 00:00:00 2001 From: Cody Soyland Date: Fri, 6 Sep 2024 15:22:43 -0400 Subject: [PATCH 33/33] 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 f053240deb6..055636ad50a 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} }