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 }