package remote import ( "context" "crypto/tls" "net" "net/http" "time" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" v1types "github.com/google/go-containerregistry/pkg/v1/types" "github.com/hashicorp/go-multierror" "github.com/samber/lo" "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/fanal/image/registry" "github.com/aquasecurity/trivy/pkg/fanal/types" "github.com/aquasecurity/trivy/pkg/log" ) type Descriptor = remote.Descriptor // Get is a wrapper of google/go-containerregistry/pkg/v1/remote.Get // so that it can try multiple authentication methods. func Get(ctx context.Context, ref name.Reference, option types.RegistryOptions) (*Descriptor, error) { transport := httpTransport(option.Insecure) var errs error // Try each authentication method until it succeeds for _, authOpt := range authOptions(ctx, ref, option) { remoteOpts := []remote.Option{ remote.WithTransport(transport), authOpt, } if option.Platform.Platform != nil { p, err := resolvePlatform(ref, option.Platform, remoteOpts) if err != nil { return nil, xerrors.Errorf("platform error: %w", err) } // Don't pass platform when the specified image is single-arch. if p.Platform != nil { remoteOpts = append(remoteOpts, remote.WithPlatform(*p.Platform)) } } desc, err := remote.Get(ref, remoteOpts...) if err != nil { errs = multierror.Append(errs, err) continue } if option.Platform.Force { if err = satisfyPlatform(desc, lo.FromPtr(option.Platform.Platform)); err != nil { return nil, err } } return desc, nil } // No authentication succeeded return nil, errs } // Image is a wrapper of google/go-containerregistry/pkg/v1/remote.Image // so that it can try multiple authentication methods. func Image(ctx context.Context, ref name.Reference, option types.RegistryOptions) (v1.Image, error) { transport := httpTransport(option.Insecure) var errs error // Try each authentication method until it succeeds for _, authOpt := range authOptions(ctx, ref, option) { remoteOpts := []remote.Option{ remote.WithTransport(transport), authOpt, } index, err := remote.Image(ref, remoteOpts...) if err != nil { errs = multierror.Append(errs, err) continue } return index, nil } // No authentication succeeded return nil, errs } // Referrers is a wrapper of google/go-containerregistry/pkg/v1/remote.Referrers // so that it can try multiple authentication methods. func Referrers(ctx context.Context, d name.Digest, option types.RegistryOptions) (*v1.IndexManifest, error) { transport := httpTransport(option.Insecure) var errs error // Try each authentication method until it succeeds for _, authOpt := range authOptions(ctx, d, option) { remoteOpts := []remote.Option{ remote.WithTransport(transport), authOpt, } index, err := remote.Referrers(d, remoteOpts...) if err != nil { errs = multierror.Append(errs, err) continue } return index, nil } // No authentication succeeded return nil, errs } func httpTransport(insecure bool) *http.Transport { d := &net.Dialer{ Timeout: 10 * time.Minute, } tr := http.DefaultTransport.(*http.Transport).Clone() tr.DialContext = d.DialContext tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: insecure} return tr } func authOptions(ctx context.Context, ref name.Reference, option types.RegistryOptions) []remote.Option { var opts []remote.Option for _, cred := range option.Credentials { opts = append(opts, remote.WithAuth(&authn.Basic{ Username: cred.Username, Password: cred.Password, })) } domain := ref.Context().RegistryStr() token := registry.GetToken(ctx, domain, option) if !lo.IsEmpty(token) { opts = append(opts, remote.WithAuth(&token)) } switch { case option.RegistryToken != "": bearer := authn.Bearer{Token: option.RegistryToken} return []remote.Option{remote.WithAuth(&bearer)} default: // Use the keychain anyway at the end opts = append(opts, remote.WithAuthFromKeychain(authn.DefaultKeychain)) return opts } } // resolvePlatform resolves the OS platform for a given image reference. // If the platform has an empty OS, the function will attempt to find the first OS // in the image's manifest list and return the platform with the detected OS. // It ignores the specified platform if the image is not multi-arch. func resolvePlatform(ref name.Reference, p types.Platform, options []remote.Option) (types.Platform, error) { if p.OS != "" { return p, nil } // OS wildcard, implicitly pick up the first os found in the image list. // e.g. */amd64 d, err := remote.Get(ref, options...) if err != nil { return types.Platform{}, xerrors.Errorf("image get error: %w", err) } switch d.MediaType { case v1types.OCIManifestSchema1, v1types.DockerManifestSchema2: // We want an index but the registry has an image, not multi-arch. We just ignore "--platform". log.Logger.Debug("Ignore --platform as the image is not multi-arch") return types.Platform{}, nil case v1types.OCIImageIndex, v1types.DockerManifestList: // These are expected. } index, err := d.ImageIndex() if err != nil { return types.Platform{}, xerrors.Errorf("image index error: %w", err) } m, err := index.IndexManifest() if err != nil { return types.Platform{}, xerrors.Errorf("remote index manifest error: %w", err) } if len(m.Manifests) == 0 { log.Logger.Debug("Ignore '--platform' as the image is not multi-arch") return types.Platform{}, nil } if m.Manifests[0].Platform != nil { newPlatform := p.DeepCopy() // Replace with the detected OS // e.g. */amd64 => linux/amd64 newPlatform.OS = m.Manifests[0].Platform.OS // Return the platform with the found OS return types.Platform{ Platform: newPlatform, Force: p.Force, }, nil } return types.Platform{}, nil } func satisfyPlatform(desc *remote.Descriptor, platform v1.Platform) error { img, err := desc.Image() if err != nil { return err } c, err := img.ConfigFile() if err != nil { return err } if !lo.FromPtr(c.Platform()).Satisfies(platform) { return xerrors.Errorf("the specified platform not found") } return nil }