mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-21 23:00:42 -08:00
272 lines
8.5 KiB
Go
272 lines
8.5 KiB
Go
package remote
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/google/go-containerregistry/pkg/authn"
|
|
"github.com/google/go-containerregistry/pkg/authn/github"
|
|
"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"
|
|
xhttp "github.com/aquasecurity/trivy/pkg/x/http"
|
|
)
|
|
|
|
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) {
|
|
return tryWithMirrors(ref, option, func(r name.Reference) (*Descriptor, error) {
|
|
return tryGet(ctx, xhttp.Transport(ctx), r, option)
|
|
})
|
|
}
|
|
|
|
// tryGet checks all auth options and tries to get Descriptor.
|
|
func tryGet(ctx context.Context, tr http.RoundTripper, ref name.Reference, option types.RegistryOptions) (*Descriptor, error) {
|
|
var errs error
|
|
for _, authOpt := range authOptions(ctx, ref, option) {
|
|
remoteOpts := []remote.Option{
|
|
remote.WithTransport(tr),
|
|
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
|
|
}
|
|
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) {
|
|
return tryWithMirrors(ref, option, func(r name.Reference) (v1.Image, error) {
|
|
return tryImage(ctx, xhttp.Transport(ctx), r, option)
|
|
})
|
|
}
|
|
|
|
// tryWithMirrors handles common mirror logic for Get and Image functions
|
|
func tryWithMirrors[T any](ref name.Reference, option types.RegistryOptions, fn func(name.Reference) (T, error)) (T, error) {
|
|
var zero T
|
|
mirrors, err := registryMirrors(ref, option)
|
|
if err != nil {
|
|
return zero, xerrors.Errorf("unable to parse mirrors: %w", err)
|
|
}
|
|
|
|
// Try each mirrors/host until it succeeds
|
|
var errs error
|
|
for _, r := range append(mirrors, ref) {
|
|
result, err := fn(r)
|
|
if err != nil {
|
|
var multiErr *multierror.Error
|
|
// All auth options failed, try the next mirror/host
|
|
if errors.As(err, &multiErr) {
|
|
errs = multierror.Append(errs, multiErr.Errors...)
|
|
continue
|
|
}
|
|
// Other errors
|
|
return zero, err
|
|
}
|
|
|
|
if ref.Context().RegistryStr() != r.Context().RegistryStr() {
|
|
log.WithPrefix("remote").Info("Using the mirror registry to get the image",
|
|
log.String("image", ref.String()), log.String("mirror", r.Context().RegistryStr()))
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// No authentication for mirrors/host succeeded
|
|
return zero, errs
|
|
}
|
|
|
|
// tryImage checks all auth options and tries to get v1.Image.
|
|
// If none of the auth options work - function returns multierrors for each auth option.
|
|
func tryImage(ctx context.Context, tr http.RoundTripper, ref name.Reference, option types.RegistryOptions) (v1.Image, error) {
|
|
var errs error
|
|
for _, authOpt := range authOptions(ctx, ref, option) {
|
|
remoteOpts := []remote.Option{
|
|
remote.WithTransport(tr),
|
|
authOpt,
|
|
}
|
|
index, err := remote.Image(ref, remoteOpts...)
|
|
if err != nil {
|
|
errs = multierror.Append(errs, err)
|
|
continue
|
|
}
|
|
|
|
return index, nil
|
|
}
|
|
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.ImageIndex, error) {
|
|
var errs error
|
|
// Try each authentication method until it succeeds
|
|
for _, authOpt := range authOptions(ctx, d, option) {
|
|
remoteOpts := []remote.Option{
|
|
remote.WithTransport(xhttp.Transport(ctx)),
|
|
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
|
|
}
|
|
|
|
// registryMirrors returns a list of mirrors for ref, obtained from options.RegistryMirrors
|
|
// `go-containerregistry` doesn't support mirrors, so we need to handle them ourselves.
|
|
// TODO: use `WithMirror` when `go-containerregistry` will support mirrors.
|
|
// cf. https://github.com/google/go-containerregistry/pull/2010
|
|
func registryMirrors(hostRef name.Reference, option types.RegistryOptions) ([]name.Reference, error) {
|
|
var mirrors []name.Reference
|
|
|
|
reg := hostRef.Context().RegistryStr()
|
|
if ms, ok := option.RegistryMirrors[reg]; ok {
|
|
for _, m := range ms {
|
|
var nameOpts []name.Option
|
|
if option.Insecure {
|
|
nameOpts = append(nameOpts, name.Insecure)
|
|
}
|
|
mirrorImageName := strings.Replace(hostRef.Name(), reg, m, 1)
|
|
ref, err := name.ParseReference(mirrorImageName, nameOpts...)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("unable to parse image from mirror registry: %w", err)
|
|
}
|
|
mirrors = append(mirrors, ref)
|
|
}
|
|
}
|
|
return mirrors, nil
|
|
}
|
|
|
|
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.NewMultiKeychain(authn.DefaultKeychain, github.Keychain)))
|
|
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.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.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
|
|
}
|