go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/providers.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package providers
     5  
     6  import (
     7  	"archive/tar"
     8  	"encoding/json"
     9  	"io"
    10  	"net/http"
    11  	"os"
    12  	osfs "os"
    13  	"path/filepath"
    14  	"runtime"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/cockroachdb/errors"
    19  	"github.com/rs/zerolog/log"
    20  	"github.com/spf13/afero"
    21  	"github.com/ulikunitz/xz"
    22  	"go.mondoo.com/cnquery/cli/config"
    23  	"go.mondoo.com/cnquery/providers-sdk/v1/plugin"
    24  	"go.mondoo.com/cnquery/providers-sdk/v1/resources"
    25  	"golang.org/x/exp/slices"
    26  )
    27  
    28  var (
    29  	SystemPath string
    30  	HomePath   string
    31  	// this is the default path for providers, it's either system or home path, if the user is root the system path is used
    32  	DefaultPath string
    33  	// CachedProviders contains all providers that have been loaded the last time
    34  	// ListActive or ListAll have been called
    35  	CachedProviders []*Provider
    36  )
    37  
    38  func init() {
    39  	SystemPath = config.SystemDataPath("providers")
    40  	DefaultPath = SystemPath
    41  	if os.Geteuid() != 0 {
    42  		HomePath, _ = config.HomePath("providers")
    43  		DefaultPath = HomePath
    44  	}
    45  }
    46  
    47  type Providers map[string]*Provider
    48  
    49  type Provider struct {
    50  	*plugin.Provider
    51  	Schema *resources.Schema
    52  	Path   string
    53  }
    54  
    55  // List providers that are going to be used in their default order:
    56  // builtin > user > system. The providers are also loaded and provider their
    57  // metadata/configuration.
    58  func ListActive() (Providers, error) {
    59  	all, err := ListAll()
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  
    64  	var res Providers = make(map[string]*Provider, len(all))
    65  	for _, v := range all {
    66  		res[v.ID] = v
    67  	}
    68  
    69  	// useful for caching; even if the structure gets updated with new providers
    70  	Coordinator.Providers = res
    71  	return res, nil
    72  }
    73  
    74  // ListAll available providers, including duplicates between builtin, user,
    75  // and system providers. We only return errors when the things we are trying
    76  // to load don't work.
    77  // Note: We load providers from cache so these expensive calls don't have
    78  // to be repeated. If you want to force a refresh, you can nil out the cache.
    79  func ListAll() ([]*Provider, error) {
    80  	if CachedProviders != nil {
    81  		return CachedProviders, nil
    82  	}
    83  
    84  	all := []*Provider{}
    85  	CachedProviders = all
    86  
    87  	// This really shouldn't happen, but just in case it does...
    88  	if SystemPath == "" && HomePath == "" {
    89  		log.Warn().Msg("can't find any paths for providers, none are configured")
    90  		return nil, nil
    91  	}
    92  
    93  	sysOk := config.ProbeDir(SystemPath)
    94  	homeOk := config.ProbeDir(HomePath)
    95  	if !sysOk && !homeOk {
    96  		msg := log.Warn()
    97  		if SystemPath != "" {
    98  			msg = msg.Str("system-path", SystemPath)
    99  		}
   100  		if HomePath != "" {
   101  			msg = msg.Str("home-path", HomePath)
   102  		}
   103  		msg.Msg("can't find any paths for providers, none are configured")
   104  	}
   105  
   106  	if sysOk {
   107  		cur, err := findProviders(SystemPath)
   108  		if err != nil {
   109  			log.Warn().Str("path", SystemPath).Err(err).Msg("failed to get providers from system path")
   110  		}
   111  		all = append(all, cur...)
   112  	}
   113  
   114  	if homeOk {
   115  		cur, err := findProviders(HomePath)
   116  		if err != nil {
   117  			log.Warn().Str("path", HomePath).Err(err).Msg("failed to get providers from home path")
   118  		}
   119  		all = append(all, cur...)
   120  	}
   121  
   122  	for _, x := range builtinProviders {
   123  		all = append(all, &Provider{
   124  			Provider: x.Config,
   125  		})
   126  	}
   127  
   128  	var res []*Provider
   129  	for i := range all {
   130  		provider := all[i]
   131  
   132  		// builtin providers don't need to be loaded, so they are ok to be returned
   133  		if provider.Path == "" {
   134  			res = append(res, provider)
   135  			continue
   136  		}
   137  
   138  		// we only add a provider if we can load it, otherwise it has bad
   139  		// consequences for other mechanisms (like attaching shell, listing etc)
   140  		if err := provider.LoadJSON(); err != nil {
   141  			log.Error().Err(err).
   142  				Str("provider", provider.Name).
   143  				Str("path", provider.Path).
   144  				Msg("failed to load provider")
   145  		} else {
   146  			res = append(res, provider)
   147  		}
   148  	}
   149  
   150  	CachedProviders = res
   151  	return res, nil
   152  }
   153  
   154  // EnsureProvider makes sure that a given provider exists and returns it.
   155  // You can supply providers either via:
   156  //  1. connectorName, which is what you see in the CLI e.g. "local", "ssh", ...
   157  //  2. connectorType, which is how assets define the connector type when
   158  //     they are moved between discovery and execution, e.g. "registry-image".
   159  //
   160  // If you disable autoUpdate, it will neither update NOR install missing providers.
   161  //
   162  // If you don't supply existing providers, it will look for alist of all
   163  // active providers first.
   164  func EnsureProvider(connectorName string, connectorType string, autoUpdate bool, existing Providers) (*Provider, error) {
   165  	if existing == nil {
   166  		var err error
   167  		existing, err = ListActive()
   168  		if err != nil {
   169  			return nil, err
   170  		}
   171  	}
   172  
   173  	provider := existing.ForConnection(connectorName, connectorType)
   174  	if provider != nil {
   175  		return provider, nil
   176  	}
   177  
   178  	if connectorName == "mock" || connectorType == "mock" {
   179  		existing.Add(&mockProvider)
   180  		return &mockProvider, nil
   181  	}
   182  
   183  	upstream := DefaultProviders.ForConnection(connectorName, connectorType)
   184  	if upstream == nil {
   185  		// we can't find any provider for this connector in our default set
   186  		// FIXME: This causes a panic in the CLI, we should handle this better
   187  		return nil, nil
   188  	}
   189  
   190  	if !autoUpdate {
   191  		return nil, errors.New("cannot find installed provider for connection " + connectorName)
   192  	}
   193  
   194  	nu, err := Install(upstream.Name, "")
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  	existing.Add(nu)
   199  	PrintInstallResults([]*Provider{nu})
   200  	return nu, nil
   201  }
   202  
   203  func Install(name string, version string) (*Provider, error) {
   204  	if version == "" {
   205  		// if no version is specified, we default to installing the latest one
   206  		latestVersion, err := LatestVersion(name)
   207  		if err != nil {
   208  			return nil, err
   209  		}
   210  		version = latestVersion
   211  	}
   212  
   213  	log.Info().
   214  		Str("version", version).
   215  		Msg("installing provider '" + name + "'")
   216  	return installVersion(name, version)
   217  }
   218  
   219  // This is the default installation source for core providers.
   220  const upstreamURL = "https://releases.mondoo.com/providers/{NAME}/{VERSION}/{NAME}_{VERSION}_{OS}_{ARCH}.tar.xz"
   221  
   222  func installVersion(name string, version string) (*Provider, error) {
   223  	url := upstreamURL
   224  	url = strings.ReplaceAll(url, "{NAME}", name)
   225  	url = strings.ReplaceAll(url, "{VERSION}", version)
   226  	url = strings.ReplaceAll(url, "{OS}", runtime.GOOS)
   227  	url = strings.ReplaceAll(url, "{ARCH}", runtime.GOARCH)
   228  
   229  	log.Debug().Str("url", url).Msg("installing provider from URL")
   230  	res, err := http.Get(url)
   231  	if err != nil {
   232  		log.Debug().Str("url", url).Msg("failed to install from URL (get request)")
   233  		return nil, errors.Wrap(err, "failed to install "+name+"-"+version)
   234  	}
   235  	if res.StatusCode == http.StatusNotFound {
   236  		return nil, errors.New("cannot find provider " + name + "-" + version + " under url " + url)
   237  	} else if res.StatusCode != http.StatusOK {
   238  		log.Debug().Str("url", url).Int("status", res.StatusCode).Msg("failed to install from URL (status code)")
   239  		return nil, errors.New("failed to install " + name + "-" + version + ", received status code: " + res.Status)
   240  	}
   241  
   242  	// else we know we got a 200 response, we can safely install
   243  	installed, err := InstallIO(res.Body, InstallConf{
   244  		Dst: DefaultPath,
   245  	})
   246  	if err != nil {
   247  		log.Debug().Str("url", url).Msg("failed to install form URL (download)")
   248  		return nil, errors.Wrap(err, "failed to install "+name+"-"+version)
   249  	}
   250  
   251  	if len(installed) == 0 {
   252  		return nil, errors.New("couldn't find installed provider")
   253  	}
   254  	if len(installed) > 1 {
   255  		log.Warn().Msg("too many providers were installed")
   256  	}
   257  	if installed[0].Version != version {
   258  		return nil, errors.New("version for provider didn't match expected install version: expected " + version + ", installed: " + installed[0].Version)
   259  	}
   260  
   261  	// we need to clear out the cache now, because we installed something new,
   262  	// otherwise it will load old data
   263  	CachedProviders = nil
   264  
   265  	return installed[0], nil
   266  }
   267  
   268  func LatestVersion(name string) (string, error) {
   269  	client := http.Client{
   270  		Timeout: time.Duration(5 * time.Second),
   271  	}
   272  	res, err := client.Get("https://releases.mondoo.com/providers/latest.json")
   273  	if err != nil {
   274  		return "", err
   275  	}
   276  	data, err := io.ReadAll(res.Body)
   277  	if err != nil {
   278  		log.Debug().Err(err).Msg("reading latest.json failed")
   279  		return "", errors.New("failed to read response from upstream provider versions")
   280  	}
   281  
   282  	var upstreamVersions ProviderVersions
   283  	err = json.Unmarshal(data, &upstreamVersions)
   284  	if err != nil {
   285  		log.Debug().Err(err).Msg("parsing latest.json failed")
   286  		return "", errors.New("failed to parse response from upstream provider versions")
   287  	}
   288  
   289  	var latestVersion string
   290  	for i := range upstreamVersions.Providers {
   291  		if upstreamVersions.Providers[i].Name == name {
   292  			latestVersion = upstreamVersions.Providers[i].Version
   293  			break
   294  		}
   295  	}
   296  
   297  	if latestVersion == "" {
   298  		return "", errors.New("cannot determine latest version of provider '" + name + "'")
   299  	}
   300  	return latestVersion, nil
   301  }
   302  
   303  func PrintInstallResults(providers []*Provider) {
   304  	for i := range providers {
   305  		provider := providers[i]
   306  		log.Info().
   307  			Str("version", provider.Version).
   308  			Str("path", provider.Path).
   309  			Msg("successfully installed " + provider.Name + " provider")
   310  	}
   311  }
   312  
   313  type InstallConf struct {
   314  	// Dst specify which path to install into.
   315  	Dst string
   316  }
   317  
   318  func InstallFile(path string, conf InstallConf) ([]*Provider, error) {
   319  	if !config.ProbeFile(path) {
   320  		return nil, errors.New("please provide a regular file when installing providers")
   321  	}
   322  
   323  	reader, err := os.Open(path)
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  	defer reader.Close()
   328  
   329  	return InstallIO(reader, conf)
   330  }
   331  
   332  func InstallIO(reader io.ReadCloser, conf InstallConf) ([]*Provider, error) {
   333  	if conf.Dst == "" {
   334  		conf.Dst = DefaultPath
   335  	}
   336  
   337  	if !config.ProbeDir(conf.Dst) {
   338  		log.Debug().Str("path", conf.Dst).Msg("creating providers directory")
   339  		if err := os.MkdirAll(conf.Dst, 0o755); err != nil {
   340  			return nil, errors.New("failed to create " + conf.Dst)
   341  		}
   342  		if !config.ProbeDir(conf.Dst) {
   343  			return nil, errors.New("cannot write to " + conf.Dst)
   344  		}
   345  	}
   346  
   347  	log.Debug().Msg("create temp directory to unpack providers")
   348  	tmpdir, err := os.MkdirTemp(conf.Dst, ".providers-unpack")
   349  	if err != nil {
   350  		return nil, errors.Wrap(err, "failed to create temporary directory to unpack files")
   351  	}
   352  
   353  	log.Debug().Str("path", tmpdir).Msg("unpacking providers")
   354  	files := map[string]struct{}{}
   355  	err = walkTarXz(reader, func(reader *tar.Reader, header *tar.Header) error {
   356  		files[header.Name] = struct{}{}
   357  		dst := filepath.Join(tmpdir, header.Name)
   358  		log.Debug().Str("name", header.Name).Str("dest", dst).Msg("unpacking file")
   359  		writer, err := os.Create(dst)
   360  		if err != nil {
   361  			return err
   362  		}
   363  		defer writer.Close()
   364  
   365  		_, err = io.Copy(writer, reader)
   366  		return err
   367  	})
   368  	if err != nil {
   369  		return nil, err
   370  	}
   371  
   372  	// If for any reason we drop here, it's best to clean up all temporary files
   373  	// so we don't spam the system with unnecessary data. Optionally we could
   374  	// keep them and re-use them, so they don't have to download again.
   375  	defer func() {
   376  		if err = os.RemoveAll(tmpdir); err != nil {
   377  			log.Error().Err(err).Msg("failed to remove temporary folder for unpacked provider")
   378  		}
   379  	}()
   380  
   381  	log.Debug().Msg("move provider to destination")
   382  	providerDirs := []string{}
   383  	for name := range files {
   384  		// we only want to identify the binary and then all associated files from it
   385  		// NOTE: we need special handling for windows since binaries have the .exe extension
   386  		if !strings.HasSuffix(name, ".exe") && strings.Contains(name, ".") {
   387  			continue
   388  		}
   389  
   390  		providerName := name
   391  		if strings.HasSuffix(name, ".exe") {
   392  			providerName = strings.TrimSuffix(name, ".exe")
   393  		}
   394  
   395  		if _, ok := files[providerName+".json"]; !ok {
   396  			return nil, errors.New("cannot find " + providerName + ".json in the archive")
   397  		}
   398  		if _, ok := files[providerName+".resources.json"]; !ok {
   399  			return nil, errors.New("cannot find " + providerName + ".resources.json in the archive")
   400  		}
   401  
   402  		dstPath := filepath.Join(conf.Dst, providerName)
   403  		if err = os.MkdirAll(dstPath, 0o755); err != nil {
   404  			return nil, err
   405  		}
   406  
   407  		// move the binary and the associated files
   408  		srcBin := filepath.Join(tmpdir, name)
   409  		dstBin := filepath.Join(dstPath, name)
   410  		log.Debug().Str("src", srcBin).Str("dst", dstBin).Msg("move provider binary")
   411  		if err = os.Rename(srcBin, dstBin); err != nil {
   412  			return nil, err
   413  		}
   414  		if err = os.Chmod(dstBin, 0o755); err != nil {
   415  			return nil, err
   416  		}
   417  
   418  		srcMeta := filepath.Join(tmpdir, providerName)
   419  		dstMeta := filepath.Join(dstPath, providerName)
   420  		if err = os.Rename(srcMeta+".json", dstMeta+".json"); err != nil {
   421  			return nil, err
   422  		}
   423  		if err = os.Rename(srcMeta+".resources.json", dstMeta+".resources.json"); err != nil {
   424  			return nil, err
   425  		}
   426  
   427  		providerDirs = append(providerDirs, dstPath)
   428  	}
   429  
   430  	log.Debug().Msg("loading providers")
   431  	res := []*Provider{}
   432  	for i := range providerDirs {
   433  		pdir := providerDirs[i]
   434  		provider, err := readProviderDir(pdir)
   435  		if err != nil {
   436  			return nil, err
   437  		}
   438  
   439  		if provider == nil {
   440  			log.Error().Err(err).Str("path", pdir).Msg("failed to read provider, please remove or fix it")
   441  			continue
   442  		}
   443  
   444  		if err := provider.LoadJSON(); err != nil {
   445  			log.Error().Err(err).Str("path", pdir).Msg("failed to read provider metadata, please remove or fix it")
   446  			continue
   447  		}
   448  
   449  		res = append(res, provider)
   450  	}
   451  
   452  	return res, nil
   453  }
   454  
   455  func walkTarXz(reader io.Reader, callback func(reader *tar.Reader, header *tar.Header) error) error {
   456  	r, err := xz.NewReader(reader)
   457  	if err != nil {
   458  		return errors.Wrap(err, "failed to read xz")
   459  	}
   460  
   461  	tarReader := tar.NewReader(r)
   462  	for {
   463  		header, err := tarReader.Next()
   464  		// end of archive
   465  		if err == io.EOF {
   466  			break
   467  		}
   468  		if err != nil {
   469  			return errors.Wrap(err, "failed to read tar")
   470  		}
   471  
   472  		switch header.Typeflag {
   473  		case tar.TypeReg:
   474  			if err = callback(tarReader, header); err != nil {
   475  				return err
   476  			}
   477  
   478  		default:
   479  			log.Warn().Str("name", header.Name).Msg("encounter a file in archive that is not supported, skipping it")
   480  		}
   481  	}
   482  	return nil
   483  }
   484  
   485  func isOverlyPermissive(path string) (bool, error) {
   486  	stat, err := config.AppFs.Stat(path)
   487  	if err != nil {
   488  		return true, errors.New("failed to analyze " + path)
   489  	}
   490  
   491  	mode := stat.Mode()
   492  	// We don't check the permissions for windows
   493  	if runtime.GOOS != "windows" && mode&0o022 != 0 {
   494  		return true, nil
   495  	}
   496  
   497  	return false, nil
   498  }
   499  
   500  func findProviders(path string) ([]*Provider, error) {
   501  	overlyPermissive, err := isOverlyPermissive(path)
   502  	if err != nil {
   503  		return nil, err
   504  	}
   505  	if overlyPermissive {
   506  		return nil, errors.New("path is overly permissive, make sure it is not writable to others or the group: " + path)
   507  	}
   508  
   509  	log.Debug().Str("path", path).Msg("searching providers in path")
   510  	files, err := afero.ReadDir(config.AppFs, path)
   511  	if err != nil {
   512  		return nil, err
   513  	}
   514  
   515  	candidates := map[string]struct{}{}
   516  	for i := range files {
   517  		file := files[i]
   518  		if file.Mode().IsDir() {
   519  			candidates[file.Name()] = struct{}{}
   520  		}
   521  	}
   522  
   523  	var res []*Provider
   524  	for name := range candidates {
   525  		pdir := filepath.Join(path, name)
   526  		provider, err := readProviderDir(pdir)
   527  		if err != nil {
   528  			return nil, err
   529  		}
   530  		if provider != nil {
   531  			res = append(res, provider)
   532  		}
   533  	}
   534  
   535  	return res, nil
   536  }
   537  
   538  func readProviderDir(pdir string) (*Provider, error) {
   539  	name := filepath.Base(pdir)
   540  	bin := filepath.Join(pdir, name)
   541  	if runtime.GOOS == "windows" {
   542  		bin += ".exe"
   543  	}
   544  	conf := filepath.Join(pdir, name+".json")
   545  	resources := filepath.Join(pdir, name+".resources.json")
   546  
   547  	if !config.ProbeFile(bin) {
   548  		log.Debug().Str("path", bin).Msg("ignoring provider, can't access the plugin")
   549  		return nil, nil
   550  	}
   551  	if !config.ProbeFile(conf) {
   552  		log.Debug().Str("path", conf).Msg("ignoring provider, can't access the plugin config")
   553  		return nil, nil
   554  	}
   555  	if !config.ProbeFile(resources) {
   556  		log.Debug().Str("path", resources).Msg("ignoring provider, can't access the plugin schema")
   557  		return nil, nil
   558  	}
   559  
   560  	return &Provider{
   561  		Provider: &plugin.Provider{
   562  			Name: name,
   563  		},
   564  		Path: pdir,
   565  	}, nil
   566  }
   567  
   568  func (p *Provider) LoadJSON() error {
   569  	path := filepath.Join(p.Path, p.Name+".json")
   570  	res, err := afero.ReadFile(config.AppFs, path)
   571  	if err != nil {
   572  		return errors.New("failed to read provider json from " + path + ": " + err.Error())
   573  	}
   574  
   575  	if err := json.Unmarshal(res, &p.Provider); err != nil {
   576  		return errors.New("failed to parse provider json from " + path + ": " + err.Error())
   577  	}
   578  	return nil
   579  }
   580  
   581  func (p *Provider) LoadResources() error {
   582  	path := filepath.Join(p.Path, p.Name+".resources.json")
   583  	res, err := afero.ReadFile(config.AppFs, path)
   584  	if err != nil {
   585  		return errors.New("failed to read provider resources json from " + path + ": " + err.Error())
   586  	}
   587  
   588  	if err := json.Unmarshal(res, &p.Schema); err != nil {
   589  		return errors.New("failed to parse provider resources json from " + path + ": " + err.Error())
   590  	}
   591  	return nil
   592  }
   593  
   594  func (p *Provider) binPath() string {
   595  	name := p.Name
   596  	if runtime.GOOS == "windows" {
   597  		name += ".exe"
   598  	}
   599  	return filepath.Join(p.Path, name)
   600  }
   601  
   602  func (p Providers) ForConnection(name string, typ string) *Provider {
   603  	if name != "" {
   604  		for _, provider := range p {
   605  			for i := range provider.Connectors {
   606  				if provider.Connectors[i].Name == name {
   607  					return provider
   608  				}
   609  				if slices.Contains(provider.Connectors[i].Aliases, name) {
   610  					return provider
   611  				}
   612  			}
   613  		}
   614  	}
   615  
   616  	if typ != "" {
   617  		for _, provider := range p {
   618  			if slices.Contains(provider.ConnectionTypes, typ) {
   619  				return provider
   620  			}
   621  			for i := range provider.Connectors {
   622  				if slices.Contains(provider.Connectors[i].Aliases, typ) {
   623  					return provider
   624  				}
   625  			}
   626  		}
   627  	}
   628  
   629  	return nil
   630  }
   631  
   632  func (p Providers) Add(nu *Provider) {
   633  	if nu != nil {
   634  		p[nu.ID] = nu
   635  	}
   636  }
   637  
   638  func MustLoadSchema(name string, data []byte) *resources.Schema {
   639  	var res resources.Schema
   640  	if err := json.Unmarshal(data, &res); err != nil {
   641  		panic("failed to embed schema for " + name)
   642  	}
   643  	return &res
   644  }
   645  
   646  func MustLoadSchemaFromFile(name string, path string) *resources.Schema {
   647  	raw, err := osfs.ReadFile(path)
   648  	if err != nil {
   649  		panic("cannot read schema file: " + path)
   650  	}
   651  	return MustLoadSchema(name, raw)
   652  }