Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2
github.com/briandowns/spinner v1.18.1
github.com/cenkalti/backoff/v4 v4.3.0
github.com/cenkalti/backoff/v5 v5.0.2
Comment on lines 12 to +13

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if actually we need both of these

github.com/charmbracelet/glamour v0.9.2-0.20250319212134-549f544650e3
github.com/charmbracelet/huh v0.7.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250319133953-166f707985bc
Expand Down Expand Up @@ -48,6 +49,7 @@ require (
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/theupdateframework/go-tuf/v2 v2.1.1
github.com/yuin/goldmark v1.7.12
github.com/zalando/go-keyring v0.2.5
golang.org/x/crypto v0.38.0
Expand All @@ -73,7 +75,6 @@ require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/charmbracelet/bubbles v0.21.0 // indirect
github.com/charmbracelet/bubbletea v1.3.4 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
Expand Down Expand Up @@ -167,7 +168,6 @@ require (
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/theupdateframework/go-tuf v0.7.0 // indirect
github.com/theupdateframework/go-tuf/v2 v2.1.1 // indirect
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/transparency-dev/merkle v0.0.2 // indirect
Expand Down
9 changes: 7 additions & 2 deletions pkg/cmd/attestation/inspect/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,18 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command
opts.Hostname, _ = ghauth.DefaultHost()
}

err := auth.IsHostSupported(opts.Hostname)
if err := auth.IsHostSupported(opts.Hostname); err != nil {
return err
}

hc, err := f.HttpClient()
if err != nil {
return err
}

config := verification.SigstoreConfig{
Logger: opts.Logger,
HttpClient: hc,
Logger: opts.Logger,
}

if ghauth.IsTenancy(opts.Hostname) {
Expand Down
45 changes: 45 additions & 0 deletions pkg/cmd/attestation/inspect/inspect_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//go:build integration

package inspect

import (
"bytes"
"fmt"
"net/http"
"strings"
"testing"

"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
)

func TestNewInspectCmd_PrintOutputJSONFormat(t *testing.T) {
testIO, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: testIO,
HttpClient: func() (*http.Client, error) {
return http.DefaultClient, nil
},
}

t.Run("Print output in JSON format", func(t *testing.T) {
var opts *Options
cmd := NewInspectCmd(f, func(o *Options) error {
opts = o
return nil
})

argv := strings.Split(fmt.Sprintf("%s --format json", bundlePath), " ")
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err := cmd.ExecuteC()
assert.NoError(t, err)

assert.Equal(t, bundlePath, opts.BundlePath)
assert.NotNil(t, opts.Logger)
assert.NotNil(t, opts.exporter)
})
}
66 changes: 1 addition & 65 deletions pkg/cmd/attestation/inspect/inspect_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package inspect

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"

"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
Expand All @@ -14,7 +10,6 @@ import (
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmdutil"

"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"

"github.com/stretchr/testify/assert"
Expand All @@ -26,66 +21,7 @@ const (
SigstoreSanRegex = "^https://github.com/sigstore/sigstore-js/"
)

var (
bundlePath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json")
)

func TestNewInspectCmd(t *testing.T) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaned up this test and moved it to pkg/cmd/attestation/inspect/inspect_interation_test.go since it requires an internet connection.

testIO, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: testIO,
HttpClient: func() (*http.Client, error) {
reg := &httpmock.Registry{}
client := &http.Client{}
httpmock.ReplaceTripper(client, reg)
return client, nil
},
}

testcases := []struct {
name string
cli string
wants Options
wantsErr bool
wantsExporter bool
}{
{
name: "Prints output in JSON format",
cli: fmt.Sprintf("%s --format json", bundlePath),
wants: Options{
BundlePath: bundlePath,
SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
},
wantsExporter: true,
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
var opts *Options
cmd := NewInspectCmd(f, func(o *Options) error {
opts = o
return nil
})

argv := strings.Split(tc.cli, " ")
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err := cmd.ExecuteC()
if tc.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)

assert.Equal(t, tc.wants.BundlePath, opts.BundlePath)
assert.NotNil(t, opts.Logger)
assert.Equal(t, tc.wantsExporter, opts.exporter != nil)
})
}
}
var bundlePath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json")

