@@ -44,12 +44,16 @@ type noopLogger struct{}
45func (noopLogger) InfoPersist(msg string, keysAndValues ...interface{}) {}
45func (noopLogger) InfoPersist(msg string, keysAndValues ...interface{}) {}
47+// CommandBlockFunc is a function that determines if a command should be blocked
48+type CommandBlockFunc func(args []string) bool
47// Shell provides cross-platform shell execution with optional state persistence
50// Shell provides cross-platform shell execution with optional state persistence
48type Shell struct {
51type Shell struct {
49− env []string
52+ env []string
50− cwd string
53+ cwd string
51− mu sync.Mutex
54+ mu sync.Mutex
52− logger Logger
55+ logger Logger
56+ blockFuncs []CommandBlockFunc
55// Options for creating a new shell
59// Options for creating a new shell
@@ -57,6 +61,7 @@ type Options struct {
57 WorkingDir string
61 WorkingDir string
58 Env []string
62 Env []string
59 Logger Logger
63 Logger Logger
64+ BlockFuncs []CommandBlockFunc
62// NewShell creates a new shell instance with the given options
67// NewShell creates a new shell instance with the given options
@@ -81,9 +86,10 @@ func NewShell(opts *Options) *Shell {
83 return &Shell{
88 return &Shell{
84− cwd: cwd,
89+ cwd: cwd,
85− env: env,
90+ env: env,
86− logger: logger,
91+ logger: logger,
92+ blockFuncs: opts.BlockFuncs,
@@ -152,6 +158,13 @@ func (s *Shell) SetEnv(key, value string) {
152 s.env = append(s.env, keyPrefix+value)
158 s.env = append(s.env, keyPrefix+value)
161+// SetBlockFuncs sets the command block functions for the shell
162+func (s *Shell) SetBlockFuncs(blockFuncs []CommandBlockFunc) {
165+ s.blockFuncs = blockFuncs
155// Windows-specific commands that should use native shell
168// Windows-specific commands that should use native shell
156var windowsNativeCommands = map[string]bool{
169var windowsNativeCommands = map[string]bool{
157 "dir": true,
170 "dir": true,
@@ -203,6 +216,59 @@ func (s *Shell) determineShellType(command string) ShellType {
203 return ShellTypePOSIX
216 return ShellTypePOSIX
219+// CreateSimpleCommandBlocker creates a CommandBlockFunc that blocks exact command matches
220+func CreateSimpleCommandBlocker(bannedCommands []string) CommandBlockFunc {
221+ bannedSet := make(map[string]bool)
222+ for _, cmd := range bannedCommands {
223+ bannedSet[cmd] = true
226+ return func(args []string) bool {
230+ return bannedSet[args[0]]
234+// CreateSubCommandBlocker creates a CommandBlockFunc that blocks specific subcommands
235+func CreateSubCommandBlocker(blockedSubCommands [][]string) CommandBlockFunc {
236+ return func(args []string) bool {
237+ for _, blocked := range blockedSubCommands {
238+ if len(args) >= len(blocked) {
240+ for i, part := range blocked {
241+ if args[i] != part {
254+func (s *Shell) createCommandBlockHandler() func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
255+ return func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
256+ return func(ctx context.Context, args []string) error {
258+ return next(ctx, args)
261+ for _, blockFunc := range s.blockFuncs {
262+ if blockFunc(args) {
263+ return fmt.Errorf("command '%s' is not allowed for security reasons", strings.Join(args, " "))
267+ return next(ctx, args)
206// execWindows executes commands using native Windows shells (cmd.exe or PowerShell)
272// execWindows executes commands using native Windows shells (cmd.exe or PowerShell)
207func (s *Shell) execWindows(ctx context.Context, command string, shell string) (string, string, error) {
273func (s *Shell) execWindows(ctx context.Context, command string, shell string) (string, string, error) {
208 var cmd *exec.Cmd
274 var cmd *exec.Cmd
@@ -291,6 +357,7 @@ func (s *Shell) execPOSIX(ctx context.Context, command string) (string, string,
291 interp.Interactive(false),
357 interp.Interactive(false),
292 interp.Env(expand.ListEnviron(s.env...)),
358 interp.Env(expand.ListEnviron(s.env...)),
293 interp.Dir(s.cwd),
359 interp.Dir(s.cwd),
360+ interp.ExecHandlers(s.createCommandBlockHandler()),
295 if err != nil {
362 if err != nil {
296 return "", "", fmt.Errorf("could not run command: %w", err)
363 return "", "", fmt.Errorf("could not run command: %w", err)