go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/_motor/providers/ssh/provider.go (about) 1 // Copyright (c) Mondoo, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package ssh 5 6 import ( 7 "fmt" 8 "io" 9 "net" 10 "os" 11 "strings" 12 13 "github.com/cockroachdb/errors" 14 rawsftp "github.com/pkg/sftp" 15 "github.com/rs/zerolog/log" 16 "github.com/spf13/afero" 17 "go.mondoo.com/cnquery/motor/providers" 18 os_provider "go.mondoo.com/cnquery/motor/providers/os" 19 "go.mondoo.com/cnquery/motor/providers/os/cmd" 20 "go.mondoo.com/cnquery/motor/providers/ssh/cat" 21 "go.mondoo.com/cnquery/motor/providers/ssh/scp" 22 "go.mondoo.com/cnquery/motor/providers/ssh/sftp" 23 "golang.org/x/crypto/ssh" 24 ) 25 26 var ( 27 _ providers.Instance = (*Provider)(nil) 28 _ providers.PlatformIdentifier = (*Provider)(nil) 29 _ os_provider.OperatingSystemProvider = (*Provider)(nil) 30 ) 31 32 func New(pCfg *providers.Config) (*Provider, error) { 33 host := pCfg.GetHost() 34 // ipv6 addresses w/o the surrounding [] will eventually error 35 // so check whether we have an ipv6 address by parsing it (the 36 // parsing will fail if the string DOES have the []s) and adding 37 // the []s 38 ip := net.ParseIP(host) 39 if ip != nil && ip.To4() == nil { 40 pCfg.Host = fmt.Sprintf("[%s]", host) 41 } 42 43 pCfg = ReadSSHConfig(pCfg) 44 45 // ensure all required configs are set 46 err := VerifyConfig(pCfg) 47 if err != nil { 48 return nil, err 49 } 50 51 activateScp := false 52 if os.Getenv("MONDOO_SSH_SCP") == "on" || pCfg.Options["ssh_scp"] == "on" { 53 activateScp = true 54 } 55 56 if pCfg.Insecure { 57 log.Debug().Msg("user allowed insecure ssh connection") 58 } 59 60 t := &Provider{ 61 ConnectionConfig: pCfg, 62 UseScpFilesystem: activateScp, 63 kind: pCfg.Kind, 64 runtime: pCfg.Runtime, 65 } 66 err = t.Connect() 67 if err != nil { 68 return nil, err 69 } 70 71 var s cmd.Wrapper 72 // check uid of user and disable sudo if uid is 0 73 if pCfg.Sudo != nil && pCfg.Sudo.Active { 74 // the id command may not be available, eg. if ssh is used with windows 75 out, _ := t.RunCommand("id -u") 76 stdout, _ := io.ReadAll(out.Stdout) 77 // just check for the explicit positive case, otherwise just activate sudo 78 // we check sudo in VerifyConnection 79 if string(stdout) != "0" { 80 // configure sudo 81 log.Debug().Msg("activated sudo for ssh connection") 82 s = cmd.NewSudo() 83 } 84 } 85 t.Sudo = s 86 87 // verify connection 88 vErr := t.VerifyConnection() 89 // NOTE: for now we do not enforce connection verification to ensure we cover edge-cases 90 // TODO: in following minor version bumps, we want to enforce this behavior to ensure proper scans 91 if vErr != nil { 92 log.Warn().Err(vErr).Send() 93 } 94 95 return t, nil 96 } 97 98 type Provider struct { 99 ConnectionConfig *providers.Config 100 SSHClient *ssh.Client 101 fs afero.Fs 102 UseScpFilesystem bool 103 HostKey ssh.PublicKey 104 Sudo cmd.Wrapper 105 kind providers.Kind 106 runtime string 107 serverVersion string 108 } 109 110 func (p *Provider) Connect() error { 111 cc := p.ConnectionConfig 112 113 // we always want to ensure we use the default port if nothing was specified 114 if cc.Port == 0 { 115 cc.Port = 22 116 } 117 118 // load known hosts and track the fingerprint of the ssh server for later identification 119 knownHostsCallback, err := KnownHostsCallback() 120 if err != nil { 121 return errors.Wrap(err, "could not read hostkey file") 122 } 123 124 var hostkey ssh.PublicKey 125 hostkeyCallback := func(hostname string, remote net.Addr, key ssh.PublicKey) error { 126 // store the hostkey for later identification 127 hostkey = key 128 129 // ignore hostkey check if the user provided an insecure flag 130 if cc.Insecure { 131 return nil 132 } 133 134 // knownhost.New returns a ssh.CertChecker which does not work with all ssh.HostKey types 135 // especially the newer edcsa keys (ssh.curve25519sha256) are not well supported. 136 // https://github.com/golang/crypto/blob/master/ssh/knownhosts/knownhosts.go#L417-L436 137 // creates the CertChecker which requires an instance of Certificate 138 // https://github.com/golang/crypto/blob/master/ssh/certs.go#L326-L348 139 // https://github.com/golang/crypto/blob/master/ssh/keys.go#L271-L283 140 // therefore it is best to skip the checking for now since it forces users to set the insecure flag otherwise 141 // TODO: implement custom host-key checking for normal public keys as well 142 _, ok := key.(*ssh.Certificate) 143 if !ok { 144 log.Debug().Msg("skip hostkey check the hostkey since the algo is not supported yet") 145 return nil 146 } 147 148 err := knownHostsCallback(hostname, remote, key) 149 if err != nil { 150 log.Debug().Err(err).Str("hostname", hostname).Str("ip", remote.String()).Msg("check known host") 151 } 152 return err 153 } 154 155 // establish connection 156 conn, _, err := establishClientConnection(cc, hostkeyCallback) 157 if err != nil { 158 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") 159 if strings.ContainsAny(cc.Host, "[]") { 160 log.Info().Str("host", cc.Host).Int32("port", cc.Port).Msg("ensure proper []s when combining IPv6 with port numbers") 161 } 162 return err 163 } 164 p.SSHClient = conn 165 p.HostKey = hostkey 166 p.serverVersion = string(conn.ServerVersion()) 167 log.Debug().Str("provider", "ssh").Str("host", cc.Host).Int32("port", cc.Port).Str("server", p.serverVersion).Msg("ssh session established") 168 return nil 169 } 170 171 func (p *Provider) VerifyConnection() error { 172 var out *os_provider.Command 173 var err error 174 175 if p.Sudo != nil { 176 // Wrap sudo command, to see proper error messages. We set /dev/null to disable stdin 177 command := "sh -c '" + p.Sudo.Build("echo 'hi'") + " < /dev/null'" 178 out, err = p.runRawCommand(command) 179 } else { 180 out, err = p.runRawCommand("echo 'hi'") 181 if err != nil { 182 return err 183 } 184 } 185 186 if out.ExitStatus == 0 { 187 return nil 188 } 189 190 stderr, _ := io.ReadAll(out.Stderr) 191 errMsg := string(stderr) 192 193 // sample messages are: 194 // sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper 195 // sudo: a password is required 196 switch { 197 case strings.Contains(errMsg, "not found"): 198 return errors.New("sudo command is missing on target") 199 case strings.Contains(errMsg, "a password is required"): 200 return errors.New("could not establish connection: sudo password is not supported yet, configure password-less sudo") 201 default: 202 return errors.New("could not establish connection: " + errMsg) 203 } 204 } 205 206 // Reconnect closes a possible current connection and re-establishes a new connection 207 func (p *Provider) Reconnect() error { 208 p.Close() 209 return p.Connect() 210 } 211 212 func (p *Provider) runRawCommand(command string) (*os_provider.Command, error) { 213 log.Debug().Str("command", command).Str("provider", "ssh").Msg("run command") 214 c := &Command{SSHProvider: p} 215 return c.Exec(command) 216 } 217 218 func (p *Provider) RunCommand(command string) (*os_provider.Command, error) { 219 if p.Sudo != nil { 220 command = p.Sudo.Build(command) 221 } 222 return p.runRawCommand(command) 223 } 224 225 func (p *Provider) FS() afero.Fs { 226 // if we cached an instance already, return it 227 if p.fs != nil { 228 return p.fs 229 } 230 231 // log the used ssh filesystem backend 232 defer func() { 233 log.Debug().Str("file-transfer", p.fs.Name()).Msg("initialized ssh filesystem") 234 }() 235 236 //// detect cisco network gear, they returns something like SSH-2.0-Cisco-1.25 237 //// NOTE: we need to understand why this happens 238 //if strings.Contains(strings.ToLower(t.serverVersion), "cisco") { 239 // log.Debug().Msg("detected cisco device, deactivate file system support") 240 // t.fs = &fsutil.NoFs{} 241 // return t.fs 242 //} 243 244 // if any privilege elevation is used, we have no other chance as to use command-based file transfer 245 if p.Sudo != nil { 246 p.fs = cat.New(p) 247 return p.fs 248 } 249 250 // we always try to use sftp first (if scp is not user-enforced) 251 // and we also fallback to scp if sftp does not work 252 if !p.UseScpFilesystem { 253 fs, err := sftp.New(p, p.SSHClient) 254 if err != nil { 255 log.Info().Msg("use scp instead of sftp") 256 // enable fallback 257 p.UseScpFilesystem = true 258 } else { 259 p.fs = fs 260 return p.fs 261 } 262 } 263 264 if p.UseScpFilesystem { 265 p.fs = scp.NewFs(p, p.SSHClient) 266 return p.fs 267 } 268 269 // always fallback to catfs, slow but it works 270 p.fs = cat.New(p) 271 return p.fs 272 } 273 274 func (p *Provider) FileInfo(path string) (os_provider.FileInfoDetails, error) { 275 fs := p.FS() 276 afs := &afero.Afero{Fs: fs} 277 stat, err := afs.Stat(path) 278 if err != nil { 279 return os_provider.FileInfoDetails{}, err 280 } 281 282 uid := int64(-1) 283 gid := int64(-1) 284 285 if p.Sudo != nil || p.UseScpFilesystem { 286 if stat, ok := stat.Sys().(*os_provider.FileInfo); ok { 287 uid = int64(stat.Uid) 288 gid = int64(stat.Gid) 289 } 290 } else { 291 if stat, ok := stat.Sys().(*rawsftp.FileStat); ok { 292 uid = int64(stat.UID) 293 gid = int64(stat.GID) 294 } 295 } 296 mode := stat.Mode() 297 298 return os_provider.FileInfoDetails{ 299 Mode: os_provider.FileModeDetails{mode}, 300 Size: stat.Size(), 301 Uid: uid, 302 Gid: gid, 303 }, nil 304 } 305 306 func (p *Provider) Close() { 307 if p.SSHClient != nil { 308 p.SSHClient.Close() 309 } 310 } 311 312 func (p *Provider) Capabilities() providers.Capabilities { 313 return providers.Capabilities{ 314 providers.Capability_RunCommand, 315 providers.Capability_File, 316 } 317 } 318 319 func (p *Provider) Kind() providers.Kind { 320 return p.kind 321 } 322 323 func (p *Provider) Runtime() string { 324 return p.runtime 325 } 326 327 func (p *Provider) PlatformIdDetectors() []providers.PlatformIdDetector { 328 return []providers.PlatformIdDetector{ 329 providers.TransportPlatformIdentifierDetector, 330 providers.HostnameDetector, 331 providers.CloudDetector, 332 } 333 } 334 335 func (p *Provider) Identifier() (string, error) { 336 return PlatformIdentifier(p.HostKey), nil 337 } 338 339 func PlatformIdentifier(publicKey ssh.PublicKey) string { 340 fingerprint := ssh.FingerprintSHA256(publicKey) 341 fingerprint = strings.Replace(fingerprint, ":", "-", 1) 342 identifier := "//platformid.api.mondoo.app/runtime/ssh/hostkey/" + fingerprint 343 return identifier 344 }