Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a78c069
pass predicate type to get attestation api methods
malancas Mar 24, 2025
faef81f
reorganize getAttestations func to check for remote gh api fetching f…
malancas Mar 24, 2025
cbe80fd
Merge branch 'trunk' into move-predicate-type-filtering
malancas Mar 24, 2025
ad20ef3
move local and oci registry attestation filtering
malancas Mar 24, 2025
95a6197
pass params object to api client methods
malancas Mar 25, 2025
5a895b9
dedpulicate if else logic
malancas Mar 25, 2025
a9cc7b4
create single fetch by digest client method
malancas Mar 25, 2025
a856a79
remove duplicate predicate filtering code
malancas Mar 25, 2025
0d06547
simplify client methods
malancas Mar 25, 2025
baeaf66
restructure api client methods
malancas Mar 25, 2025
d1c4bf7
comment
malancas Mar 25, 2025
e3fbe90
reduce test duplication
malancas Mar 25, 2025
166e211
clean up test fixtures
malancas Mar 25, 2025
a04be55
Merge branch 'trunk' into move-predicate-type-filtering
malancas Apr 1, 2025
05d9156
add check for nil api client
malancas Apr 1, 2025
cdfb1b7
Merge branch 'move-predicate-type-filtering' of github.com:malancas/c…
malancas Apr 1, 2025
13dafef
add missing nil struct checks and udpate error messages
malancas Apr 1, 2025
f43ec00
add test for predicate type filtering
malancas Apr 1, 2025
56d924d
getAttestations unit tests
malancas Apr 1, 2025
164a56c
move filterAttestations function
malancas Apr 3, 2025
6950728
restore deleted file
malancas Apr 3, 2025
579fae1
Merge branch 'trunk' into move-predicate-type-filtering
malancas Apr 3, 2025
050c68c
Merge branch 'trunk' into move-predicate-type-filtering
malancas Apr 30, 2025
e4ef9bc
Merge branch 'trunk' into move-predicate-type-filtering
malancas May 5, 2025
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
35 changes: 35 additions & 0 deletions pkg/cmd/attestation/api/attestation.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package api

import (
"encoding/json"
"errors"
"fmt"

"github.com/sigstore/sigstore-go/pkg/bundle"
)

Expand All @@ -20,3 +23,35 @@ type Attestation struct {
type AttestationsResponse struct {
Attestations []*Attestation `json:"attestations"`
}

type IntotoStatement struct {
PredicateType string `json:"predicateType"`
}

func FilterAttestations(predicateType string, attestations []*Attestation) ([]*Attestation, error) {
Comment thread
malancas marked this conversation as resolved.
filteredAttestations := []*Attestation{}

for _, each := range attestations {
dsseEnvelope := each.Bundle.GetDsseEnvelope()
if dsseEnvelope != nil {
if dsseEnvelope.PayloadType != "application/vnd.in-toto+json" {
// Don't fail just because an entry isn't intoto
continue
}
var intotoStatement IntotoStatement
if err := json.Unmarshal([]byte(dsseEnvelope.Payload), &intotoStatement); err != nil {
// Don't fail just because a single entry can't be unmarshalled
continue
}
if intotoStatement.PredicateType == predicateType {
filteredAttestations = append(filteredAttestations, each)
}
}
}

if len(filteredAttestations) == 0 {
return nil, fmt.Errorf("no attestations found with predicate type: %s", predicateType)
}

return filteredAttestations, nil
}
94 changes: 61 additions & 33 deletions pkg/cmd/attestation/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@ const (
// Allow injecting backoff interval in tests.
var getAttestationRetryInterval = time.Millisecond * 200

// FetchParams are the parameters for fetching attestations from the GitHub API
type FetchParams struct {
Digest string
Limit int
Owner string
PredicateType string
Repo string
}

func (p *FetchParams) Validate() error {
if p.Digest == "" {
return fmt.Errorf("digest must be provided")
}
if p.Limit <= 0 || p.Limit > maxLimitForFlag {
return fmt.Errorf("limit must be greater than 0 and less than or equal to %d", maxLimitForFlag)
}
if p.Repo == "" && p.Owner == "" {
return fmt.Errorf("owner or repo must be provided")
}
return nil
}

// githubApiClient makes REST calls to the GitHub API
type githubApiClient interface {
REST(hostname, method, p string, body io.Reader, data interface{}) error
Expand All @@ -39,8 +61,7 @@ type httpClient interface {
}

type Client interface {
GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error)
Comment thread
malancas marked this conversation as resolved.
GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error)
GetByDigest(params FetchParams) ([]*Attestation, error)
GetTrustDomain() (string, error)
}

Expand All @@ -60,22 +81,11 @@ func NewLiveClient(hc *http.Client, host string, l *ioconfig.Handler) *LiveClien
}
}

