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

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package provider
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"net/url"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/rs/zerolog/log"
    14  	"go.mondoo.com/cnquery/llx"
    15  	"go.mondoo.com/cnquery/providers-sdk/v1/inventory"
    16  	"go.mondoo.com/cnquery/providers-sdk/v1/plugin"
    17  	"go.mondoo.com/cnquery/providers-sdk/v1/upstream"
    18  	"go.mondoo.com/cnquery/providers-sdk/v1/vault"
    19  	"go.mondoo.com/cnquery/providers/os/connection"
    20  	"go.mondoo.com/cnquery/providers/os/connection/mock"
    21  	"go.mondoo.com/cnquery/providers/os/connection/shared"
    22  	"go.mondoo.com/cnquery/providers/os/id/ids"
    23  	"go.mondoo.com/cnquery/providers/os/resources"
    24  	"go.mondoo.com/cnquery/providers/os/resources/discovery/container_registry"
    25  	"go.mondoo.com/cnquery/providers/os/resources/discovery/docker_engine"
    26  	"go.mondoo.com/cnquery/utils/stringx"
    27  )
    28  
    29  const (
    30  	LocalConnectionType             = "local"
    31  	SshConnectionType               = "ssh"
    32  	TarConnectionType               = "tar"
    33  	DockerSnapshotConnectionType    = "docker-snapshot"
    34  	VagrantConnectionType           = "vagrant"
    35  	DockerImageConnectionType       = "docker-image"
    36  	DockerContainerConnectionType   = "docker-container"
    37  	DockerRegistryConnectionType    = "docker-registry"
    38  	ContainerRegistryConnectionType = "container-registry"
    39  	RegistryImageConnectionType     = "registry-image"
    40  	FilesystemConnectionType        = "filesystem"
    41  )
    42  
    43  type Service struct {
    44  	runtimes         map[uint32]*plugin.Runtime
    45  	lastConnectionID uint32
    46  }
    47  
    48  func Init() *Service {
    49  	return &Service{
    50  		runtimes:         map[uint32]*plugin.Runtime{},
    51  		lastConnectionID: 0,
    52  	}
    53  }
    54  
    55  func parseDiscover(flags map[string]*llx.Primitive) *inventory.Discovery {
    56  	discovery := &inventory.Discovery{Targets: []string{"auto"}}
    57  	if flag, ok := flags["discover"]; ok && len(flag.Array) > 0 {
    58  		discovery.Targets = []string{}
    59  		for i := range flag.Array {
    60  			discovery.Targets = append(discovery.Targets, string(flag.Array[i].Value))
    61  		}
    62  	}
    63  	return discovery
    64  }
    65  
    66  func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) {
    67  	flags := req.Flags
    68  	if flags == nil {
    69  		flags = map[string]*llx.Primitive{}
    70  	}
    71  
    72  	conf := &inventory.Config{
    73  		Sudo:     shared.ParseSudo(flags),
    74  		Discover: parseDiscover(flags),
    75  		Type:     req.Connector,
    76  	}
    77  
    78  	port := 0
    79  	containerID := ""
    80  	switch req.Connector {
    81  	case "local":
    82  		conf.Type = "local"
    83  	case "ssh":
    84  		conf.Type = "ssh"
    85  		port = 22
    86  	case "winrm":
    87  		conf.Type = "winrm"
    88  		port = 5985
    89  	case "vagrant":
    90  		conf.Type = "vagrant"
    91  	case "docker":
    92  		if len(req.Args) > 1 {
    93  			switch req.Args[0] {
    94  			case "image":
    95  				conf.Type = "docker-image"
    96  				conf.Host = req.Args[1]
    97  			case "registry":
    98  				conf.Type = "docker-registry"
    99  				conf.Host = req.Args[1]
   100  			case "tar":
   101  				conf.Type = "docker-snapshot"
   102  				conf.Path = req.Args[1]
   103  			case "container":
   104  				conf.Type = "docker-container"
   105  				conf.Host = req.Args[1]
   106  			}
   107  		} else {
   108  			connType, err := connection.FetchConnectionType(req.Args[0])
   109  			if err != nil {
   110  				return nil, err
   111  			}
   112  			conf.Type = connType
   113  			containerID = req.Args[0]
   114  		}
   115  	case "container":
   116  		if len(req.Args) > 1 {
   117  			switch req.Args[0] {
   118  			case "image":
   119  				conf.Type = "docker-image"
   120  				conf.Host = req.Args[1]
   121  			case "registry":
   122  				conf.Type = "docker-registry"
   123  				conf.Host = req.Args[1]
   124  			case "tar":
   125  				conf.Type = "docker-snapshot"
   126  				conf.Path = req.Args[1]
   127  			}
   128  		} else {
   129  			conf.Type = "docker-container"
   130  			containerID = req.Args[0]
   131  		}
   132  	case "filesystem", "fs":
   133  		conf.Type = "filesystem"
   134  	}
   135  
   136  	user := ""
   137  	if len(req.Args) != 0 && !(strings.HasPrefix(req.Connector, "docker") || strings.HasPrefix(req.Connector, "container")) {
   138  		target := req.Args[0]
   139  		if !strings.Contains(target, "://") {
   140  			target = "ssh://" + target
   141  		}
   142  
   143  		x, err := url.Parse(target)
   144  		if err != nil {
   145  			return nil, errors.New("incorrect format of target, please use user@host:port")
   146  		}
   147  
   148  		user = x.User.Username()
   149  		conf.Host = x.Hostname()
   150  		conf.Path = x.Path
   151  		if sPort := x.Port(); sPort != "" {
   152  			port, err = strconv.Atoi(x.Port())
   153  			if err != nil {
   154  				return nil, errors.New("port '" + x.Port() + "'is incorrectly formatted, must be a number")
   155  			}
   156  		}
   157  	}
   158  
   159  	if port > 0 {
   160  		conf.Port = int32(port)
   161  	}
   162  
   163  	if x, ok := flags["password"]; ok && len(x.Value) != 0 {
   164  		conf.Credentials = append(conf.Credentials, vault.NewPasswordCredential(user, string(x.Value)))
   165  	}
   166  
   167  	if x, ok := flags["identity-file"]; ok && len(x.Value) != 0 {
   168  		credential, err := vault.NewPrivateKeyCredentialFromPath(user, string(x.Value), "")
   169  		if err != nil {
   170  			return nil, err
   171  		}
   172  		conf.Credentials = append(conf.Credentials, credential)
   173  	}
   174  
   175  	if x, ok := flags["path"]; ok && len(x.Value) != 0 {
   176  		conf.Path = string(x.Value)
   177  	}
   178  
   179  	if user != "" {
   180  		conf.Credentials = append(conf.Credentials, &vault.Credential{Type: vault.CredentialType_ssh_agent, User: user})
   181  	}
   182  
   183  	asset := &inventory.Asset{
   184  		Connections: []*inventory.Config{conf},
   185  	}
   186  
   187  	if containerID != "" {
   188  		asset.Name = containerID
   189  		conf.Host = containerID
   190  	}
   191  
   192  	idDetector := ""
   193  	if flag, ok := flags["id-detector"]; ok {
   194  		if string(flag.Value) != "" {
   195  			idDetector = string(flag.Value)
   196  		}
   197  	}
   198  	if idDetector != "" {
   199  		asset.IdDetector = []string{idDetector}
   200  	}
   201  
   202  	res := plugin.ParseCLIRes{
   203  		Asset: asset,
   204  	}
   205  
   206  	return &res, nil
   207  }
   208  
   209  // LocalAssetReq ist a sample request to connect to the local OS.
   210  // Useful for test automation.
   211  var LocalAssetReq = &plugin.ConnectReq{
   212  	Asset: &inventory.Asset{
   213  		Connections: []*inventory.Config{{
   214  			Type: "local",
   215  		}},
   216  	},
   217  }
   218  
   219  func (s *Service) Connect(req *plugin.ConnectReq, callback plugin.ProviderCallback) (*plugin.ConnectRes, error) {
   220  	if req == nil || req.Asset == nil {
   221  		return nil, errors.New("no connection data provided")
   222  	}
   223  
   224  	conn, err := s.connect(req, callback)
   225  	if err != nil {
   226  		return nil, err
   227  	}
   228  
   229  	// We only need to run the detection step when we don't have any asset information yet.
   230  	if req.Asset.Platform == nil {
   231  		if err := s.detect(req.Asset, conn); err != nil {
   232  			return nil, err
   233  		}
   234  	}
   235  	log.Debug().Str("asset", req.Asset.Name).Msg("detected asset")
   236  
   237  	var inv *inventory.Inventory
   238  	connType := conn.Asset().Connections[0].Type
   239  	switch connType {
   240  	case "docker-registry":
   241  		tarConn := conn.(*connection.TarConnection)
   242  		inv, err = s.discoverRegistry(tarConn)
   243  		if err != nil {
   244  			return nil, err
   245  		}
   246  	case "local", "docker-container":
   247  		inv, err = s.discoverLocalContainers(conn.Asset().Connections[0])
   248  		if err != nil {
   249  			return nil, err
   250  		}
   251  	}
   252  
   253  	return &plugin.ConnectRes{
   254  		Id:        uint32(conn.ID()),
   255  		Name:      conn.Name(),
   256  		Asset:     req.Asset,
   257  		Inventory: inv,
   258  	}, nil
   259  }
   260  
   261  func (s *Service) MockConnect(req *plugin.ConnectReq, callback plugin.ProviderCallback) (*plugin.ConnectRes, error) {
   262  	if req == nil || req.Asset == nil {
   263  		return nil, errors.New("no connection data provided")
   264  	}
   265  
   266  	asset := &inventory.Asset{
   267  		PlatformIds: req.Asset.PlatformIds,
   268  		Platform:    req.Asset.Platform,
   269  		Connections: []*inventory.Config{{
   270  			Type: "mock",
   271  		}},
   272  	}
   273  
   274  	conn, err := s.connect(&plugin.ConnectReq{
   275  		Features: req.Features,
   276  		Upstream: req.Upstream,
   277  		Asset:    asset,
   278  	}, callback)
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  
   283  	return &plugin.ConnectRes{
   284  		Id:    uint32(conn.ID()),
   285  		Name:  conn.Name(),
   286  		Asset: asset,
   287  	}, nil
   288  }
   289  
   290  // Shutdown is automatically called when the shell closes.
   291  // It is not necessary to implement this method.
   292  // If you want to do some cleanup, you can do it here.
   293  func (s *Service) Shutdown(req *plugin.ShutdownReq) (*plugin.ShutdownRes, error) {
   294  	for i := range s.runtimes {
   295  		runtime := s.runtimes[i]
   296  		if x, ok := runtime.Connection.(*connection.TarConnection); ok {
   297  			x.CloseFN()
   298  		}
   299  	}
   300  	return &plugin.ShutdownRes{}, nil
   301  }
   302  
   303  func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallback) (shared.Connection, error) {
   304  	if len(req.Asset.Connections) == 0 {
   305  		return nil, errors.New("no connection options for asset")
   306  	}
   307  
   308  	asset := req.Asset
   309  	conf := asset.Connections[0]
   310  	var conn shared.Connection
   311  	var err error
   312  
   313  	switch conf.Type {
   314  	case LocalConnectionType:
   315  		s.lastConnectionID++
   316  		conn = connection.NewLocalConnection(s.lastConnectionID, conf, asset)
   317  		idDetectors := asset.IdDetector
   318  		if len(idDetectors) == 0 {
   319  			// fallback to default id detectors
   320  			idDetectors = []string{ids.IdDetector_Hostname, ids.IdDetector_CloudDetect}
   321  		}
   322  
   323  		fingerprint, err := IdentifyPlatform(conn, asset.Platform, idDetectors)
   324  		if err == nil {
   325  			asset.Name = fingerprint.Name
   326  			asset.PlatformIds = fingerprint.PlatformIDs
   327  		}
   328  
   329  	case SshConnectionType:
   330  		s.lastConnectionID++
   331  		conn, err = connection.NewSshConnection(s.lastConnectionID, conf, asset)
   332  		if err != nil {
   333  			return nil, err
   334  		}
   335  		idDetectors := asset.IdDetector
   336  		if len(idDetectors) == 0 {
   337  			// fallback to default id detectors
   338  			idDetectors = []string{ids.IdDetector_Hostname, ids.IdDetector_CloudDetect, ids.IdDetector_SshHostkey}
   339  		}
   340  
   341  		fingerprint, err := IdentifyPlatform(conn, asset.Platform, idDetectors)
   342  		if err == nil {
   343  			if conn.Asset().Connections[0].Runtime != "vagrant" {
   344  				asset.Name = fingerprint.Name
   345  			}
   346  			asset.PlatformIds = fingerprint.PlatformIDs
   347  		}
   348  
   349  	case TarConnectionType:
   350  		s.lastConnectionID++
   351  		conn, err = connection.NewTarConnection(s.lastConnectionID, conf, asset)
   352  		if err != nil {
   353  			return nil, err
   354  		}
   355  
   356  		idDetectors := asset.IdDetector
   357  		if len(idDetectors) == 0 {
   358  			// fallback to default id detectors
   359  			idDetectors = []string{ids.IdDetector_Hostname}
   360  		}
   361  		fingerprint, err := IdentifyPlatform(conn, asset.Platform, idDetectors)
   362  		if err == nil {
   363  			asset.Name = fingerprint.Name
   364  			asset.PlatformIds = fingerprint.PlatformIDs
   365  		}
   366  
   367  	case DockerSnapshotConnectionType:
   368  		s.lastConnectionID++
   369  		conn, err = connection.NewDockerSnapshotConnection(s.lastConnectionID, conf, asset)
   370  		if err != nil {
   371  			return nil, err
   372  		}
   373  
   374  		idDetectors := asset.IdDetector
   375  		if len(idDetectors) == 0 {
   376  			// fallback to default id detectors
   377  			idDetectors = []string{ids.IdDetector_Hostname}
   378  		}
   379  
   380  		fingerprint, err := IdentifyPlatform(conn, asset.Platform, idDetectors)
   381  		if err == nil {
   382  			asset.Name = fingerprint.Name
   383  			asset.PlatformIds = fingerprint.PlatformIDs
   384  		}
   385  
   386  	case VagrantConnectionType:
   387  		s.lastConnectionID++
   388  		conn, err = connection.NewVagrantConnection(s.lastConnectionID, conf, asset)
   389  		if err != nil {
   390  			return nil, err
   391  		}
   392  		// We need to detect the platform for the connection asset here, because
   393  		// this platform information will be used to determine the package manager
   394  		err := s.detect(conn.Asset(), conn)
   395  		if err != nil {
   396  			return nil, err
   397  		}
   398  
   399  	case DockerImageConnectionType:
   400  		s.lastConnectionID++
   401  		conn, err = connection.NewDockerContainerImageConnection(s.lastConnectionID, conf, asset)
   402  
   403  	case DockerContainerConnectionType:
   404  		s.lastConnectionID++
   405  		conn, err = connection.NewDockerEngineContainer(s.lastConnectionID, conf, asset)
   406  
   407  	case DockerRegistryConnectionType, ContainerRegistryConnectionType:
   408  		s.lastConnectionID++
   409  		conn, err = connection.NewContainerRegistryImage(s.lastConnectionID, conf, asset)
   410  
   411  	case RegistryImageConnectionType:
   412  		s.lastConnectionID++
   413  		conn, err = connection.NewContainerRegistryImage(s.lastConnectionID, conf, asset)
   414  
   415  	case FilesystemConnectionType:
   416  		s.lastConnectionID++
   417  		conn, err = connection.NewFileSystemConnection(s.lastConnectionID, conf, asset)
   418  		if err != nil {
   419  			return nil, err
   420  		}
   421  		fingerprint, err := IdentifyPlatform(conn, asset.Platform, []string{ids.IdDetector_Hostname})
   422  		if err == nil {
   423  			asset.Name = fingerprint.Name
   424  			asset.PlatformIds = fingerprint.PlatformIDs
   425  		}
   426  
   427  	// Do not expose mock connection as a supported type
   428  	case "mock":
   429  		s.lastConnectionID++
   430  		conn, err = mock.New("", asset)
   431  
   432  	default:
   433  		return nil, errors.New("cannot find connection type " + conf.Type)
   434  	}
   435  
   436  	if err != nil {
   437  		return nil, err
   438  	}
   439  
   440  	var upstream *upstream.UpstreamClient
   441  	if req.Upstream != nil && !req.Upstream.Incognito {
   442  		upstream, err = req.Upstream.InitClient()
   443  		if err != nil {
   444  			return nil, err
   445  		}
   446  	}
   447  
   448  	conf.Id = conn.ID()
   449  	conf.Capabilities = conn.Capabilities().String()
   450  
   451  	s.runtimes[conn.ID()] = &plugin.Runtime{
   452  		Connection:     conn,
   453  		Callback:       callback,
   454  		HasRecording:   req.HasRecording,
   455  		CreateResource: resources.CreateResource,
   456  		Upstream:       upstream,
   457  	}
   458  
   459  	return conn, err
   460  }
   461  
   462  func (s *Service) GetData(req *plugin.DataReq) (*plugin.DataRes, error) {
   463  	runtime, ok := s.runtimes[req.Connection]
   464  	if !ok {
   465  		return nil, errors.New("connection " + strconv.FormatUint(uint64(req.Connection), 10) + " not found")
   466  	}
   467  
   468  	args := plugin.PrimitiveArgsToRawDataArgs(req.Args, runtime)
   469  
   470  	if req.ResourceId == "" && req.Field == "" {
   471  		res, err := resources.NewResource(runtime, req.Resource, args)
   472  		if err != nil {
   473  			return nil, err
   474  		}
   475  
   476  		rd := llx.ResourceData(res, res.MqlName()).Result()
   477  		return &plugin.DataRes{
   478  			Data: rd.Data,
   479  		}, nil
   480  	}
   481  
   482  	resource, ok := runtime.Resources.Get(req.Resource + "\x00" + req.ResourceId)
   483  	if !ok {
   484  		// Note: Since resources are internally always created, there are only very
   485  		// few cases where we arrive here:
   486  		// 1. The caller is wrong. Possibly a mixup with IDs
   487  		// 2. The resource was loaded from a recording, but the field is not
   488  		// in the recording. Thus the resource was never created inside the
   489  		// plugin. We will attempt to create the resource and see if the field
   490  		// can be computed.
   491  		if !runtime.HasRecording {
   492  			return nil, errors.New("resource '" + req.Resource + "' (id: " + req.ResourceId + ") doesn't exist")
   493  		}
   494  
   495  		args, err := runtime.ResourceFromRecording(req.Resource, req.ResourceId)
   496  		if err != nil {
   497  			return nil, errors.New("attempted to load resource '" + req.Resource + "' (id: " + req.ResourceId + ") from recording failed: " + err.Error())
   498  		}
   499  
   500  		resource, err = resources.CreateResource(runtime, req.Resource, args)
   501  		if err != nil {
   502  			return nil, errors.New("attempted to create resource '" + req.Resource + "' (id: " + req.ResourceId + ") from recording failed: " + err.Error())
   503  		}
   504  	}
   505  
   506  	return resources.GetData(resource, req.Field, args), nil
   507  }
   508  
   509  func (s *Service) StoreData(req *plugin.StoreReq) (*plugin.StoreRes, error) {
   510  	runtime, ok := s.runtimes[req.Connection]
   511  	if !ok {
   512  		return nil, errors.New("connection " + strconv.FormatUint(uint64(req.Connection), 10) + " not found")
   513  	}
   514  
   515  	var errs []string
   516  	for i := range req.Resources {
   517  		info := req.Resources[i]
   518  
   519  		args, err := plugin.ProtoArgsToRawDataArgs(info.Fields)
   520  		if err != nil {
   521  			errs = append(errs, "failed to add cached "+info.Name+" (id: "+info.Id+"), failed to parse arguments")
   522  			continue
   523  		}
   524  
   525  		resource, ok := runtime.Resources.Get(info.Name + "\x00" + info.Id)
   526  		if !ok {
   527  			resource, err = resources.CreateResource(runtime, info.Name, args)
   528  			if err != nil {
   529  				errs = append(errs, "failed to add cached "+info.Name+" (id: "+info.Id+"), creation failed: "+err.Error())
   530  				continue
   531  			}
   532  
   533  			runtime.Resources.Set(info.Name+"\x00"+info.Id, resource)
   534  		} else {
   535  			if err := resources.SetAllData(resource, args); err != nil {
   536  				errs = append(errs, "failed to add cached "+info.Name+" (id: "+info.Id+"), field error: "+err.Error())
   537  			}
   538  		}
   539  	}
   540  
   541  	if len(errs) != 0 {
   542  		return nil, errors.New(strings.Join(errs, ", "))
   543  	}
   544  	return &plugin.StoreRes{}, nil
   545  }
   546  
   547  func (s *Service) discoverRegistry(conn *connection.TarConnection) (*inventory.Inventory, error) {
   548  	conf := conn.Asset().Connections[0]
   549  	if conf == nil {
   550  		return nil, nil
   551  	}
   552  
   553  	resolver := container_registry.Resolver{}
   554  	resolvedAssets, err := resolver.Resolve(context.Background(), conn.Asset(), conf, nil)
   555  	if err != nil {
   556  		return nil, err
   557  	}
   558  
   559  	inventory := &inventory.Inventory{}
   560  	inventory.AddAssets(resolvedAssets...)
   561  
   562  	return inventory, nil
   563  }
   564  
   565  func (s *Service) discoverLocalContainers(conf *inventory.Config) (*inventory.Inventory, error) {
   566  	if conf == nil || conf.Discover == nil {
   567  		return nil, nil
   568  	}
   569  
   570  	if !stringx.ContainsAnyOf(conf.Discover.Targets, "all", docker_engine.DiscoveryContainerRunning, docker_engine.DiscoveryContainerImages) {
   571  		return nil, nil
   572  	}
   573  
   574  	resolvedAssets, err := docker_engine.DiscoverDockerEngineAssets(conf)
   575  	if err != nil {
   576  		return nil, err
   577  	}
   578  
   579  	inventory := &inventory.Inventory{}
   580  	inventory.AddAssets(resolvedAssets...)
   581  
   582  	return inventory, nil
   583  }