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 }