github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/artifact/image/remote_sbom.go (about)

     1  package image
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  
    10  	"github.com/google/go-containerregistry/pkg/name"
    11  	v1 "github.com/google/go-containerregistry/pkg/v1"
    12  	"github.com/samber/lo"
    13  	"golang.org/x/exp/slices"
    14  	"golang.org/x/xerrors"
    15  
    16  	sbomatt "github.com/devseccon/trivy/pkg/attestation/sbom"
    17  	"github.com/devseccon/trivy/pkg/fanal/artifact/sbom"
    18  	"github.com/devseccon/trivy/pkg/fanal/log"
    19  	ftypes "github.com/devseccon/trivy/pkg/fanal/types"
    20  	"github.com/devseccon/trivy/pkg/oci"
    21  	"github.com/devseccon/trivy/pkg/remote"
    22  	"github.com/devseccon/trivy/pkg/types"
    23  )
    24  
    25  var errNoSBOMFound = xerrors.New("remote SBOM not found")
    26  
    27  type inspectRemoteSBOM func(context.Context) (ftypes.ArtifactReference, error)
    28  
    29  func (a Artifact) retrieveRemoteSBOM(ctx context.Context) (ftypes.ArtifactReference, error) {
    30  	for _, sbomSource := range a.artifactOption.SBOMSources {
    31  		var inspect inspectRemoteSBOM
    32  		switch sbomSource {
    33  		case types.SBOMSourceOCI:
    34  			inspect = a.inspectOCIReferrerSBOM
    35  		case types.SBOMSourceRekor:
    36  			inspect = a.inspectRekorSBOMAttestation
    37  		default:
    38  			// Never reach here as the "--sbom-sources" values are validated beforehand
    39  			continue
    40  		}
    41  
    42  		ref, err := inspect(ctx)
    43  		if errors.Is(err, errNoSBOMFound) {
    44  			// Try the next SBOM source
    45  			log.Logger.Debugf("No SBOM found in the source: %s", sbomSource)
    46  			continue
    47  		} else if err != nil {
    48  			return ftypes.ArtifactReference{}, xerrors.Errorf("SBOM searching error: %w", err)
    49  		}
    50  		return ref, nil
    51  	}
    52  	return ftypes.ArtifactReference{}, errNoSBOMFound
    53  }
    54  
    55  func (a Artifact) inspectOCIReferrerSBOM(ctx context.Context) (ftypes.ArtifactReference, error) {
    56  	digest, err := repoDigest(a.image, a.artifactOption.Insecure)
    57  	if err != nil {
    58  		return ftypes.ArtifactReference{}, xerrors.Errorf("repo digest error: %w", err)
    59  	}
    60  
    61  	// Fetch referrers
    62  	index, err := remote.Referrers(ctx, digest, a.artifactOption.ImageOption.RegistryOptions)
    63  	if err != nil {
    64  		return ftypes.ArtifactReference{}, xerrors.Errorf("unable to fetch referrers: %w", err)
    65  	}
    66  	manifest, err := index.IndexManifest()
    67  	if err != nil {
    68  		return ftypes.ArtifactReference{}, xerrors.Errorf("unable to get manifest: %w", err)
    69  	}
    70  	for _, m := range lo.FromPtr(manifest).Manifests {
    71  		// Unsupported artifact type
    72  		if !slices.Contains(oci.SupportedSBOMArtifactTypes, m.ArtifactType) {
    73  			continue
    74  		}
    75  		res, err := a.parseReferrer(ctx, digest.Context().String(), m)
    76  		if err != nil {
    77  			log.Logger.Warnf("Error with SBOM via OCI referrers (%s): %s", m.Digest.String(), err)
    78  			continue
    79  		}
    80  		return res, nil
    81  	}
    82  	return ftypes.ArtifactReference{}, errNoSBOMFound
    83  }
    84  
    85  func (a Artifact) parseReferrer(ctx context.Context, repo string, desc v1.Descriptor) (ftypes.ArtifactReference, error) {
    86  	const fileName string = "referrer.sbom"
    87  	repoName := fmt.Sprintf("%s@%s", repo, desc.Digest)
    88  	referrer, err := oci.NewArtifact(repoName, true, a.artifactOption.ImageOption.RegistryOptions)
    89  	if err != nil {
    90  		return ftypes.ArtifactReference{}, xerrors.Errorf("OCI error: %w", err)
    91  	}
    92  
    93  	tmpDir, err := os.MkdirTemp("", "trivy-sbom-*")
    94  	if err != nil {
    95  		return ftypes.ArtifactReference{}, xerrors.Errorf("mkdir temp error: %w", err)
    96  	}
    97  	defer os.RemoveAll(tmpDir)
    98  
    99  	// Download SBOM to local filesystem
   100  	if err = referrer.Download(ctx, tmpDir, oci.DownloadOption{
   101  		MediaType: desc.ArtifactType,
   102  		Filename:  fileName,
   103  	}); err != nil {
   104  		return ftypes.ArtifactReference{}, xerrors.Errorf("SBOM download error: %w", err)
   105  	}
   106  
   107  	res, err := a.inspectSBOMFile(ctx, filepath.Join(tmpDir, fileName))
   108  	if err != nil {
   109  		return res, xerrors.Errorf("SBOM error: %w", err)
   110  	}
   111  
   112  	// Found SBOM
   113  	log.Logger.Infof("Found SBOM (%s) in the OCI referrers", res.Type)
   114  
   115  	return res, nil
   116  }
   117  
   118  func (a Artifact) inspectRekorSBOMAttestation(ctx context.Context) (ftypes.ArtifactReference, error) {
   119  	digest, err := repoDigest(a.image, a.artifactOption.Insecure)
   120  	if err != nil {
   121  		return ftypes.ArtifactReference{}, xerrors.Errorf("repo digest error: %w", err)
   122  	}
   123  
   124  	client, err := sbomatt.NewRekor(a.artifactOption.RekorURL)
   125  	if err != nil {
   126  		return ftypes.ArtifactReference{}, xerrors.Errorf("failed to create rekor client: %w", err)
   127  	}
   128  
   129  	raw, err := client.RetrieveSBOM(ctx, digest.DigestStr())
   130  	if errors.Is(err, sbomatt.ErrNoSBOMAttestation) {
   131  		return ftypes.ArtifactReference{}, errNoSBOMFound
   132  	} else if err != nil {
   133  		return ftypes.ArtifactReference{}, xerrors.Errorf("failed to retrieve SBOM attestation: %w", err)
   134  	}
   135  
   136  	f, err := os.CreateTemp("", "sbom-*")
   137  	if err != nil {
   138  		return ftypes.ArtifactReference{}, xerrors.Errorf("failed to create a temporary file: %w", err)
   139  	}
   140  	defer os.Remove(f.Name())
   141  
   142  	if _, err = f.Write(raw); err != nil {
   143  		return ftypes.ArtifactReference{}, xerrors.Errorf("copy error: %w", err)
   144  	}
   145  	if err = f.Close(); err != nil {
   146  		return ftypes.ArtifactReference{}, xerrors.Errorf("failed to close %s: %w", f.Name(), err)
   147  	}
   148  	res, err := a.inspectSBOMFile(ctx, f.Name())
   149  	if err != nil {
   150  		return res, xerrors.Errorf("SBOM error: %w", err)
   151  	}
   152  
   153  	// Found SBOM
   154  	log.Logger.Infof("Found SBOM (%s) in Rekor (%s)", res.Type, a.artifactOption.RekorURL)
   155  
   156  	return res, nil
   157  }
   158  
   159  func (a Artifact) inspectSBOMFile(ctx context.Context, filePath string) (ftypes.ArtifactReference, error) {
   160  	ar, err := sbom.NewArtifact(filePath, a.cache, a.artifactOption)
   161  	if err != nil {
   162  		return ftypes.ArtifactReference{}, xerrors.Errorf("failed to new artifact: %w", err)
   163  	}
   164  
   165  	results, err := ar.Inspect(ctx)
   166  	if err != nil {
   167  		return ftypes.ArtifactReference{}, xerrors.Errorf("failed to inspect: %w", err)
   168  	}
   169  	results.Name = a.image.Name()
   170  
   171  	return results, nil
   172  }
   173  
   174  func repoDigest(img ftypes.Image, insecure bool) (name.Digest, error) {
   175  	repoNameFull := img.Name()
   176  	ref, err := name.ParseReference(repoNameFull)
   177  	if err != nil {
   178  		return name.Digest{}, xerrors.Errorf("image name parse error: %w", err)
   179  	}
   180  
   181  	for _, rd := range img.RepoDigests() {
   182  		opts := lo.Ternary(insecure, []name.Option{name.Insecure}, nil)
   183  		digest, err := name.NewDigest(rd, opts...)
   184  		if err != nil {
   185  			continue
   186  		}
   187  		if ref.Context().String() == digest.Context().String() {
   188  			return digest, nil
   189  		}
   190  	}
   191  	return name.Digest{}, xerrors.Errorf("no repo digest found: %w", errNoSBOMFound)
   192  }