github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/attestation/sbom/rekor.go (about)

     1  package sbom
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  
     9  	"github.com/in-toto/in-toto-golang/in_toto"
    10  	"github.com/samber/lo"
    11  	"golang.org/x/xerrors"
    12  
    13  	"github.com/devseccon/trivy/pkg/attestation"
    14  	"github.com/devseccon/trivy/pkg/log"
    15  	"github.com/devseccon/trivy/pkg/rekor"
    16  )
    17  
    18  var ErrNoSBOMAttestation = xerrors.New("no SBOM attestation found")
    19  
    20  type Rekor struct {
    21  	client *rekor.Client
    22  }
    23  
    24  func NewRekor(url string) (Rekor, error) {
    25  	c, err := rekor.NewClient(url)
    26  	if err != nil {
    27  		return Rekor{}, xerrors.Errorf("rekor client error: %w", err)
    28  	}
    29  	return Rekor{
    30  		client: c,
    31  	}, nil
    32  }
    33  
    34  func (r *Rekor) RetrieveSBOM(ctx context.Context, digest string) ([]byte, error) {
    35  	entryIDs, err := r.client.Search(ctx, digest)
    36  	if err != nil {
    37  		return nil, xerrors.Errorf("failed to search rekor records: %w", err)
    38  	} else if len(entryIDs) == 0 {
    39  		return nil, ErrNoSBOMAttestation
    40  	}
    41  
    42  	log.Logger.Debugf("Found matching Rekor entries: %s", entryIDs)
    43  
    44  	for _, ids := range lo.Chunk[rekor.EntryID](entryIDs, rekor.MaxGetEntriesLimit) {
    45  		entries, err := r.client.GetEntries(ctx, ids)
    46  		if err != nil {
    47  			return nil, xerrors.Errorf("failed to get entries: %w", err)
    48  		}
    49  
    50  		for _, entry := range entries {
    51  			ref, err := r.inspectRecord(entry)
    52  			if errors.Is(err, ErrNoSBOMAttestation) {
    53  				continue
    54  			} else if err != nil {
    55  				return nil, xerrors.Errorf("rekor record inspection error: %w", err)
    56  			}
    57  			return ref, nil
    58  		}
    59  	}
    60  	return nil, ErrNoSBOMAttestation
    61  }
    62  
    63  func (r *Rekor) inspectRecord(entry rekor.Entry) ([]byte, error) {
    64  	// TODO: Trivy SBOM should take precedence
    65  	raw, err := r.parseStatement(entry)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  	return raw, nil
    70  }
    71  
    72  func (r *Rekor) parseStatement(entry rekor.Entry) (json.RawMessage, error) {
    73  	// Skip base64-encoded attestation
    74  	if bytes.HasPrefix(entry.Statement, []byte(`eyJ`)) {
    75  		return nil, ErrNoSBOMAttestation
    76  	}
    77  
    78  	// Parse statement of in-toto attestation
    79  	var raw json.RawMessage
    80  	statement := &in_toto.Statement{
    81  		Predicate: &attestation.CosignPredicate{
    82  			Data: &raw, // Extract CycloneDX or SPDX
    83  		},
    84  	}
    85  	if err := json.Unmarshal(entry.Statement, &statement); err != nil {
    86  		return nil, xerrors.Errorf("attestation parse error: %w", err)
    87  	}
    88  
    89  	// TODO: add support for SPDX
    90  	if statement.PredicateType != in_toto.PredicateCycloneDX {
    91  		return nil, xerrors.Errorf("unsupported predicate type %s: %w", statement.PredicateType, ErrNoSBOMAttestation)
    92  	}
    93  	return raw, nil
    94  }