go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/os/resources/discovery/container_registry/registry.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package container_registry
     5  
     6  import (
     7  	"crypto/tls"
     8  	"fmt"
     9  	"net"
    10  	"net/http"
    11  	"net/url"
    12  	"time"
    13  
    14  	"github.com/google/go-containerregistry/pkg/authn"
    15  
    16  	"github.com/cockroachdb/errors"
    17  	"github.com/google/go-containerregistry/pkg/name"
    18  	"github.com/google/go-containerregistry/pkg/v1/remote"
    19  	"github.com/rs/zerolog/log"
    20  	"go.mondoo.com/cnquery/providers-sdk/v1/inventory"
    21  	"go.mondoo.com/cnquery/providers-sdk/v1/vault"
    22  	"go.mondoo.com/cnquery/providers/os/connection/container/auth"
    23  	"go.mondoo.com/cnquery/providers/os/connection/container/image"
    24  	"go.mondoo.com/cnquery/providers/os/id/containerid"
    25  )
    26  
    27  func NewContainerRegistryResolver() *DockerRegistryImages {
    28  	return &DockerRegistryImages{}
    29  }
    30  
    31  type DockerRegistryImages struct {
    32  	Insecure            bool
    33  	DisableKeychainAuth bool
    34  }
    35  
    36  func (a *DockerRegistryImages) remoteOptions() []remote.Option {
    37  	options := []remote.Option{}
    38  
    39  	// does not work with bearer auth, therefore it need to be disabled when other remote auth options are used
    40  	// TODO: we should implement this a bit differently
    41  	if a.DisableKeychainAuth == false {
    42  		options = append(options, remote.WithAuthFromKeychain(authn.DefaultKeychain))
    43  	}
    44  
    45  	if a.Insecure {
    46  		// NOTE: config to get remote running with an insecure registry, we need to override the TLSClientConfig
    47  		tr := &http.Transport{
    48  			Proxy: http.ProxyFromEnvironment,
    49  			DialContext: (&net.Dialer{
    50  				Timeout:   30 * time.Second,
    51  				KeepAlive: 30 * time.Second,
    52  				DualStack: true,
    53  			}).DialContext,
    54  			ForceAttemptHTTP2:     true,
    55  			MaxIdleConns:          100,
    56  			IdleConnTimeout:       90 * time.Second,
    57  			TLSHandshakeTimeout:   10 * time.Second,
    58  			ExpectContinueTimeout: 1 * time.Second,
    59  			TLSClientConfig: &tls.Config{
    60  				InsecureSkipVerify: true,
    61  			},
    62  		}
    63  		options = append(options, remote.WithTransport(tr))
    64  	}
    65  
    66  	return options
    67  }
    68  
    69  func (a *DockerRegistryImages) Repositories(reg name.Registry) ([]string, error) {
    70  	n := 100
    71  	last := ""
    72  	var res []string
    73  	for {
    74  		page, err := remote.CatalogPage(reg, last, n, a.remoteOptions()...)
    75  		if err != nil {
    76  			return nil, err
    77  		}
    78  
    79  		if len(page) > 0 {
    80  			last = page[len(page)-1]
    81  			res = append(res, page...)
    82  		}
    83  
    84  		if len(page) < n {
    85  			break
    86  		}
    87  	}
    88  
    89  	return res, nil
    90  }
    91  
    92  // ListRegistry tries to iterate over all repositores in one registry
    93  // eg. 1234567.dkr.ecr.us-east-1.amazonaws.com
    94  func (a *DockerRegistryImages) ListRegistry(registry string) ([]*inventory.Asset, error) {
    95  	reg, err := name.NewRegistry(registry)
    96  	if err != nil {
    97  		return nil, errors.Wrap(err, "resolve registry")
    98  	}
    99  
   100  	repos, err := a.Repositories(reg)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  
   105  	assets := []*inventory.Asset{}
   106  	for i := range repos {
   107  		repoName := reg.RegistryStr() + "/" + repos[i]
   108  		log.Debug().Str("repository", repoName).Msg("discovered repository")
   109  
   110  		// iterate over all repository digests
   111  		repoImages, err := a.ListRepository(repoName)
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  		assets = append(assets, repoImages...)
   116  	}
   117  
   118  	return assets, nil
   119  }
   120  
   121  // ListRepository tries to fetch all details about a specific repository
   122  // index.docker.io/mondoo
   123  // index.docker.io/mondoo/client
   124  // harbor.lunalectric.com/library
   125  // harbor.lunalectric.com/library/ubuntu
   126  func (a *DockerRegistryImages) ListRepository(repoName string) ([]*inventory.Asset, error) {
   127  	assets := []*inventory.Asset{}
   128  
   129  	repo, err := name.NewRepository(repoName)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  
   134  	// fetch tags
   135  	tags, err := remote.List(repo, a.remoteOptions()...)
   136  	if err != nil {
   137  		return nil, handleUnauthorizedError(err, repo.Name())
   138  	}
   139  
   140  	foundAssets := map[string]*inventory.Asset{}
   141  	for i := range tags {
   142  		repoWithTag := repo.Name() + ":" + tags[i]
   143  
   144  		ref, err := name.ParseReference(repoWithTag)
   145  		if err != nil {
   146  			return nil, fmt.Errorf("parsing reference %q: %v", repoWithTag, err)
   147  		}
   148  
   149  		a, err := a.toAsset(ref, nil)
   150  		if err != nil {
   151  			return nil, err
   152  		}
   153  		if foundAsset, ok := foundAssets[a.PlatformIds[0]]; ok {
   154  			// only add tags to the first asset
   155  			foundAsset.Labels["docker.io/tags"] = foundAsset.Labels["docker.io/tags"] + "," + a.Labels["docker.io/tags"]
   156  			log.Debug().Str("tags", foundAsset.Labels["docker.io/tags"]).Str("image", foundAsset.Name).Msg("found additional tags for image")
   157  			continue
   158  		}
   159  		foundAssets[a.PlatformIds[0]] = a
   160  	}
   161  
   162  	// flatten map
   163  	for k := range foundAssets {
   164  		assets = append(assets, foundAssets[k])
   165  	}
   166  	return assets, nil
   167  }
   168  
   169  // ListImages either takes a registry or a repository and tries to fetch as many images as possible
   170  func (a *DockerRegistryImages) ListImages(repoName string) ([]*inventory.Asset, error) {
   171  	url, err := url.Parse("//" + repoName)
   172  	if err != nil {
   173  		return nil, fmt.Errorf("registries must be valid RFC 3986 URI authorities: %s", repoName)
   174  	}
   175  
   176  	if url.Host == repoName {
   177  		// fetch registry information
   178  		return a.ListRegistry(repoName)
   179  	} else {
   180  		// fetch repo information
   181  		return a.ListRepository(repoName)
   182  	}
   183  }
   184  
   185  func (a *DockerRegistryImages) GetImage(ref name.Reference, creds []*vault.Credential, opts ...remote.Option) (*inventory.Asset, error) {
   186  	return a.toAsset(ref, creds, opts...)
   187  }
   188  
   189  func (a *DockerRegistryImages) toAsset(ref name.Reference, creds []*vault.Credential, opts ...remote.Option) (*inventory.Asset, error) {
   190  	desc, err := image.GetImageDescriptor(ref, auth.AuthOption(creds)...)
   191  	if err != nil {
   192  		return nil, handleUnauthorizedError(err, ref.Name())
   193  	}
   194  	imgDigest := desc.Digest.String()
   195  	repoName := ref.Context().Name()
   196  	imgTag := ref.Context().Tag(ref.Identifier()).TagStr()
   197  	name := repoName + "@" + containerid.ShortContainerImageID(imgDigest)
   198  	imageUrl := repoName + "@" + imgDigest
   199  	asset := &inventory.Asset{
   200  		PlatformIds: []string{containerid.MondooContainerImageID(imgDigest)},
   201  		Name:        name,
   202  		Connections: []*inventory.Config{
   203  			{
   204  				Type:        "container-registry",
   205  				Host:        imageUrl,
   206  				Credentials: creds,
   207  			},
   208  		},
   209  		State:  inventory.State_STATE_ONLINE,
   210  		Labels: make(map[string]string),
   211  	}
   212  
   213  	// store digest and tag
   214  	asset.Labels["docker.io/digest"] = imgDigest
   215  	asset.Labels["docker.io/tags"] = imgTag
   216  	log.Debug().Strs("platform-ids", asset.PlatformIds).Msg("asset platform ids")
   217  	return asset, nil
   218  }