diff --git a/pkg/cmd/attestation/artifact/artifact.go b/pkg/cmd/attestation/artifact/artifact.go index de354b9477f..13178516636 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/google/go-containerregistry/pkg/name" + "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" ) @@ -22,6 +24,7 @@ type DigestedArtifact struct { URL string digest string digestAlg string + nameRef name.Reference } 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) NameRef() name.Reference { + return a.nameRef +} diff --git a/pkg/cmd/attestation/artifact/image.go b/pkg/cmd/attestation/artifact/image.go index 2af13e72357..dda5f65dbf0 100644 --- a/pkg/cmd/attestation/artifact/image.go +++ b/pkg/cmd/attestation/artifact/image.go @@ -15,7 +15,8 @@ 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, nameRef, err := client.GetImageDigest(named.String()) + if err != nil { return nil, err } @@ -24,5 +25,6 @@ func digestContainerImageArtifact(url string, client oci.Client) (*DigestedArtif 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 5b8d8cf7a00..5428fff2fcc 100644 --- a/pkg/cmd/attestation/artifact/oci/client.go +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -3,10 +3,16 @@ package oci import ( "errors" "fmt" + "io" + "net/http" + "strings" "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" ) @@ -15,7 +21,8 @@ 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, error) + GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) + GetAttestations(name name.Reference, digest string) ([]*api.Attestation, error) } func checkForUnauthorizedOrDeniedErr(err transport.Error) error { @@ -35,13 +42,16 @@ type LiveClient struct { get func(name.Reference, ...remote.Option) (*remote.Descriptor, error) } +func (c LiveClient) ParseReference(ref string) (name.Reference, error) { + return c.parseReference(ref) +} + // where name is formed like ghcr.io/github/my-image-repo -func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, error) { +func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) { name, err := c.parseReference(imgName) if err != nil { - return nil, fmt.Errorf("failed to create image tag: %v", err) + 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 @@ -50,13 +60,92 @@ func (c LiveClient) GetImageDigest(imgName string) (*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, 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 err != nil { + return resp, err + } + if resp.StatusCode == http.StatusNotAcceptable && strings.Contains(req.URL.Path, "/referrers/") { + resp.StatusCode = http.StatusNotFound + } + + return resp, err +} + +func (c LiveClient) GetAttestations(ref name.Reference, digest string) ([]*api.Attestation, error) { + attestations := make([]*api.Attestation, 0) + + 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("error getting referrers: %w", err) } + refManifest, err := referrers.IndexManifest() + if err != nil { + return attestations, fmt.Errorf("error getting referrers manifest: %w", err) + } + + for _, refDesc := range refManifest.Manifests { + if !strings.HasPrefix(refDesc.ArtifactType, "application/vnd.dev.sigstore.bundle") { + continue + } + + refImg, err := remote.Image(ref.Context().Digest(refDesc.Digest.String()), remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return attestations, fmt.Errorf("error getting referrer image: %w", err) + } + layers, err := refImg.Layers() + if err != nil { + return attestations, fmt.Errorf("error getting referrer image: %w", err) + } + + if len(layers) > 0 { + layer0, err := layers[0].Uncompressed() + if err != nil { + return attestations, fmt.Errorf("error getting referrer image: %w", err) + } + defer layer0.Close() - return &desc.Digest, nil + bundleBytes, err := io.ReadAll(layer0) + + if err != nil { + return attestations, fmt.Errorf("error getting referrer image: %w", err) + } + + b := &bundle.ProtobufBundle{} + err = b.UnmarshalJSON(bundleBytes) + + if err != nil { + return attestations, fmt.Errorf("error unmarshalling bundle: %w", err) + } + + a := api.Attestation{Bundle: b} + attestations = append(attestations, &a) + } else { + return attestations, fmt.Errorf("error getting referrer image: no layers found") + } + } + return attestations, nil } // 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..b869c60a931 100644 --- a/pkg/cmd/attestation/artifact/oci/mock_client.go +++ b/pkg/cmd/attestation/artifact/oci/mock_client.go @@ -3,32 +3,83 @@ 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 NoAttestationsClient struct{} + +func (c NoAttestationsClient) GetImageDigest(imgName string) (*v1.Hash, name.Reference, error) { + return &v1.Hash{ + Hex: "1234567890abcdef", + Algorithm: "sha256", + }, nil, nil +} + +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 52a8ff02547..5feca47eac7 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,12 +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 + 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 { @@ -32,6 +37,11 @@ func GetAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) { if c.IsBundleProvided() { return GetLocalAttestations(c.BundlePath) } + + if c.UseBundleFromRegistry { + return GetOCIAttestations(c) + } + return GetRemoteAttestations(c) } @@ -115,6 +125,17 @@ 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 attestations found in the OCI registry. Retry the command without the --bundle-from-oci flag to check GitHub for the attestation") + } + return attestations, nil +} + type IntotoStatement struct { PredicateType string `json:"predicateType"` } diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index da2c7bb4e88..3ec2d49f1c8 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 @@ -83,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-oci flag is only used with OCI artifact paths + if opts.UseBundleFromRegistry && !strings.HasPrefix(opts.ArtifactPath, "oci://") { + return fmt.Errorf("bundle-from-oci flag can only be used with OCI artifact paths") + } + + // 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-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 aea131b279f..b9be054a512 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-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) { + 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-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 e1a5b1c50ce..f053240deb6 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-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 /") @@ -182,12 +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, + 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 { @@ -198,6 +202,8 @@ 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 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")) } @@ -207,6 +213,8 @@ 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 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..89c5ae7c1a7 100644 --- a/pkg/cmd/attestation/verify/verify_test.go +++ b/pkg/cmd/attestation/verify/verify_test.go @@ -463,4 +463,41 @@ 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 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 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) { + customOpts := publicGoodOpts + customOpts.ArtifactPath = "oci://ghcr.io/github/test" + customOpts.BundlePath = "" + customOpts.UseBundleFromRegistry = true + customOpts.OCIClient = oci.NoAttestationsClient{} + + 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") + }) }