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

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package provider
     5  
     6  import (
     7  	"errors"
     8  	"net/url"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"go.mondoo.com/cnquery/llx"
    13  	"go.mondoo.com/cnquery/providers-sdk/v1/inventory"
    14  	"go.mondoo.com/cnquery/providers-sdk/v1/plugin"
    15  	"go.mondoo.com/cnquery/providers-sdk/v1/upstream"
    16  	"go.mondoo.com/cnquery/providers/network/connection"
    17  	"go.mondoo.com/cnquery/providers/network/resources"
    18  	"go.mondoo.com/cnquery/providers/network/resources/domain"
    19  )
    20  
    21  const (
    22  	defaultConnection uint32 = 1
    23  	ConnectionType           = "host"
    24  )
    25  
    26  // This is a small selection of common ports that are supported.
    27  // Outside of this range, users will have to specify ports explicitly.
    28  // We could expand this to cover more of IANA.
    29  var commonPorts = map[string]int{
    30  	"https":  443,
    31  	"http":   80,
    32  	"ssh":    22,
    33  	"ftp":    21,
    34  	"telnet": 23,
    35  	"smtp":   25,
    36  	"dns":    53,
    37  	"pop3":   110,
    38  	"imap4":  143,
    39  }
    40  
    41  type Service struct {
    42  	runtimes         map[uint32]*plugin.Runtime
    43  	lastConnectionID uint32
    44  }
    45  
    46  func Init() *Service {
    47  	return &Service{
    48  		runtimes: map[uint32]*plugin.Runtime{},
    49  	}
    50  }
    51  
    52  func (s *Service) ParseCLI(req *plugin.ParseCLIReq) (*plugin.ParseCLIRes, error) {
    53  	target := req.Args[0]
    54  	if i := strings.Index(target, "://"); i == -1 {
    55  		target = "http://" + target
    56  	}
    57  
    58  	url, err := url.Parse(target)
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	host, port := domain.SplitHostPort(url.Host)
    64  	if port == 0 {
    65  		port = commonPorts[url.Scheme]
    66  	}
    67  
    68  	insecure := false
    69  	if found, ok := req.Flags["insecure"]; ok {
    70  		insecure, _ = found.RawData().Value.(bool)
    71  	}
    72  
    73  	asset := inventory.Asset{
    74  		Connections: []*inventory.Config{{
    75  			Type:     "host",
    76  			Port:     int32(port),
    77  			Host:     host,
    78  			Path:     url.Path,
    79  			Insecure: insecure,
    80  		}},
    81  	}
    82  
    83  	return &plugin.ParseCLIRes{Asset: &asset}, nil
    84  }
    85  
    86  // Shutdown is automatically called when the shell closes.
    87  // It is not necessary to implement this method.
    88  // If you want to do some cleanup, you can do it here.
    89  func (s *Service) Shutdown(req *plugin.ShutdownReq) (*plugin.ShutdownRes, error) {
    90  	return &plugin.ShutdownRes{}, nil
    91  }
    92  
    93  func (s *Service) MockConnect(req *plugin.ConnectReq, callback plugin.ProviderCallback) (*plugin.ConnectRes, error) {
    94  	return nil, errors.New("mock connect not yet implemented")
    95  }
    96  
    97  func (s *Service) Connect(req *plugin.ConnectReq, callback plugin.ProviderCallback) (*plugin.ConnectRes, error) {
    98  	if req == nil || req.Asset == nil {
    99  		return nil, errors.New("no connection data provided")
   100  	}
   101  
   102  	conn, err := s.connect(req, callback)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	// We only need to run the detection step when we don't have any asset information yet.
   108  	if req.Asset.Platform == nil {
   109  		if err := s.detect(req.Asset, conn); err != nil {
   110  			return nil, err
   111  		}
   112  	}
   113  
   114  	// TODO: discovery of related assets and use them in the inventory below
   115  
   116  	return &plugin.ConnectRes{
   117  		Id:        uint32(conn.ID()),
   118  		Name:      conn.Name(),
   119  		Asset:     req.Asset,
   120  		Inventory: nil,
   121  	}, nil
   122  }
   123  
   124  func (s *Service) connect(req *plugin.ConnectReq, callback plugin.ProviderCallback) (*connection.HostConnection, error) {
   125  	if len(req.Asset.Connections) == 0 {
   126  		return nil, errors.New("no connection options for asset")
   127  	}
   128  
   129  	asset := req.Asset
   130  	conf := asset.Connections[0]
   131  	var conn *connection.HostConnection
   132  	var err error
   133  
   134  	switch conf.Type {
   135  	case "host":
   136  		s.lastConnectionID++
   137  		conn = connection.NewHostConnection(s.lastConnectionID, asset, conf)
   138  
   139  	default:
   140  		// generic host connection, without anything else
   141  		s.lastConnectionID++
   142  		conn = connection.NewHostConnection(s.lastConnectionID, asset, conf)
   143  	}
   144  
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  
   149  	var upstream *upstream.UpstreamClient
   150  	if req.Upstream != nil && !req.Upstream.Incognito {
   151  		upstream, err = req.Upstream.InitClient()
   152  		if err != nil {
   153  			return nil, err
   154  		}
   155  	}
   156  
   157  	asset.Connections[0].Id = conn.ID()
   158  	s.runtimes[conn.ID()] = &plugin.Runtime{
   159  		Connection:     conn,
   160  		Callback:       callback,
   161  		HasRecording:   req.HasRecording,
   162  		CreateResource: resources.CreateResource,
   163  		Upstream:       upstream,
   164  	}
   165  
   166  	return conn, err
   167  }
   168  
   169  func (s *Service) detect(asset *inventory.Asset, conn *connection.HostConnection) error {
   170  	if conn.Conf.Port == 0 {
   171  		return errors.New("a port for the network connection is required")
   172  	}
   173  
   174  	asset.Name = conn.Conf.Host
   175  	asset.Platform = &inventory.Platform{
   176  		Name:   "host",
   177  		Family: []string{"network"},
   178  		Kind:   "network",
   179  		Title:  "Network API",
   180  	}
   181  
   182  	asset.PlatformIds = []string{"//platformid.api.mondoo.app/runtime/network/host/" + conn.Conf.Host}
   183  
   184  	return nil
   185  }
   186  
   187  func (s *Service) GetData(req *plugin.DataReq) (*plugin.DataRes, error) {
   188  	runtime, ok := s.runtimes[req.Connection]
   189  	if !ok {
   190  		return nil, errors.New("connection " + strconv.FormatUint(uint64(req.Connection), 10) + " not found")
   191  	}
   192  
   193  	args := plugin.PrimitiveArgsToRawDataArgs(req.Args, runtime)
   194  
   195  	if req.ResourceId == "" && req.Field == "" {
   196  		res, err := resources.NewResource(runtime, req.Resource, args)
   197  		if err != nil {
   198  			return nil, err
   199  		}
   200  
   201  		rd := llx.ResourceData(res, res.MqlName()).Result()
   202  		return &plugin.DataRes{
   203  			Data: rd.Data,
   204  		}, nil
   205  	}
   206  
   207  	resource, ok := runtime.Resources.Get(req.Resource + "\x00" + req.ResourceId)
   208  	if !ok {
   209  		// Note: Since resources are internally always created, there are only very
   210  		// few cases where we arrive here:
   211  		// 1. The caller is wrong. Possibly a mixup with IDs
   212  		// 2. The resource was loaded from a recording, but the field is not
   213  		// in the recording. Thus the resource was never created inside the
   214  		// plugin. We will attempt to create the resource and see if the field
   215  		// can be computed.
   216  		if !runtime.HasRecording {
   217  			return nil, errors.New("resource '" + req.Resource + "' (id: " + req.ResourceId + ") doesn't exist")
   218  		}
   219  
   220  		args, err := runtime.ResourceFromRecording(req.Resource, req.ResourceId)
   221  		if err != nil {
   222  			return nil, errors.New("attempted to load resource '" + req.Resource + "' (id: " + req.ResourceId + ") from recording failed: " + err.Error())
   223  		}
   224  
   225  		resource, err = resources.CreateResource(runtime, req.Resource, args)
   226  		if err != nil {
   227  			return nil, errors.New("attempted to create resource '" + req.Resource + "' (id: " + req.ResourceId + ") from recording failed: " + err.Error())
   228  		}
   229  	}
   230  
   231  	return resources.GetData(resource, req.Field, args), nil
   232  }
   233  
   234  func (s *Service) StoreData(req *plugin.StoreReq) (*plugin.StoreRes, error) {
   235  	runtime, ok := s.runtimes[req.Connection]
   236  	if !ok {
   237  		return nil, errors.New("connection " + strconv.FormatUint(uint64(req.Connection), 10) + " not found")
   238  	}
   239  
   240  	var errs []string
   241  	for i := range req.Resources {
   242  		info := req.Resources[i]
   243  
   244  		args, err := plugin.ProtoArgsToRawDataArgs(info.Fields)
   245  		if err != nil {
   246  			errs = append(errs, "failed to add cached "+info.Name+" (id: "+info.Id+"), failed to parse arguments")
   247  			continue
   248  		}
   249  
   250  		resource, ok := runtime.Resources.Get(info.Name + "\x00" + info.Id)
   251  		if !ok {
   252  			resource, err = resources.CreateResource(runtime, info.Name, args)
   253  			if err != nil {
   254  				errs = append(errs, "failed to add cached "+info.Name+" (id: "+info.Id+"), creation failed: "+err.Error())
   255  				continue
   256  			}
   257  
   258  			runtime.Resources.Set(info.Name+"\x00"+info.Id, resource)
   259  		}
   260  
   261  		for k, v := range args {
   262  			if err := resources.SetData(resource, k, v); err != nil {
   263  				errs = append(errs, "failed to add cached "+info.Name+" (id: "+info.Id+"), field error: "+err.Error())
   264  			}
   265  		}
   266  	}
   267  
   268  	if len(errs) != 0 {
   269  		return nil, errors.New(strings.Join(errs, ", "))
   270  	}
   271  	return &plugin.StoreRes{}, nil
   272  }