go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers/os/connection/local.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package connection
     5  
     6  import (
     7  	"bytes"
     8  	"os/exec"
     9  	"runtime"
    10  	"strings"
    11  	"syscall"
    12  	"time"
    13  
    14  	"github.com/rs/zerolog/log"
    15  	"github.com/spf13/afero"
    16  	"go.mondoo.com/cnquery/providers-sdk/v1/inventory"
    17  	"go.mondoo.com/cnquery/providers/os/connection/shared"
    18  	"go.mondoo.com/cnquery/providers/os/connection/ssh/cat"
    19  )
    20  
    21  const (
    22  	Local shared.ConnectionType = "local"
    23  )
    24  
    25  type LocalConnection struct {
    26  	shell   []string
    27  	fs      afero.Fs
    28  	Sudo    *inventory.Sudo
    29  	runtime string
    30  	id      uint32
    31  	asset   *inventory.Asset
    32  }
    33  
    34  func NewLocalConnection(id uint32, conf *inventory.Config, asset *inventory.Asset) *LocalConnection {
    35  	// expect unix shell by default
    36  	res := LocalConnection{
    37  		id:    id,
    38  		asset: asset,
    39  	}
    40  	if conf != nil {
    41  		res.Sudo = conf.Sudo
    42  	}
    43  
    44  	if runtime.GOOS == "windows" {
    45  		// It does not make any sense to use cmd as default shell
    46  		// shell = []string{"cmd", "/C"}
    47  		res.shell = []string{"powershell", "-c"}
    48  	} else {
    49  		res.shell = []string{"sh", "-c"}
    50  	}
    51  
    52  	return &res
    53  }
    54  
    55  func (p *LocalConnection) ID() uint32 {
    56  	return p.id
    57  }
    58  
    59  func (p *LocalConnection) Name() string {
    60  	return "local"
    61  }
    62  
    63  func (p *LocalConnection) Type() shared.ConnectionType {
    64  	return Local
    65  }
    66  
    67  func (p *LocalConnection) Asset() *inventory.Asset {
    68  	return p.asset
    69  }
    70  
    71  func (p *LocalConnection) Capabilities() shared.Capabilities {
    72  	return shared.Capability_File | shared.Capability_RunCommand
    73  }
    74  
    75  func (p *LocalConnection) RunCommand(command string) (*shared.Command, error) {
    76  	if p.Sudo != nil {
    77  		command = shared.BuildSudoCommand(p.Sudo, command)
    78  	}
    79  	log.Debug().Msgf("local> run command %s", command)
    80  	c := &CommandRunner{Shell: p.shell}
    81  	args := []string{}
    82  
    83  	res, err := c.Exec(command, args)
    84  	return res, err
    85  }
    86  
    87  func (p *LocalConnection) FileSystem() afero.Fs {
    88  	if p.fs != nil {
    89  		return p.fs
    90  	}
    91  
    92  	if p.Sudo != nil && p.Sudo.Active {
    93  		p.fs = cat.New(p)
    94  	} else {
    95  		p.fs = afero.NewOsFs()
    96  	}
    97  
    98  	return p.fs
    99  }
   100  
   101  func (p *LocalConnection) FileInfo(path string) (shared.FileInfoDetails, error) {
   102  	fs := p.FileSystem()
   103  	afs := &afero.Afero{Fs: fs}
   104  	stat, err := afs.Stat(path)
   105  	if err != nil {
   106  		return shared.FileInfoDetails{}, err
   107  	}
   108  
   109  	uid, gid := p.fileowner(stat)
   110  
   111  	mode := stat.Mode()
   112  	return shared.FileInfoDetails{
   113  		Mode: shared.FileModeDetails{mode},
   114  		Size: stat.Size(),
   115  		Uid:  uid,
   116  		Gid:  gid,
   117  	}, nil
   118  }
   119  
   120  func (p *LocalConnection) Close() {
   121  	// TODO: we need to close all commands and file handles
   122  }
   123  
   124  type CommandRunner struct {
   125  	shared.Command
   126  	cmdExecutor *exec.Cmd
   127  	Shell       []string
   128  }
   129  
   130  func (c *CommandRunner) Exec(usercmd string, args []string) (*shared.Command, error) {
   131  	c.Command.Stats.Start = time.Now()
   132  
   133  	var cmd string
   134  	cmdArgs := []string{}
   135  
   136  	if len(c.Shell) > 0 {
   137  		shellCommand, shellArgs := c.Shell[0], c.Shell[1:]
   138  		cmd = shellCommand
   139  		cmdArgs = append(cmdArgs, shellArgs...)
   140  		cmdArgs = append(cmdArgs, usercmd)
   141  	} else {
   142  		cmd = usercmd
   143  	}
   144  	cmdArgs = append(cmdArgs, args...)
   145  
   146  	// this only stores the user command, not the shell
   147  	c.Command.Command = usercmd + " " + strings.Join(args, " ")
   148  	c.cmdExecutor = exec.Command(cmd, cmdArgs...)
   149  
   150  	var stdoutBuffer bytes.Buffer
   151  	var stderrBuffer bytes.Buffer
   152  
   153  	// create buffered stream
   154  	c.Command.Stdout = &stdoutBuffer
   155  	c.Command.Stderr = &stderrBuffer
   156  
   157  	c.cmdExecutor.Stdout = c.Command.Stdout
   158  	c.cmdExecutor.Stderr = c.Command.Stderr
   159  
   160  	err := c.cmdExecutor.Run()
   161  	c.Command.Stats.Duration = time.Since(c.Command.Stats.Start)
   162  
   163  	// command completed successfully, great :-)
   164  	if err == nil {
   165  		return &c.Command, nil
   166  	}
   167  
   168  	// if the program failed, we do not return err but its exit code
   169  	if exiterr, ok := err.(*exec.ExitError); ok {
   170  		if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
   171  			c.Command.ExitStatus = status.ExitStatus()
   172  		}
   173  		return &c.Command, nil
   174  	}
   175  
   176  	// all other errors are real errors and not expected
   177  	return &c.Command, err
   178  }