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

     1  package oci
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"sync"
     9  
    10  	"github.com/cheggaaa/pb/v3"
    11  	"github.com/google/go-containerregistry/pkg/name"
    12  	v1 "github.com/google/go-containerregistry/pkg/v1"
    13  	"golang.org/x/xerrors"
    14  
    15  	"github.com/devseccon/trivy/pkg/downloader"
    16  	"github.com/devseccon/trivy/pkg/fanal/types"
    17  	"github.com/devseccon/trivy/pkg/remote"
    18  )
    19  
    20  const (
    21  	// Artifact types
    22  	CycloneDXArtifactType = "application/vnd.cyclonedx+json"
    23  	SPDXArtifactType      = "application/spdx+json"
    24  
    25  	// Media types
    26  	OCIImageManifest = "application/vnd.oci.image.manifest.v1+json"
    27  
    28  	// Annotations
    29  	titleAnnotation = "org.opencontainers.image.title"
    30  )
    31  
    32  var SupportedSBOMArtifactTypes = []string{
    33  	CycloneDXArtifactType,
    34  	SPDXArtifactType,
    35  }
    36  
    37  // Option is a functional option
    38  type Option func(*Artifact)
    39  
    40  // WithImage takes an OCI v1 Image
    41  func WithImage(img v1.Image) Option {
    42  	return func(a *Artifact) {
    43  		a.image = img
    44  	}
    45  }
    46  
    47  // Artifact is used to download artifacts such as vulnerability database and policies from OCI registries.
    48  type Artifact struct {
    49  	m          sync.Mutex
    50  	repository string
    51  	quiet      bool
    52  
    53  	// For OCI registries
    54  	types.RegistryOptions
    55  
    56  	image v1.Image // For testing
    57  }
    58  
    59  // NewArtifact returns a new artifact
    60  func NewArtifact(repo string, quiet bool, registryOpt types.RegistryOptions, opts ...Option) (*Artifact, error) {
    61  	art := &Artifact{
    62  		repository:      repo,
    63  		quiet:           quiet,
    64  		RegistryOptions: registryOpt,
    65  	}
    66  
    67  	for _, o := range opts {
    68  		o(art)
    69  	}
    70  	return art, nil
    71  }
    72  
    73  func (a *Artifact) populate(ctx context.Context, opt types.RegistryOptions) error {
    74  	if a.image != nil {
    75  		return nil
    76  	}
    77  
    78  	a.m.Lock()
    79  	defer a.m.Unlock()
    80  
    81  	var nameOpts []name.Option
    82  	if opt.Insecure {
    83  		nameOpts = append(nameOpts, name.Insecure)
    84  	}
    85  
    86  	ref, err := name.ParseReference(a.repository, nameOpts...)
    87  	if err != nil {
    88  		return xerrors.Errorf("repository name error (%s): %w", a.repository, err)
    89  	}
    90  
    91  	a.image, err = remote.Image(ctx, ref, opt)
    92  	if err != nil {
    93  		return xerrors.Errorf("OCI repository error: %w", err)
    94  	}
    95  	return nil
    96  }
    97  
    98  type DownloadOption struct {
    99  	MediaType string // Accept any media type if not specified
   100  	Filename  string // Use the annotation if not specified
   101  }
   102  
   103  func (a *Artifact) Download(ctx context.Context, dir string, opt DownloadOption) error {
   104  	if err := a.populate(ctx, a.RegistryOptions); err != nil {
   105  		return err
   106  	}
   107  
   108  	layers, err := a.image.Layers()
   109  	if err != nil {
   110  		return xerrors.Errorf("OCI layer error: %w", err)
   111  	}
   112  
   113  	manifest, err := a.image.Manifest()
   114  	if err != nil {
   115  		return xerrors.Errorf("OCI manifest error: %w", err)
   116  	}
   117  
   118  	// A single layer is only supported now.
   119  	if len(layers) != 1 || len(manifest.Layers) != 1 {
   120  		return xerrors.Errorf("OCI artifact must be a single layer")
   121  	}
   122  
   123  	// Take the first layer
   124  	layer := layers[0]
   125  
   126  	// Take the file name of the first layer if not specified
   127  	fileName := opt.Filename
   128  	if fileName == "" {
   129  		if v, ok := manifest.Layers[0].Annotations[titleAnnotation]; !ok {
   130  			return xerrors.Errorf("annotation %s is missing", titleAnnotation)
   131  		} else {
   132  			fileName = v
   133  		}
   134  	}
   135  
   136  	layerMediaType, err := layer.MediaType()
   137  	if err != nil {
   138  		return xerrors.Errorf("media type error: %w", err)
   139  	} else if opt.MediaType != "" && opt.MediaType != string(layerMediaType) {
   140  		return xerrors.Errorf("unacceptable media type: %s", string(layerMediaType))
   141  	}
   142  
   143  	if err = a.download(ctx, layer, fileName, dir); err != nil {
   144  		return xerrors.Errorf("oci download error: %w", err)
   145  	}
   146  
   147  	return nil
   148  }
   149  
   150  func (a *Artifact) download(ctx context.Context, layer v1.Layer, fileName, dir string) error {
   151  	size, err := layer.Size()
   152  	if err != nil {
   153  		return xerrors.Errorf("size error: %w", err)
   154  	}
   155  
   156  	rc, err := layer.Compressed()
   157  	if err != nil {
   158  		return xerrors.Errorf("failed to fetch the layer: %w", err)
   159  	}
   160  	defer rc.Close()
   161  
   162  	// Show progress bar
   163  	bar := pb.Full.Start64(size)
   164  	if a.quiet {
   165  		bar.SetWriter(io.Discard)
   166  	}
   167  	pr := bar.NewProxyReader(rc)
   168  	defer bar.Finish()
   169  
   170  	// https://github.com/hashicorp/go-getter/issues/326
   171  	tempDir, err := os.MkdirTemp("", "trivy")
   172  	if err != nil {
   173  		return xerrors.Errorf("failed to create a temp dir: %w", err)
   174  	}
   175  
   176  	f, err := os.Create(filepath.Join(tempDir, fileName))
   177  	if err != nil {
   178  		return xerrors.Errorf("failed to create a temp file: %w", err)
   179  	}
   180  	defer func() {
   181  		_ = f.Close()
   182  		_ = os.RemoveAll(tempDir)
   183  	}()
   184  
   185  	// Download the layer content into a temporal file
   186  	if _, err = io.Copy(f, pr); err != nil {
   187  		return xerrors.Errorf("copy error: %w", err)
   188  	}
   189  
   190  	// Decompress the downloaded file if it is compressed and copy it into the dst
   191  	if err = downloader.Download(ctx, f.Name(), dir, dir); err != nil {
   192  		return xerrors.Errorf("download error: %w", err)
   193  	}
   194  
   195  	return nil
   196  }
   197  
   198  func (a *Artifact) Digest(ctx context.Context) (string, error) {
   199  	if err := a.populate(ctx, a.RegistryOptions); err != nil {
   200  		return "", err
   201  	}
   202  
   203  	digest, err := a.image.Digest()
   204  	if err != nil {
   205  		return "", xerrors.Errorf("digest error: %w", err)
   206  	}
   207  	return digest.String(), nil
   208  }