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 }