go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/os/connection/ssh.go (about) 1 // Copyright (c) Mondoo, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package connection 5 6 import ( 7 "bytes" 8 "context" 9 "errors" 10 "io" 11 "net" 12 "os" 13 "path/filepath" 14 "strconv" 15 "strings" 16 "time" 17 18 awsconf "github.com/aws/aws-sdk-go-v2/config" 19 "github.com/kevinburke/ssh_config" 20 "github.com/mitchellh/go-homedir" 21 rawsftp "github.com/pkg/sftp" 22 "github.com/rs/zerolog/log" 23 "github.com/spf13/afero" 24 "go.mondoo.com/cnquery/providers-sdk/v1/inventory" 25 "go.mondoo.com/cnquery/providers-sdk/v1/vault" 26 "go.mondoo.com/cnquery/providers/os/connection/shared" 27 "go.mondoo.com/cnquery/providers/os/connection/ssh/awsinstanceconnect" 28 "go.mondoo.com/cnquery/providers/os/connection/ssh/awsssmsession" 29 "go.mondoo.com/cnquery/providers/os/connection/ssh/cat" 30 "go.mondoo.com/cnquery/providers/os/connection/ssh/scp" 31 "go.mondoo.com/cnquery/providers/os/connection/ssh/sftp" 32 "go.mondoo.com/cnquery/providers/os/connection/ssh/signers" 33 "go.mondoo.com/cnquery/utils/multierr" 34 "golang.org/x/crypto/ssh" 35 "golang.org/x/crypto/ssh/agent" 36 "golang.org/x/crypto/ssh/knownhosts" 37 ) 38 39 const ( 40 SSH shared.ConnectionType = "ssh" 41 ) 42 43 type SshConnection struct { 44 id uint32 45 conf *inventory.Config 46 asset *inventory.Asset 47 48 fs afero.Fs 49 Sudo *inventory.Sudo 50 51 serverVersion string 52 UseScpFilesystem bool 53 HostKey ssh.PublicKey 54 SSHClient *ssh.Client 55 } 56 57 func NewSshConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) (*SshConnection, error) { 58 res := SshConnection{ 59 id: id, 60 conf: conf, 61 asset: asset, 62 } 63 64 host := conf.GetHost() 65 66 // ipv6 addresses w/o the surrounding [] will eventually error 67 // so check whether we have an ipv6 address by parsing it (the 68 // parsing will fail if the string DOES have the []s) and adding 69 // the []s 70 ip := net.ParseIP(host) 71 if ip != nil && ip.To4() == nil { 72 conf.Host = "[" + host + "]" 73 } 74 75 conf = readSSHConfig(conf) 76 if err := verifyConfig(conf); err != nil { 77 return nil, err 78 } 79 80 if os.Getenv("MONDOO_SSH_SCP") == "on" || conf.Options["ssh_scp"] == "on" { 81 res.UseScpFilesystem = true 82 } 83 84 if conf.Insecure { 85 log.Debug().Msg("user allowed insecure ssh connection") 86 } 87 88 if err := res.Connect(); err != nil { 89 return nil, err 90 } 91 92 // check uid of user and disable sudo if uid is 0 93 if conf.Sudo != nil && conf.Sudo.Active { 94 // the id command may not be available, eg. if ssh is used with windows 95 out, _ := res.RunCommand("id -u") 96 stdout, _ := io.ReadAll(out.Stdout) 97 // just check for the explicit positive case, otherwise just activate sudo 98 // we check sudo in VerifyConnection 99 if string(stdout) != "0" { 100 // configure sudo 101 log.Debug().Msg("activated sudo for ssh connection") 102 res.Sudo = conf.Sudo 103 } 104 } 105 106 // verify connection 107 vErr := res.verify() 108 // NOTE: for now we do not enforce connection verification to ensure we cover edge-cases 109 // TODO: in following minor version bumps, we want to enforce this behavior to ensure proper scans 110 if vErr != nil { 111 log.Warn().Err(vErr).Send() 112 } 113 114 return &res, nil 115 } 116 117 func (c *SshConnection) ID() uint32 { 118 return c.id 119 } 120 121 func (c *SshConnection) Name() string { 122 return "ssh" 123 } 124 125 func (c *SshConnection) Type() shared.ConnectionType { 126 return SSH 127 } 128 129 func (p *SshConnection) Asset() *inventory.Asset { 130 return p.asset 131 } 132 133 func (p *SshConnection) Capabilities() shared.Capabilities { 134 return shared.Capability_File | shared.Capability_RunCommand 135 } 136 137 func (c *SshConnection) RunCommand(command string) (*shared.Command, error) { 138 if c.Sudo != nil && c.Sudo.Active { 139 command = shared.BuildSudoCommand(c.Sudo, command) 140 } 141 return c.runRawCommand(command) 142 } 143 144 func (c *SshConnection) runRawCommand(command string) (*shared.Command, error) { 145 log.Debug().Str("command", command).Str("provider", "ssh").Msg("run command") 146 147 if c.SSHClient == nil { 148 return nil, errors.New("SSH session not established") 149 } 150 151 res := shared.Command{ 152 Command: command, 153 Stats: shared.PerfStats{ 154 Start: time.Now(), 155 }, 156 Stdout: &bytes.Buffer{}, 157 Stderr: &bytes.Buffer{}, 158 } 159 defer func() { 160 res.Stats.Duration = time.Since(res.Stats.Start) 161 }() 162 163 session, err := c.SSHClient.NewSession() 164 if err != nil { 165 log.Debug().Msg("could not open new session, try to re-establish connection") 166 167 c.Close() 168 if err = c.Connect(); err != nil { 169 return nil, multierr.Wrap(err, "failed to open SSH session (reconnect failed)") 170 } 171 172 session, err = c.SSHClient.NewSession() 173 if err != nil { 174 return nil, err 175 } 176 } 177 defer session.Close() 178 179 // start ssh call 180 session.Stdout = res.Stdout 181 session.Stderr = res.Stderr 182 err = session.Run(res.Command) 183 if err == nil { 184 return &res, nil 185 } 186 187 // if the program failed, we do not return err but its exit code 188 var e *ssh.ExitError 189 match := errors.As(err, &e) 190 if match { 191 res.ExitStatus = e.ExitStatus() 192 return &res, nil 193 } 194 195 // all other errors are real errors and not expected 196 return &res, err 197 } 198 199 func (c *SshConnection) FileSystem() afero.Fs { 200 if c.fs != nil { 201 return c.fs 202 } 203 204 // log the used ssh filesystem backend 205 defer func() { 206 log.Debug().Str("file-transfer", c.fs.Name()).Msg("initialized ssh filesystem") 207 }() 208 209 //// detect cisco network gear, they returns something like SSH-2.0-Cisco-1.25 210 //// NOTE: we need to understand why this happens 211 //if strings.Contains(strings.ToLower(t.serverVersion), "cisco") { 212 // log.Debug().Msg("detected cisco device, deactivate file system support") 213 // t.fs = &fsutil.NoFs{} 214 // return t.fs 215 //} 216 217 if c.Sudo != nil && c.Sudo.Active { 218 c.fs = cat.New(c) 219 return c.fs 220 } 221 222 // we always try to use sftp first (if scp is not user-enforced) 223 // and we also fallback to scp if sftp does not work 224 if !c.UseScpFilesystem { 225 fs, err := sftp.New(c, c.SSHClient) 226 if err != nil { 227 log.Info().Msg("use scp instead of sftp") 228 // enable fallback 229 c.UseScpFilesystem = true 230 } else { 231 c.fs = fs 232 return c.fs 233 } 234 } 235 236 if c.UseScpFilesystem { 237 c.fs = scp.NewFs(c, c.SSHClient) 238 return c.fs 239 } 240 241 // always fallback to catfs, slow but it works 242 c.fs = cat.New(c) 243 return c.fs 244 } 245 246 func (c *SshConnection) FileInfo(path string) (shared.FileInfoDetails, error) { 247 fs := c.FileSystem() 248 afs := &afero.Afero{Fs: fs} 249 stat, err := afs.Stat(path) 250 if err != nil { 251 return shared.FileInfoDetails{}, err 252 } 253 254 uid := int64(-1) 255 gid := int64(-1) 256 257 if c.Sudo != nil || c.UseScpFilesystem { 258 if stat, ok := stat.Sys().(*shared.FileInfo); ok { 259 uid = int64(stat.Uid) 260 gid = int64(stat.Gid) 261 } 262 } else { 263 if stat, ok := stat.Sys().(*rawsftp.FileStat); ok { 264 uid = int64(stat.UID) 265 gid = int64(stat.GID) 266 } 267 } 268 mode := stat.Mode() 269 270 return shared.FileInfoDetails{ 271 Mode: shared.FileModeDetails{mode}, 272 Size: stat.Size(), 273 Uid: uid, 274 Gid: gid, 275 }, nil 276 } 277 278 func (c *SshConnection) Close() { 279 if c.SSHClient != nil { 280 c.SSHClient.Close() 281 } 282 } 283 284 func (c *SshConnection) Connect() error { 285 cc := c.conf 286 287 // we always want to ensure we use the default port if nothing was specified 288 if cc.Port == 0 { 289 cc.Port = 22 290 } 291 292 // load known hosts and track the fingerprint of the ssh server for later identification 293 knownHostsCallback, err := knownHostsCallback() 294 if err != nil { 295 return multierr.Wrap(err, "could not read hostkey file") 296 } 297 298 var hostkey ssh.PublicKey 299 hostkeyCallback := func(hostname string, remote net.Addr, key ssh.PublicKey) error { 300 // store the hostkey for later identification 301 hostkey = key 302 303 // ignore hostkey check if the user provided an insecure flag 304 if cc.Insecure { 305 return nil 306 } 307 308 // knownhost.New returns a ssh.CertChecker which does not work with all ssh.HostKey types 309 // especially the newer edcsa keys (ssh.curve25519sha256) are not well supported. 310 // https://github.com/golang/crypto/blob/master/ssh/knownhosts/knownhosts.go#L417-L436 311 // creates the CertChecker which requires an instance of Certificate 312 // https://github.com/golang/crypto/blob/master/ssh/certs.go#L326-L348 313 // https://github.com/golang/crypto/blob/master/ssh/keys.go#L271-L283 314 // therefore it is best to skip the checking for now since it forces users to set the insecure flag otherwise 315 // TODO: implement custom host-key checking for normal public keys as well 316 _, ok := key.(*ssh.Certificate) 317 if !ok { 318 log.Debug().Msg("skip hostkey check the hostkey since the algo is not supported yet") 319 return nil 320 } 321 322 err := knownHostsCallback(hostname, remote, key) 323 if err != nil { 324 log.Debug().Err(err).Str("hostname", hostname).Str("ip", remote.String()).Msg("check known host") 325 } 326 return err 327 } 328 329 // establish connection 330 conn, _, err := establishClientConnection(cc, hostkeyCallback) 331 if err != nil { 332 log.Debug().Err(err).Str("provider", "ssh").Str("host", cc.Host).Int32("port", cc.Port).Bool("insecure", cc.Insecure).Msg("could not establish ssh session") 333 if strings.ContainsAny(cc.Host, "[]") { 334 log.Info().Str("host", cc.Host).Int32("port", cc.Port).Msg("ensure proper []s when combining IPv6 with port numbers") 335 } 336 return err 337 } 338 c.SSHClient = conn 339 c.HostKey = hostkey 340 c.serverVersion = string(conn.ServerVersion()) 341 log.Debug().Str("provider", "ssh").Str("host", cc.Host).Int32("port", cc.Port).Str("server", c.serverVersion).Msg("ssh session established") 342 return nil 343 } 344 345 func (c *SshConnection) PlatformID() (string, error) { 346 return PlatformIdentifier(c.HostKey), nil 347 } 348 349 func PlatformIdentifier(publicKey ssh.PublicKey) string { 350 fingerprint := ssh.FingerprintSHA256(publicKey) 351 fingerprint = strings.Replace(fingerprint, ":", "-", 1) 352 identifier := "//platformid.api.mondoo.app/runtime/ssh/hostkey/" + fingerprint 353 return identifier 354 } 355 356 func readSSHConfig(cc *inventory.Config) *inventory.Config { 357 host := cc.Host 358 359 home, err := homedir.Dir() 360 if err != nil { 361 log.Debug().Err(err).Msg("ssh> failed to determine user home directory") 362 return cc 363 } 364 365 sshUserConfigPath := filepath.Join(home, ".ssh", "config") 366 f, err := os.Open(sshUserConfigPath) 367 if err != nil { 368 log.Debug().Err(err).Str("file", sshUserConfigPath).Msg("ssh> could not read ssh config") 369 return cc 370 } 371 372 cfg, err := ssh_config.Decode(f) 373 if err != nil { 374 log.Debug().Err(err).Str("file", sshUserConfigPath).Msg("could not parse ssh config") 375 return cc 376 } 377 378 // optional step, tries to parse the ssh config to see if additional information 379 // is already available 380 hostname, err := cfg.Get(host, "HostName") 381 if err == nil && len(hostname) > 0 { 382 cc.Host = hostname 383 } 384 385 if len(cc.Credentials) == 0 || (len(cc.Credentials) == 1 && cc.Credentials[0].Type == vault.CredentialType_password && len(cc.Credentials[0].Secret) == 0) { 386 user, _ := cfg.Get(host, "User") 387 port, err := cfg.Get(host, "Port") 388 if err == nil { 389 portNum, err := strconv.Atoi(port) 390 if err != nil { 391 log.Debug().Err(err).Str("file", sshUserConfigPath).Str("port", port).Msg("could not parse ssh port") 392 } else { 393 cc.Port = int32(portNum) 394 } 395 } 396 397 entry, err := cfg.Get(host, "IdentityFile") 398 399 // TODO: the ssh_config uses os/home but instead should be use go-homedir, could become a compile issue 400 // TODO: the problem is that the lib returns defaults and we cannot properly distingush 401 if err == nil && ssh_config.Default("IdentityFile") != entry { 402 // commonly ssh config included paths like ~ 403 expandedPath, err := homedir.Expand(entry) 404 if err == nil { 405 log.Debug().Str("key", expandedPath).Str("host", host).Msg("ssh> read ssh identity key from ssh config") 406 // NOTE: we ignore the error here for now but this should probably been caught earlier anyway 407 credential, _ := vault.NewPrivateKeyCredentialFromPath(user, expandedPath, "") 408 // apply the option manually 409 if credential != nil { 410 cc.Credentials = append(cc.Credentials, credential) 411 } 412 } 413 } 414 } 415 416 // handle disable of strict hostkey checking: 417 // Host * 418 // StrictHostKeyChecking no 419 entry, err := cfg.Get(host, "StrictHostKeyChecking") 420 if err == nil && strings.ToLower(entry) == "no" { 421 cc.Insecure = true 422 } 423 return cc 424 } 425 426 func verifyConfig(conf *inventory.Config) error { 427 if conf.Type != "ssh" { 428 return inventory.ErrProviderTypeDoesNotMatch 429 } 430 431 return nil 432 } 433 434 func knownHostsCallback() (ssh.HostKeyCallback, error) { 435 home, err := homedir.Dir() 436 if err != nil { 437 log.Debug().Err(err).Msg("Failed to determine user home directory") 438 return nil, err 439 } 440 441 // load default host keys 442 files := []string{ 443 filepath.Join(home, ".ssh", "known_hosts"), 444 // see https://cloud.google.com/compute/docs/instances/connecting-to-instance 445 // NOTE: content in that file is structured by compute.instanceid key 446 // TODO: we need to keep the instance information during the resolve step 447 filepath.Join(home, ".ssh", "google_compute_known_hosts"), 448 } 449 450 // filter all files that do not exits 451 existentKnownHosts := []string{} 452 for i := range files { 453 _, err := os.Stat(files[i]) 454 if err == nil { 455 log.Debug().Str("file", files[i]).Msg("load ssh known_hosts file") 456 existentKnownHosts = append(existentKnownHosts, files[i]) 457 } 458 } 459 460 return knownhosts.New(existentKnownHosts...) 461 } 462 463 func establishClientConnection(pCfg *inventory.Config, hostKeyCallback ssh.HostKeyCallback) (*ssh.Client, []io.Closer, error) { 464 authMethods, closer, err := prepareConnection(pCfg) 465 if err != nil { 466 return nil, nil, err 467 } 468 469 if len(authMethods) == 0 { 470 return nil, nil, errors.New("no authentication method defined") 471 } 472 473 // TODO: hack: we want to establish a proper connection per configured connection so that we could use multiple users 474 user := "" 475 for i := range pCfg.Credentials { 476 if pCfg.Credentials[i].User != "" { 477 user = pCfg.Credentials[i].User 478 } 479 } 480 481 log.Debug().Int("methods", len(authMethods)).Str("user", user).Msg("connect to remote ssh") 482 conn, err := ssh.Dial("tcp", pCfg.Host+":"+strconv.Itoa(int(pCfg.Port)), &ssh.ClientConfig{ 483 User: user, 484 Auth: authMethods, 485 HostKeyCallback: hostKeyCallback, 486 }) 487 return conn, closer, err 488 } 489 490 // hasAgentLoadedKey returns if the ssh agent has loaded the key file 491 // This may not be 100% accurate. The key can be stored in multiple locations with the 492 // same fingerprint. We cannot determine the fingerprint without decoding the encrypted 493 // key, `ssh-keygen -lf /Users/chartmann/.ssh/id_rsa` seems to use the ssh agent to 494 // determine the fingerprint without prompting for the password 495 func hasAgentLoadedKey(list []*agent.Key, filename string) bool { 496 for i := range list { 497 if list[i].Comment == filename { 498 return true 499 } 500 } 501 return false 502 } 503 504 // prepareConnection determines the auth methods required for a ssh connection and also prepares any other 505 // pre-conditions for the connection like tunnelling the connection via AWS SSM session 506 func prepareConnection(conf *inventory.Config) ([]ssh.AuthMethod, []io.Closer, error) { 507 auths := []ssh.AuthMethod{} 508 closer := []io.Closer{} 509 510 // only one public auth method is allowed, therefore multiple keys need to be encapsulated into one auth method 511 sshSigners := []ssh.Signer{} 512 513 // if no credential was provided, fallback to ssh-agent and ssh-config 514 if len(conf.Credentials) == 0 { 515 sshSigners = append(sshSigners, signers.GetSignersFromSSHAgent()...) 516 } 517 518 // use key auth, only load if the key was not found in ssh agent 519 for i := range conf.Credentials { 520 credential := conf.Credentials[i] 521 522 switch credential.Type { 523 case vault.CredentialType_private_key: 524 log.Debug().Msg("enabled ssh private key authentication") 525 priv, err := signers.GetSignerFromPrivateKeyWithPassphrase(credential.Secret, []byte(credential.Password)) 526 if err != nil { 527 log.Debug().Err(err).Msg("could not read private key") 528 } else { 529 sshSigners = append(sshSigners, priv) 530 } 531 case vault.CredentialType_password: 532 // use password auth if the password was set, this is also used when only the username is set 533 if len(credential.Secret) > 0 { 534 log.Debug().Msg("enabled ssh password authentication") 535 auths = append(auths, ssh.Password(string(credential.Secret))) 536 } 537 case vault.CredentialType_ssh_agent: 538 log.Debug().Msg("enabled ssh agent authentication") 539 sshSigners = append(sshSigners, signers.GetSignersFromSSHAgent()...) 540 case vault.CredentialType_aws_ec2_ssm_session: 541 // when the user establishes the ssm session we do the following 542 // 1. start websocket connection and start the session-manager-plugin to map the websocket to a local port 543 // 2. create new ssh key via instance connect so that we do not rely on any pre-existing ssh key 544 err := awsssmsession.CheckPlugin() 545 if err != nil { 546 return nil, nil, errors.New("Local AWS Session Manager plugin is missing. See https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html for information on the AWS Session Manager plugin and installation instructions") 547 } 548 549 loadOpts := []func(*awsconf.LoadOptions) error{} 550 if conf.Options != nil && conf.Options["region"] != "" { 551 loadOpts = append(loadOpts, awsconf.WithRegion(conf.Options["region"])) 552 } 553 profile := "" 554 if conf.Options != nil && conf.Options["profile"] != "" { 555 loadOpts = append(loadOpts, awsconf.WithSharedConfigProfile(conf.Options["profile"])) 556 profile = conf.Options["profile"] 557 } 558 log.Debug().Str("profile", conf.Options["profile"]).Str("region", conf.Options["region"]).Msg("using aws creds") 559 560 cfg, err := awsconf.LoadDefaultConfig(context.Background(), loadOpts...) 561 if err != nil { 562 return nil, nil, err 563 } 564 565 // we use ec2 instance connect api to create credentials for an aws instance 566 eic := awsinstanceconnect.New(cfg) 567 host := conf.Host 568 if id, ok := conf.Options["instance"]; ok { 569 host = id 570 } 571 creds, err := eic.GenerateCredentials(host, credential.User) 572 if err != nil { 573 return nil, nil, err 574 } 575 576 // we use ssm session manager to connect to instance via websockets 577 sManager, err := awsssmsession.NewAwsSsmSessionManager(cfg, profile) 578 if err != nil { 579 return nil, nil, err 580 } 581 582 // prepare websocket connection and bind it to a free local port 583 localIp := "localhost" 584 remotePort := "22" 585 // NOTE: for SSM we always target the instance id 586 conf.Host = creds.InstanceId 587 localPort, err := awsssmsession.GetAvailablePort() 588 if err != nil { 589 return nil, nil, errors.New("could not find an available port to start the ssm proxy") 590 } 591 ssmConn, err := sManager.Dial(conf, strconv.Itoa(localPort), remotePort) 592 if err != nil { 593 return nil, nil, err 594 } 595 596 // update endpoint information for ssh to connect via local ssm proxy 597 // TODO: this has a side-effect, we may need extend the struct to include resolved connection data 598 conf.Host = localIp 599 conf.Port = int32(localPort) 600 601 // NOTE: we need to set insecure so that ssh does not complain about the host key 602 // It is okay do that since the connection is established via aws api itself and it ensures that 603 // the instance id is okay 604 conf.Insecure = true 605 606 // use the generated ssh credentials for authentication 607 priv, err := signers.GetSignerFromPrivateKeyWithPassphrase(creds.KeyPair.PrivateKey, creds.KeyPair.Passphrase) 608 if err != nil { 609 return nil, nil, multierr.Wrap(err, "could not read generated private key") 610 } 611 sshSigners = append(sshSigners, priv) 612 closer = append(closer, ssmConn) 613 case vault.CredentialType_aws_ec2_instance_connect: 614 log.Debug().Str("profile", conf.Options["profile"]).Str("region", conf.Options["region"]).Msg("using aws creds") 615 616 loadOpts := []func(*awsconf.LoadOptions) error{} 617 if conf.Options != nil && conf.Options["region"] != "" { 618 loadOpts = append(loadOpts, awsconf.WithRegion(conf.Options["region"])) 619 } 620 if conf.Options != nil && conf.Options["profile"] != "" { 621 loadOpts = append(loadOpts, awsconf.WithSharedConfigProfile(conf.Options["profile"])) 622 } 623 cfg, err := awsconf.LoadDefaultConfig(context.Background(), loadOpts...) 624 if err != nil { 625 return nil, nil, err 626 } 627 log.Debug().Msg("generating instance connect credentials") 628 eic := awsinstanceconnect.New(cfg) 629 host := conf.Host 630 if id, ok := conf.Options["instance"]; ok { 631 host = id 632 } 633 creds, err := eic.GenerateCredentials(host, credential.User) 634 if err != nil { 635 return nil, nil, err 636 } 637 638 priv, err := signers.GetSignerFromPrivateKeyWithPassphrase(creds.KeyPair.PrivateKey, creds.KeyPair.Passphrase) 639 if err != nil { 640 return nil, nil, multierr.Wrap(err, "could not read generated private key") 641 } 642 sshSigners = append(sshSigners, priv) 643 644 // NOTE: this creates a side-effect where the host is overwritten 645 conf.Host = creds.PublicIpAddress 646 default: 647 return nil, nil, errors.New("unsupported authentication mechanism for ssh: " + credential.Type.String()) 648 } 649 } 650 651 if len(sshSigners) > 0 { 652 auths = append(auths, ssh.PublicKeys(sshSigners...)) 653 } 654 655 return auths, closer, nil 656 } 657 658 func (c *SshConnection) verify() error { 659 var out *shared.Command 660 var err error 661 if c.Sudo != nil { 662 // Wrap sudo command, to see proper error messages. We set /dev/null to disable stdin 663 command := "sh -c '" + shared.BuildSudoCommand(c.Sudo, "echo 'hi'") + " < /dev/null'" 664 out, err = c.runRawCommand(command) 665 } else { 666 out, err = c.runRawCommand("echo 'hi'") 667 } 668 if err != nil { 669 return err 670 } 671 672 if out.ExitStatus == 0 { 673 return nil 674 } 675 676 stderr, _ := io.ReadAll(out.Stderr) 677 errMsg := string(stderr) 678 679 // sample messages are: 680 // sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper 681 // sudo: a password is required 682 switch { 683 case strings.Contains(errMsg, "not found"): 684 return errors.New("sudo command is missing on target") 685 case strings.Contains(errMsg, "a password is required"): 686 return errors.New("could not establish connection: sudo password is not supported yet, configure password-less sudo") 687 default: 688 return errors.New("could not establish connection: " + errMsg) 689 } 690 }