diff --git a/.golangci.yml b/.golangci.yml index 8159ff476..81c84ca09 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,6 +15,7 @@ linters: - sqlclosecheck - unconvert - paralleltest + - forbidigo - sloglint - revive disable: @@ -34,6 +35,10 @@ linters-settings: ignore: github.com/go-kit/kit/log:Log gofmt: simplify: false + forbidigo: + forbid: + - p: ^exec\.Command.*$ + msg: use pkg/allowedcmd functions instead sloglint: kv-only: true context-only: true diff --git a/cmd/launcher/uninstall_darwin.go b/cmd/launcher/uninstall_darwin.go index 57567931c..4927188fd 100644 --- a/cmd/launcher/uninstall_darwin.go +++ b/cmd/launcher/uninstall_darwin.go @@ -7,9 +7,10 @@ import ( "context" "fmt" "os" - "os/exec" "strings" "time" + + "github.com/kolide/launcher/pkg/allowedcmd" ) func removeLauncher(ctx context.Context, identifier string) error { @@ -19,12 +20,15 @@ func removeLauncher(ctx context.Context, identifier string) error { } launchDaemonPList := fmt.Sprintf("/Library/LaunchDaemons/com.%s.launcher.plist", identifier) - launchCtlPath := "/bin/launchctl" launchCtlArgs := []string{"unload", launchDaemonPList} launchctlCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - cmd := exec.CommandContext(launchctlCtx, launchCtlPath, launchCtlArgs...) + cmd, err := allowedcmd.Launchctl(launchctlCtx, launchCtlArgs...) + if err != nil { + fmt.Printf("could not find launchctl: %s\n", err) + return err + } if out, err := cmd.Output(); err != nil { fmt.Printf("error occurred while unloading launcher daemon, launchctl output %s: err: %s\n", out, err) return err @@ -55,7 +59,11 @@ func removeLauncher(ctx context.Context, identifier string) error { pkgutiltCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - pkgUtilcmd := exec.CommandContext(pkgutiltCtx, "/usr/sbin/pkgutil", "--forget", fmt.Sprintf("com.%s.launcher", identifier)) + pkgUtilcmd, err := allowedcmd.Pkgutil(pkgutiltCtx, "--forget", fmt.Sprintf("com.%s.launcher", identifier)) + if err != nil { + fmt.Printf("could not find pkgutil: %s\n", err) + return err + } if out, err := pkgUtilcmd.Output(); err != nil { fmt.Printf("error occurred while forgetting package: output %s: err: %s\n", out, err) diff --git a/cmd/launcher/uninstall_linux.go b/cmd/launcher/uninstall_linux.go index 490ddade9..aad211565 100644 --- a/cmd/launcher/uninstall_linux.go +++ b/cmd/launcher/uninstall_linux.go @@ -5,10 +5,12 @@ package main import ( "context" + "errors" "fmt" "os" - "os/exec" "strings" + + "github.com/kolide/launcher/pkg/allowedcmd" ) func removeLauncher(ctx context.Context, identifier string) error { @@ -21,33 +23,27 @@ func removeLauncher(ctx context.Context, identifier string) error { packageName := fmt.Sprintf("launcher-%s", identifier) // Stop and disable launcher service - cmd := exec.CommandContext(ctx, "systemctl", []string{"disable", "--now", serviceName}...) + cmd, err := allowedcmd.Systemctl(ctx, []string{"disable", "--now", serviceName}...) + if err != nil { + fmt.Printf("could not find systemctl: %s\n", err) + return err + } if out, err := cmd.CombinedOutput(); err != nil { // Don't exit. Log and move on to the next uninstall command fmt.Printf("error occurred while stopping/disabling launcher service, systemctl output %s: err: %s\n", string(out), err) } - fileExists := func(f string) bool { - if _, err := os.Stat(f); err == nil { - return true - } - return false - } - // Tell the appropriate package manager to remove launcher - switch { - case fileExists("/usr/bin/dpkg"): - cmd = exec.CommandContext(ctx, "/usr/bin/dpkg", []string{"--purge", packageName}...) + if cmd, err := allowedcmd.Dpkg(ctx, []string{"--purge", packageName}...); err == nil { if out, err := cmd.CombinedOutput(); err != nil { fmt.Printf("error occurred while running dpkg --purge, output %s: err: %s\n", string(out), err) } - case fileExists("/bin/rpm"): - cmd = exec.CommandContext(ctx, "/bin/rpm", []string{"-e", packageName}...) + } else if cmd, err := allowedcmd.Rpm(ctx, []string{"-e", packageName}...); err == nil { if out, err := cmd.CombinedOutput(); err != nil { fmt.Printf("error occurred while running rpm -e, output %s: err: %s\n", string(out), err) } - default: - return fmt.Errorf("unsupported package manager") + } else { + return errors.New("unsupported package manager") } pathsToRemove := []string{ diff --git a/ee/consoleuser/consoleuser_darwin.go b/ee/consoleuser/consoleuser_darwin.go index 04639aab0..fe1f0ae51 100644 --- a/ee/consoleuser/consoleuser_darwin.go +++ b/ee/consoleuser/consoleuser_darwin.go @@ -8,9 +8,10 @@ import ( "bytes" "context" "fmt" - "os/exec" "strconv" "strings" + + "github.com/kolide/launcher/pkg/allowedcmd" ) // example scutil output @@ -90,7 +91,10 @@ const ( ) func CurrentUids(ctx context.Context) ([]string, error) { - cmd := exec.CommandContext(ctx, "scutil") + cmd, err := allowedcmd.Scutil(ctx) + if err != nil { + return nil, fmt.Errorf("creating scutil command: %w", err) + } cmd.Stdin = strings.NewReader("show State:/Users/ConsoleUser") output, err := cmd.CombinedOutput() diff --git a/ee/consoleuser/consoleuser_linux.go b/ee/consoleuser/consoleuser_linux.go index e62b20356..723bdfd5b 100644 --- a/ee/consoleuser/consoleuser_linux.go +++ b/ee/consoleuser/consoleuser_linux.go @@ -7,8 +7,9 @@ import ( "context" "encoding/json" "fmt" - "os/exec" "strings" + + "github.com/kolide/launcher/pkg/allowedcmd" ) type listSessionsResult []struct { @@ -18,7 +19,11 @@ type listSessionsResult []struct { } func CurrentUids(ctx context.Context) ([]string, error) { - output, err := exec.CommandContext(ctx, "loginctl", "list-sessions", "--no-legend", "--no-pager", "--output=json").Output() + cmd, err := allowedcmd.Loginctl(ctx, "list-sessions", "--no-legend", "--no-pager", "--output=json") + if err != nil { + return nil, fmt.Errorf("creating loginctl command: %w", err) + } + output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("loginctl list-sessions: %w", err) } @@ -36,13 +41,16 @@ func CurrentUids(ctx context.Context) ([]string, error) { continue } - output, err := exec.CommandContext(ctx, - "loginctl", + cmd, err := allowedcmd.Loginctl(ctx, "show-session", s.Session, "--property=Remote", "--property=Active", - ).Output() + ) + if err != nil { + return nil, fmt.Errorf("creating loginctl command: %w", err) + } + output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("loginctl show-session (for sessionId %s): %w", s.Session, err) } diff --git a/ee/desktop/runner/runner.go b/ee/desktop/runner/runner.go index 9c3c30e5d..cf7fa1c65 100644 --- a/ee/desktop/runner/runner.go +++ b/ee/desktop/runner/runner.go @@ -755,7 +755,7 @@ func (r *DesktopUsersProcessesRunner) menuTemplatePath() string { // desktopCommand invokes the launcher desktop executable with the appropriate env vars func (r *DesktopUsersProcessesRunner) desktopCommand(executablePath, uid, socketPath, menuPath string) (*exec.Cmd, error) { - cmd := exec.Command(executablePath, "desktop") + cmd := exec.Command(executablePath, "desktop") //nolint:forbidigo // We trust that the launcher executable path is correct, so we don't need to use allowedcmd cmd.Env = []string{ // When we set cmd.Env (as we're doing here/below), cmd will no longer include the default cmd.Environ() diff --git a/ee/desktop/runner/runner_linux.go b/ee/desktop/runner/runner_linux.go index 789e9d498..188d78ae7 100644 --- a/ee/desktop/runner/runner_linux.go +++ b/ee/desktop/runner/runner_linux.go @@ -16,6 +16,7 @@ import ( "syscall" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/traces" "github.com/shirou/gopsutil/v3/process" ) @@ -90,7 +91,16 @@ func (r *DesktopUsersProcessesRunner) userEnvVars(ctx context.Context, uid strin } // Get the user's session so we can get their display (needed for opening notification action URLs in browser) - sessionOutput, err := exec.CommandContext(ctx, "loginctl", "show-user", uid, "--value", "--property=Sessions").Output() + cmd, err := allowedcmd.Loginctl(ctx, "show-user", uid, "--value", "--property=Sessions") + if err != nil { + level.Debug(r.logger).Log( + "msg", "could not create loginctl command", + "uid", uid, + "err", err, + ) + return envVars + } + sessionOutput, err := cmd.Output() if err != nil { level.Debug(r.logger).Log( "msg", "could not get user session", @@ -108,7 +118,16 @@ func (r *DesktopUsersProcessesRunner) userEnvVars(ctx context.Context, uid strin sessionList := strings.Split(sessions, " ") for _, session := range sessionList { // Figure out what type of graphical session the user has -- x11, wayland? - typeOutput, err := exec.CommandContext(ctx, "loginctl", "show-session", session, "--value", "--property=Type").Output() + cmd, err := allowedcmd.Loginctl(ctx, "show-session", session, "--value", "--property=Type") + if err != nil { + level.Debug(r.logger).Log( + "msg", "could not create loginctl command to get session type", + "uid", uid, + "err", err, + ) + continue + } + typeOutput, err := cmd.Output() if err != nil { level.Debug(r.logger).Log( "msg", "could not get session type", @@ -147,7 +166,15 @@ func (r *DesktopUsersProcessesRunner) userEnvVars(ctx context.Context, uid strin func (r *DesktopUsersProcessesRunner) displayFromX11(ctx context.Context, session string) string { // We can read $DISPLAY from the session properties - xDisplayOutput, err := exec.CommandContext(ctx, "loginctl", "show-session", session, "--value", "--property=Display").Output() + cmd, err := allowedcmd.Loginctl(ctx, "show-session", session, "--value", "--property=Display") + if err != nil { + level.Debug(r.logger).Log( + "msg", "could not create command to get Display from user session", + "err", err, + ) + return defaultDisplay + } + xDisplayOutput, err := cmd.Output() if err != nil { level.Debug(r.logger).Log( "msg", "could not get Display from user session", diff --git a/ee/desktop/runner/runner_test.go b/ee/desktop/runner/runner_test.go index 2b934cb04..7ca7cb4d4 100644 --- a/ee/desktop/runner/runner_test.go +++ b/ee/desktop/runner/runner_test.go @@ -33,7 +33,7 @@ func TestDesktopUserProcessRunner_Execute(t *testing.T) { // CPU consumption go way up. // To get around the issue mentioned above, build the binary first and set its path as the executable path on the runner. - executablePath := filepath.Join(t.TempDir(), "desktop-test") + executablePath := filepath.Join(t.TempDir(), "desktop-test", "launcher") if runtime.GOOS == "windows" { executablePath = fmt.Sprintf("%s.exe", executablePath) @@ -44,7 +44,7 @@ func TestDesktopUserProcessRunner_Execute(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - cmd := exec.CommandContext(ctx, "go", "build", "-o", executablePath, "../../../cmd/launcher") + cmd := exec.CommandContext(ctx, "go", "build", "-o", executablePath, "../../../cmd/launcher") //nolint:forbidigo // Fine to use exec.CommandContext in test buildStartTime := time.Now() out, err := cmd.CombinedOutput() if err != nil { diff --git a/ee/desktop/user/menu/action_open_url_darwin.go b/ee/desktop/user/menu/action_open_url_darwin.go index 5ccb2c219..e5d9236ea 100644 --- a/ee/desktop/user/menu/action_open_url_darwin.go +++ b/ee/desktop/user/menu/action_open_url_darwin.go @@ -4,11 +4,19 @@ package menu import ( - "os/exec" + "context" + "fmt" + + "github.com/kolide/launcher/pkg/allowedcmd" ) // open opens the specified URL in the default browser of the user // See https://stackoverflow.com/a/39324149/1705598 func open(url string) error { - return exec.Command("/usr/bin/open", url).Start() + cmd, err := allowedcmd.Open(context.TODO(), url) + if err != nil { + return fmt.Errorf("creating command: %w", err) + } + + return cmd.Start() } diff --git a/ee/desktop/user/menu/action_open_url_linux.go b/ee/desktop/user/menu/action_open_url_linux.go index 813bf5ab3..338cf9ab8 100644 --- a/ee/desktop/user/menu/action_open_url_linux.go +++ b/ee/desktop/user/menu/action_open_url_linux.go @@ -4,11 +4,19 @@ package menu import ( - "os/exec" + "context" + "fmt" + + "github.com/kolide/launcher/pkg/allowedcmd" ) // open opens the specified URL in the default browser of the user // See https://stackoverflow.com/a/39324149/1705598 func open(url string) error { - return exec.Command("xdg-open", url).Start() + cmd, err := allowedcmd.XdgOpen(context.TODO(), url) + if err != nil { + return fmt.Errorf("creating command: %w", err) + } + + return cmd.Start() } diff --git a/ee/desktop/user/menu/action_open_url_windows.go b/ee/desktop/user/menu/action_open_url_windows.go index 0fb31cd6c..54b16c879 100644 --- a/ee/desktop/user/menu/action_open_url_windows.go +++ b/ee/desktop/user/menu/action_open_url_windows.go @@ -4,14 +4,21 @@ package menu import ( - "os/exec" + "context" + "fmt" "syscall" + + "github.com/kolide/launcher/pkg/allowedcmd" ) // open opens the specified URL in the default browser of the user // See https://stackoverflow.com/a/39324149/1705598 func open(url string) error { - cmd := exec.Command("cmd", "/C", "start", url) + cmd, err := allowedcmd.CommandPrompt(context.TODO(), "/C", "start", url) + if err != nil { + return fmt.Errorf("creating command: %w", err) + } + // https://stackoverflow.com/questions/42500570/how-to-hide-command-prompt-window-when-using-exec-in-golang // Otherwise the cmd window will appear briefly cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} diff --git a/ee/desktop/user/notify/notify_linux.go b/ee/desktop/user/notify/notify_linux.go index 946b3cf73..c39cda5fe 100644 --- a/ee/desktop/user/notify/notify_linux.go +++ b/ee/desktop/user/notify/notify_linux.go @@ -6,13 +6,13 @@ package notify import ( "context" "fmt" - "os/exec" "sync" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/godbus/dbus/v5" + "github.com/kolide/launcher/pkg/allowedcmd" ) type dbusNotifier struct { @@ -33,7 +33,7 @@ const ( // We default to xdg-open first because, if available, it appears to be better at picking // the correct default browser. -var browserLaunchers = []string{"xdg-open", "x-www-browser"} +var browserLaunchers = []allowedcmd.AllowedCommand{allowedcmd.XdgOpen, allowedcmd.XWwwBrowser} func NewDesktopNotifier(logger log.Logger, iconFilepath string) *dbusNotifier { conn, err := dbus.ConnectSessionBus() @@ -89,7 +89,11 @@ func (d *dbusNotifier) Listen() error { for _, browserLauncher := range browserLaunchers { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - cmd := exec.CommandContext(ctx, browserLauncher, actionUri) + cmd, err := browserLauncher(ctx, actionUri) + if err != nil { + level.Warn(d.logger).Log("msg", "couldn't create command to start process", "err", err, "browser_launcher", browserLauncher) + continue + } if err := cmd.Start(); err != nil { level.Error(d.logger).Log("msg", "couldn't start process", "err", err, "browser_launcher", browserLauncher) } else { @@ -166,12 +170,6 @@ func (d *dbusNotifier) sendNotificationViaDbus(n Notification) error { } func (d *dbusNotifier) sendNotificationViaNotifySend(n Notification) error { - notifySend, err := exec.LookPath("notify-send") - if err != nil { - level.Debug(d.logger).Log("msg", "notify-send not installed", "err", err) - return fmt.Errorf("notify-send not installed: %w", err) - } - // notify-send doesn't support actions, but URLs in notifications are clickable in at least // some desktop environments. if n.ActionUri != "" { @@ -183,7 +181,10 @@ func (d *dbusNotifier) sendNotificationViaNotifySend(n Notification) error { args = append(args, "-i", d.iconFilepath) } - cmd := exec.Command(notifySend, args...) + cmd, err := allowedcmd.NotifySend(context.TODO(), args...) + if err != nil { + return fmt.Errorf("creating command: %w", err) + } if out, err := cmd.CombinedOutput(); err != nil { level.Error(d.logger).Log("msg", "could not send notification via notify-send", "output", string(out), "err", err) return fmt.Errorf("could not send notification via notify-send: %s: %w", string(out), err) diff --git a/ee/ui/assets/generator/generator.go b/ee/ui/assets/generator/generator.go index 250898b3a..d4935598a 100644 --- a/ee/ui/assets/generator/generator.go +++ b/ee/ui/assets/generator/generator.go @@ -152,7 +152,7 @@ func generatePng(ctx context.Context, logger log.Logger, name string) error { } // Scaling these doesn't seem to be a win for space or resolution. So leave them as is - cmd := exec.CommandContext(ctx, "cp", input, output) + cmd := exec.CommandContext(ctx, "cp", input, output) //nolint:forbidigo // Fine to use exec.CommandContext since it's not in launcher proper if err := cmd.Run(); err != nil { return fmt.Errorf("copy: %w", err) } @@ -178,7 +178,7 @@ func generateIco(ctx context.Context, logger log.Logger, name string) error { // First, we need to generate all the sizes for _, size := range icoSizes { - cmd := exec.CommandContext( + cmd := exec.CommandContext( //nolint:forbidigo // Fine to use exec.CommandContext since it's not in launcher proper ctx, "convert", "-resize", fmt.Sprintf("%sx%s", size, size), @@ -192,7 +192,7 @@ func generateIco(ctx context.Context, logger log.Logger, name string) error { } // Now that we have the intermediary sizes, we can stich them into a single ico - cmd := exec.CommandContext(ctx, "convert", fmt.Sprintf("%s/%s-*.ico", tmpDir, name), output) + cmd := exec.CommandContext(ctx, "convert", fmt.Sprintf("%s/%s-*.ico", tmpDir, name), output) //nolint:forbidigo // Fine to use exec.CommandContext since it's not in launcher proper level.Debug(logger).Log("msg", "Consolodating ico with", "cmd", cmd.String()) if err := cmd.Run(); err != nil { diff --git a/pkg/allowedcmd/cmd.go b/pkg/allowedcmd/cmd.go new file mode 100644 index 000000000..eb054b761 --- /dev/null +++ b/pkg/allowedcmd/cmd.go @@ -0,0 +1,51 @@ +package allowedcmd + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" +) + +type AllowedCommand func(ctx context.Context, arg ...string) (*exec.Cmd, error) + +func newCmd(ctx context.Context, fullPathToCmd string, arg ...string) *exec.Cmd { + return exec.CommandContext(ctx, fullPathToCmd, arg...) //nolint:forbidigo // This is our approved usage of exec.CommandContext +} + +func validatedCommand(ctx context.Context, knownPath string, arg ...string) (*exec.Cmd, error) { + knownPath = filepath.Clean(knownPath) + + if _, err := os.Stat(knownPath); err == nil { + return newCmd(ctx, knownPath, arg...), nil + } + + // Not found at known location -- return error for darwin and windows. + // We expect to know the exact location for allowlisted commands on all + // OSes except for a few Linux distros. + if !allowSearchPath() { + return nil, fmt.Errorf("not found: %s", knownPath) + } + + cmdName := filepath.Base(knownPath) + if foundPath, err := exec.LookPath(cmdName); err == nil { + return newCmd(ctx, foundPath, arg...), nil + } + + return nil, fmt.Errorf("%s not found at %s and could not be located elsewhere", cmdName, knownPath) +} + +func allowSearchPath() bool { + if runtime.GOOS != "linux" { + return false + } + + // We only allow searching for binaries in PATH on NixOS + if _, err := os.Stat("/etc/NIXOS"); err == nil { + return true + } + + return false +} diff --git a/pkg/allowedcmd/cmd_darwin.go b/pkg/allowedcmd/cmd_darwin.go new file mode 100644 index 000000000..4cc898f50 --- /dev/null +++ b/pkg/allowedcmd/cmd_darwin.go @@ -0,0 +1,129 @@ +//go:build darwin +// +build darwin + +package allowedcmd + +import ( + "context" + "os/exec" +) + +func Airport(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport", arg...) +} + +func Bioutil(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/bioutil", arg...) +} + +func Bputil(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/bputil", arg...) +} + +func Diskutil(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/diskutil", arg...) +} + +func Echo(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/bin/echo", arg...) +} + +func Falconctl(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/Applications/Falcon.app/Contents/Resources/falconctl", arg...) +} + +func Fdesetup(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/fdesetup", arg...) +} + +func Firmwarepasswd(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/firmwarepasswd", arg...) +} + +func Ifconfig(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/sbin/ifconfig", arg...) +} + +func Ioreg(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/ioreg", arg...) +} + +func Launchctl(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/bin/launchctl", arg...) +} + +func Lsof(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/lsof", arg...) +} + +func Mdfind(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/mdfind", arg...) +} + +func Mdmclient(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/libexec/mdmclient", arg...) +} + +func Netstat(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/netstat", arg...) +} + +func Open(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/open", arg...) +} + +func Pkgutil(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/pkgutil", arg...) +} + +func Powermetrics(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/powermetrics", arg...) +} + +func Profiles(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/profiles", arg...) +} + +func Ps(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/bin/ps", arg...) +} + +func Pwpolicy(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/pwpolicy", arg...) +} + +func Remotectl(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/libexec/remotectl", arg...) +} + +func Repcli(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/Applications/VMware Carbon Black Cloud/repcli.bundle/Contents/MacOS/repcli", arg...) +} + +func Scutil(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/scutil", arg...) +} + +func Softwareupdate(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/softwareupdate", arg...) +} + +func SystemProfiler(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/system_profiler", arg...) +} + +func Tmutil(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/tmutil", arg...) +} + +func ZerotierCli(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/local/bin/zerotier-cli", arg...) +} + +func Zfs(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/zfs", arg...) +} + +func Zpool(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/zpool", arg...) +} diff --git a/pkg/allowedcmd/cmd_linux.go b/pkg/allowedcmd/cmd_linux.go new file mode 100644 index 000000000..4923644ed --- /dev/null +++ b/pkg/allowedcmd/cmd_linux.go @@ -0,0 +1,145 @@ +//go:build linux +// +build linux + +package allowedcmd + +import ( + "context" + "errors" + "os/exec" +) + +func Apt(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/apt", arg...) +} + +func Cryptsetup(ctx context.Context, arg ...string) (*exec.Cmd, error) { + for _, p := range []string{"/usr/sbin/cryptsetup", "/sbin/cryptsetup"} { + validatedCmd, err := validatedCommand(ctx, p, arg...) + if err != nil { + continue + } + + return validatedCmd, nil + } + + return nil, errors.New("cryptsetup not found") +} + +func Dnf(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/dnf", arg...) +} + +func Dpkg(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/dpkg", arg...) +} + +func Echo(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/echo", arg...) +} + +func Falconctl(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/opt/CrowdStrike/falconctl", arg...) +} + +func FalconKernelCheck(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/opt/CrowdStrike/falcon-kernel-check", arg...) +} + +func GnomeExtensions(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/gnome-extensions", arg...) +} + +func Gsettings(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/gsettings", arg...) +} + +func Ifconfig(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/ifconfig", arg...) +} + +func Ip(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/ip", arg...) +} + +func Loginctl(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/loginctl", arg...) +} + +func Lsblk(ctx context.Context, arg ...string) (*exec.Cmd, error) { + for _, p := range []string{"/bin/lsblk", "/usr/bin/lsblk"} { + validatedCmd, err := validatedCommand(ctx, p, arg...) + if err != nil { + continue + } + + return validatedCmd, nil + } + + return nil, errors.New("lsblk not found") +} + +func Lsof(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/lsof", arg...) +} + +func Nmcli(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/nmcli", arg...) +} + +func NotifySend(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/notify-send", arg...) +} + +func Pacman(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/pacman", arg...) +} + +func Ps(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/ps", arg...) +} + +func Repcli(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/opt/carbonblack/psc/bin/repcli", arg...) +} + +func Rpm(ctx context.Context, arg ...string) (*exec.Cmd, error) { + for _, p := range []string{"/bin/rpm", "/usr/bin/rpm"} { + validatedCmd, err := validatedCommand(ctx, p, arg...) + if err != nil { + continue + } + + return validatedCmd, nil + } + + return nil, errors.New("rpm not found") +} + +func Systemctl(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/systemctl", arg...) +} + +func XdgOpen(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/xdg-open", arg...) +} + +func Xrdb(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/xrdb", arg...) +} + +func XWwwBrowser(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/bin/x-www-browser", arg...) +} + +func ZerotierCli(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/local/bin/zerotier-cli", arg...) +} + +func Zfs(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/zfs", arg...) +} + +func Zpool(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, "/usr/sbin/zpool", arg...) +} diff --git a/pkg/allowedcmd/cmd_test.go b/pkg/allowedcmd/cmd_test.go new file mode 100644 index 000000000..9f071ba57 --- /dev/null +++ b/pkg/allowedcmd/cmd_test.go @@ -0,0 +1,58 @@ +package allowedcmd + +import ( + "context" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEcho(t *testing.T) { + t.Parallel() + + // echo is the only one available on all platforms and likely to be available in CI + cmd, err := Echo(context.TODO(), "hello") + require.NoError(t, err) + require.Contains(t, cmd.Path, "echo") + require.Contains(t, cmd.Args, "hello") +} + +func Test_newCmd(t *testing.T) { + t.Parallel() + + cmdPath := filepath.Join("some", "path", "to", "a", "command") + cmd := newCmd(context.TODO(), cmdPath) + require.Equal(t, cmdPath, cmd.Path) +} + +func Test_validatedCommand(t *testing.T) { + t.Parallel() + + var cmdPath string + switch runtime.GOOS { + case "darwin", "linux": + cmdPath = "/bin/bash" + case "windows": + cmdPath = `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe` + } + + cmd, err := validatedCommand(context.TODO(), cmdPath) + + require.NoError(t, err) + require.Equal(t, cmdPath, cmd.Path) +} + +func Test_validatedCommand_doesNotSearchPathOnNonNixOS(t *testing.T) { + t.Parallel() + + if runtime.GOOS != "linux" { + t.SkipNow() + } + + cmdPath := "/not/the/real/path/to/bash" + _, err := validatedCommand(context.TODO(), cmdPath) + + require.Error(t, err) +} diff --git a/pkg/allowedcmd/cmd_windows.go b/pkg/allowedcmd/cmd_windows.go new file mode 100644 index 000000000..5425a5926 --- /dev/null +++ b/pkg/allowedcmd/cmd_windows.go @@ -0,0 +1,57 @@ +//go:build windows +// +build windows + +package allowedcmd + +import ( + "context" + "os" + "os/exec" + "path/filepath" +) + +func CommandPrompt(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, filepath.Join(os.Getenv("WINDIR"), "System32", "cmd.exe"), arg...) +} + +func Dism(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, filepath.Join(os.Getenv("WINDIR"), "System32", "Dism.exe"), arg...) +} + +func Dsregcmd(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, filepath.Join(os.Getenv("WINDIR"), "System32", "dsregcmd.exe"), arg...) +} + +func Echo(ctx context.Context, arg ...string) (*exec.Cmd, error) { + // echo on Windows is only available as a command in cmd.exe + return newCmd(ctx, "echo", arg...), nil +} + +func Ipconfig(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, filepath.Join(os.Getenv("WINDIR"), "System32", "ipconfig.exe"), arg...) +} + +func Powercfg(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, filepath.Join(os.Getenv("WINDIR"), "System32", "powercfg.exe"), arg...) +} + +func Powershell(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, filepath.Join(os.Getenv("WINDIR"), "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), arg...) +} + +func Repcli(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, filepath.Join(os.Getenv("PROGRAMFILES"), "Confer", "repcli"), arg...) +} + +func Secedit(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, filepath.Join(os.Getenv("WINDIR"), "System32", "SecEdit.exe"), arg...) +} + +func Taskkill(ctx context.Context, arg ...string) (*exec.Cmd, error) { + return validatedCommand(ctx, filepath.Join(os.Getenv("WINDIR"), "System32", "taskkill.exe"), arg...) +} + +func ZerotierCli(ctx context.Context, arg ...string) (*exec.Cmd, error) { + // For windows, "-q" should be prepended before all other args + return validatedCommand(ctx, filepath.Join(os.Getenv("SYSTEMROOT"), "ProgramData", "ZeroTier", "One", "zerotier-one_x64.exe"), append([]string{"-q"}, arg...)...) +} diff --git a/pkg/autoupdate/findnew.go b/pkg/autoupdate/findnew.go index 6fbf65058..ada078edf 100644 --- a/pkg/autoupdate/findnew.go +++ b/pkg/autoupdate/findnew.go @@ -377,7 +377,7 @@ func CheckExecutable(ctx context.Context, potentialBinary string, args ...string ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() - cmd := exec.CommandContext(ctx, potentialBinary, args...) + cmd := exec.CommandContext(ctx, potentialBinary, args...) //nolint:forbidigo // We trust the autoupdate library to find the correct location so we don't need allowedcmd // Set env, this should prevent launcher for fork-bombing cmd.Env = append(cmd.Env, "LAUNCHER_SKIP_UPDATES=TRUE") diff --git a/pkg/debug/checkups/gnome-extensions.go b/pkg/debug/checkups/gnome_extensions_linux.go similarity index 94% rename from pkg/debug/checkups/gnome-extensions.go rename to pkg/debug/checkups/gnome_extensions_linux.go index 17b9a9d11..f79e2ed18 100644 --- a/pkg/debug/checkups/gnome-extensions.go +++ b/pkg/debug/checkups/gnome_extensions_linux.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package checkups import ( @@ -6,11 +9,11 @@ import ( "fmt" "io" "os" - "os/exec" "path/filepath" - "runtime" "strings" "time" + + "github.com/kolide/launcher/pkg/allowedcmd" ) type gnomeExtensions struct { @@ -27,10 +30,6 @@ const ( ) func (c *gnomeExtensions) Name() string { - if runtime.GOOS != "linux" { - return "" - } - return "Gnome Extensions" } @@ -94,7 +93,10 @@ func execGnomeExtension(ctx context.Context, extraWriter io.Writer, rundir strin // pkg/osquery/tables/gsettings/gsettings.go probably has appropriate prior art. // But do we really want the forloop? - cmd := exec.CommandContext(ctx, "/usr/bin/gnome-extensions", args...) + cmd, err := allowedcmd.GnomeExtensions(ctx, args...) + if err != nil { + return nil, fmt.Errorf("creating gnome-extensions command: %w", err) + } // gnome seems to do things through this env cmd.Env = append(cmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", rundir)) diff --git a/pkg/debug/checkups/gnome_extensions_other.go b/pkg/debug/checkups/gnome_extensions_other.go new file mode 100644 index 000000000..67ba77913 --- /dev/null +++ b/pkg/debug/checkups/gnome_extensions_other.go @@ -0,0 +1,36 @@ +//go:build !linux +// +build !linux + +package checkups + +import ( + "context" + "io" +) + +type gnomeExtensions struct { +} + +func (c *gnomeExtensions) Name() string { + return "" +} + +func (c *gnomeExtensions) ExtraFileName() string { + return "" +} + +func (c *gnomeExtensions) Run(_ context.Context, _ io.Writer) error { + return nil +} + +func (c *gnomeExtensions) Status() Status { + return Informational +} + +func (c *gnomeExtensions) Summary() string { + return "" +} + +func (c *gnomeExtensions) Data() any { + return nil +} diff --git a/pkg/debug/checkups/launchd.go b/pkg/debug/checkups/launchd_darwin.go similarity index 89% rename from pkg/debug/checkups/launchd.go rename to pkg/debug/checkups/launchd_darwin.go index 4cd6c1b08..e8bfa1255 100644 --- a/pkg/debug/checkups/launchd.go +++ b/pkg/debug/checkups/launchd_darwin.go @@ -1,3 +1,6 @@ +//go:build darwin +// +build darwin + package checkups import ( @@ -7,10 +10,10 @@ import ( "fmt" "io" "os" - "os/exec" "path/filepath" - "runtime" "strings" + + "github.com/kolide/launcher/pkg/allowedcmd" ) const ( @@ -24,10 +27,6 @@ type launchdCheckup struct { } func (c *launchdCheckup) Name() string { - if runtime.GOOS != "darwin" { - return "" - } - return "Launchd" } @@ -59,13 +58,18 @@ func (c *launchdCheckup) Run(ctx context.Context, extraWriter io.Writer) error { c.status = Erroring c.summary = fmt.Sprintf("unable to write extra information: %s", err) return nil - } // run launchctl to check status var printOut bytes.Buffer - cmd := exec.CommandContext(ctx, "/bin/launchctl", "print", launchdServiceName) + cmd, err := allowedcmd.Launchctl(ctx, "print", launchdServiceName) + if err != nil { + c.status = Erroring + c.summary = fmt.Sprintf("unable to create launchctl command: %s", err) + return nil + } + cmd.Stdout = &printOut cmd.Stderr = &printOut if err := cmd.Run(); err != nil { diff --git a/pkg/debug/checkups/launchd_other.go b/pkg/debug/checkups/launchd_other.go new file mode 100644 index 000000000..59af86cab --- /dev/null +++ b/pkg/debug/checkups/launchd_other.go @@ -0,0 +1,36 @@ +//go:build !darwin +// +build !darwin + +package checkups + +import ( + "context" + "io" +) + +type launchdCheckup struct { +} + +func (c *launchdCheckup) Name() string { + return "" +} + +func (c *launchdCheckup) Run(_ context.Context, _ io.Writer) error { + return nil +} + +func (c *launchdCheckup) ExtraFileName() string { + return "" +} + +func (c *launchdCheckup) Status() Status { + return Informational +} + +func (c *launchdCheckup) Summary() string { + return "" +} + +func (c *launchdCheckup) Data() any { + return nil +} diff --git a/pkg/debug/checkups/network.go b/pkg/debug/checkups/network.go index a0c546c1e..a2da238ef 100644 --- a/pkg/debug/checkups/network.go +++ b/pkg/debug/checkups/network.go @@ -6,9 +6,9 @@ import ( "fmt" "io" "net" - "os/exec" - "runtime" "time" + + "github.com/kolide/launcher/pkg/allowedcmd" ) type networkCheckup struct { @@ -47,15 +47,14 @@ func (n *networkCheckup) Run(ctx context.Context, extraWriter io.Writer) error { return fmt.Errorf("creating zip file: %w", err) } - for _, commandArr := range listCommands() { - if len(commandArr) < 1 { - // how did this happen - continue - } + for _, c := range listCommands() { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() - cmd := exec.CommandContext(ctx, commandArr[0], commandArr[1:]...) + cmd, err := c.cmd(ctx, c.args...) + if err != nil { + continue + } _ = runCmdMarkdownLogged(cmd, commandOutput) } @@ -82,44 +81,7 @@ func (n *networkCheckup) Data() any { return nil } -func listCommands() [][]string { - switch runtime.GOOS { - case "darwin": - return [][]string{ - {"ifconfig", "-a"}, - {"netstat", "-nr"}, - } - case "linux": - return [][]string{ - {"ifconfig", "-a"}, - {"ip", "-N", "-d", "-h", "-a", "address"}, - {"ip", "-N", "-d", "-h", "-a", "route"}, - } - case "windows": - return [][]string{ - {"ipconfig", "/all"}, - } - default: - return nil - } -} - -func listFiles() []string { - switch runtime.GOOS { - case "darwin": - return []string{ - "/etc/hosts", - "/etc/resolv.conf", - } - case "linux": - return []string{ - "/etc/nsswitch.conf", - "/etc/hosts", - "/etc/resolv.conf", - } - case "windows": - return []string{} - default: - return nil - } +type networkCommand struct { + cmd allowedcmd.AllowedCommand + args []string } diff --git a/pkg/debug/checkups/network_darwin.go b/pkg/debug/checkups/network_darwin.go new file mode 100644 index 000000000..a61ef6d6f --- /dev/null +++ b/pkg/debug/checkups/network_darwin.go @@ -0,0 +1,26 @@ +//go:build darwin +// +build darwin + +package checkups + +import "github.com/kolide/launcher/pkg/allowedcmd" + +func listCommands() []networkCommand { + return []networkCommand{ + { + cmd: allowedcmd.Ifconfig, + args: []string{"-a"}, + }, + { + cmd: allowedcmd.Netstat, + args: []string{"-nr"}, + }, + } +} + +func listFiles() []string { + return []string{ + "/etc/hosts", + "/etc/resolv.conf", + } +} diff --git a/pkg/debug/checkups/network_linux.go b/pkg/debug/checkups/network_linux.go new file mode 100644 index 000000000..fd7654467 --- /dev/null +++ b/pkg/debug/checkups/network_linux.go @@ -0,0 +1,31 @@ +//go:build linux +// +build linux + +package checkups + +import "github.com/kolide/launcher/pkg/allowedcmd" + +func listCommands() []networkCommand { + return []networkCommand{ + { + cmd: allowedcmd.Ifconfig, + args: []string{"-a"}, + }, + { + cmd: allowedcmd.Ip, + args: []string{"-N", "-d", "-h", "-a", "address"}, + }, + { + cmd: allowedcmd.Ip, + args: []string{"-N", "-d", "-h", "-a", "route"}, + }, + } +} + +func listFiles() []string { + return []string{ + "/etc/nsswitch.conf", + "/etc/hosts", + "/etc/resolv.conf", + } +} diff --git a/pkg/debug/checkups/network_windows.go b/pkg/debug/checkups/network_windows.go new file mode 100644 index 000000000..996c8b3e7 --- /dev/null +++ b/pkg/debug/checkups/network_windows.go @@ -0,0 +1,19 @@ +//go:build windows +// +build windows + +package checkups + +import "github.com/kolide/launcher/pkg/allowedcmd" + +func listCommands() []networkCommand { + return []networkCommand{ + { + cmd: allowedcmd.Ipconfig, + args: []string{"/all"}, + }, + } +} + +func listFiles() []string { + return []string{} +} diff --git a/pkg/debug/checkups/osquery.go b/pkg/debug/checkups/osquery.go index 20a49330b..981d843d6 100644 --- a/pkg/debug/checkups/osquery.go +++ b/pkg/debug/checkups/osquery.go @@ -48,7 +48,7 @@ func (o *osqueryCheckup) version(ctx context.Context) (string, error) { cmdCtx, cmdCancel := context.WithTimeout(ctx, 10*time.Second) defer cmdCancel() - cmd := exec.CommandContext(cmdCtx, osquerydPath, "--version") + cmd := exec.CommandContext(cmdCtx, osquerydPath, "--version") //nolint:forbidigo // We trust the autoupdate library to find the correct path hideWindow(cmd) startTime := time.Now().UnixMilli() out, err := cmd.CombinedOutput() @@ -72,7 +72,8 @@ func (o *osqueryCheckup) interactive(ctx context.Context) error { cmdCtx, cmdCancel := context.WithTimeout(ctx, 20*time.Second) defer cmdCancel() - cmd := exec.CommandContext(cmdCtx, launcherPath, "interactive") + // We trust the autoupdate library to find the correct path + cmd := exec.CommandContext(cmdCtx, launcherPath, "interactive") //nolint:forbidigo // We trust the autoupdate library to find the correct path hideWindow(cmd) cmd.Stdin = strings.NewReader(`select * from osquery_info;`) diff --git a/pkg/debug/checkups/power_windows.go b/pkg/debug/checkups/power_windows.go index c63ab1ce7..cc31608a2 100644 --- a/pkg/debug/checkups/power_windows.go +++ b/pkg/debug/checkups/power_windows.go @@ -8,9 +8,9 @@ import ( "fmt" "io" "os" - "os/exec" "github.com/kolide/launcher/pkg/agent" + "github.com/kolide/launcher/pkg/allowedcmd" ) type powerCheckup struct{} @@ -25,7 +25,10 @@ func (p *powerCheckup) Run(ctx context.Context, extraWriter io.Writer) error { defer os.Remove(tmpFilePath) // See: https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/powercfg-command-line-options#option_systempowerreport - powerCfgCmd := exec.CommandContext(ctx, "powercfg.exe", "/systempowerreport", "/output", tmpFilePath) + powerCfgCmd, err := allowedcmd.Powercfg(ctx, "/systempowerreport", "/output", tmpFilePath) + if err != nil { + return fmt.Errorf("creating powercfg command: %w", err) + } hideWindow(powerCfgCmd) if out, err := powerCfgCmd.CombinedOutput(); err != nil { return fmt.Errorf("running powercfg.exe: error %w, output %s", err, string(out)) diff --git a/pkg/debug/checkups/services_windows.go b/pkg/debug/checkups/services_windows.go index ea5b8c7fc..a971ce2de 100644 --- a/pkg/debug/checkups/services_windows.go +++ b/pkg/debug/checkups/services_windows.go @@ -9,8 +9,8 @@ import ( "encoding/json" "fmt" "io" - "os/exec" + "github.com/kolide/launcher/pkg/allowedcmd" "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/mgr" @@ -244,7 +244,10 @@ func gatherServiceManagerEventLogs(ctx context.Context, z *zip.Writer) error { "Format-Table", "-Wrap", "-AutoSize", // ensure output doesn't get truncated } - getEventLogCmd := exec.CommandContext(ctx, "powershell.exe", cmdletArgs...) + getEventLogCmd, err := allowedcmd.Powershell(ctx, cmdletArgs...) + if err != nil { + return fmt.Errorf("creating powershell command: %w", err) + } hideWindow(getEventLogCmd) getEventLogCmd.Stdout = eventLogOut // write directly to zip if err := getEventLogCmd.Run(); err != nil { diff --git a/pkg/execwrapper/exec_windows.go b/pkg/execwrapper/exec_windows.go index bbb273527..09f37637c 100644 --- a/pkg/execwrapper/exec_windows.go +++ b/pkg/execwrapper/exec_windows.go @@ -19,7 +19,7 @@ import ( func Exec(ctx context.Context, argv0 string, argv []string, envv []string) error { logger := log.With(ctxlog.FromContext(ctx), "caller", log.DefaultCaller) - cmd := exec.CommandContext(ctx, argv0, argv[1:]...) + cmd := exec.CommandContext(ctx, argv0, argv[1:]...) //nolint:forbidigo // execwrapper is used exclusively to exec launcher, and we trust the autoupdate library to find the correct path. cmd.Env = envv cmd.Stdin = os.Stdin diff --git a/pkg/log/log.go b/pkg/log/log.go index 7caec0ccd..b728f8f5e 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -2,16 +2,11 @@ package log import ( "bytes" - "context" "fmt" "os" - "os/exec" - "path/filepath" "regexp" - "runtime" "strconv" "strings" - "time" kitlog "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" @@ -160,90 +155,6 @@ func (l *OsqueryLogAdapter) logInfoAboutUnrecognizedProcessLockingPidfile(p []by level.Debug(l.logger).Log(append(processInfo, "msg", "detected non-osqueryd process using pidfile")...) } -// runAndLogPs runs ps filtering on the given PID, and logs the output. -func (l *OsqueryLogAdapter) runAndLogPs(pidStr string) { - if runtime.GOOS == "windows" { - return - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - cmd := exec.CommandContext(ctx, "ps", "-p", pidStr, "-o", "user,pid,ppid,pgid,stat,time,command") - out, err := cmd.CombinedOutput() - if err != nil { - level.Debug(l.logger).Log( - "msg", "error running ps on non-osqueryd process using pidfile", - "pid", pidStr, - "err", err, - ) - return - } - - level.Debug(l.logger).Log( - "msg", "ran ps on non-osqueryd process using pidfile", - "pid", pidStr, - "output", string(out), - ) -} - -// runAndLogLsofByPID runs lsof filtering on the given PID, and logs the output. -func (l *OsqueryLogAdapter) runAndLogLsofByPID(pidStr string) { - if runtime.GOOS == "windows" { - return - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - cmd := exec.CommandContext(ctx, "lsof", "-R", "-n", "-p", pidStr) - out, err := cmd.CombinedOutput() - if err != nil { - level.Debug(l.logger).Log( - "msg", "error running lsof on non-osqueryd process using pidfile", - "pid", pidStr, - "err", err, - ) - return - } - - level.Debug(l.logger).Log( - "msg", "ran lsof on non-osqueryd process using pidfile", - "pid", pidStr, - "output", string(out), - ) -} - -// runAndLogLsofOnPidfile runs lsof filtering by the osquery pidfile, and logs -// the output. -func (l *OsqueryLogAdapter) runAndLogLsofOnPidfile() { - if runtime.GOOS == "windows" { - return - } - - fullPidfile := filepath.Join(l.rootDirectory, "osquery.pid") - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - cmd := exec.CommandContext(ctx, "lsof", "-R", "-n", fullPidfile) - out, err := cmd.CombinedOutput() - if err != nil { - level.Debug(l.logger).Log( - "msg", "error running lsof on osqueryd pidfile", - "pidfile", fullPidfile, - "err", err, - ) - return - } - - level.Debug(l.logger).Log( - "msg", "ran lsof on osqueryd pidfile", - "pidfile", fullPidfile, - "output", string(out), - ) -} - // getStringStat is a small wrapper around gopsutil/process functions // to return the stat if available, or an error message if not, so // that either way the info will be captured in the log. diff --git a/pkg/log/log_posix.go b/pkg/log/log_posix.go new file mode 100644 index 000000000..412676523 --- /dev/null +++ b/pkg/log/log_posix.go @@ -0,0 +1,106 @@ +//go:build !windows +// +build !windows + +package log + +import ( + "context" + "path/filepath" + "time" + + "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" +) + +// runAndLogPs runs ps filtering on the given PID, and logs the output. +func (l *OsqueryLogAdapter) runAndLogPs(pidStr string) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cmd, err := allowedcmd.Ps(ctx, "-p", pidStr, "-o", "user,pid,ppid,pgid,stat,time,command") + if err != nil { + level.Debug(l.logger).Log( + "msg", "error creating command to run ps on osqueryd pidfile", + "err", err, + ) + return + } + out, err := cmd.CombinedOutput() + if err != nil { + level.Debug(l.logger).Log( + "msg", "error running ps on non-osqueryd process using pidfile", + "pid", pidStr, + "err", err, + ) + return + } + + level.Debug(l.logger).Log( + "msg", "ran ps on non-osqueryd process using pidfile", + "pid", pidStr, + "output", string(out), + ) +} + +// runAndLogLsofByPID runs lsof filtering on the given PID, and logs the output. +func (l *OsqueryLogAdapter) runAndLogLsofByPID(pidStr string) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cmd, err := allowedcmd.Lsof(ctx, "-R", "-n", "-p", pidStr) + if err != nil { + level.Debug(l.logger).Log( + "msg", "error creating command to run lsof on osqueryd pidfile", + "err", err, + ) + return + } + out, err := cmd.CombinedOutput() + if err != nil { + level.Debug(l.logger).Log( + "msg", "error running lsof on non-osqueryd process using pidfile", + "pid", pidStr, + "err", err, + ) + return + } + + level.Debug(l.logger).Log( + "msg", "ran lsof on non-osqueryd process using pidfile", + "pid", pidStr, + "output", string(out), + ) +} + +// runAndLogLsofOnPidfile runs lsof filtering by the osquery pidfile, and logs +// the output. +func (l *OsqueryLogAdapter) runAndLogLsofOnPidfile() { + fullPidfile := filepath.Join(l.rootDirectory, "osquery.pid") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cmd, err := allowedcmd.Lsof(ctx, "-R", "-n", fullPidfile) + if err != nil { + level.Debug(l.logger).Log( + "msg", "error creating command to run lsof on osqueryd pidfile", + "err", err, + ) + return + } + out, err := cmd.CombinedOutput() + if err != nil { + level.Debug(l.logger).Log( + "msg", "error running lsof on osqueryd pidfile", + "pidfile", fullPidfile, + "err", err, + ) + return + } + + level.Debug(l.logger).Log( + "msg", "ran lsof on osqueryd pidfile", + "pidfile", fullPidfile, + "output", string(out), + ) +} diff --git a/pkg/log/log_windows.go b/pkg/log/log_windows.go new file mode 100644 index 000000000..7d0dee265 --- /dev/null +++ b/pkg/log/log_windows.go @@ -0,0 +1,16 @@ +//go:build windows +// +build windows + +package log + +func (l *OsqueryLogAdapter) runAndLogPs(_ string) { + return +} + +func (l *OsqueryLogAdapter) runAndLogLsofByPID(_ string) { + return +} + +func (l *OsqueryLogAdapter) runAndLogLsofOnPidfile() { + return +} diff --git a/pkg/make/builder.go b/pkg/make/builder.go index c71adf073..2dfd1669d 100644 --- a/pkg/make/builder.go +++ b/pkg/make/builder.go @@ -1,11 +1,12 @@ -/* Package make provides some simple functions to handle build and go +/* + Package make provides some simple functions to handle build and go + dependencies. We used to do this with gnumake rules, but as we added windows compatibility, we found make too limiting. Moving this into go allows us to write cleaner cross-platform code. */ - package make import ( @@ -128,7 +129,7 @@ func New(opts ...Option) *Builder { goPath: "go", goVer: strings.TrimPrefix(runtime.Version(), "go"), - execCC: exec.CommandContext, + execCC: exec.CommandContext, //nolint:forbidigo // Fine to use exec.CommandContext outside of launcher proper } for _, opt := range opts { diff --git a/pkg/make/builder_test.go b/pkg/make/builder_test.go index 987732243..e21b6bc13 100644 --- a/pkg/make/builder_test.go +++ b/pkg/make/builder_test.go @@ -20,7 +20,7 @@ import ( func helperCommandContext(ctx context.Context, command string, args ...string) (cmd *exec.Cmd) { cs := []string{"-test.run=TestHelperProcess", "--", command} cs = append(cs, args...) - cmd = exec.CommandContext(ctx, os.Args[0], cs...) + cmd = exec.CommandContext(ctx, os.Args[0], cs...) //nolint:forbidigo // Fine to use exec.CommandContext in tests cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} // Do we have an ENV key? (type assert) diff --git a/pkg/osquery/runsimple/osqueryrunner.go b/pkg/osquery/runsimple/osqueryrunner.go index e409d85f9..783a0cdf5 100644 --- a/pkg/osquery/runsimple/osqueryrunner.go +++ b/pkg/osquery/runsimple/osqueryrunner.go @@ -88,7 +88,7 @@ func (p osqueryProcess) RunSql(ctx context.Context, sql []byte) error { p.stdin = bytes.NewReader(sql) - cmd := exec.CommandContext(ctx, p.osquerydPath, args...) + cmd := exec.CommandContext(ctx, p.osquerydPath, args...) //nolint:forbidigo // We trust the autoupdate library to find the correct path // It's okay for these to be nil, so we can just set them without checking. cmd.Stdin = p.stdin @@ -103,7 +103,7 @@ func (p osqueryProcess) RunVersion(ctx context.Context) error { "--version", } - cmd := exec.CommandContext(ctx, p.osquerydPath, args...) + cmd := exec.CommandContext(ctx, p.osquerydPath, args...) //nolint:forbidigo // We trust the autoupdate library to find the correct path // It's okay for these to be nil, so we can just set them without checking. cmd.Stdin = p.stdin diff --git a/pkg/osquery/runtime/osqueryinstance.go b/pkg/osquery/runtime/osqueryinstance.go index b7ebb6f5b..274cbae7b 100644 --- a/pkg/osquery/runtime/osqueryinstance.go +++ b/pkg/osquery/runtime/osqueryinstance.go @@ -480,7 +480,7 @@ func (opts *osqueryOptions) createOsquerydCommand(osquerydBinary string, paths * if !opts.enableWatchdog { args = append(args, "--disable_watchdog") } - cmd := exec.Command( + cmd := exec.Command( //nolint:forbidigo // We trust the autoupdate library to find the correct path osquerydBinary, args..., ) diff --git a/pkg/osquery/runtime/runtime_helpers_windows.go b/pkg/osquery/runtime/runtime_helpers_windows.go index 4a4ffb18e..9c82c52e7 100644 --- a/pkg/osquery/runtime/runtime_helpers_windows.go +++ b/pkg/osquery/runtime/runtime_helpers_windows.go @@ -4,11 +4,13 @@ package runtime import ( + "context" "fmt" "os/exec" "syscall" "github.com/kolide/kit/ulid" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/pkg/errors" ) @@ -21,7 +23,11 @@ func setpgid() *syscall.SysProcAttr { func killProcessGroup(cmd *exec.Cmd) error { // some discussion here https://github.com/golang/dep/pull/857 // TODO: should we check err? - exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprint(cmd.Process.Pid)).Run() + cmd, err := allowedcmd.Taskkill(context.TODO(), "/F", "/T", "/PID", fmt.Sprint(cmd.Process.Pid)) + if err != nil { + return fmt.Errorf("creating command: %w", err) + } + cmd.Run() return nil } diff --git a/pkg/osquery/table/mdfind_darwin.go b/pkg/osquery/table/mdfind_darwin.go index bd8811b7d..b71417a6c 100644 --- a/pkg/osquery/table/mdfind_darwin.go +++ b/pkg/osquery/table/mdfind_darwin.go @@ -7,18 +7,22 @@ import ( "bufio" "bytes" "context" + "fmt" "io" - "os/exec" "time" + + "github.com/kolide/launcher/pkg/allowedcmd" ) func mdfind(args ...string) ([]string, error) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() - path := "/usr/bin/mdfind" - - out, err := exec.CommandContext(ctx, path, args...).Output() + cmd, err := allowedcmd.Mdfind(ctx, args...) + if err != nil { + return nil, fmt.Errorf("creating mdfind command: %w", err) + } + out, err := cmd.Output() if err != nil { return nil, err } diff --git a/pkg/osquery/table/mdm.go b/pkg/osquery/table/mdm.go index d5dec77f1..65a8920d7 100644 --- a/pkg/osquery/table/mdm.go +++ b/pkg/osquery/table/mdm.go @@ -1,15 +1,18 @@ +//go:build darwin +// +build darwin + package table import ( "bytes" "context" "fmt" - "os/exec" "strconv" "time" "github.com/go-kit/kit/log" "github.com/groob/plist" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/osquery/osquery-go/plugin/table" ) @@ -89,7 +92,10 @@ func getMDMProfile(ctx context.Context) (*profilesOutput, error) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - cmd := exec.CommandContext(ctx, "/usr/bin/profiles", "-L", "-o", "stdout-xml") + cmd, err := allowedcmd.Profiles(ctx, "-L", "-o", "stdout-xml") + if err != nil { + return nil, fmt.Errorf("creating profiles command: %w", err) + } out, err := cmd.Output() if err != nil { return nil, fmt.Errorf("calling /usr/bin/profiles to get MDM profile payload: %w", err) @@ -132,7 +138,10 @@ func getMDMProfileStatus(ctx context.Context) (profileStatus, error) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - cmd := exec.CommandContext(ctx, "/usr/bin/profiles", "status", "-type", "enrollment") + cmd, err := allowedcmd.Profiles(ctx, "status", "-type", "enrollment") + if err != nil { + return profileStatus{}, fmt.Errorf("creating profiles command: %w", err) + } out, err := cmd.Output() if err != nil { return profileStatus{}, fmt.Errorf("calling /usr/bin/profiles to get MDM profile status: %w", err) diff --git a/pkg/osquery/table/platform_tables_darwin.go b/pkg/osquery/table/platform_tables_darwin.go index fd618527e..d6522213b 100644 --- a/pkg/osquery/table/platform_tables_darwin.go +++ b/pkg/osquery/table/platform_tables_darwin.go @@ -7,6 +7,7 @@ import ( "github.com/go-kit/kit/log" "github.com/knightsc/system_policy/osquery/table/kextpolicy" "github.com/knightsc/system_policy/osquery/table/legacyexec" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/osquery/tables/airport" appicons "github.com/kolide/launcher/pkg/osquery/tables/app-icons" "github.com/kolide/launcher/pkg/osquery/tables/apple_silicon_security_policy" @@ -24,6 +25,7 @@ import ( "github.com/kolide/launcher/pkg/osquery/tables/profiles" "github.com/kolide/launcher/pkg/osquery/tables/pwpolicy" "github.com/kolide/launcher/pkg/osquery/tables/systemprofiler" + "github.com/kolide/launcher/pkg/osquery/tables/zfs" _ "github.com/mattn/go-sqlite3" osquery "github.com/osquery/osquery-go" "github.com/osquery/osquery-go/plugin/table" @@ -99,25 +101,27 @@ func platformTables(logger log.Logger, currentOsquerydBinaryPath string) []osque apple_silicon_security_policy.TablePlugin(logger), legacyexec.TablePlugin(), dataflattentable.TablePluginExec(logger, - "kolide_diskutil_list", dataflattentable.PlistType, []string{"/usr/sbin/diskutil", "list", "-plist"}), + "kolide_diskutil_list", dataflattentable.PlistType, allowedcmd.Diskutil, []string{"list", "-plist"}), dataflattentable.TablePluginExec(logger, - "kolide_falconctl_stats", dataflattentable.PlistType, []string{"/Applications/Falcon.app/Contents/Resources/falconctl", "stats", "-p"}), + "kolide_falconctl_stats", dataflattentable.PlistType, allowedcmd.Falconctl, []string{"stats", "-p"}), dataflattentable.TablePluginExec(logger, - "kolide_apfs_list", dataflattentable.PlistType, []string{"/usr/sbin/diskutil", "apfs", "list", "-plist"}), + "kolide_apfs_list", dataflattentable.PlistType, allowedcmd.Diskutil, []string{"apfs", "list", "-plist"}), dataflattentable.TablePluginExec(logger, - "kolide_apfs_users", dataflattentable.PlistType, []string{"/usr/sbin/diskutil", "apfs", "listUsers", "/", "-plist"}), + "kolide_apfs_users", dataflattentable.PlistType, allowedcmd.Diskutil, []string{"apfs", "listUsers", "/", "-plist"}), dataflattentable.TablePluginExec(logger, - "kolide_tmutil_destinationinfo", dataflattentable.PlistType, []string{"/usr/bin/tmutil", "destinationinfo", "-X"}), + "kolide_tmutil_destinationinfo", dataflattentable.PlistType, allowedcmd.Tmutil, []string{"destinationinfo", "-X"}), dataflattentable.TablePluginExec(logger, - "kolide_powermetrics", dataflattentable.PlistType, []string{"/usr/bin/powermetrics", "-n", "1", "-f", "plist"}), + "kolide_powermetrics", dataflattentable.PlistType, allowedcmd.Powermetrics, []string{"-n", "1", "-f", "plist"}), screenlockTable, pwpolicy.TablePlugin(logger), systemprofiler.TablePlugin(logger), munki.ManagedInstalls(logger), munki.MunkiReport(logger), - dataflattentable.NewExecAndParseTable(logger, "kolide_remotectl", remotectl.Parser, []string{`/usr/libexec/remotectl`, `dumpstate`}), - dataflattentable.NewExecAndParseTable(logger, "kolide_softwareupdate", softwareupdate.Parser, []string{`/usr/sbin/softwareupdate`, `--list`, `--no-scan`}, dataflattentable.WithIncludeStderr()), - dataflattentable.NewExecAndParseTable(logger, "kolide_softwareupdate_scan", softwareupdate.Parser, []string{`/usr/sbin/softwareupdate`, `--list`}, dataflattentable.WithIncludeStderr()), - dataflattentable.NewExecAndParseTable(logger, "kolide_carbonblack_repcli_status", repcli.Parser, []string{"/Applications/VMware Carbon Black Cloud/repcli.bundle/Contents/MacOS/repcli", "status"}, dataflattentable.WithIncludeStderr()), + dataflattentable.NewExecAndParseTable(logger, "kolide_remotectl", remotectl.Parser, allowedcmd.Remotectl, []string{`dumpstate`}), + dataflattentable.NewExecAndParseTable(logger, "kolide_softwareupdate", softwareupdate.Parser, allowedcmd.Softwareupdate, []string{`--list`, `--no-scan`}, dataflattentable.WithIncludeStderr()), + dataflattentable.NewExecAndParseTable(logger, "kolide_softwareupdate_scan", softwareupdate.Parser, allowedcmd.Softwareupdate, []string{`--list`}, dataflattentable.WithIncludeStderr()), + dataflattentable.NewExecAndParseTable(logger, "kolide_carbonblack_repcli_status", repcli.Parser, allowedcmd.Repcli, []string{"status"}, dataflattentable.WithIncludeStderr()), + zfs.ZfsPropertiesPlugin(logger), + zfs.ZpoolPropertiesPlugin(logger), } } diff --git a/pkg/osquery/table/platform_tables_linux.go b/pkg/osquery/table/platform_tables_linux.go index d6572cb0d..a55f5a1cb 100644 --- a/pkg/osquery/table/platform_tables_linux.go +++ b/pkg/osquery/table/platform_tables_linux.go @@ -5,6 +5,7 @@ package table import ( "github.com/go-kit/kit/log" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/osquery/tables/crowdstrike/falcon_kernel_check" "github.com/kolide/launcher/pkg/osquery/tables/crowdstrike/falconctl" "github.com/kolide/launcher/pkg/osquery/tables/cryptsetup" @@ -12,9 +13,9 @@ import ( "github.com/kolide/launcher/pkg/osquery/tables/execparsers/apt" "github.com/kolide/launcher/pkg/osquery/tables/execparsers/dnf" "github.com/kolide/launcher/pkg/osquery/tables/execparsers/dpkg" - "github.com/kolide/launcher/pkg/osquery/tables/execparsers/pacman/group" - "github.com/kolide/launcher/pkg/osquery/tables/execparsers/pacman/info" - "github.com/kolide/launcher/pkg/osquery/tables/execparsers/pacman/upgradeable" + pacman_group "github.com/kolide/launcher/pkg/osquery/tables/execparsers/pacman/group" + pacman_info "github.com/kolide/launcher/pkg/osquery/tables/execparsers/pacman/info" + pacman_upgradeable "github.com/kolide/launcher/pkg/osquery/tables/execparsers/pacman/upgradeable" "github.com/kolide/launcher/pkg/osquery/tables/execparsers/repcli" "github.com/kolide/launcher/pkg/osquery/tables/execparsers/rpm" "github.com/kolide/launcher/pkg/osquery/tables/execparsers/simple_array" @@ -23,6 +24,7 @@ import ( "github.com/kolide/launcher/pkg/osquery/tables/secureboot" "github.com/kolide/launcher/pkg/osquery/tables/xfconf" "github.com/kolide/launcher/pkg/osquery/tables/xrdb" + "github.com/kolide/launcher/pkg/osquery/tables/zfs" osquery "github.com/osquery/osquery-go" ) @@ -40,20 +42,22 @@ func platformTables(logger log.Logger, currentOsquerydBinaryPath string) []osque dataflattentable.TablePluginExec(logger, "kolide_nmcli_wifi", dataflattentable.KeyValueType, - []string{"/usr/bin/nmcli", "--mode=multiline", "--fields=all", "device", "wifi", "list"}, + allowedcmd.Nmcli, + []string{"--mode=multiline", "--fields=all", "device", "wifi", "list"}, dataflattentable.WithKVSeparator(":")), dataflattentable.TablePluginExec(logger, "kolide_lsblk", dataflattentable.JsonType, - []string{"lsblk", "-J"}, - dataflattentable.WithBinDirs("/usr/bin", "/bin"), + allowedcmd.Lsblk, []string{"-J"}, ), - dataflattentable.NewExecAndParseTable(logger, "kolide_falconctl_systags", simple_array.New("systags"), []string{"/opt/CrowdStrike/falconctl", "-g", "--systags"}), - dataflattentable.NewExecAndParseTable(logger, "kolide_apt_upgradeable", apt.Parser, []string{"/usr/bin/apt", "list", "--upgradeable"}, dataflattentable.WithIncludeStderr()), - dataflattentable.NewExecAndParseTable(logger, "kolide_dnf_upgradeable", dnf.Parser, []string{"/usr/bin/dnf", "check-update"}, dataflattentable.WithIncludeStderr()), - dataflattentable.NewExecAndParseTable(logger, "kolide_dpkg_version_info", dpkg.Parser, []string{"/usr/bin/dpkg", "-p"}, dataflattentable.WithIncludeStderr()), - dataflattentable.NewExecAndParseTable(logger, "kolide_pacman_group", pacman_group.Parser, []string{"/usr/bin/pacman", "-Qg"}, dataflattentable.WithIncludeStderr()), - dataflattentable.NewExecAndParseTable(logger, "kolide_pacman_version_info", pacman_info.Parser, []string{"/usr/bin/pacman", "-Qi"}, dataflattentable.WithIncludeStderr()), - dataflattentable.NewExecAndParseTable(logger, "kolide_pacman_upgradeable", pacman_upgradeable.Parser, []string{"/usr/bin/pacman", "-Qu"}, dataflattentable.WithIncludeStderr()), - dataflattentable.NewExecAndParseTable(logger, "kolide_rpm_version_info", rpm.Parser, []string{"/usr/bin/rpm", "-qai"}, dataflattentable.WithIncludeStderr()), - dataflattentable.NewExecAndParseTable(logger, "kolide_carbonblack_repcli_status", repcli.Parser, []string{"/opt/carbonblack/psc/bin/repcli", "status"}, dataflattentable.WithIncludeStderr()), + dataflattentable.NewExecAndParseTable(logger, "kolide_falconctl_systags", simple_array.New("systags"), allowedcmd.Falconctl, []string{"-g", "--systags"}), + dataflattentable.NewExecAndParseTable(logger, "kolide_apt_upgradeable", apt.Parser, allowedcmd.Apt, []string{"list", "--upgradeable"}, dataflattentable.WithIncludeStderr()), + dataflattentable.NewExecAndParseTable(logger, "kolide_dnf_upgradeable", dnf.Parser, allowedcmd.Dnf, []string{"check-update"}, dataflattentable.WithIncludeStderr()), + dataflattentable.NewExecAndParseTable(logger, "kolide_dpkg_version_info", dpkg.Parser, allowedcmd.Dpkg, []string{"-p"}, dataflattentable.WithIncludeStderr()), + dataflattentable.NewExecAndParseTable(logger, "kolide_pacman_group", pacman_group.Parser, allowedcmd.Pacman, []string{"-Qg"}, dataflattentable.WithIncludeStderr()), + dataflattentable.NewExecAndParseTable(logger, "kolide_pacman_version_info", pacman_info.Parser, allowedcmd.Pacman, []string{"-Qi"}, dataflattentable.WithIncludeStderr()), + dataflattentable.NewExecAndParseTable(logger, "kolide_pacman_upgradeable", pacman_upgradeable.Parser, allowedcmd.Pacman, []string{"-Qu"}, dataflattentable.WithIncludeStderr()), + dataflattentable.NewExecAndParseTable(logger, "kolide_rpm_version_info", rpm.Parser, allowedcmd.Rpm, []string{"-qai"}, dataflattentable.WithIncludeStderr()), + dataflattentable.NewExecAndParseTable(logger, "kolide_carbonblack_repcli_status", repcli.Parser, allowedcmd.Repcli, []string{"status"}, dataflattentable.WithIncludeStderr()), + zfs.ZfsPropertiesPlugin(logger), + zfs.ZpoolPropertiesPlugin(logger), } } diff --git a/pkg/osquery/table/platform_tables_windows.go b/pkg/osquery/table/platform_tables_windows.go index 4e241366f..4d2289600 100644 --- a/pkg/osquery/table/platform_tables_windows.go +++ b/pkg/osquery/table/platform_tables_windows.go @@ -4,6 +4,7 @@ package table import ( + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/kolide/launcher/pkg/osquery/tables/dsim_default_associations" "github.com/kolide/launcher/pkg/osquery/tables/execparsers/dsregcmd" @@ -25,6 +26,6 @@ func platformTables(logger log.Logger, currentOsquerydBinaryPath string) []osque windowsupdatetable.TablePlugin(windowsupdatetable.UpdatesTable, logger), windowsupdatetable.TablePlugin(windowsupdatetable.HistoryTable, logger), wmitable.TablePlugin(logger), - dataflattentable.NewExecAndParseTable(logger, "kolide_dsregcmd", dsregcmd.Parser, []string{`/Windows/System32/dsregcmd.exe`, `/status`}), + dataflattentable.NewExecAndParseTable(logger, "kolide_dsregcmd", dsregcmd.Parser, allowedcmd.Dsregcmd, []string{`/status`}), } } diff --git a/pkg/osquery/table/table.go b/pkg/osquery/table/table.go index f42930708..2afc9da33 100644 --- a/pkg/osquery/table/table.go +++ b/pkg/osquery/table/table.go @@ -2,6 +2,7 @@ package table import ( "github.com/kolide/launcher/pkg/agent/types" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/osquery/tables/cryptoinfotable" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/kolide/launcher/pkg/osquery/tables/desktopprocs" @@ -11,7 +12,6 @@ import ( "github.com/kolide/launcher/pkg/osquery/tables/osquery_instance_history" "github.com/kolide/launcher/pkg/osquery/tables/tdebug" "github.com/kolide/launcher/pkg/osquery/tables/tufinfo" - "github.com/kolide/launcher/pkg/osquery/tables/zfs" "github.com/go-kit/kit/log" osquery "github.com/osquery/osquery-go" @@ -48,14 +48,12 @@ func PlatformTables(logger log.Logger, currentOsquerydBinaryPath string) []osque dev_table_tooling.TablePlugin(logger), firefox_preferences.TablePlugin(logger), dataflattentable.TablePluginExec(logger, - "kolide_zerotier_info", dataflattentable.JsonType, zerotierCli("info")), + "kolide_zerotier_info", dataflattentable.JsonType, allowedcmd.ZerotierCli, []string{"info"}), dataflattentable.TablePluginExec(logger, - "kolide_zerotier_networks", dataflattentable.JsonType, zerotierCli("listnetworks")), + "kolide_zerotier_networks", dataflattentable.JsonType, allowedcmd.ZerotierCli, []string{"listnetworks"}), dataflattentable.TablePluginExec(logger, - "kolide_zerotier_peers", dataflattentable.JsonType, zerotierCli("listpeers")), + "kolide_zerotier_peers", dataflattentable.JsonType, allowedcmd.ZerotierCli, []string{"listpeers"}), tdebug.LauncherGcInfo(logger), - zfs.ZfsPropertiesPlugin(logger), - zfs.ZpoolPropertiesPlugin(logger), } // The dataflatten tables diff --git a/pkg/osquery/table/touchid_system_darwin.go b/pkg/osquery/table/touchid_system_darwin.go index 93e6ce120..0a750381d 100644 --- a/pkg/osquery/table/touchid_system_darwin.go +++ b/pkg/osquery/table/touchid_system_darwin.go @@ -4,12 +4,13 @@ import ( "bytes" "context" "fmt" - "os/exec" "regexp" "strings" "time" "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/osquery/osquery-go/plugin/table" ) @@ -41,7 +42,11 @@ func (t *touchIDSystemConfigTable) generate(ctx context.Context, queryContext ta // Read the security chip from system_profiler var stdout bytes.Buffer - cmd := exec.CommandContext(ctx, "/usr/sbin/system_profiler", "SPiBridgeDataType") + cmd, err := allowedcmd.SystemProfiler(ctx, "SPiBridgeDataType") + if err != nil { + level.Debug(t.logger).Log("msg", "could not create system_profiler command", "err", err) + return results, nil + } cmd.Stdout = &stdout if err := cmd.Run(); err != nil { return nil, fmt.Errorf("calling system_profiler: %w", err) @@ -57,7 +62,11 @@ func (t *touchIDSystemConfigTable) generate(ctx context.Context, queryContext ta // Read the system's bioutil configuration stdout.Reset() - cmd = exec.CommandContext(ctx, "/usr/bin/bioutil", "-r", "-s") + cmd, err = allowedcmd.Bioutil(ctx, "-r", "-s") + if err != nil { + level.Debug(t.logger).Log("msg", "could not create bioutil command", "err", err) + return results, nil + } cmd.Stdout = &stdout if err := cmd.Run(); err != nil { return nil, fmt.Errorf("calling bioutil for system configuration: %w", err) diff --git a/pkg/osquery/table/touchid_user_darwin.go b/pkg/osquery/table/touchid_user_darwin.go index 99af08515..9818ff3ae 100644 --- a/pkg/osquery/table/touchid_user_darwin.go +++ b/pkg/osquery/table/touchid_user_darwin.go @@ -4,7 +4,7 @@ import ( "bytes" "context" "errors" - "os/exec" + "fmt" "os/user" "strconv" "strings" @@ -13,6 +13,7 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/osquery/osquery-go/plugin/table" ) @@ -64,7 +65,7 @@ func (t *touchIDUserConfigTable) generate(ctx context.Context, queryContext tabl uid, _ := strconv.Atoi(constraint.Expression) // Get the user's TouchID config - configOutput, err := runCommandContext(ctx, uid, "/usr/bin/bioutil", "-r") + configOutput, err := runCommandContext(ctx, uid, allowedcmd.Bioutil, "-r") if err != nil { level.Debug(t.logger).Log( "msg", "could not run bioutil -r", @@ -94,7 +95,7 @@ func (t *touchIDUserConfigTable) generate(ctx context.Context, queryContext tabl } // Grab the fingerprint count - countOutStr, err := runCommandContext(ctx, uid, "/usr/bin/bioutil", "-c") + countOutStr, err := runCommandContext(ctx, uid, allowedcmd.Bioutil, "-c") if err != nil { level.Debug(t.logger).Log( "msg", "could not run bioutil -c", @@ -128,13 +129,16 @@ func (t *touchIDUserConfigTable) generate(ctx context.Context, queryContext tabl } // runCommand runs a given command and arguments as the supplied user -func runCommandContext(ctx context.Context, uid int, cmd string, args ...string) (string, error) { +func runCommandContext(ctx context.Context, uid int, cmd allowedcmd.AllowedCommand, args ...string) (string, error) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() // Set up the command var stdout bytes.Buffer - c := exec.CommandContext(ctx, cmd, args...) + c, err := cmd(ctx, args...) + if err != nil { + return "", fmt.Errorf("creating command: %w", err) + } c.Stdout = &stdout // Check if the supplied UID is that of the current user diff --git a/pkg/osquery/table/zerotier.go b/pkg/osquery/table/zerotier.go deleted file mode 100644 index 9ec32b920..000000000 --- a/pkg/osquery/table/zerotier.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !windows -// +build !windows - -package table - -// zerotierCli returns the path to the CLI executable. -func zerotierCli(args ...string) []string { - cmd := []string{"/usr/local/bin/zerotier-cli"} - - return append(cmd, args...) -} diff --git a/pkg/osquery/table/zerotier_windows.go b/pkg/osquery/table/zerotier_windows.go deleted file mode 100644 index ae94fb357..000000000 --- a/pkg/osquery/table/zerotier_windows.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build windows -// +build windows - -package table - -import ( - "os" - "path" -) - -// zerotierCli returns the path to the CLI executable. -func zerotierCli(args ...string) []string { - cmd := []string{ - path.Join(os.Getenv("SYSTEMROOT"), "ProgramData", "ZeroTier", "One", "zerotier-one_x64.exe"), - "-q", - } - - return append(cmd, args...) -} diff --git a/pkg/osquery/tables/airport/table_darwin.go b/pkg/osquery/tables/airport/table_darwin.go index 629805b53..5f2cb579c 100644 --- a/pkg/osquery/tables/airport/table_darwin.go +++ b/pkg/osquery/tables/airport/table_darwin.go @@ -13,6 +13,7 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" @@ -21,7 +22,6 @@ import ( var ( allowedOptions = []string{"getinfo", "scan"} - airportPaths = []string{"/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport"} ) type Table struct { @@ -47,11 +47,10 @@ func TablePlugin(logger log.Logger) *table.Plugin { type airportExecutor struct { ctx context.Context // nolint:containedctx logger log.Logger - paths []string } func (a *airportExecutor) Exec(option string) ([]byte, error) { - return tablehelpers.Exec(a.ctx, a.logger, 30, airportPaths, []string{"--" + option}, false) + return tablehelpers.Exec(a.ctx, a.logger, 30, allowedcmd.Airport, []string{"--" + option}, false) } type executor interface { @@ -63,7 +62,6 @@ func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ( airportExecutor := &airportExecutor{ ctx: ctx, logger: t.logger, - paths: airportPaths, } return generateAirportData(queryContext, airportExecutor, t.logger) diff --git a/pkg/osquery/tables/apple_silicon_security_policy/table.go b/pkg/osquery/tables/apple_silicon_security_policy/table.go index 1ca2d67da..017300d29 100644 --- a/pkg/osquery/tables/apple_silicon_security_policy/table.go +++ b/pkg/osquery/tables/apple_silicon_security_policy/table.go @@ -10,13 +10,13 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" ) -const bootPolicyUtilPath = "/usr/bin/bputil" const bootPolicyUtilArgs = "--display-all-policies" type Table struct { @@ -38,7 +38,7 @@ func TablePlugin(logger log.Logger) *table.Plugin { func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { var results []map[string]string - output, err := tablehelpers.Exec(ctx, t.logger, 30, []string{bootPolicyUtilPath}, []string{bootPolicyUtilArgs}, false) + output, err := tablehelpers.Exec(ctx, t.logger, 30, allowedcmd.Bputil, []string{bootPolicyUtilArgs}, false) if err != nil { level.Info(t.logger).Log("msg", "bputil failed", "err", err) return nil, nil diff --git a/pkg/osquery/tables/crowdstrike/falcon_kernel_check/table.go b/pkg/osquery/tables/crowdstrike/falcon_kernel_check/table.go index 092cb5ff6..79b60d3a9 100644 --- a/pkg/osquery/tables/crowdstrike/falcon_kernel_check/table.go +++ b/pkg/osquery/tables/crowdstrike/falcon_kernel_check/table.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package falcon_kernel_check import ( @@ -7,12 +10,11 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" ) -const kernelCheckUtilPath = "/opt/CrowdStrike/falcon-kernel-check" - type Table struct { logger log.Logger } @@ -34,7 +36,7 @@ func TablePlugin(logger log.Logger) *table.Plugin { } func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { - output, err := tablehelpers.Exec(ctx, t.logger, 5, []string{kernelCheckUtilPath}, []string{}, false) + output, err := tablehelpers.Exec(ctx, t.logger, 5, allowedcmd.FalconKernelCheck, []string{}, false) if err != nil { level.Info(t.logger).Log("msg", "exec failed", "err", err) return nil, err diff --git a/pkg/osquery/tables/crowdstrike/falcon_kernel_check/table_test.go b/pkg/osquery/tables/crowdstrike/falcon_kernel_check/table_test.go index 01392b92e..6b9136e53 100644 --- a/pkg/osquery/tables/crowdstrike/falcon_kernel_check/table_test.go +++ b/pkg/osquery/tables/crowdstrike/falcon_kernel_check/table_test.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package falcon_kernel_check import ( diff --git a/pkg/osquery/tables/crowdstrike/falconctl/parser.go b/pkg/osquery/tables/crowdstrike/falconctl/parser.go index b5313758d..5b1bc115e 100644 --- a/pkg/osquery/tables/crowdstrike/falconctl/parser.go +++ b/pkg/osquery/tables/crowdstrike/falconctl/parser.go @@ -1,3 +1,6 @@ +//go:build !windows +// +build !windows + package falconctl import ( diff --git a/pkg/osquery/tables/crowdstrike/falconctl/parser_test.go b/pkg/osquery/tables/crowdstrike/falconctl/parser_test.go index 22df1f915..1f1768175 100644 --- a/pkg/osquery/tables/crowdstrike/falconctl/parser_test.go +++ b/pkg/osquery/tables/crowdstrike/falconctl/parser_test.go @@ -1,3 +1,6 @@ +//go:build !windows +// +build !windows + package falconctl import ( diff --git a/pkg/osquery/tables/crowdstrike/falconctl/table.go b/pkg/osquery/tables/crowdstrike/falconctl/table.go index c0710b4af..11acfae1e 100644 --- a/pkg/osquery/tables/crowdstrike/falconctl/table.go +++ b/pkg/osquery/tables/crowdstrike/falconctl/table.go @@ -1,3 +1,6 @@ +//go:build !windows +// +build !windows + package falconctl import ( @@ -8,6 +11,7 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" @@ -15,8 +19,6 @@ import ( ) var ( - falconctlPaths = []string{"/opt/CrowdStrike/falconctl"} - // allowedOptions is the list of options this table is allowed to query. Notable exceptions // are `systags` (which is parsed seperatedly) and `provisioning-token` (which is a secret). allowedOptions = []string{ @@ -36,7 +38,7 @@ var ( defaultOption = strings.Join(allowedOptions, " ") ) -type execFunc func(context.Context, log.Logger, int, []string, []string, bool) ([]byte, error) +type execFunc func(context.Context, log.Logger, int, allowedcmd.AllowedCommand, []string, bool) ([]byte, error) type falconctlOptionsTable struct { logger log.Logger @@ -87,7 +89,7 @@ OUTER: // then the list of options to fetch. Set the command line thusly. args := append([]string{"-g"}, options...) - output, err := t.execFunc(ctx, t.logger, 30, falconctlPaths, args, false) + output, err := t.execFunc(ctx, t.logger, 30, allowedcmd.Falconctl, args, false) if err != nil { level.Info(t.logger).Log("msg", "exec failed", "err", err) synthesizedData := map[string]string{ diff --git a/pkg/osquery/tables/crowdstrike/falconctl/table_test.go b/pkg/osquery/tables/crowdstrike/falconctl/table_test.go index 5b5731f52..81c6b7421 100644 --- a/pkg/osquery/tables/crowdstrike/falconctl/table_test.go +++ b/pkg/osquery/tables/crowdstrike/falconctl/table_test.go @@ -1,3 +1,6 @@ +//go:build !windows +// +build !windows + package falconctl import ( @@ -7,6 +10,7 @@ import ( "testing" "github.com/go-kit/kit/log" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/stretchr/testify/require" ) @@ -81,7 +85,7 @@ func TestOptionRestrictions(t *testing.T) { } } -func noopExec(_ context.Context, log log.Logger, _ int, _ []string, args []string, _ bool) ([]byte, error) { +func noopExec(_ context.Context, log log.Logger, _ int, _ allowedcmd.AllowedCommand, args []string, _ bool) ([]byte, error) { log.Log("exec", "exec-in-test", "args", strings.Join(args, " ")) return []byte{}, nil } diff --git a/pkg/osquery/tables/cryptsetup/parser.go b/pkg/osquery/tables/cryptsetup/parser.go index 18c97ec39..5d80d9c95 100644 --- a/pkg/osquery/tables/cryptsetup/parser.go +++ b/pkg/osquery/tables/cryptsetup/parser.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package cryptsetup import ( diff --git a/pkg/osquery/tables/cryptsetup/parser_test.go b/pkg/osquery/tables/cryptsetup/parser_test.go index 82632fa67..e141b2cda 100644 --- a/pkg/osquery/tables/cryptsetup/parser_test.go +++ b/pkg/osquery/tables/cryptsetup/parser_test.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package cryptsetup import ( diff --git a/pkg/osquery/tables/cryptsetup/table.go b/pkg/osquery/tables/cryptsetup/table.go index 05bd01dcc..d0c5575db 100644 --- a/pkg/osquery/tables/cryptsetup/table.go +++ b/pkg/osquery/tables/cryptsetup/table.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package cryptsetup import ( @@ -7,17 +10,13 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" ) -var cryptsetupPaths = []string{ - "/usr/sbin/cryptsetup", - "/sbin/cryptsetup", -} - const allowedNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-/_" type Table struct { @@ -51,7 +50,7 @@ func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ( } for _, name := range requestedNames { - output, err := tablehelpers.Exec(ctx, t.logger, 15, cryptsetupPaths, []string{"--readonly", "status", name}, false) + output, err := tablehelpers.Exec(ctx, t.logger, 15, allowedcmd.Cryptsetup, []string{"--readonly", "status", name}, false) if err != nil { level.Debug(t.logger).Log("msg", "Error execing for status", "name", name, "err", err) continue diff --git a/pkg/osquery/tables/dataflattentable/exec.go b/pkg/osquery/tables/dataflattentable/exec.go index f270f759d..5261c3fba 100644 --- a/pkg/osquery/tables/dataflattentable/exec.go +++ b/pkg/osquery/tables/dataflattentable/exec.go @@ -6,13 +6,12 @@ import ( "fmt" "os" - "os/exec" - "path/filepath" "strings" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" @@ -29,18 +28,13 @@ func WithKVSeparator(separator string) ExecTableOpt { } } -func WithBinDirs(binDirs ...string) ExecTableOpt { - return func(t *Table) { - t.binDirs = binDirs - } -} - -func TablePluginExec(logger log.Logger, tableName string, dataSourceType DataSourceType, execArgs []string, opts ...ExecTableOpt) *table.Plugin { +func TablePluginExec(logger log.Logger, tableName string, dataSourceType DataSourceType, cmdGen allowedcmd.AllowedCommand, execArgs []string, opts ...ExecTableOpt) *table.Plugin { columns := Columns() t := &Table{ logger: level.NewFilter(logger, level.AllowInfo()), tableName: tableName, + cmdGen: cmdGen, execArgs: execArgs, keyValueSeparator: ":", } @@ -103,37 +97,25 @@ func (t *Table) exec(ctx context.Context) ([]byte, error) { ctx, cancel := context.WithTimeout(ctx, 50*time.Second) defer cancel() - possibleBinaries := []string{} + var stdout bytes.Buffer + var stderr bytes.Buffer - if t.binDirs == nil || len(t.binDirs) == 0 { - possibleBinaries = []string{t.execArgs[0]} - } else { - for _, possiblePath := range t.binDirs { - possibleBinaries = append(possibleBinaries, filepath.Join(possiblePath, t.execArgs[0])) - } + cmd, err := t.cmdGen(ctx, t.execArgs...) + if err != nil { + return nil, fmt.Errorf("creating command: %w", err) } - for _, execPath := range possibleBinaries { - var stdout bytes.Buffer - var stderr bytes.Buffer - - cmd := exec.CommandContext(ctx, execPath, t.execArgs[1:]...) - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - level.Debug(t.logger).Log("msg", "calling %s", "args", cmd.String()) + cmd.Stdout = &stdout + cmd.Stderr = &stderr - if err := cmd.Run(); os.IsNotExist(err) { - // try the next binary - continue - } else if err != nil { - return nil, fmt.Errorf("calling %s. Got: %s: %w", t.execArgs[0], string(stderr.Bytes()), err) - } + level.Debug(t.logger).Log("msg", "calling %s", "args", cmd.String()) - // success! - return stdout.Bytes(), nil + if err := cmd.Run(); os.IsNotExist(err) { + return nil, fmt.Errorf("command %s not found: %w", cmd.Path, err) + } else if err != nil { + return nil, fmt.Errorf("calling %s. Got: %s: %w", cmd.Path, string(stderr.Bytes()), err) } - // None of the possible execs were found - return nil, fmt.Errorf("Unable to exec '%s'. No binary found is specified paths", t.execArgs[0]) + // success! + return stdout.Bytes(), nil } diff --git a/pkg/osquery/tables/dataflattentable/exec_and_parse.go b/pkg/osquery/tables/dataflattentable/exec_and_parse.go index 4bdb1afc6..9d3239239 100644 --- a/pkg/osquery/tables/dataflattentable/exec_and_parse.go +++ b/pkg/osquery/tables/dataflattentable/exec_and_parse.go @@ -7,6 +7,7 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/kolide/launcher/pkg/traces" @@ -26,7 +27,7 @@ type execTableV2 struct { timeoutSeconds int tabledebug bool includeStderr bool - execPaths []string + cmd allowedcmd.AllowedCommand execArgs []string } @@ -44,26 +45,20 @@ func WithTableDebug() execTableV2Opt { } } -func WithAdditionalExecPaths(paths ...string) execTableV2Opt { - return func(t *execTableV2) { - t.execPaths = append(t.execPaths, paths...) - } -} - func WithIncludeStderr() execTableV2Opt { return func(t *execTableV2) { t.includeStderr = true } } -func NewExecAndParseTable(logger log.Logger, tableName string, p parser, execCmd []string, opts ...execTableV2Opt) *table.Plugin { +func NewExecAndParseTable(logger log.Logger, tableName string, p parser, cmd allowedcmd.AllowedCommand, execArgs []string, opts ...execTableV2Opt) *table.Plugin { t := &execTableV2{ logger: level.NewFilter(log.With(logger, "table", tableName), level.AllowInfo()), tableName: tableName, flattener: flattenerFromParser(p), timeoutSeconds: 30, - execPaths: execCmd[:1], - execArgs: execCmd[1:], + cmd: cmd, + execArgs: execArgs, } for _, opt := range opts { @@ -83,7 +78,7 @@ func (t *execTableV2) generate(ctx context.Context, queryContext table.QueryCont var results []map[string]string - execOutput, err := tablehelpers.Exec(ctx, t.logger, t.timeoutSeconds, t.execPaths, t.execArgs, t.includeStderr) + execOutput, err := tablehelpers.Exec(ctx, t.logger, t.timeoutSeconds, t.cmd, t.execArgs, t.includeStderr) if err != nil { // exec will error if there's no binary, so we never want to record that if os.IsNotExist(errors.Cause(err)) { diff --git a/pkg/osquery/tables/dataflattentable/tables.go b/pkg/osquery/tables/dataflattentable/tables.go index a2249bce5..3d32c9a9e 100644 --- a/pkg/osquery/tables/dataflattentable/tables.go +++ b/pkg/osquery/tables/dataflattentable/tables.go @@ -8,6 +8,7 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go" @@ -34,8 +35,8 @@ type Table struct { flattenFileFunc func(string, ...dataflatten.FlattenOpts) ([]dataflatten.Row, error) flattenBytesFunc func([]byte, ...dataflatten.FlattenOpts) ([]dataflatten.Row, error) + cmdGen allowedcmd.AllowedCommand execArgs []string - binDirs []string keyValueSeparator string } diff --git a/pkg/osquery/tables/dev_table_tooling/commands_darwin.go b/pkg/osquery/tables/dev_table_tooling/commands_darwin.go index 7cab3f504..f09c77ceb 100644 --- a/pkg/osquery/tables/dev_table_tooling/commands_darwin.go +++ b/pkg/osquery/tables/dev_table_tooling/commands_darwin.go @@ -3,13 +3,15 @@ package dev_table_tooling +import "github.com/kolide/launcher/pkg/allowedcmd" + var allowedCommands = map[string]allowedCommand{ "echo": { - binPaths: []string{"echo"}, - args: []string{"hello"}, + bin: allowedcmd.Echo, + args: []string{"hello"}, }, "cb_repcli": { - binPaths: []string{"/Applications/VMware Carbon Black Cloud/repcli.bundle/Contents/MacOS/repcli"}, - args: []string{"status"}, + bin: allowedcmd.Repcli, + args: []string{"status"}, }, } diff --git a/pkg/osquery/tables/dev_table_tooling/commands_linux.go b/pkg/osquery/tables/dev_table_tooling/commands_linux.go index 08650b21e..573ce4d05 100644 --- a/pkg/osquery/tables/dev_table_tooling/commands_linux.go +++ b/pkg/osquery/tables/dev_table_tooling/commands_linux.go @@ -3,13 +3,15 @@ package dev_table_tooling +import "github.com/kolide/launcher/pkg/allowedcmd" + var allowedCommands = map[string]allowedCommand{ "echo": { - binPaths: []string{"echo"}, - args: []string{"hello"}, + bin: allowedcmd.Echo, + args: []string{"hello"}, }, "cb_repcli": { - binPaths: []string{"/opt/carbonblack/psc/bin/repcli"}, - args: []string{"status"}, + bin: allowedcmd.Repcli, + args: []string{"status"}, }, } diff --git a/pkg/osquery/tables/dev_table_tooling/commands_windows.go b/pkg/osquery/tables/dev_table_tooling/commands_windows.go index bd84e12ab..25e242f09 100644 --- a/pkg/osquery/tables/dev_table_tooling/commands_windows.go +++ b/pkg/osquery/tables/dev_table_tooling/commands_windows.go @@ -3,15 +3,15 @@ package dev_table_tooling -import "path/filepath" +import "github.com/kolide/launcher/pkg/allowedcmd" var allowedCommands = map[string]allowedCommand{ "echo": { - binPaths: []string{"echo"}, - args: []string{"hello"}, + bin: allowedcmd.Echo, + args: []string{"hello"}, }, "cb_repcli": { - binPaths: []string{filepath.Join("Program Files", "Confer", "repcli")}, - args: []string{"status"}, + bin: allowedcmd.Repcli, + args: []string{"status"}, }, } diff --git a/pkg/osquery/tables/dev_table_tooling/table.go b/pkg/osquery/tables/dev_table_tooling/table.go index 179bb44b8..5d9028c84 100644 --- a/pkg/osquery/tables/dev_table_tooling/table.go +++ b/pkg/osquery/tables/dev_table_tooling/table.go @@ -7,6 +7,7 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" ) @@ -14,8 +15,8 @@ import ( // allowedCommand encapsulates the possible binary path(s) of a command allowed to execute // along with a strict list of arguments. type allowedCommand struct { - binPaths []string - args []string + bin allowedcmd.AllowedCommand + args []string } type Table struct { @@ -62,7 +63,7 @@ func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ( "error": "", } - if output, err := tablehelpers.Exec(ctx, t.logger, 30, cmd.binPaths, cmd.args, false); err != nil { + if output, err := tablehelpers.Exec(ctx, t.logger, 30, cmd.bin, cmd.args, false); err != nil { level.Info(t.logger).Log("msg", "execution failed", "name", name, "err", err) result["error"] = err.Error() } else { diff --git a/pkg/osquery/tables/dsim_default_associations/dsim_default_associations.go b/pkg/osquery/tables/dsim_default_associations/dsim_default_associations.go index f22984d53..4e84be115 100644 --- a/pkg/osquery/tables/dsim_default_associations/dsim_default_associations.go +++ b/pkg/osquery/tables/dsim_default_associations/dsim_default_associations.go @@ -8,7 +8,6 @@ import ( "context" "fmt" "os" - "os/exec" "path/filepath" "strings" "time" @@ -16,14 +15,13 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/kolide/launcher/pkg/agent" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" ) -const dismCmd = "dism.exe" - type Table struct { logger log.Logger } @@ -84,7 +82,10 @@ func (t *Table) execDism(ctx context.Context) ([]byte, error) { args := []string{"/online", "/Export-DefaultAppAssociations:" + dstFile} - cmd := exec.CommandContext(ctx, dismCmd, args...) + cmd, err := allowedcmd.Dism(ctx, args...) + if err != nil { + return nil, fmt.Errorf("creating command: %w", err) + } cmd.Dir = dir cmd.Stdout = &stdout cmd.Stderr = &stderr diff --git a/pkg/osquery/tables/filevault/filevault.go b/pkg/osquery/tables/filevault/filevault.go index 2ec6ad8c4..d018eda94 100644 --- a/pkg/osquery/tables/filevault/filevault.go +++ b/pkg/osquery/tables/filevault/filevault.go @@ -11,13 +11,12 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" "github.com/pkg/errors" ) -const fdesetupPath = "/usr/bin/fdesetup" - type Table struct { logger log.Logger } @@ -35,7 +34,7 @@ func TablePlugin(logger log.Logger) *table.Plugin { } func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) { - output, err := tablehelpers.Exec(ctx, t.logger, 10, []string{fdesetupPath}, []string{"status"}, false) + output, err := tablehelpers.Exec(ctx, t.logger, 10, allowedcmd.Fdesetup, []string{"status"}, false) if err != nil { level.Info(t.logger).Log("msg", "fdesetup failed", "err", err) diff --git a/pkg/osquery/tables/firmwarepasswd/firmwarepasswd.go b/pkg/osquery/tables/firmwarepasswd/firmwarepasswd.go index 93e77b655..281f14c63 100644 --- a/pkg/osquery/tables/firmwarepasswd/firmwarepasswd.go +++ b/pkg/osquery/tables/firmwarepasswd/firmwarepasswd.go @@ -1,3 +1,6 @@ +//go:build darwin +// +build darwin + // firmwarepasswd is a simple wrapper around the // `/usr/sbin/firmwarepasswd` tool. This should be considered beta at // best. It serves a bit as a pattern for future exec work. @@ -9,13 +12,13 @@ import ( "context" "fmt" "os" - "os/exec" "strings" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/kolide/launcher/pkg/agent" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/osquery/osquery-go/plugin/table" ) @@ -92,7 +95,10 @@ func (t *Table) runFirmwarepasswd(ctx context.Context, subcommand string, output ctx, cancel := context.WithTimeout(ctx, 1*time.Second) defer cancel() - cmd := exec.CommandContext(ctx, "/usr/sbin/firmwarepasswd", subcommand) + cmd, err := allowedcmd.Firmwarepasswd(ctx, subcommand) + if err != nil { + return fmt.Errorf("creating command: %w", err) + } dir, err := agent.MkdirTemp("osq-firmwarepasswd") if err != nil { diff --git a/pkg/osquery/tables/firmwarepasswd/firmwarepasswd_test.go b/pkg/osquery/tables/firmwarepasswd/firmwarepasswd_test.go index 5f8311a6c..fd6cdef28 100644 --- a/pkg/osquery/tables/firmwarepasswd/firmwarepasswd_test.go +++ b/pkg/osquery/tables/firmwarepasswd/firmwarepasswd_test.go @@ -1,3 +1,6 @@ +//go:build darwin +// +build darwin + package firmwarepasswd import ( diff --git a/pkg/osquery/tables/firmwarepasswd/parser.go b/pkg/osquery/tables/firmwarepasswd/parser.go index f3f46c64d..34a5a85ac 100644 --- a/pkg/osquery/tables/firmwarepasswd/parser.go +++ b/pkg/osquery/tables/firmwarepasswd/parser.go @@ -1,3 +1,6 @@ +//go:build darwin +// +build darwin + package firmwarepasswd import ( diff --git a/pkg/osquery/tables/gsettings/gsettings.go b/pkg/osquery/tables/gsettings/gsettings.go index 21d4cc57a..e78cb84ec 100644 --- a/pkg/osquery/tables/gsettings/gsettings.go +++ b/pkg/osquery/tables/gsettings/gsettings.go @@ -1,5 +1,5 @@ -//go:build !windows -// +build !windows +//go:build linux +// +build linux package gsettings @@ -11,7 +11,6 @@ import ( "fmt" "io" "os" - "os/exec" "os/user" "strconv" "strings" @@ -21,12 +20,11 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/kolide/launcher/pkg/agent" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" ) -const gsettingsPath = "/usr/bin/gsettings" - const allowedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-." type gsettingsExecer func(ctx context.Context, username string, buf *bytes.Buffer) error @@ -92,7 +90,10 @@ func execGsettings(ctx context.Context, username string, buf *bytes.Buffer) erro return fmt.Errorf("finding user by username '%s': %w", username, err) } - cmd := exec.CommandContext(ctx, gsettingsPath, "list-recursively") + cmd, err := allowedcmd.Gsettings(ctx, "list-recursively") + if err != nil { + return fmt.Errorf("creating gsettings command: %w", err) + } // set the HOME for the the cmd so that gsettings is exec'd properly as the // new user. diff --git a/pkg/osquery/tables/gsettings/gsettings_test.go b/pkg/osquery/tables/gsettings/gsettings_test.go index 8bad6ae8c..f3eacacda 100644 --- a/pkg/osquery/tables/gsettings/gsettings_test.go +++ b/pkg/osquery/tables/gsettings/gsettings_test.go @@ -1,5 +1,5 @@ -//go:build !windows -// +build !windows +//go:build linux +// +build linux package gsettings diff --git a/pkg/osquery/tables/gsettings/metadata.go b/pkg/osquery/tables/gsettings/metadata.go index 7e8f1a6e2..d4d6d3bd4 100644 --- a/pkg/osquery/tables/gsettings/metadata.go +++ b/pkg/osquery/tables/gsettings/metadata.go @@ -1,5 +1,5 @@ -//go:build !windows -// +build !windows +//go:build linux +// +build linux package gsettings @@ -10,13 +10,13 @@ import ( "errors" "fmt" "os" - "os/exec" "strings" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/kolide/launcher/pkg/agent" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" ) @@ -221,7 +221,10 @@ func execGsettingsCommand(ctx context.Context, args []string, tmpdir string, out defer cancel() command := args[0] - cmd := exec.CommandContext(ctx, gsettingsPath, args...) + cmd, err := allowedcmd.Gsettings(ctx, args...) + if err != nil { + return fmt.Errorf("creating gsettings command: %w", err) + } cmd.Dir = tmpdir cmd.Stderr = new(bytes.Buffer) diff --git a/pkg/osquery/tables/ioreg/ioreg.go b/pkg/osquery/tables/ioreg/ioreg.go index eff0703fc..2325df387 100644 --- a/pkg/osquery/tables/ioreg/ioreg.go +++ b/pkg/osquery/tables/ioreg/ioreg.go @@ -16,14 +16,13 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" ) -const ioregPath = "/usr/sbin/ioreg" - const allowedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" type Table struct { @@ -102,7 +101,7 @@ func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ( for _, dataQuery := range tablehelpers.GetConstraints(queryContext, "query", tablehelpers.WithDefaults("*")) { // Finally, an inner loop - ioregOutput, err := tablehelpers.Exec(ctx, t.logger, 30, []string{ioregPath}, ioregArgs, false) + ioregOutput, err := tablehelpers.Exec(ctx, t.logger, 30, allowedcmd.Ioreg, ioregArgs, false) if err != nil { level.Info(t.logger).Log("msg", "ioreg failed", "err", err) continue diff --git a/pkg/osquery/tables/mdmclient/mdmclient.go b/pkg/osquery/tables/mdmclient/mdmclient.go index 634880c37..3b48cf2c5 100644 --- a/pkg/osquery/tables/mdmclient/mdmclient.go +++ b/pkg/osquery/tables/mdmclient/mdmclient.go @@ -1,7 +1,5 @@ -//go:build !windows -// +build !windows - -// (skip building windows, since the newline replacement doesn't work there) +//go:build darwin +// +build darwin // Package mdmclient provides a table that parses the mdmclient // output. Empirically, this seems to be an almost gnustep @@ -18,14 +16,13 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" ) -const mdmclientPath = "/usr/libexec/mdmclient" - const allowedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // headerRegex matches the header that may be included at the beginning of the mdmclient response, @@ -92,7 +89,7 @@ func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ( for _, dataQuery := range tablehelpers.GetConstraints(queryContext, "query", tablehelpers.WithDefaults("*")) { - mdmclientOutput, err := tablehelpers.Exec(ctx, t.logger, 30, []string{mdmclientPath}, []string{mdmclientCommand}, false) + mdmclientOutput, err := tablehelpers.Exec(ctx, t.logger, 30, allowedcmd.Mdmclient, []string{mdmclientCommand}, false) if err != nil { level.Info(t.logger).Log("msg", "mdmclient failed", "err", err) continue diff --git a/pkg/osquery/tables/mdmclient/mdmclient_test.go b/pkg/osquery/tables/mdmclient/mdmclient_test.go index 0b89fd455..65628451e 100644 --- a/pkg/osquery/tables/mdmclient/mdmclient_test.go +++ b/pkg/osquery/tables/mdmclient/mdmclient_test.go @@ -1,7 +1,5 @@ -//go:build !windows -// +build !windows - -// (skip building windows, since the newline replacement doesn't work there) +//go:build darwin +// +build darwin package mdmclient diff --git a/pkg/osquery/tables/profiles/profiles.go b/pkg/osquery/tables/profiles/profiles.go index 7dacd12f2..e029c263a 100644 --- a/pkg/osquery/tables/profiles/profiles.go +++ b/pkg/osquery/tables/profiles/profiles.go @@ -21,6 +21,7 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/kolide/launcher/pkg/agent" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" @@ -28,7 +29,6 @@ import ( ) const ( - profilesPath = "/usr/bin/profiles" userAllowedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_" typeAllowedCharacters = "abcdefghijklmnopqrstuvwxyz" ) @@ -103,7 +103,7 @@ func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ( return nil, fmt.Errorf("Unknown user argument: %s", user) } - output, err := tablehelpers.Exec(ctx, t.logger, 30, []string{profilesPath}, profileArgs, false) + output, err := tablehelpers.Exec(ctx, t.logger, 30, allowedcmd.Profiles, profileArgs, false) if err != nil { level.Info(t.logger).Log("msg", "ioreg exec failed", "err", err) continue diff --git a/pkg/osquery/tables/pwpolicy/pwpolicy.go b/pkg/osquery/tables/pwpolicy/pwpolicy.go index a4332b76b..08276418c 100644 --- a/pkg/osquery/tables/pwpolicy/pwpolicy.go +++ b/pkg/osquery/tables/pwpolicy/pwpolicy.go @@ -14,25 +14,24 @@ import ( "bytes" "context" "fmt" - "os/exec" "strings" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" ) -const pwpolicyPath = "/usr/bin/pwpolicy" const pwpolicyCmd = "getaccountpolicies" type Table struct { logger log.Logger tableName string - execCC func(context.Context, string, ...string) *exec.Cmd + execCC allowedcmd.AllowedCommand } func TablePlugin(logger log.Logger) *table.Plugin { @@ -44,7 +43,7 @@ func TablePlugin(logger log.Logger) *table.Plugin { t := &Table{ logger: logger, tableName: "kolide_pwpolicy", - execCC: exec.CommandContext, + execCC: allowedcmd.Pwpolicy, } return table.NewPlugin(t.tableName, columns, t.generate) @@ -96,7 +95,10 @@ func (t *Table) execPwpolicy(ctx context.Context, args []string) ([]byte, error) ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - cmd := t.execCC(ctx, pwpolicyPath, args...) + cmd, err := t.execCC(ctx, args...) + if err != nil { + return nil, fmt.Errorf("creating command: %w", err) + } cmd.Stdout = &stdout cmd.Stderr = &stderr diff --git a/pkg/osquery/tables/pwpolicy/pwpolicy_test.go b/pkg/osquery/tables/pwpolicy/pwpolicy_test.go index 84d6e60ff..dd193af6c 100644 --- a/pkg/osquery/tables/pwpolicy/pwpolicy_test.go +++ b/pkg/osquery/tables/pwpolicy/pwpolicy_test.go @@ -71,8 +71,8 @@ func TestQueries(t *testing.T) { } -func execFaker(filename string) func(context.Context, string, ...string) *exec.Cmd { - return func(ctx context.Context, _ string, _ ...string) *exec.Cmd { - return exec.CommandContext(ctx, "/bin/cat", filename) +func execFaker(filename string) func(context.Context, ...string) (*exec.Cmd, error) { + return func(ctx context.Context, _ ...string) (*exec.Cmd, error) { + return exec.CommandContext(ctx, "/bin/cat", filename), nil //nolint:forbidigo // Fine to use exec.CommandContext in test } } diff --git a/pkg/osquery/tables/secedit/secedit.go b/pkg/osquery/tables/secedit/secedit.go index b6d6c3873..d43fe284a 100644 --- a/pkg/osquery/tables/secedit/secedit.go +++ b/pkg/osquery/tables/secedit/secedit.go @@ -9,7 +9,6 @@ import ( "fmt" "io" "os" - "os/exec" "path/filepath" "strconv" "strings" @@ -18,6 +17,7 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/kolide/launcher/pkg/agent" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" @@ -27,8 +27,6 @@ import ( "golang.org/x/text/transform" ) -const seceditCmd = "secedit" - type Table struct { logger log.Logger } @@ -109,7 +107,10 @@ func (t *Table) execSecedit(ctx context.Context, mergedPolicy bool) ([]byte, err args = append(args, "/mergedpolicy") } - cmd := exec.CommandContext(ctx, seceditCmd, args...) + cmd, err := allowedcmd.Secedit(ctx, args...) + if err != nil { + return nil, fmt.Errorf("creating secedit command: %w", err) + } cmd.Stdout = &stdout cmd.Stderr = &stderr diff --git a/pkg/osquery/tables/systemprofiler/systemprofiler.go b/pkg/osquery/tables/systemprofiler/systemprofiler.go index 9ed47cf40..24755c04f 100644 --- a/pkg/osquery/tables/systemprofiler/systemprofiler.go +++ b/pkg/osquery/tables/systemprofiler/systemprofiler.go @@ -41,20 +41,18 @@ import ( "bytes" "context" "fmt" - "os/exec" "strings" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/groob/plist" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/osquery/osquery-go/plugin/table" ) -const systemprofilerPath = "/usr/sbin/system_profiler" - var knownDetailLevels = []string{ "mini", // short report (contains no identifying or personal information) "basic", // basic hardware and network information @@ -208,7 +206,10 @@ func (t *Table) execSystemProfiler(ctx context.Context, detailLevel string, subc args = append(args, subcommands...) - cmd := exec.CommandContext(ctx, systemprofilerPath, args...) + cmd, err := allowedcmd.SystemProfiler(ctx, args...) + if err != nil { + return nil, fmt.Errorf("creating system_profiler command: %w", err) + } cmd.Stdout = &stdout cmd.Stderr = &stderr diff --git a/pkg/osquery/tables/tablehelpers/exec.go b/pkg/osquery/tables/tablehelpers/exec.go index 0ab9440c1..02aedb4be 100644 --- a/pkg/osquery/tables/tablehelpers/exec.go +++ b/pkg/osquery/tables/tablehelpers/exec.go @@ -5,11 +5,11 @@ import ( "context" "fmt" "os" - "os/exec" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/traces" ) @@ -21,10 +21,12 @@ import ( // 3. It moves the stderr into the return error, if needed. // // This is not suitable for high performance work -- it allocates new buffers each time. -func Exec(ctx context.Context, logger log.Logger, timeoutSeconds int, possibleBins []string, args []string, includeStderr bool) ([]byte, error) { - ctx, span := traces.StartSpan(ctx, - "possible_binaries", possibleBins, - "args", args) +// +// `possibleBins` can be either a list of command names, or a list of paths to commands. +// Where reasonable, `possibleBins` should be command names only, so that we can perform +// lookup against PATH. +func Exec(ctx context.Context, logger log.Logger, timeoutSeconds int, execCmd allowedcmd.AllowedCommand, args []string, includeStderr bool) ([]byte, error) { + ctx, span := traces.StartSpan(ctx) defer span.End() ctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second) @@ -33,36 +35,30 @@ func Exec(ctx context.Context, logger log.Logger, timeoutSeconds int, possibleBi var stdout bytes.Buffer var stderr bytes.Buffer - for _, bin := range possibleBins { - stdout.Reset() - stderr.Reset() - - cmd := exec.CommandContext(ctx, bin, args...) - cmd.Stdout = &stdout - if includeStderr { - cmd.Stderr = &stdout - } else { - cmd.Stderr = &stderr - } + cmd, err := execCmd(ctx, args...) + if err != nil { + return nil, fmt.Errorf("creating command: %w", err) + } - level.Debug(logger).Log( - "msg", "execing", - "cmd", cmd.String(), - ) + cmd.Stdout = &stdout + if includeStderr { + cmd.Stderr = &stdout + } else { + cmd.Stderr = &stderr + } - switch err := cmd.Run(); { - case err == nil: - return stdout.Bytes(), nil - case os.IsNotExist(err): - // try the next binary - continue - default: - // an actual error - traces.SetError(span, err) - return nil, fmt.Errorf("exec '%s'. Got: '%s': %w", cmd.String(), string(stderr.Bytes()), err) - } + level.Debug(logger).Log( + "msg", "execing", + "cmd", cmd.String(), + ) + switch err := cmd.Run(); { + case err == nil: + return stdout.Bytes(), nil + case os.IsNotExist(err): + return nil, fmt.Errorf("could not find %s to run: %w", cmd.Path, err) + default: + traces.SetError(span, err) + return nil, fmt.Errorf("exec '%s'. Got: '%s': %w", cmd.String(), string(stderr.Bytes()), err) } - // Getting here means no binary was found - return nil, fmt.Errorf("No binary found in specified paths: %v: %w", possibleBins, os.ErrNotExist) } diff --git a/pkg/osquery/tables/tablehelpers/exec_osquery_launchctl.go b/pkg/osquery/tables/tablehelpers/exec_osquery_launchctl.go index d3a9ac48b..bfda01704 100644 --- a/pkg/osquery/tables/tablehelpers/exec_osquery_launchctl.go +++ b/pkg/osquery/tables/tablehelpers/exec_osquery_launchctl.go @@ -1,4 +1,5 @@ -// build +darwin +//go:build darwin +// +build darwin package tablehelpers @@ -8,13 +9,13 @@ import ( "encoding/json" "fmt" "os" - "os/exec" "os/user" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/kolide/launcher/pkg/agent" + "github.com/kolide/launcher/pkg/allowedcmd" ) // ExecOsqueryLaunchctl runs osquery under launchctl, in a user context. @@ -27,8 +28,7 @@ func ExecOsqueryLaunchctl(ctx context.Context, logger log.Logger, timeoutSeconds return nil, fmt.Errorf("looking up username %s: %w", username, err) } - cmd := exec.CommandContext(ctx, - "launchctl", + cmd, err := allowedcmd.Launchctl(ctx, "asuser", targetUser.Uid, osqueryPath, @@ -41,6 +41,9 @@ func ExecOsqueryLaunchctl(ctx context.Context, logger log.Logger, timeoutSeconds "--json", query, ) + if err != nil { + return nil, fmt.Errorf("creating launchctl command: %w", err) + } dir, err := agent.MkdirTemp("osq-launchctl") if err != nil { diff --git a/pkg/osquery/tables/tablehelpers/exec_test.go b/pkg/osquery/tables/tablehelpers/exec_test.go index e4a303f34..9b2f8abae 100644 --- a/pkg/osquery/tables/tablehelpers/exec_test.go +++ b/pkg/osquery/tables/tablehelpers/exec_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/go-kit/kit/log" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/stretchr/testify/assert" ) @@ -17,35 +18,14 @@ func TestExec(t *testing.T) { var tests = []struct { name string timeout int - bins []string + bin allowedcmd.AllowedCommand args []string err bool output string }{ - { - name: "timeout", - timeout: 1, - bins: []string{"/bin/sleep", "/usr/bin/sleep"}, - args: []string{"30"}, - err: true, - }, - { - name: "no binaries", - bins: []string{"/hello/world", "/hello/friends"}, - err: true, - }, - { - name: "false", - bins: []string{"/bin/false", "/usr/bin/false"}, - err: true, - }, - { - name: "eventually finds binary", - bins: []string{"/hello/world", "/bin/true", "/usr/bin/true"}, - }, { name: "output", - bins: []string{"/bin/echo"}, + bin: allowedcmd.Echo, args: []string{"hello"}, output: "hello\n", }, @@ -62,7 +42,7 @@ func TestExec(t *testing.T) { if tt.timeout == 0 { tt.timeout = 30 } - output, err := Exec(ctx, logger, tt.timeout, tt.bins, tt.args, false) + output, err := Exec(ctx, logger, tt.timeout, tt.bin, tt.args, false) if tt.err { assert.Error(t, err) assert.Empty(t, output) @@ -70,7 +50,6 @@ func TestExec(t *testing.T) { assert.NoError(t, err) assert.Equal(t, []byte(tt.output), output) } - }) } } diff --git a/pkg/osquery/tables/wifi_networks/wifi_networks.go b/pkg/osquery/tables/wifi_networks/wifi_networks.go index f3e2881dd..e5a1e5d2b 100644 --- a/pkg/osquery/tables/wifi_networks/wifi_networks.go +++ b/pkg/osquery/tables/wifi_networks/wifi_networks.go @@ -9,13 +9,13 @@ import ( _ "embed" "fmt" "os" - "os/exec" "path/filepath" "time" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/kolide/launcher/pkg/agent" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/dataflatten" "github.com/kolide/launcher/pkg/osquery/tables/dataflattentable" "github.com/osquery/osquery-go/plugin/table" @@ -90,13 +90,11 @@ func execPwsh(logger log.Logger) execer { return fmt.Errorf("writing native wifi code: %w", err) } - pwsh, err := exec.LookPath("powershell.exe") + args := append([]string{"-NoProfile", "-NonInteractive"}, string(pwshScript)) + cmd, err := allowedcmd.Powershell(ctx, args...) if err != nil { - return fmt.Errorf("finding powershell.exe path: %w", err) + return fmt.Errorf("creating powershell command: %w", err) } - - args := append([]string{"-NoProfile", "-NonInteractive"}, string(pwshScript)) - cmd := exec.CommandContext(ctx, pwsh, args...) cmd.Dir = dir var stderr bytes.Buffer cmd.Stdout = buf diff --git a/pkg/osquery/tables/xrdb/xrdb.go b/pkg/osquery/tables/xrdb/xrdb.go index 97f86e49c..2fc027339 100644 --- a/pkg/osquery/tables/xrdb/xrdb.go +++ b/pkg/osquery/tables/xrdb/xrdb.go @@ -11,7 +11,6 @@ import ( "fmt" "io" "os" - "os/exec" "os/user" "strconv" "strings" @@ -21,12 +20,11 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/kolide/launcher/pkg/agent" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" ) -var xrdbPath = "/usr/bin/xrdb" - const allowedUsernameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-." const allowedDisplayCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:." @@ -97,7 +95,10 @@ func execXRDB(ctx context.Context, displayNum, username string, buf *bytes.Buffe return fmt.Errorf("finding user by username '%s': %w", username, err) } - cmd := exec.CommandContext(ctx, xrdbPath, "-display", displayNum, "-global", "-query") + cmd, err := allowedcmd.Xrdb(ctx, "-display", displayNum, "-global", "-query") + if err != nil { + return fmt.Errorf("creating xrdb command: %w", err) + } // set the HOME cmd so that xrdb is exec'd properly as the new user. cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", u.HomeDir)) diff --git a/pkg/osquery/tables/zfs/tables.go b/pkg/osquery/tables/zfs/tables.go index 449f8b238..4e8abd884 100644 --- a/pkg/osquery/tables/zfs/tables.go +++ b/pkg/osquery/tables/zfs/tables.go @@ -1,3 +1,6 @@ +//go:build !windows +// +build !windows + package zfs import ( @@ -10,21 +13,17 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/allowedcmd" "github.com/kolide/launcher/pkg/osquery/tables/tablehelpers" "github.com/osquery/osquery-go/plugin/table" "github.com/pkg/errors" ) -const ( - zfsPath = "/usr/sbin/zfs" - zpoolPath = "/usr/sbin/zpool" -) - const allowedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.@/" type Table struct { logger log.Logger - cmd string + cmd allowedcmd.AllowedCommand } func columns() []table.ColumnDefinition { @@ -39,7 +38,7 @@ func columns() []table.ColumnDefinition { func ZfsPropertiesPlugin(logger log.Logger) *table.Plugin { t := &Table{ logger: logger, - cmd: zfsPath, + cmd: allowedcmd.Zfs, } return table.NewPlugin("kolide_zfs_properties", columns(), t.generate) @@ -48,7 +47,7 @@ func ZfsPropertiesPlugin(logger log.Logger) *table.Plugin { func ZpoolPropertiesPlugin(logger log.Logger) *table.Plugin { t := &Table{ logger: logger, - cmd: zpoolPath, + cmd: allowedcmd.Zpool, } return table.NewPlugin("kolide_zpool_properties", columns(), t.generate) @@ -75,7 +74,7 @@ func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ( args = append(args, names...) - output, err := tablehelpers.Exec(ctx, t.logger, 15, []string{t.cmd}, args, false) + output, err := tablehelpers.Exec(ctx, t.logger, 15, t.cmd, args, false) if err != nil { // exec will error if there's no binary, so we never want to record that if os.IsNotExist(errors.Cause(err)) { diff --git a/pkg/osquery/tables/zfs/tables_test.go b/pkg/osquery/tables/zfs/tables_test.go index f5c2e3d6c..5977ce34d 100644 --- a/pkg/osquery/tables/zfs/tables_test.go +++ b/pkg/osquery/tables/zfs/tables_test.go @@ -1,3 +1,6 @@ +//go:build !windows +// +build !windows + package zfs import ( diff --git a/pkg/packagekit/applenotarization/applenotarization.go b/pkg/packagekit/applenotarization/applenotarization.go index 4640aeac4..a6312ffd2 100644 --- a/pkg/packagekit/applenotarization/applenotarization.go +++ b/pkg/packagekit/applenotarization/applenotarization.go @@ -106,7 +106,7 @@ func (n *Notarizer) runNotarytool(ctx context.Context, command string, target st return []byte(n.fakeResponse), nil } - cmd := exec.CommandContext(ctx, "xcrun", baseArgs...) + cmd := exec.CommandContext(ctx, "xcrun", baseArgs...) //nolint:forbidigo // Fine to use exec.CommandContext outside of launcher proper out, err := cmd.CombinedOutput() if err != nil { diff --git a/pkg/packagekit/authenticode/authenticode_test.go b/pkg/packagekit/authenticode/authenticode_test.go index 40ee4c494..a472f1a5a 100644 --- a/pkg/packagekit/authenticode/authenticode_test.go +++ b/pkg/packagekit/authenticode/authenticode_test.go @@ -29,7 +29,7 @@ func TestSign(t *testing.T) { // create a signtoolOptions object so we can call the exec method so := &signtoolOptions{ - execCC: exec.CommandContext, + execCC: exec.CommandContext, //nolint:forbidigo // Fine to use exec.CommandContext in test } ctx, ctxCancel := context.WithTimeout(context.Background(), 120*time.Second) diff --git a/pkg/packagekit/authenticode/authenticode_windows.go b/pkg/packagekit/authenticode/authenticode_windows.go index 5ab933627..a5aef243e 100644 --- a/pkg/packagekit/authenticode/authenticode_windows.go +++ b/pkg/packagekit/authenticode/authenticode_windows.go @@ -46,7 +46,7 @@ func Sign(ctx context.Context, file string, opts ...SigntoolOpt) error { signtoolPath: "signtool.exe", timestampServer: "http://timestamp.verisign.com/scripts/timstamp.dll", rfc3161Server: "http://sha256timestamp.ws.symantec.com/sha256/timestamp", - execCC: exec.CommandContext, + execCC: exec.CommandContext, //nolint:forbidigo // Fine to use exec.CommandContext outside of launcher proper } for _, opt := range opts { diff --git a/pkg/packagekit/package_fpm.go b/pkg/packagekit/package_fpm.go index 520fa8ec7..bebd766a3 100644 --- a/pkg/packagekit/package_fpm.go +++ b/pkg/packagekit/package_fpm.go @@ -143,7 +143,7 @@ func PackageFPM(ctx context.Context, w io.Writer, po *PackageOptions, fpmOpts .. "kolide/fpm:latest", } - cmd := exec.CommandContext(ctx, "docker", append(dockerArgs, fpmCommand...)...) + cmd := exec.CommandContext(ctx, "docker", append(dockerArgs, fpmCommand...)...) //nolint:forbidigo // Fine to use exec.CommandContext outside of launcher proper stderr := new(bytes.Buffer) stdout := new(bytes.Buffer) diff --git a/pkg/packagekit/package_pkg.go b/pkg/packagekit/package_pkg.go index 165a08cc8..82ffad135 100644 --- a/pkg/packagekit/package_pkg.go +++ b/pkg/packagekit/package_pkg.go @@ -104,7 +104,7 @@ func runPkbuild(ctx context.Context, outputPath string, po *PackageOptions) erro // Run analyze to generate our component plist componentPlist := "./launcher.plist" - analyzeCmd := exec.CommandContext(ctx, "pkgbuild", "--analyze", "--root", po.Root, componentPlist) + analyzeCmd := exec.CommandContext(ctx, "pkgbuild", "--analyze", "--root", po.Root, componentPlist) //nolint:forbidigo // Fine to use exec.CommandContext outside of launcher proper if err := analyzeCmd.Run(); err != nil { return fmt.Errorf("running analyze: %w", err) } @@ -121,7 +121,7 @@ func runPkbuild(ctx context.Context, outputPath string, po *PackageOptions) erro // Set BundleIsRelocatable in the component plist to false -- this makes sure that the installer // will install Kolide.app to the location that we expect - replaceCmd := exec.CommandContext(ctx, "plutil", "-replace", "BundleIsRelocatable", "-bool", "false", componentPlist) + replaceCmd := exec.CommandContext(ctx, "plutil", "-replace", "BundleIsRelocatable", "-bool", "false", componentPlist) //nolint:forbidigo // Fine to use exec.CommandContext outside of launcher proper if err := replaceCmd.Run(); err != nil { return fmt.Errorf("running plutil -replace: %w", err) } @@ -148,7 +148,7 @@ func runPkbuild(ctx context.Context, outputPath string, po *PackageOptions) erro "args", fmt.Sprintf("%v", args), ) - cmd := exec.CommandContext(ctx, "pkgbuild", args...) + cmd := exec.CommandContext(ctx, "pkgbuild", args...) //nolint:forbidigo // Fine to use exec.CommandContext outside of launcher proper stderr := new(bytes.Buffer) cmd.Stderr = stderr @@ -227,7 +227,7 @@ func runProductbuild(ctx context.Context, flatPkgPath, distributionPkgPath strin "args", fmt.Sprintf("%v", args), ) - cmd := exec.CommandContext(ctx, "productbuild", args...) + cmd := exec.CommandContext(ctx, "productbuild", args...) //nolint:forbidigo // Fine to use exec.CommandContext outside of launcher proper stderr := new(bytes.Buffer) cmd.Stderr = stderr diff --git a/pkg/packagekit/wix/wix.go b/pkg/packagekit/wix/wix.go index 8a0f720ea..9c7c41343 100644 --- a/pkg/packagekit/wix/wix.go +++ b/pkg/packagekit/wix/wix.go @@ -107,7 +107,7 @@ func New(packageRoot string, mainWxsContent []byte, wixOpts ...WixOpt) (*wixTool wixPath: FindWixInstall(), packageRoot: packageRoot, - execCC: exec.CommandContext, + execCC: exec.CommandContext, //nolint:forbidigo // Fine to use exec.CommandContext outside of launcher proper } for _, opt := range wixOpts { diff --git a/pkg/packagekit/wix/wix_test.go b/pkg/packagekit/wix/wix_test.go index 8adcf61bb..a0c86bab3 100644 --- a/pkg/packagekit/wix/wix_test.go +++ b/pkg/packagekit/wix/wix_test.go @@ -62,7 +62,7 @@ func TestWixPackage(t *testing.T) { // which can mostly read MSI files. func verifyMsi(ctx context.Context, t *testing.T, outMsi string) { // Use the wix struct for its execOut - execWix := &wixTool{execCC: exec.CommandContext} + execWix := &wixTool{execCC: exec.CommandContext} //nolint:forbidigo // Fine to use exec.CommandContext in test fileContents, err := execWix.execOut(ctx, "7z", "x", "-so", outMsi) require.NoError(t, err) diff --git a/pkg/packaging/packaging.go b/pkg/packaging/packaging.go index 7e9367ded..84a8cf589 100644 --- a/pkg/packaging/packaging.go +++ b/pkg/packaging/packaging.go @@ -752,7 +752,7 @@ func (p *PackageOptions) setupDirectories() error { func (p *PackageOptions) execOut(ctx context.Context, argv0 string, args ...string) (string, error) { // Since PackageOptions is sometimes instantiated directly, set execCC if it's nil. if p.execCC == nil { - p.execCC = exec.CommandContext + p.execCC = exec.CommandContext //nolint:forbidigo // Fine to use exec.CommandContext outside of launcher proper } cmd := p.execCC(ctx, argv0, args...) diff --git a/pkg/packaging/packaging_test.go b/pkg/packaging/packaging_test.go index fd958d649..dc147cf32 100644 --- a/pkg/packaging/packaging_test.go +++ b/pkg/packaging/packaging_test.go @@ -19,7 +19,7 @@ import ( func helperCommandContext(ctx context.Context, command string, args ...string) (cmd *exec.Cmd) { cs := []string{"-test.run=TestHelperProcess", "--", command} cs = append(cs, args...) - cmd = exec.CommandContext(ctx, os.Args[0], cs...) + cmd = exec.CommandContext(ctx, os.Args[0], cs...) //nolint:forbidigo // Fine to use exec.CommandContext in test cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} return cmd } diff --git a/pkg/ptycmd/command.go b/pkg/ptycmd/command.go deleted file mode 100644 index 00aa8ee78..000000000 --- a/pkg/ptycmd/command.go +++ /dev/null @@ -1,129 +0,0 @@ -//go:build !windows -// +build !windows - -package ptycmd - -import ( - "fmt" - "os" - "os/exec" - "syscall" - "time" - "unsafe" - - "github.com/kr/pty" -) - -// NewCmd creates a new command attached to a pty -func NewCmd(command string, argv []string, options ...Option) (*Cmd, error) { - // create the command - cmd := exec.Command(command, argv...) - - // open a pty - pty, tty, err := pty.Open() - if err != nil { - return nil, fmt.Errorf("opening pty: %w", err) - } - defer tty.Close() - - // attach the stdin/stdout/stderr of the pty's tty to the cmd - cmd.Stdout = tty - cmd.Stdin = tty - cmd.Stderr = tty - - // start the cmd, closing the PTY if there's an error - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("starting command `%s`: %w", command, err) - } - - ptyClosed := make(chan struct{}) - - // create the command with default options - ptyCmd := &Cmd{ - command: command, - argv: argv, - cmd: cmd, - pty: pty, - ptyClosed: ptyClosed, - closeSignal: syscall.SIGINT, - closeTimeout: 10 * time.Second, - } - - // apply the given options using the functional opts pattern - for _, option := range options { - option(ptyCmd) - } - - // if the process is closed, close the pty - go func() { - ptyCmd.cmd.Wait() - ptyCmd.pty.Close() - close(ptyCmd.ptyClosed) - }() - - return ptyCmd, nil -} - -// Read reads into the specified buffer from the pty -func (c *Cmd) Read(p []byte) (n int, err error) { - return c.pty.Read(p) -} - -// Write writes from the specified buffer into the pty -func (c *Cmd) Write(p []byte) (n int, err error) { - return c.pty.Write(p) -} - -// Close closes the underlying process and the pty -func (c *Cmd) Close() error { - // if the process exists, send the close signal - if c.cmd != nil && c.cmd.Process != nil { - c.cmd.Process.Signal(c.closeSignal) - } - for { - select { - // either it was closed - case <-c.ptyClosed: - return nil - - // or timeout - case <-time.After(c.closeTimeout): - c.cmd.Process.Signal(syscall.SIGKILL) - return nil - } - } -} - -// Title returns a title for the TTY window -func (c *Cmd) Title() string { - hostName, err := os.Hostname() - if err != nil { - hostName = "remote-host" - } - return fmt.Sprintf("%s@%s", c.command, hostName) -} - -// Resize resizes the terminal of the process -func (c *Cmd) Resize(width int, height int) error { - window := struct { - row uint16 - col uint16 - x uint16 - y uint16 - }{ - uint16(height), - uint16(width), - 0, - 0, - } - _, _, errno := syscall.Syscall( - syscall.SYS_IOCTL, - c.pty.Fd(), - syscall.TIOCSWINSZ, - uintptr(unsafe.Pointer(&window)), - ) - if errno != 0 { - return errno - } - return nil -} diff --git a/pkg/ptycmd/command_windows.go b/pkg/ptycmd/command_windows.go deleted file mode 100644 index b20623f94..000000000 --- a/pkg/ptycmd/command_windows.go +++ /dev/null @@ -1,34 +0,0 @@ -//go:build windows -// +build windows - -package ptycmd - -import ( - "errors" -) - -// NewCmd creates a new command attached to a pty -func NewCmd(command string, argv []string, options ...Option) (*Cmd, error) { - return nil, errors.New("Not supported on windows") -} - -func (c *Cmd) Close() error { - return errors.New("Not supported on windows") -} - -func (c *Cmd) Read(p []byte) (n int, err error) { - return c.pty.Read(p) -} - -// Write writes from the specified buffer into the pty -func (c *Cmd) Write(p []byte) (n int, err error) { - return c.pty.Write(p) -} - -func (c *Cmd) Resize(width int, height int) error { - return nil -} - -func (c *Cmd) Title() string { - return "" -} diff --git a/pkg/ptycmd/options.go b/pkg/ptycmd/options.go deleted file mode 100644 index 00e25f141..000000000 --- a/pkg/ptycmd/options.go +++ /dev/null @@ -1,51 +0,0 @@ -package ptycmd - -import ( - "os" - "os/exec" - "syscall" - "time" -) - -// Option is a configuration option for a Cmd -type Option func(*Cmd) - -// Cmd is a shelled out command and an attached pty -type Cmd struct { - // the command that is being relayed - command string // nolint:unused - - // args passed to the command - argv []string // nolint:unused - - // the external command struct - cmd *exec.Cmd // nolint:unused - - // the pseudoterminal attached to the command - pty *os.File - - // channel to signal closing the pty - ptyClosed chan struct{} // nolint:unused - - // signal to close process - closeSignal syscall.Signal - - // time to wait to close process - closeTimeout time.Duration -} - -// WithCloseSignal lets you specify the signal to send to the -// underlying process on close -func WithCloseSignal(signal syscall.Signal) Option { - return func(c *Cmd) { - c.closeSignal = signal - } -} - -// WithCloseTimeout lets you specify how long to wait for the -// underlying process to terminate before sending a SIGKILL -func WithCloseTimeout(timeout time.Duration) Option { - return func(c *Cmd) { - c.closeTimeout = timeout - } -}