diff --git a/docs/docs/configuration/others.md b/docs/docs/configuration/others.md index 371350f07a96..a89171af5984 100644 --- a/docs/docs/configuration/others.md +++ b/docs/docs/configuration/others.md @@ -117,3 +117,46 @@ The following example will fail when a critical vulnerability is found or the OS ``` $ trivy image --exit-code 1 --exit-on-eol 1 --severity CRITICAL alpine:3.16.3 ``` + +## Mirror Registries + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Trivy supports mirrors for [remote container images](../target/container_image.md#container-registry) and [databases](./db.md). + +To configure them, add a list of mirrors along with the host to the [trivy config file](../references/configuration/config-file.md#registry-options). + +!!! note + Use the `index.docker.io` host for images from `Docker Hub`, even if you don't use that prefix. + +Example for `index.docker.io`: +```yaml +registry: + mirrors: + index.docker.io: + - mirror.gcr.io +``` + +### Registry check procedure +Trivy uses the following registry order to get the image: + +- mirrors in the same order as they are specified in the configuration file +- source registry + +In cases where we can't get the image from the mirror registry (e.g. when authentication fails, image doesn't exist, etc.) - Trivy will check other mirrors (or the source registry if all mirrors have already been checked). + +Example: +```yaml +registry: + mirrors: + index.docker.io: + - mirror.with.bad.auth // We don't have credentials for this registry + - mirror.without.image // Registry doesn't have this image +``` + +When we want to get the image `alpine` with the settings above. The logic will be as follows: + +1. Try to get the image from `mirror.with.bad.auth/library/alpine`, but we get an error because there are no credentials for this registry. +2. Try to get the image from `mirror.without.image/library/alpine`, but we get an error because this registry doesn't have this image (but most likely it will be an error about authorization). +3. Get the image from `index.docker.io` (the original registry). diff --git a/docs/docs/references/configuration/config-file.md b/docs/docs/references/configuration/config-file.md index fe6332522ee0..1fa3e4d26219 100644 --- a/docs/docs/references/configuration/config-file.md +++ b/docs/docs/references/configuration/config-file.md @@ -461,6 +461,8 @@ pkg: ```yaml registry: + mirrors: + # Same as '--password' password: [] diff --git a/magefiles/docs.go b/magefiles/docs.go index bfdd7480d2a9..f37e0d5d04ea 100644 --- a/magefiles/docs.go +++ b/magefiles/docs.go @@ -147,6 +147,14 @@ func writeFlagValue(val any, ind string, w *os.File) { } else { w.WriteString(" []\n") } + case map[string][]string: + w.WriteString("\n") + for k, vv := range v { + fmt.Fprintf(w, "%s %s:\n", ind, k) + for _, vvv := range vv { + fmt.Fprintf(w, " %s - %s\n", ind, vvv) + } + } case string: fmt.Fprintf(w, " %q\n", v) default: diff --git a/pkg/fanal/types/image.go b/pkg/fanal/types/image.go index 91cdfb44b60c..f6064c006a40 100644 --- a/pkg/fanal/types/image.go +++ b/pkg/fanal/types/image.go @@ -81,6 +81,9 @@ type RegistryOptions struct { // RegistryToken is a bearer token to be sent to a registry RegistryToken string + // RegistryMirrors is a map of hosts with mirrors for them + RegistryMirrors map[string][]string + // SSL/TLS Insecure bool diff --git a/pkg/flag/options.go b/pkg/flag/options.go index a3bcb0f61ec2..775eec1366cd 100644 --- a/pkg/flag/options.go +++ b/pkg/flag/options.go @@ -30,7 +30,7 @@ import ( ) type FlagType interface { - int | string | []string | bool | time.Duration | float64 + int | string | []string | bool | time.Duration | float64 | map[string][]string } type Flag[T FlagType] struct { @@ -161,6 +161,8 @@ func (f *Flag[T]) cast(val any) any { return cast.ToFloat64(val) case time.Duration: return cast.ToDuration(val) + case map[string][]string: + return cast.ToStringMapStringSlice(val) case []string: if s, ok := val.(string); ok && strings.Contains(s, ",") { // Split environmental variables by comma as it is not done by viper. @@ -467,11 +469,12 @@ func (o *Options) ScanOpts() types.ScanOptions { // RegistryOpts returns options for OCI registries func (o *Options) RegistryOpts() ftypes.RegistryOptions { return ftypes.RegistryOptions{ - Credentials: o.Credentials, - RegistryToken: o.RegistryToken, - Insecure: o.Insecure, - Platform: o.Platform, - AWSRegion: o.AWSOptions.Region, + Credentials: o.Credentials, + RegistryToken: o.RegistryToken, + Insecure: o.Insecure, + Platform: o.Platform, + AWSRegion: o.AWSOptions.Region, + RegistryMirrors: o.RegistryMirrors, } } diff --git a/pkg/flag/registry_flags.go b/pkg/flag/registry_flags.go index 9c03a07b8872..50de707ea602 100644 --- a/pkg/flag/registry_flags.go +++ b/pkg/flag/registry_flags.go @@ -31,26 +31,33 @@ var ( ConfigName: "registry.token", Usage: "registry token", } + RegistryMirrorsFlag = Flag[map[string][]string]{ + ConfigName: "registry.mirrors", + Usage: "map of hosts and registries for them.", + } ) type RegistryFlagGroup struct { - Username *Flag[[]string] - Password *Flag[[]string] - PasswordStdin *Flag[bool] - RegistryToken *Flag[string] + Username *Flag[[]string] + Password *Flag[[]string] + PasswordStdin *Flag[bool] + RegistryToken *Flag[string] + RegistryMirrors *Flag[map[string][]string] } type RegistryOptions struct { - Credentials []types.Credential - RegistryToken string + Credentials []types.Credential + RegistryToken string + RegistryMirrors map[string][]string } func NewRegistryFlagGroup() *RegistryFlagGroup { return &RegistryFlagGroup{ - Username: UsernameFlag.Clone(), - Password: PasswordFlag.Clone(), - PasswordStdin: PasswordStdinFlag.Clone(), - RegistryToken: RegistryTokenFlag.Clone(), + Username: UsernameFlag.Clone(), + Password: PasswordFlag.Clone(), + PasswordStdin: PasswordStdinFlag.Clone(), + RegistryToken: RegistryTokenFlag.Clone(), + RegistryMirrors: RegistryMirrorsFlag.Clone(), } } @@ -64,6 +71,7 @@ func (f *RegistryFlagGroup) Flags() []Flagger { f.Password, f.PasswordStdin, f.RegistryToken, + f.RegistryMirrors, } } @@ -97,7 +105,8 @@ func (f *RegistryFlagGroup) ToOptions() (RegistryOptions, error) { } return RegistryOptions{ - Credentials: credentials, - RegistryToken: f.RegistryToken.Value(), + Credentials: credentials, + RegistryToken: f.RegistryToken.Value(), + RegistryMirrors: f.RegistryMirrors.Value(), }, nil } diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go index a8bca7aafe1f..c9f75e834991 100644 --- a/pkg/remote/remote.go +++ b/pkg/remote/remote.go @@ -3,9 +3,11 @@ package remote import ( "context" "crypto/tls" + "errors" "fmt" "net" "net/http" + "strings" "time" "github.com/google/go-containerregistry/pkg/authn" @@ -35,8 +37,14 @@ func Get(ctx context.Context, ref name.Reference, option types.RegistryOptions) return nil, xerrors.Errorf("failed to create http transport: %w", err) } + return tryWithMirrors(ref, option, func(r name.Reference) (*Descriptor, error) { + return tryGet(ctx, tr, 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 - // Try each authentication method until it succeeds for _, authOpt := range authOptions(ctx, ref, option) { remoteOpts := []remote.Option{ remote.WithTransport(tr), @@ -67,8 +75,6 @@ func Get(ctx context.Context, ref name.Reference, option types.RegistryOptions) } return desc, nil } - - // No authentication succeeded return nil, errs } @@ -80,8 +86,49 @@ func Image(ctx context.Context, ref name.Reference, option types.RegistryOptions return nil, xerrors.Errorf("failed to create http transport: %w", err) } + return tryWithMirrors(ref, option, func(r name.Reference) (v1.Image, error) { + return tryImage(ctx, tr, 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 - // Try each authentication method until it succeeds for _, authOpt := range authOptions(ctx, ref, option) { remoteOpts := []remote.Option{ remote.WithTransport(tr), @@ -92,10 +139,9 @@ func Image(ctx context.Context, ref name.Reference, option types.RegistryOptions errs = multierror.Append(errs, err) continue } + return index, nil } - - // No authentication succeeded return nil, errs } @@ -126,6 +172,31 @@ func Referrers(ctx context.Context, d name.Digest, option types.RegistryOptions) 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 httpTransport(option types.RegistryOptions) (http.RoundTripper, error) { d := &net.Dialer{ Timeout: 10 * time.Minute, diff --git a/pkg/remote/remote_test.go b/pkg/remote/remote_test.go index 27ea8079153b..639a340a112b 100644 --- a/pkg/remote/remote_test.go +++ b/pkg/remote/remote_test.go @@ -93,6 +93,81 @@ func TestGet(t *testing.T) { }, }, }, + { + name: "mirror", + args: args{ + imageName: "foo.bar.io/library/alpine:3.10", + option: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "test", + Password: "testpass", + }, + }, + RegistryMirrors: map[string][]string{ + "foo.bar.io": { + serverAddr, + }, + }, + Insecure: true, + }, + }, + }, + { + name: "mirror for dockerhub", + args: args{ + imageName: "alpine:3.10", + option: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "test", + Password: "testpass", + }, + }, + RegistryMirrors: map[string][]string{ + "index.docker.io": { + serverAddr, + }, + }, + Insecure: true, + }, + }, + }, + { + name: "non-existent mirror image - use image from host", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "test", + Password: "testpass", + }, + }, + RegistryMirrors: map[string][]string{ + serverAddr: { + "wrong.repository", + }, + }, + Insecure: true, + }, + }, + }, + { + name: "wrong mirror", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.RegistryOptions{ + RegistryMirrors: map[string][]string{ + serverAddr: { + "wrong.repository:tag@digest", + }, + }, + Insecure: true, + }, + }, + wantErr: "could not parse reference: wrong.repository:tag@digest/library/alpine:3.10", + }, { name: "multiple credential", args: args{ @@ -182,6 +257,28 @@ func TestGet(t *testing.T) { }, wantErr: "invalid username/password", }, + { + name: "bad credential for multiple mirrors", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "foo", + Password: "bar", + }, + }, + Insecure: true, + RegistryMirrors: map[string][]string{ + serverAddr: { + serverAddr, + serverAddr, + }, + }, + }, + }, + wantErr: "6 errors occurred:", // 2 errors for each repository (for 2 mirrors and the original repository) + }, { name: "bad keychain", args: args{