Added tenancy aware attestation commands by kommendorkapten · Pull Request #9542 · cli/cli · GitHub
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
3 changes: 3 additions & 0 deletions .gitignore
24 changes: 20 additions & 4 deletions pkg/cmd/attestation/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

"github.com/cli/cli/v2/api"
ioconfig "github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/go-gh/v2/pkg/auth"
)

const (
Expand All @@ -18,12 +17,14 @@ const (
)

type apiClient interface {
REST(hostname, method, p string, body io.Reader, data interface{}) error
RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error)
}

type Client interface {
GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error)
GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error)
GetTrustDomain() (string, error)
}

type LiveClient struct {
Expand All @@ -32,9 +33,7 @@ type LiveClient struct {
logger *ioconfig.Handler
}

func NewLiveClient(hc *http.Client, l *ioconfig.Handler) *LiveClient {
host, _ := auth.DefaultHost()

func NewLiveClient(hc *http.Client, host string, l *ioconfig.Handler) *LiveClient {
return &LiveClient{
api: api.NewClientFromHTTP(hc),
host: strings.TrimSuffix(host, "/"),
Expand Down Expand Up @@ -64,6 +63,12 @@ func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*At
return c.getAttestations(url, owner, digest, limit)
}

// GetTrustDomain returns the current trust domain. If the default is used
// the empty string is returned
func (c *LiveClient) GetTrustDomain() (string, error) {
return c.getTrustDomain(MetaPath)
}

func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*Attestation, error) {
c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest)

Expand Down Expand Up @@ -102,3 +107,14 @@ func (c *LiveClient) getAttestations(url, name, digest string, limit int) ([]*At

return attestations, nil
}

func (c *LiveClient) getTrustDomain(url string) (string, error) {
var resp MetaResponse

err := c.api.REST(c.host, http.MethodGet, url, nil, &resp)
if err != nil {
return "", err
}

return resp.Domains.ArtifactAttestations.TrustDomain, nil
}
32 changes: 32 additions & 0 deletions pkg/cmd/attestation/api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,35 @@ func TestGetByDigest_Error(t *testing.T) {
require.Error(t, err)
require.Nil(t, attestations)
}

func TestGetTrustDomain(t *testing.T) {
fetcher := mockMetaGenerator{
TrustDomain: "foo",
}

t.Run("with returned trust domain", func(t *testing.T) {
c := LiveClient{
api: mockAPIClient{
OnREST: fetcher.OnREST,
},
logger: io.NewTestHandler(),
}
td, err := c.GetTrustDomain()
require.Nil(t, err)
require.Equal(t, "foo", td)

})

t.Run("with error", func(t *testing.T) {
c := LiveClient{
api: mockAPIClient{
OnREST: fetcher.OnRESTError,
},
logger: io.NewTestHandler(),
}
td, err := c.GetTrustDomain()
require.Equal(t, "", td)
require.ErrorContains(t, err, "test error")
})

}
28 changes: 28 additions & 0 deletions pkg/cmd/attestation/api/mock_apiClient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ import (

type mockAPIClient struct {
OnRESTWithNext func(hostname, method, p string, body io.Reader, data interface{}) (string, error)
OnREST func(hostname, method, p string, body io.Reader, data interface{}) error
}

func (m mockAPIClient) RESTWithNext(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
return m.OnRESTWithNext(hostname, method, p, body, data)
}

func (m mockAPIClient) REST(hostname, method, p string, body io.Reader, data interface{}) error {
return m.OnREST(hostname, method, p, body, data)
}

type mockDataGenerator struct {
NumAttestations int
}
Expand Down Expand Up @@ -87,3 +92,26 @@ func (m mockDataGenerator) OnRESTWithNextNoAttestations(hostname, method, p stri
func (m mockDataGenerator) OnRESTWithNextError(hostname, method, p string, body io.Reader, data interface{}) (string, error) {
return "", errors.New("failed to get attestations")
}

type mockMetaGenerator struct {
TrustDomain string
}

func (m mockMetaGenerator) OnREST(hostname, method, p string, body io.Reader, data interface{}) error {
var template = `
{
"domains": {
"artifact_attestations": {
"trust_domain": "%s"
}
}
}
`
var jsonString = fmt.Sprintf(template, m.TrustDomain)
return json.Unmarshal([]byte(jsonString), &data)

}

func (m mockMetaGenerator) OnRESTError(hostname, method, p string, body io.Reader, data interface{}) error {
return errors.New("test error")
}
5 changes: 5 additions & 0 deletions pkg/cmd/attestation/api/mock_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
type MockClient struct {
OnGetByRepoAndDigest func(repo, digest string, limit int) ([]*Attestation, error)
OnGetByOwnerAndDigest func(owner, digest string, limit int) ([]*Attestation, error)
OnGetTrustDomain func() (string, error)
}

func (m MockClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) {
Expand All @@ -19,6 +20,10 @@ func (m MockClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Att
return m.OnGetByOwnerAndDigest(owner, digest, limit)
}

func (m MockClient) GetTrustDomain() (string, error) {
return m.OnGetTrustDomain()
}

func makeTestAttestation() Attestation {
return Attestation{Bundle: data.SigstoreBundle(nil)}
}
Expand Down
15 changes: 15 additions & 0 deletions pkg/cmd/attestation/api/trust_domain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package api

const MetaPath = "meta"

type ArtifactAttestations struct {
TrustDomain string `json:"trust_domain"`
}

type Domain struct {
ArtifactAttestations ArtifactAttestations `json:"artifact_attestations"`
}

type MetaResponse struct {
Domains Domain `json:"domains"`
}
5 changes: 1 addition & 4 deletions pkg/cmd/attestation/auth/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@ import (
"errors"

"github.com/cli/cli/v2/internal/ghinstance"
"github.com/cli/go-gh/v2/pkg/auth"
)

var ErrUnsupportedHost = errors.New("An unsupported host was detected. Note that gh attestation does not currently support GHES")

func IsHostSupported() error {
host, _ := auth.DefaultHost()

func IsHostSupported(host string) error {
// Note that this check is slightly redundant as Tenancy should not be considered Enterprise
// but the ghinstance package has not been updated to reflect this yet.
if ghinstance.IsEnterprise(host) && !ghinstance.IsTenancy(host) {
Expand Down
5 changes: 4 additions & 1 deletion pkg/cmd/attestation/auth/host_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package auth
import (
"testing"

ghauth "github.com/cli/go-gh/v2/pkg/auth"

"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -38,7 +40,8 @@ func TestIsHostSupported(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("GH_HOST", tc.host)

err := IsHostSupported()
host, _ := ghauth.DefaultHost()
err := IsHostSupported(host)
if tc.expectedErr {
require.ErrorIs(t, err, ErrUnsupportedHost)
} else {
Expand Down
16 changes: 10 additions & 6 deletions pkg/cmd/attestation/download/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/cli/cli/v2/pkg/cmd/attestation/io"
"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
"github.com/cli/cli/v2/pkg/cmdutil"
ghauth "github.com/cli/go-gh/v2/pkg/auth"

"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -78,16 +79,18 @@ func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Comman
if err != nil {
return err
}
opts.APIClient = api.NewLiveClient(hc, opts.Logger)

opts.OCIClient = oci.NewLiveClient()

opts.Store = NewLiveStore("")

if err := auth.IsHostSupported(); err != nil {
if opts.Hostname == "" {
opts.Hostname, _ = ghauth.DefaultHost()
}
if err := auth.IsHostSupported(opts.Hostname); err != nil {
return err
}

opts.APIClient = api.NewLiveClient(hc, opts.Hostname, opts.Logger)
opts.OCIClient = oci.NewLiveClient()
opts.Store = NewLiveStore("")

if runF != nil {
return runF(opts)
}
Expand All @@ -106,6 +109,7 @@ func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Comman
downloadCmd.Flags().StringVarP(&opts.PredicateType, "predicate-type", "", "", "Filter attestations by provided predicate type")
cmdutil.StringEnumFlag(downloadCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact")
downloadCmd.Flags().IntVarP(&opts.Limit, "limit", "L", api.DefaultLimit, "Maximum number of attestations to fetch")
downloadCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use")

return downloadCmd
}
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/attestation/download/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Options struct {
Owner string
PredicateType string
Repo string
Hostname string
}

func (opts *Options) AreFlagsValid() error {
Expand Down
30 changes: 20 additions & 10 deletions pkg/cmd/attestation/inspect/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,27 @@ type AttestationDetail struct {
WorkflowID string `json:"workflowId"`
}

func getOrgAndRepo(repoURL string) (string, string, error) {
after, found := strings.CutPrefix(repoURL, "https://github.com/")
if !found {
return "", "", fmt.Errorf("failed to get org and repo from %s", repoURL)
func getOrgAndRepo(tenant, repoURL string) (string, string, error) {
var after string
var found bool
if tenant == "" {
after, found = strings.CutPrefix(repoURL, "https://github.com/")
if !found {
return "", "", fmt.Errorf("failed to get org and repo from %s", repoURL)
}
} else {
after, found = strings.CutPrefix(repoURL,
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
fmt.Sprintf("https://%s.ghe.com/", tenant))
if !found {
return "", "", fmt.Errorf("failed to get org and repo from %s", repoURL)
}
}

parts := strings.Split(after, "/")
return parts[0], parts[1], nil
}

func getAttestationDetail(attr api.Attestation) (AttestationDetail, error) {
func getAttestationDetail(tenant string, attr api.Attestation) (AttestationDetail, error) {
envelope, err := attr.Bundle.Envelope()
if err != nil {
return AttestationDetail{}, fmt.Errorf("failed to get envelope from bundle: %v", err)
Expand All @@ -87,7 +97,7 @@ func getAttestationDetail(attr api.Attestation) (AttestationDetail, error) {
return AttestationDetail{}, fmt.Errorf("failed to unmarshal predicate: %v", err)
}

org, repo, err := getOrgAndRepo(predicate.BuildDefinition.ExternalParameters.Workflow.Repository)
org, repo, err := getOrgAndRepo(tenant, predicate.BuildDefinition.ExternalParameters.Workflow.Repository)
if err != nil {
return AttestationDetail{}, fmt.Errorf("failed to parse attestation content: %v", err)
}
Expand All @@ -101,11 +111,11 @@ func getAttestationDetail(attr api.Attestation) (AttestationDetail, error) {
}, nil
}

func getDetailsAsSlice(results []*verification.AttestationProcessingResult) ([][]string, error) {
func getDetailsAsSlice(tenant string, results []*verification.AttestationProcessingResult) ([][]string, error) {
details := make([][]string, len(results))

for i, result := range results {
detail, err := getAttestationDetail(*result.Attestation)
detail, err := getAttestationDetail(tenant, *result.Attestation)
if err != nil {
return nil, fmt.Errorf("failed to get attestation detail: %v", err)
}
Expand All @@ -114,11 +124,11 @@ func getDetailsAsSlice(results []*verification.AttestationProcessingResult) ([][
return details, nil
}

func getAttestationDetails(results []*verification.AttestationProcessingResult) ([]AttestationDetail, error) {
func getAttestationDetails(tenant string, results []*verification.AttestationProcessingResult) ([]AttestationDetail, error) {
details := make([]AttestationDetail, len(results))

for i, result := range results {
detail, err := getAttestationDetail(*result.Attestation)
detail, err := getAttestationDetail(tenant, *result.Attestation)
if err != nil {
return nil, fmt.Errorf("failed to get attestation detail: %v", err)
}
Expand Down
14 changes: 11 additions & 3 deletions pkg/cmd/attestation/inspect/bundle_test.go
Loading