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 }