diff --git a/service/internal/config/config.go b/service/internal/config/config.go index 9db0593..47ed103 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -11,6 +11,7 @@ type Action struct { Title string Icon string Shell string + Exec []string ShellAfterCompleted string Timeout int Acls []string diff --git a/service/internal/executor/arguments.go b/service/internal/executor/arguments.go index 2485078..46e16bf 100644 --- a/service/internal/executor/arguments.go +++ b/service/internal/executor/arguments.go @@ -294,3 +294,68 @@ func mangleInvalidDatetimeValues(req *ExecutionRequest, arg *config.ActionArgume req.Arguments[arg.Name] = timestamp.Format("2006-01-02T15:04:05") } } + +func parseActionExec(values map[string]string, action *config.Action, entityPrefix string) ([]string, error) { + for _, arg := range action.Arguments { + argName := arg.Name + argValue := values[argName] + + err := typecheckActionArgument(&arg, argValue, action) + + if err != nil { + return nil, err + } + + log.WithFields(log.Fields{ + "name": argName, + "value": argValue, + }).Debugf("Arg assigned") + } + + parsedArgs := make([]string, len(action.Exec)) + for i, arg := range action.Exec { + parsedArg, err := parseCommandForReplacements(arg, values) + if err != nil { + return nil, err + } + + parsedArg = sv.ReplaceEntityVars(entityPrefix, parsedArg) + parsedArgs[i] = parsedArg + } + + redactedArgs := redactExecArgs(parsedArgs, action.Arguments, values) + + log.WithFields(log.Fields{ + "actionTitle": action.Title, + "cmd": redactedArgs, + }).Infof("Action parse args - After (Exec)") + + return parsedArgs, nil +} + +//gocyclo:ignore +func redactExecArgs(execArgs []string, arguments []config.ActionArgument, argumentValues map[string]string) []string { + redacted := make([]string, len(execArgs)) + for i, arg := range execArgs { + redacted[i] = redactShellCommand(arg, arguments, argumentValues) + } + return redacted +} + +func checkShellArgumentSafety(action *config.Action) error { + if action.Shell == "" { + return nil + } + + unsafeTypes := []string{"url", "email", "raw_string_multiline", "very_dangerous_raw_string"} + + for _, arg := range action.Arguments { + for _, unsafeType := range unsafeTypes { + if arg.Type == unsafeType { + return fmt.Errorf("unsafe argument type '%s' cannot be used with Shell execution. Use 'exec' instead. See https://docs.olivetin.app/action_execution/shellvsexec.html", arg.Type) + } + } + } + + return nil +} diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 4ae17f3..1da6bd8 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -15,6 +15,7 @@ import ( "context" "fmt" "os" + "os/exec" "path" "strings" "sync" @@ -67,6 +68,8 @@ type ExecutionRequest struct { logEntry *InternalLogEntry finalParsedCommand string + execArgs []string + useDirectExec bool executor *Executor } @@ -405,7 +408,21 @@ func stepParseArgs(req *ExecutionRequest) bool { mangleInvalidArgumentValues(req) - req.finalParsedCommand, err = parseActionArguments(req.Arguments, req.Action, req.EntityPrefix) + if len(req.Action.Exec) > 0 { + req.useDirectExec = true + req.execArgs, err = parseActionExec(req.Arguments, req.Action, req.EntityPrefix) + } else { + req.useDirectExec = false + + err = checkShellArgumentSafety(req.Action) + if err != nil { + req.logEntry.Output = err.Error() + log.Warn(err.Error()) + return false + } + + req.finalParsedCommand, err = parseActionArguments(req.Arguments, req.Action, req.EntityPrefix) + } if err != nil { req.logEntry.Output = err.Error() @@ -545,7 +562,13 @@ func stepExec(req *ExecutionRequest) bool { streamer := &OutputStreamer{Req: req} - cmd := wrapCommandInShell(ctx, req.finalParsedCommand) + var cmd *exec.Cmd + if req.useDirectExec { + cmd = wrapCommandDirect(ctx, req.execArgs) + } else { + cmd = wrapCommandInShell(ctx, req.finalParsedCommand) + } + cmd.Stdout = streamer cmd.Stderr = streamer cmd.Env = buildEnv(req.Arguments) diff --git a/service/internal/executor/executor_unix.go b/service/internal/executor/executor_unix.go index 54ef686..1a54685 100644 --- a/service/internal/executor/executor_unix.go +++ b/service/internal/executor/executor_unix.go @@ -21,5 +21,17 @@ func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cm cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} return cmd - +} + +func wrapCommandDirect(ctx context.Context, execArgs []string) *exec.Cmd { + if len(execArgs) == 0 { + return nil + } + + cmd := exec.CommandContext(ctx, execArgs[0], execArgs[1:]...) + + // This is to ensure that the process group is killed when the parent process is killed. + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + return cmd } diff --git a/service/internal/executor/executor_windows.go b/service/internal/executor/executor_windows.go index 511a7e4..fcb115b 100644 --- a/service/internal/executor/executor_windows.go +++ b/service/internal/executor/executor_windows.go @@ -22,3 +22,11 @@ func wrapCommandInShell(ctx context.Context, finalParsedCommand string) *exec.Cm return exec.CommandContext(ctx, "cmd", "/u", "/C", finalParsedCommand) } } + +func wrapCommandDirect(ctx context.Context, execArgs []string) *exec.Cmd { + if len(execArgs) == 0 { + return nil + } + + return exec.CommandContext(ctx, execArgs[0], execArgs[1:]...) +}