// GetByRepoAndDigest fetches the attestation by repo and digest
func (c *LiveClient) GetByRepoAndDigest(repo, digest string, limit int) ([]*Attestation, error) {
c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest)
url := fmt.Sprintf(GetAttestationByRepoAndSubjectDigestPath, repo, digest)
return c.getByURL(url, limit)
}

// GetByOwnerAndDigest fetches attestation by owner and digest
func (c *LiveClient) GetByOwnerAndDigest(owner, digest string, limit int) ([]*Attestation, error) {
c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", digest)
url := fmt.Sprintf(GetAttestationByOwnerAndSubjectDigestPath, owner, digest)
return c.getByURL(url, limit)
}

func (c *LiveClient) getByURL(url string, limit int) ([]*Attestation, error) {
attestations, err := c.getAttestations(url, limit)
// GetByDigest fetches the attestation by digest and either owner or repo
// depending on which is provided
func (c *LiveClient) GetByDigest(params FetchParams) ([]*Attestation, error) {
c.logger.VerbosePrintf("Fetching attestations for artifact digest %s\n\n", params.Digest)
attestations, err := c.getAttestations(params)
if err != nil {
return nil, err
}
Expand All @@ -88,40 +98,52 @@ func (c *LiveClient) getByURL(url string, limit int) ([]*Attestation, error) {
return bundles, nil
}

// 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) buildRequestURL(params FetchParams) (string, error) {
if err := params.Validate(); err != nil {
return "", err
}

func (c *LiveClient) getAttestations(url string, limit int) ([]*Attestation, error) {
perPage := limit
if perPage <= 0 || perPage > maxLimitForFlag {
return nil, fmt.Errorf("limit must be greater than 0 and less than or equal to %d", maxLimitForFlag)
var url string
if params.Repo != "" {
// check if Repo is set first because if Repo has been set, Owner will be set using the value of Repo.
// If Repo is not set, the field will remain empty. It will not be populated using the value of Owner.
url = fmt.Sprintf(GetAttestationByRepoAndSubjectDigestPath, params.Repo, params.Digest)
} else {
url = fmt.Sprintf(GetAttestationByOwnerAndSubjectDigestPath, params.Owner, params.Digest)
}

perPage := params.Limit
if perPage > maxLimitForFetch {
perPage = maxLimitForFetch
}

// ref: https://github.com/cli/go-gh/blob/d32c104a9a25c9de3d7c7b07a43ae0091441c858/example_gh_test.go#L96
url = fmt.Sprintf("%s?per_page=%d", url, perPage)
if params.PredicateType != "" {
url = fmt.Sprintf("%s&predicate_type=%s", url, params.PredicateType)
}
return url, nil
}

func (c *LiveClient) getAttestations(params FetchParams) ([]*Attestation, error) {
url, err := c.buildRequestURL(params)
if err != nil {
return nil, err
}

var attestations []*Attestation
var resp AttestationsResponse
bo := backoff.NewConstantBackOff(getAttestationRetryInterval)

// if no attestation or less than limit, then keep fetching
for url != "" && len(attestations) < limit {
for url != "" && len(attestations) < params.Limit {
err := backoff.Retry(func() error {
newURL, restErr := c.githubAPI.RESTWithNext(c.host, http.MethodGet, url, nil, &resp)

if restErr != nil {
if shouldRetry(restErr) {
return restErr
} else {
return backoff.Permanent(restErr)
}
return backoff.Permanent(restErr)
}

url = newURL
Expand All @@ -140,8 +162,8 @@ func (c *LiveClient) getAttestations(url string, limit int) ([]*Attestation, err
return nil, ErrNoAttestationsFound
}

if len(attestations) > limit {
return attestations[:limit], nil
if len(attestations) > params.Limit {
return attestations[:params.Limit], nil
}

return attestations, nil
Expand Down Expand Up @@ -241,6 +263,12 @@ func shouldRetry(err error) bool {
return false
}

// GetTrustDomain returns the current trust domain. If the default is used
Comment thread
malancas marked this conversation as resolved.
// the empty string is returned
func (c *LiveClient) GetTrustDomain() (string, error) {
return c.getTrustDomain(MetaPath)
}

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

Expand Down
Loading
Loading