func TestRunInspect(t *testing.T) {
opts := Options{
Expand Down
18 changes: 9 additions & 9 deletions pkg/cmd/attestation/trustedroot/trustedroot.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"

"github.com/cli/cli/v2/pkg/cmd/attestation/api"
Expand Down Expand Up @@ -68,6 +69,10 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com
return err
}

hc, err := f.HttpClient()
if err != nil {
return err
}
if ghauth.IsTenancy(opts.Hostname) {
c, err := f.Config()
if err != nil {
Expand All @@ -77,11 +82,6 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com
if !c.Authentication().HasActiveToken(opts.Hostname) {
return fmt.Errorf("not authenticated with %s", opts.Hostname)
}

hc, err := f.HttpClient()
if err != nil {
return err
}
logger := io.NewHandler(f.IOStreams)
apiClient := api.NewLiveClient(hc, opts.Hostname, logger)
td, err := apiClient.GetTrustDomain()
Expand All @@ -95,7 +95,7 @@ func NewTrustedRootCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Com
return runF(opts)
}

if err := getTrustedRoot(tuf.New, opts); err != nil {
if err := getTrustedRoot(tuf.New, opts, hc); err != nil {
return fmt.Errorf("Failed to verify the TUF repository: %w", err)
}

Expand All @@ -118,11 +118,11 @@ type tufConfig struct {
targets []string
}

func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error {
func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options, hc *http.Client) error {
var tufOptions []tufConfig
var defaultTR = "trusted_root.json"

tufOpt := verification.DefaultOptionsWithCacheSetting(o.None[string]())
tufOpt := verification.DefaultOptionsWithCacheSetting(o.None[string](), hc)
// Disable local caching, so we get up-to-date response from TUF repository
tufOpt.CacheValidity = 0

Expand Down Expand Up @@ -151,7 +151,7 @@ func getTrustedRoot(makeTUF tufClientInstantiator, opts *Options) error {
targets: []string{defaultTR},
})

tufOpt = verification.GitHubTUFOptions(o.None[string]())
tufOpt = verification.GitHubTUFOptions(o.None[string](), hc)
tufOpt.CacheValidity = 0
tufOptions = append(tufOptions, tufConfig{
tufOptions: tufOpt,
Expand Down
15 changes: 13 additions & 2 deletions pkg/cmd/attestation/trustedroot/trustedroot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ func TestNewTrustedRootCmd(t *testing.T) {
Config: func() (gh.Config, error) {
return &ghmock.ConfigMock{}, nil
},
HttpClient: func() (*http.Client, error) {
reg := &httpmock.Registry{}
client := &http.Client{}
httpmock.ReplaceTripper(client, reg)
return client, nil
},
}

testcases := []struct {
Expand Down Expand Up @@ -113,6 +119,7 @@ func TestNewTrustedRootWithTenancy(t *testing.T) {
},
}, nil
},
HttpClient: httpClientFunc,
}

cmd := NewTrustedRootCmd(f, func(_ *Options) error {
Expand Down Expand Up @@ -171,15 +178,19 @@ func TestGetTrustedRoot(t *testing.T) {
TufRootPath: root,
}

reg := &httpmock.Registry{}
client := &http.Client{}
httpmock.ReplaceTripper(client, reg)

t.Run("failed to create TUF root", func(t *testing.T) {
err := getTrustedRoot(newTUFErrClient, opts)
err := getTrustedRoot(newTUFErrClient, opts, client)
require.Error(t, err)
require.ErrorContains(t, err, "failed to create TUF client")
})

t.Run("fails because the root cannot be found", func(t *testing.T) {
opts.TufRootPath = test.NormalizeRelativePath("./does/not/exist/root.json")
err := getTrustedRoot(tuf.New, opts)
err := getTrustedRoot(tuf.New, opts, client)
require.Error(t, err)
require.ErrorContains(t, err, "failed to read root file")
})
Expand Down
14 changes: 8 additions & 6 deletions pkg/cmd/attestation/verification/sigstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/x509"
"errors"
"fmt"
"net/http"
"os"

"github.com/cli/cli/v2/pkg/cmd/attestation/api"
Expand Down Expand Up @@ -33,6 +34,7 @@ type SigstoreConfig struct {
TrustedRoot string
Logger *io.Handler
NoPublicGood bool
HttpClient *http.Client
// If tenancy mode is not used, trust domain is empty
TrustDomain string
// TUFMetadataDir
Expand Down Expand Up @@ -71,13 +73,13 @@ func NewLiveSigstoreVerifier(config SigstoreConfig) (*LiveSigstoreVerifier, erro
return liveVerifier, nil
}
if !config.NoPublicGood {
publicGoodVerifier, err := newPublicGoodVerifier(config.TUFMetadataDir)
publicGoodVerifier, err := newPublicGoodVerifier(config.TUFMetadataDir, config.HttpClient)
if err != nil {
return nil, err
}
liveVerifier.PublicGood = publicGoodVerifier
}
github, err := newGitHubVerifier(config.TrustDomain, config.TUFMetadataDir)
github, err := newGitHubVerifier(config.TrustDomain, config.TUFMetadataDir, config.HttpClient)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -314,10 +316,10 @@ func newCustomVerifier(trustedRoot *root.TrustedRoot) (*verify.Verifier, error)
return gv, nil
}

func newGitHubVerifier(trustDomain string, tufMetadataDir o.Option[string]) (*verify.Verifier, error) {
func newGitHubVerifier(trustDomain string, tufMetadataDir o.Option[string], hc *http.Client) (*verify.Verifier, error) {
var tr string

opts := GitHubTUFOptions(tufMetadataDir)
opts := GitHubTUFOptions(tufMetadataDir, hc)
client, err := tuf.New(opts)
if err != nil {
return nil, fmt.Errorf("failed to create TUF client: %v", err)
Expand Down Expand Up @@ -348,8 +350,8 @@ func newGitHubVerifierWithTrustedRoot(trustedRoot *root.TrustedRoot) (*verify.Ve
return gv, nil
}

func newPublicGoodVerifier(tufMetadataDir o.Option[string]) (*verify.Verifier, error) {
opts := DefaultOptionsWithCacheSetting(tufMetadataDir)
func newPublicGoodVerifier(tufMetadataDir o.Option[string], hc *http.Client) (*verify.Verifier, error) {
opts := DefaultOptionsWithCacheSetting(tufMetadataDir, hc)
client, err := tuf.New(opts)
if err != nil {
return nil, fmt.Errorf("failed to create TUF client: %v", err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package verification

import (
"net/http"
"testing"

"github.com/cli/cli/v2/pkg/cmd/attestation/api"
Expand Down Expand Up @@ -51,6 +52,7 @@ func TestLiveSigstoreVerifier(t *testing.T) {
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{
HttpClient: http.DefaultClient,
Logger: io.NewTestHandler(),
TUFMetadataDir: o.Some(t.TempDir()),
})
Expand All @@ -71,6 +73,7 @@ func TestLiveSigstoreVerifier(t *testing.T) {

t.Run("with 2/3 verified attestations", func(t *testing.T) {
verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{
HttpClient: http.DefaultClient,
Logger: io.NewTestHandler(),
TUFMetadataDir: o.Some(t.TempDir()),
})
Expand All @@ -89,6 +92,7 @@ func TestLiveSigstoreVerifier(t *testing.T) {

t.Run("fail with 0/2 verified attestations", func(t *testing.T) {
verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{
HttpClient: http.DefaultClient,
Logger: io.NewTestHandler(),
TUFMetadataDir: o.Some(t.TempDir()),
})
Expand All @@ -114,6 +118,7 @@ func TestLiveSigstoreVerifier(t *testing.T) {
attestations := getAttestationsFor(t, "../test/data/github_provenance_demo-0.0.12-py3-none-any-bundle.jsonl")

verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{
HttpClient: http.DefaultClient,
Logger: io.NewTestHandler(),
TUFMetadataDir: o.Some(t.TempDir()),
})
Expand All @@ -128,6 +133,7 @@ func TestLiveSigstoreVerifier(t *testing.T) {
attestations := getAttestationsFor(t, "../test/data/sigstore-js-2.1.0_with_2_bundles.jsonl")

verifier, err := NewLiveSigstoreVerifier(SigstoreConfig{
HttpClient: http.DefaultClient,
Logger: io.NewTestHandler(),
TrustedRoot: test.NormalizeRelativePath("../test/data/trusted_root.json"),
TUFMetadataDir: o.Some(t.TempDir()),
Expand Down
Loading