diff --git a/ee/tables/crowdstrike/falconctl/table.go b/ee/tables/crowdstrike/falconctl/table.go index 67215e298..4442b5c0d 100644 --- a/ee/tables/crowdstrike/falconctl/table.go +++ b/ee/tables/crowdstrike/falconctl/table.go @@ -38,7 +38,7 @@ var ( defaultOption = strings.Join(allowedOptions, " ") ) -type execFunc func(context.Context, *slog.Logger, int, allowedcmd.AllowedCommand, []string, bool) ([]byte, error) +type execFunc func(context.Context, *slog.Logger, int, allowedcmd.AllowedCommand, []string, bool, ...tablehelpers.ExecOps) ([]byte, error) type falconctlOptionsTable struct { slogger *slog.Logger diff --git a/ee/tables/crowdstrike/falconctl/table_test.go b/ee/tables/crowdstrike/falconctl/table_test.go index a1f618ec4..6a443aebb 100644 --- a/ee/tables/crowdstrike/falconctl/table_test.go +++ b/ee/tables/crowdstrike/falconctl/table_test.go @@ -86,7 +86,7 @@ func TestOptionRestrictions(t *testing.T) { } } -func noopExec(ctx context.Context, slogger *slog.Logger, _ int, _ allowedcmd.AllowedCommand, args []string, _ bool) ([]byte, error) { +func noopExec(ctx context.Context, slogger *slog.Logger, _ int, _ allowedcmd.AllowedCommand, args []string, _ bool, _ ...tablehelpers.ExecOps) ([]byte, error) { slogger.Log(ctx, slog.LevelInfo, "exec-in-test", "args", strings.Join(args, " ")) return []byte{}, nil } diff --git a/ee/tables/homebrew/upgradeable.go b/ee/tables/homebrew/upgradeable.go index 45689dbe9..92f9ea183 100644 --- a/ee/tables/homebrew/upgradeable.go +++ b/ee/tables/homebrew/upgradeable.go @@ -6,13 +6,8 @@ package brew_upgradeable import ( "context" "fmt" - "io" "log/slog" - "os/exec" - "os/user" - "strconv" "strings" - "syscall" "github.com/kolide/launcher/ee/allowedcmd" "github.com/kolide/launcher/ee/dataflatten" @@ -25,7 +20,6 @@ const allowedCharacters = "0123456789" type Table struct { slogger *slog.Logger - execCC allowedcmd.AllowedCommand } func TablePlugin(slogger *slog.Logger) *table.Plugin { @@ -35,7 +29,6 @@ func TablePlugin(slogger *slog.Logger) *table.Plugin { t := &Table{ slogger: slogger.With("table", "kolide_brew_upgradeable"), - execCC: allowedcmd.Brew, } return table.NewPlugin("kolide_brew_upgradeable", columns, t.generate) @@ -51,7 +44,8 @@ func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ( for _, uid := range uids { for _, dataQuery := range tablehelpers.GetConstraints(queryContext, "query", tablehelpers.WithDefaults("*")) { - output, err := t.getBrewOutdated(ctx, uid) + // Brew can take a while to load the first time the command is ran, so leaving 60 seconds for the timeout here. + output, err := tablehelpers.Exec(ctx, t.slogger, 60, allowedcmd.Brew, []string{"outdated", "--json"}, true, tablehelpers.WithUid(uid)) if err != nil { t.slogger.Log(ctx, slog.LevelInfo, "failure querying user brew installed packages", "err", err, "target_uid", uid) continue @@ -78,69 +72,3 @@ func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ( return results, nil } - -func (t *Table) getBrewOutdated(ctx context.Context, uid string) ([]byte, error) { - cmd, err := t.execCC(ctx, "outdated", "--json") - if err != nil { - return nil, fmt.Errorf("creating brew outdated command: %w", err) - } - - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, fmt.Errorf("assigning StdoutPipe for brew outdated command: %w", err) - } - - if err := runAsUser(ctx, uid, cmd); err != nil { - return nil, fmt.Errorf("runAsUser brew outdated command as user %s: %w", uid, err) - } - - data, err := io.ReadAll(stdout) - if err != nil { - return nil, fmt.Errorf("ReadAll brew outdated stdout: %w", err) - } - - if err := cmd.Wait(); err != nil { - return nil, fmt.Errorf("deallocation of brew outdated command as user %s: %w", uid, err) - } - - return data, nil -} - -func runAsUser(ctx context.Context, uid string, cmd *exec.Cmd) error { - currentUser, err := user.Current() - if err != nil { - return fmt.Errorf("getting current user: %w", err) - } - - runningUser, err := user.LookupId(uid) - if err != nil { - return fmt.Errorf("looking up user with uid %s: %w", uid, err) - } - - if currentUser.Uid != "0" { - if currentUser.Uid != runningUser.Uid { - return fmt.Errorf("current user %s is not root and can't start process for other user %s", currentUser.Uid, uid) - } - - return cmd.Start() - } - - runningUserUid, err := strconv.ParseUint(runningUser.Uid, 10, 32) - if err != nil { - return fmt.Errorf("converting uid %s to int: %w", runningUser.Uid, err) - } - - runningUserGid, err := strconv.ParseUint(runningUser.Gid, 10, 32) - if err != nil { - return fmt.Errorf("converting gid %s to int: %w", runningUser.Gid, err) - } - - cmd.SysProcAttr = &syscall.SysProcAttr{ - Credential: &syscall.Credential{ - Uid: uint32(runningUserUid), - Gid: uint32(runningUserGid), - }, - } - - return cmd.Start() -} diff --git a/ee/tables/nix_env/upgradeable/upgradeable.go b/ee/tables/nix_env/upgradeable/upgradeable.go index 129481c99..61cdff126 100644 --- a/ee/tables/nix_env/upgradeable/upgradeable.go +++ b/ee/tables/nix_env/upgradeable/upgradeable.go @@ -6,13 +6,8 @@ package nix_env_upgradeable import ( "context" "fmt" - "io" "log/slog" - "os/exec" - "os/user" - "strconv" "strings" - "syscall" "github.com/kolide/launcher/ee/allowedcmd" "github.com/kolide/launcher/ee/dataflatten" @@ -25,7 +20,6 @@ const allowedCharacters = "0123456789" type Table struct { slogger *slog.Logger - execCC allowedcmd.AllowedCommand } func TablePlugin(slogger *slog.Logger) *table.Plugin { @@ -35,7 +29,6 @@ func TablePlugin(slogger *slog.Logger) *table.Plugin { t := &Table{ slogger: slogger.With("table", "kolide_nix_upgradeable"), - execCC: allowedcmd.NixEnv, } return table.NewPlugin("kolide_nix_upgradeable", columns, t.generate) @@ -51,13 +44,10 @@ func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ( for _, uid := range uids { for _, dataQuery := range tablehelpers.GetConstraints(queryContext, "query", tablehelpers.WithDefaults("*")) { - output, err := t.getUserPackages(ctx, uid) + // Nix takes a while to load, so leaving a minute timeout here to give enough time. More might be needed. + output, err := tablehelpers.Exec(ctx, t.slogger, 60, allowedcmd.NixEnv, []string{"--query", "--installed", "-c", "--xml"}, true, tablehelpers.WithUid(uid)) if err != nil { - t.slogger.Log(ctx, slog.LevelInfo, - "failure querying user installed packages", - "err", err, - "target_uid", uid, - ) + t.slogger.Log(ctx, slog.LevelInfo, "failure querying user installed packages", "err", err, "target_uid", uid) continue } @@ -68,10 +58,7 @@ func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ( flattened, err := dataflatten.Xml(output, flattenOpts...) if err != nil { - t.slogger.Log(ctx, slog.LevelInfo, - "failure flattening output", - "err", err, - ) + t.slogger.Log(ctx, slog.LevelInfo, "failure flattening output", "err", err) continue } @@ -85,69 +72,3 @@ func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ( return results, nil } - -func (t *Table) getUserPackages(ctx context.Context, uid string) ([]byte, error) { - cmd, err := t.execCC(ctx, "--query", "--installed", "-c", "--xml") - if err != nil { - return nil, fmt.Errorf("creating nix-env command: %w", err) - } - - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, fmt.Errorf("assigning StdoutPipe for nix-env command: %w", err) - } - - if err := runAsUser(ctx, uid, cmd); err != nil { - return nil, fmt.Errorf("runAsUser nix-env command as user %s: %w", uid, err) - } - - data, err := io.ReadAll(stdout) - if err != nil { - return nil, fmt.Errorf("ReadAll nix-env stdout: %w", err) - } - - if err := cmd.Wait(); err != nil { - return nil, fmt.Errorf("deallocation of nix-env command as user %s: %w", uid, err) - } - - return data, nil -} - -func runAsUser(ctx context.Context, uid string, cmd *exec.Cmd) error { - currentUser, err := user.Current() - if err != nil { - return fmt.Errorf("getting current user: %w", err) - } - - runningUser, err := user.LookupId(uid) - if err != nil { - return fmt.Errorf("looking up user with uid %s: %w", uid, err) - } - - if currentUser.Uid != "0" { - if currentUser.Uid != runningUser.Uid { - return fmt.Errorf("current user %s is not root and can't start process for other user %s", currentUser.Uid, uid) - } - - return cmd.Start() - } - - runningUserUid, err := strconv.ParseUint(runningUser.Uid, 10, 32) - if err != nil { - return fmt.Errorf("converting uid %s to int: %w", runningUser.Uid, err) - } - - runningUserGid, err := strconv.ParseUint(runningUser.Gid, 10, 32) - if err != nil { - return fmt.Errorf("converting gid %s to int: %w", runningUser.Gid, err) - } - - cmd.SysProcAttr = &syscall.SysProcAttr{ - Credential: &syscall.Credential{ - Uid: uint32(runningUserUid), - Gid: uint32(runningUserGid), - }, - } - - return cmd.Start() -} diff --git a/ee/tables/tablehelpers/exec.go b/ee/tables/tablehelpers/exec.go index 50b86876b..f8392052c 100644 --- a/ee/tables/tablehelpers/exec.go +++ b/ee/tables/tablehelpers/exec.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "os" + "os/exec" "path/filepath" "time" @@ -14,6 +15,10 @@ import ( "go.opentelemetry.io/otel/attribute" ) +// ExecOps is a type for functional arguments to Exec, which changes the behavior of the exec command. +// An example of this is to run the exec as a specific user instead of root. +type ExecOps func(*exec.Cmd) error + // Exec is a wrapper over exec.CommandContext. It does a couple of // additional things to help with table usage: // 1. It enforces a timeout. @@ -26,7 +31,7 @@ import ( // `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, slogger *slog.Logger, timeoutSeconds int, execCmd allowedcmd.AllowedCommand, args []string, includeStderr bool) ([]byte, error) { +func Exec(ctx context.Context, slogger *slog.Logger, timeoutSeconds int, execCmd allowedcmd.AllowedCommand, args []string, includeStderr bool, opts ...ExecOps) ([]byte, error) { ctx, span := traces.StartSpan(ctx) defer span.End() @@ -41,6 +46,12 @@ func Exec(ctx context.Context, slogger *slog.Logger, timeoutSeconds int, execCmd return nil, fmt.Errorf("creating command: %w", err) } + for _, opt := range opts { + if err := opt(cmd); err != nil { + return nil, err + } + } + span.SetAttributes(attribute.String("exec.path", cmd.Path)) span.SetAttributes(attribute.String("exec.binary", filepath.Base(cmd.Path))) span.SetAttributes(attribute.StringSlice("exec.args", args)) diff --git a/ee/tables/tablehelpers/run_as_user_posix.go b/ee/tables/tablehelpers/run_as_user_posix.go new file mode 100644 index 000000000..923c1f7f1 --- /dev/null +++ b/ee/tables/tablehelpers/run_as_user_posix.go @@ -0,0 +1,50 @@ +//go:build !windows +// +build !windows + +package tablehelpers + +import ( + "fmt" + "os/exec" + "os/user" + "strconv" + "syscall" +) + +// WithUid is a functional argument which modifies the input exec command to run as a specific user. +func WithUid(uid string) ExecOps { + return func(cmd *exec.Cmd) error { + currentUser, err := user.Current() + if err != nil { + return fmt.Errorf("getting current user: %w", err) + } + + runningUser, err := user.LookupId(uid) + if err != nil { + return fmt.Errorf("looking up user with uid %s: %w", uid, err) + } + + if currentUser.Uid != "0" && currentUser.Uid != runningUser.Uid { + return fmt.Errorf("current user %s is not root and can't start process for other user %s", currentUser.Uid, uid) + } + + runningUserUid, err := strconv.ParseUint(runningUser.Uid, 10, 32) + if err != nil { + return fmt.Errorf("converting uid %s to int: %w", runningUser.Uid, err) + } + + runningUserGid, err := strconv.ParseUint(runningUser.Gid, 10, 32) + if err != nil { + return fmt.Errorf("converting gid %s to int: %w", runningUser.Gid, err) + } + + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{ + Uid: uint32(runningUserUid), + Gid: uint32(runningUserGid), + }, + } + + return nil + } +}