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

     1  package remote
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"net"
     7  	"net/http"
     8  	"time"
     9  
    10  	"github.com/google/go-containerregistry/pkg/authn"
    11  	"github.com/google/go-containerregistry/pkg/name"
    12  	v1 "github.com/google/go-containerregistry/pkg/v1"
    13  	"github.com/google/go-containerregistry/pkg/v1/remote"
    14  	v1types "github.com/google/go-containerregistry/pkg/v1/types"
    15  	"github.com/hashicorp/go-multierror"
    16  	"github.com/samber/lo"
    17  	"golang.org/x/xerrors"
    18  
    19  	"github.com/devseccon/trivy/pkg/fanal/image/registry"
    20  	"github.com/devseccon/trivy/pkg/fanal/types"
    21  	"github.com/devseccon/trivy/pkg/log"
    22  )
    23  
    24  type Descriptor = remote.Descriptor
    25  
    26  // Get is a wrapper of google/go-containerregistry/pkg/v1/remote.Get
    27  // so that it can try multiple authentication methods.
    28  func Get(ctx context.Context, ref name.Reference, option types.RegistryOptions) (*Descriptor, error) {
    29  	transport, err := httpTransport(option)
    30  	if err != nil {
    31  		return nil, xerrors.Errorf("failed to create http transport: %w", err)
    32  	}
    33  
    34  	var errs error
    35  	// Try each authentication method until it succeeds
    36  	for _, authOpt := range authOptions(ctx, ref, option) {
    37  		remoteOpts := []remote.Option{
    38  			remote.WithTransport(transport),
    39  			authOpt,
    40  		}
    41  
    42  		if option.Platform.Platform != nil {
    43  			p, err := resolvePlatform(ref, option.Platform, remoteOpts)
    44  			if err != nil {
    45  				return nil, xerrors.Errorf("platform error: %w", err)
    46  			}
    47  			// Don't pass platform when the specified image is single-arch.
    48  			if p.Platform != nil {
    49  				remoteOpts = append(remoteOpts, remote.WithPlatform(*p.Platform))
    50  			}
    51  		}
    52  
    53  		desc, err := remote.Get(ref, remoteOpts...)
    54  		if err != nil {
    55  			errs = multierror.Append(errs, err)
    56  			continue
    57  		}
    58  
    59  		if option.Platform.Force {
    60  			if err = satisfyPlatform(desc, lo.FromPtr(option.Platform.Platform)); err != nil {
    61  				return nil, err
    62  			}
    63  		}
    64  		return desc, nil
    65  	}
    66  
    67  	// No authentication succeeded
    68  	return nil, errs
    69  }
    70  
    71  // Image is a wrapper of google/go-containerregistry/pkg/v1/remote.Image
    72  // so that it can try multiple authentication methods.
    73  func Image(ctx context.Context, ref name.Reference, option types.RegistryOptions) (v1.Image, error) {
    74  	transport, err := httpTransport(option)
    75  	if err != nil {
    76  		return nil, xerrors.Errorf("failed to create http transport: %w", err)
    77  	}
    78  
    79  	var errs error
    80  	// Try each authentication method until it succeeds
    81  	for _, authOpt := range authOptions(ctx, ref, option) {
    82  		remoteOpts := []remote.Option{
    83  			remote.WithTransport(transport),
    84  			authOpt,
    85  		}
    86  		index, err := remote.Image(ref, remoteOpts...)
    87  		if err != nil {
    88  			errs = multierror.Append(errs, err)
    89  			continue
    90  		}
    91  		return index, nil
    92  	}
    93  
    94  	// No authentication succeeded
    95  	return nil, errs
    96  }
    97  
    98  // Referrers is a wrapper of google/go-containerregistry/pkg/v1/remote.Referrers
    99  // so that it can try multiple authentication methods.
   100  func Referrers(ctx context.Context, d name.Digest, option types.RegistryOptions) (v1.ImageIndex, error) {
   101  	transport, err := httpTransport(option)
   102  	if err != nil {
   103  		return nil, xerrors.Errorf("failed to create http transport: %w", err)
   104  	}
   105  
   106  	var errs error
   107  	// Try each authentication method until it succeeds
   108  	for _, authOpt := range authOptions(ctx, d, option) {
   109  		remoteOpts := []remote.Option{
   110  			remote.WithTransport(transport),
   111  			authOpt,
   112  		}
   113  		index, err := remote.Referrers(d, remoteOpts...)
   114  		if err != nil {
   115  			errs = multierror.Append(errs, err)
   116  			continue
   117  		}
   118  		return index, nil
   119  	}
   120  
   121  	// No authentication succeeded
   122  	return nil, errs
   123  }
   124  
   125  func httpTransport(option types.RegistryOptions) (*http.Transport, error) {
   126  	d := &net.Dialer{
   127  		Timeout: 10 * time.Minute,
   128  	}
   129  	tr := http.DefaultTransport.(*http.Transport).Clone()
   130  	tr.DialContext = d.DialContext
   131  	tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: option.Insecure}
   132  
   133  	if len(option.ClientCert) != 0 && len(option.ClientKey) != 0 {
   134  		cert, err := tls.X509KeyPair(option.ClientCert, option.ClientKey)
   135  		if err != nil {
   136  			return nil, err
   137  		}
   138  		tr.TLSClientConfig.Certificates = []tls.Certificate{cert}
   139  	}
   140  
   141  	return tr, nil
   142  }
   143  
   144  func authOptions(ctx context.Context, ref name.Reference, option types.RegistryOptions) []remote.Option {
   145  	var opts []remote.Option
   146  	for _, cred := range option.Credentials {
   147  		opts = append(opts, remote.WithAuth(&authn.Basic{
   148  			Username: cred.Username,
   149  			Password: cred.Password,
   150  		}))
   151  	}
   152  
   153  	domain := ref.Context().RegistryStr()
   154  	token := registry.GetToken(ctx, domain, option)
   155  	if !lo.IsEmpty(token) {
   156  		opts = append(opts, remote.WithAuth(&token))
   157  	}
   158  
   159  	switch {
   160  	case option.RegistryToken != "":
   161  		bearer := authn.Bearer{Token: option.RegistryToken}
   162  		return []remote.Option{remote.WithAuth(&bearer)}
   163  	default:
   164  		// Use the keychain anyway at the end
   165  		opts = append(opts, remote.WithAuthFromKeychain(authn.DefaultKeychain))
   166  		return opts
   167  	}
   168  }
   169  
   170  // resolvePlatform resolves the OS platform for a given image reference.
   171  // If the platform has an empty OS, the function will attempt to find the first OS
   172  // in the image's manifest list and return the platform with the detected OS.
   173  // It ignores the specified platform if the image is not multi-arch.
   174  func resolvePlatform(ref name.Reference, p types.Platform, options []remote.Option) (types.Platform, error) {
   175  	if p.OS != "" {
   176  		return p, nil
   177  	}
   178  
   179  	// OS wildcard, implicitly pick up the first os found in the image list.
   180  	// e.g. */amd64
   181  	d, err := remote.Get(ref, options...)
   182  	if err != nil {
   183  		return types.Platform{}, xerrors.Errorf("image get error: %w", err)
   184  	}
   185  	switch d.MediaType {
   186  	case v1types.OCIManifestSchema1, v1types.DockerManifestSchema2:
   187  		// We want an index but the registry has an image, not multi-arch. We just ignore "--platform".
   188  		log.Logger.Debug("Ignore --platform as the image is not multi-arch")
   189  		return types.Platform{}, nil
   190  	case v1types.OCIImageIndex, v1types.DockerManifestList:
   191  		// These are expected.
   192  	}
   193  
   194  	index, err := d.ImageIndex()
   195  	if err != nil {
   196  		return types.Platform{}, xerrors.Errorf("image index error: %w", err)
   197  	}
   198  
   199  	m, err := index.IndexManifest()
   200  	if err != nil {
   201  		return types.Platform{}, xerrors.Errorf("remote index manifest error: %w", err)
   202  	}
   203  	if len(m.Manifests) == 0 {
   204  		log.Logger.Debug("Ignore '--platform' as the image is not multi-arch")
   205  		return types.Platform{}, nil
   206  	}
   207  	if m.Manifests[0].Platform != nil {
   208  		newPlatform := p.DeepCopy()
   209  		// Replace with the detected OS
   210  		// e.g. */amd64 => linux/amd64
   211  		newPlatform.OS = m.Manifests[0].Platform.OS
   212  
   213  		// Return the platform with the found OS
   214  		return types.Platform{
   215  			Platform: newPlatform,
   216  			Force:    p.Force,
   217  		}, nil
   218  	}
   219  	return types.Platform{}, nil
   220  }
   221  
   222  func satisfyPlatform(desc *remote.Descriptor, platform v1.Platform) error {
   223  	img, err := desc.Image()
   224  	if err != nil {
   225  		return err
   226  	}
   227  	c, err := img.ConfigFile()
   228  	if err != nil {
   229  		return err
   230  	}
   231  	if !lo.FromPtr(c.Platform()).Satisfies(platform) {
   232  		return xerrors.Errorf("the specified platform not found")
   233  	}
   234  	return nil
   235  }