diff --git a/README.md b/README.md index 2fb7c7d8..7f834d8d 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,13 @@ Every access path is default-deny: | Resource | Default | Opt-in | |----------------------|-------------------------------------|----------------------------------------------| -| Command execution | All commands blocked (exit code 127)| `AllowedCommands` with namespaced command list (e.g. `rshell:cat`), or `AllowAllCommands` | +| Command execution | All commands blocked (exit code 127)| `AllowedCommands` with namespaced command list (e.g. `rshell:cat`) | | External commands | Blocked (exit code 127) | Provide an `ExecHandler` | | Filesystem access | Blocked | Configure `AllowedPaths` with directory list | | Environment variables| Empty (no host env inherited) | Pass variables via the `Env` option | | Output redirections | Only `/dev/null` allowed (exit code 2 for other targets) | `>/dev/null`, `2>/dev/null`, `&>/dev/null`, `2>&1` | -**AllowedCommands** restricts which commands (builtins or external) the interpreter may execute. Commands must be specified with the `rshell:` namespace prefix (e.g. `rshell:cat`, `rshell:echo`). If not set, no commands are allowed. Use `AllowAllCommands` to permit all commands (useful for testing). +**AllowedCommands** restricts which commands (builtins or external) the interpreter may execute. Commands must be specified with the `rshell:` namespace prefix (e.g. `rshell:cat`, `rshell:echo`). If not set, no commands are allowed. **AllowedPaths** restricts all file operations to specified directories using Go's `os.Root` API (`openat` syscalls), making it immune to symlink traversal, TOCTOU races, and `..` escape attacks. diff --git a/SHELL_FEATURES.md b/SHELL_FEATURES.md index fa675703..fc395fd2 100644 --- a/SHELL_FEATURES.md +++ b/SHELL_FEATURES.md @@ -96,7 +96,6 @@ Blocked features are rejected before execution with exit code 2. ## Execution - ✅ AllowedCommands — restricts which commands (builtins or external) may be executed; commands require the `rshell:` namespace prefix (e.g. `rshell:cat`); if not set, no commands are allowed -- ✅ AllowAllCommands — permits any command (testing convenience) - ✅ AllowedPaths filesystem sandboxing — restricts all file access to specified directories - ✅ Whole-run execution timeout — callers can bound a `Run()` call via `context.Context`, `interp.MaxExecutionTime`, or the CLI `--timeout` flag; the deadline applies to the entire script, not each individual command - ✅ ProcPath — overrides the proc filesystem path used by `ps` (default `/proc`; Linux-only; useful for testing/container environments) diff --git a/builtins/ps/ps_procpath_linux_test.go b/builtins/ps/ps_procpath_linux_test.go index c0dc11eb..b5801ce0 100644 --- a/builtins/ps/ps_procpath_linux_test.go +++ b/builtins/ps/ps_procpath_linux_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/require" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -34,7 +35,7 @@ func runScriptWithProcPath(t *testing.T, script, procPath string) (stdout, stder var outBuf, errBuf bytes.Buffer runner, err := interp.New( interp.StdIO(nil, &outBuf, &errBuf), - interp.AllowAllCommands(), + interpoption.AllowAllCommands().(interp.RunnerOption), interp.ProcPath(procPath), ) if err != nil { diff --git a/builtins/ps/ps_test.go b/builtins/ps/ps_test.go index 97389554..3c28fc6e 100644 --- a/builtins/ps/ps_test.go +++ b/builtins/ps/ps_test.go @@ -16,6 +16,7 @@ import ( "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -27,7 +28,7 @@ func runScript(t *testing.T, script string) (stdout, stderr string, code int) { t.Fatal(err) } var outBuf, errBuf bytes.Buffer - runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()) + runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)) if err != nil { t.Fatal(err) } diff --git a/builtins/tests/cat/helpers_test.go b/builtins/tests/cat/helpers_test.go index 0023ae7f..58915405 100644 --- a/builtins/tests/cat/helpers_test.go +++ b/builtins/tests/cat/helpers_test.go @@ -14,6 +14,7 @@ import ( "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -25,7 +26,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts .. t.Fatal(err) } var outBuf, errBuf bytes.Buffer - allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...) + allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...) runner, err := interp.New(allOpts...) if err != nil { t.Fatal(err) diff --git a/builtins/tests/cut/cut_gnu_compat_test.go b/builtins/tests/cut/cut_gnu_compat_test.go index a54aa0ff..01ffa6d6 100644 --- a/builtins/tests/cut/cut_gnu_compat_test.go +++ b/builtins/tests/cut/cut_gnu_compat_test.go @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/require" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -31,7 +32,7 @@ func cutRun(t *testing.T, script, dir string) (string, string, int) { opts := []interp.RunnerOption{ interp.StdIO(nil, &outBuf, &errBuf), interp.AllowedPaths([]string{dir}), - interp.AllowAllCommands(), + interpoption.AllowAllCommands().(interp.RunnerOption), } runner, err := interp.New(opts...) diff --git a/builtins/tests/cut/cut_pentest_test.go b/builtins/tests/cut/cut_pentest_test.go index 5e8ca8b3..0252388c 100644 --- a/builtins/tests/cut/cut_pentest_test.go +++ b/builtins/tests/cut/cut_pentest_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -37,7 +38,7 @@ func cutPentestRunCtx(ctx context.Context, t *testing.T, script, dir string) (st opts := []interp.RunnerOption{ interp.StdIO(nil, &outBuf, &errBuf), interp.AllowedPaths([]string{dir}), - interp.AllowAllCommands(), + interpoption.AllowAllCommands().(interp.RunnerOption), } runner, err := interp.New(opts...) diff --git a/builtins/tests/cut/cut_test.go b/builtins/tests/cut/cut_test.go index c26518bc..e8c57fb2 100644 --- a/builtins/tests/cut/cut_test.go +++ b/builtins/tests/cut/cut_test.go @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/require" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -32,7 +33,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts .. prog, err := parser.Parse(strings.NewReader(script), "") require.NoError(t, err) var outBuf, errBuf bytes.Buffer - allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...) + allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...) runner, err := interp.New(allOpts...) require.NoError(t, err) defer runner.Close() diff --git a/builtins/tests/head/helpers_test.go b/builtins/tests/head/helpers_test.go index 0050716b..67d86053 100644 --- a/builtins/tests/head/helpers_test.go +++ b/builtins/tests/head/helpers_test.go @@ -14,6 +14,7 @@ import ( "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -25,7 +26,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts .. t.Fatal(err) } var outBuf, errBuf bytes.Buffer - allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...) + allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...) runner, err := interp.New(allOpts...) if err != nil { t.Fatal(err) diff --git a/builtins/tests/help/help_test.go b/builtins/tests/help/help_test.go index 9ff0b31b..ff7ec50f 100644 --- a/builtins/tests/help/help_test.go +++ b/builtins/tests/help/help_test.go @@ -16,6 +16,7 @@ import ( "github.com/stretchr/testify/require" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -53,7 +54,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts .. // --- Exit code --- func TestHelpExitCode(t *testing.T) { - stdout, stderr, code := runScript(t, "help", "", interp.AllowAllCommands()) + stdout, stderr, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 0, code) assert.Empty(t, stderr) assert.NotEmpty(t, stdout) @@ -62,7 +63,7 @@ func TestHelpExitCode(t *testing.T) { // --- Output content --- func TestHelpListsAllBuiltins(t *testing.T) { - stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands()) + stdout, _, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 0, code) // Every registered builtin should appear in the output. @@ -78,7 +79,7 @@ func TestHelpListsAllBuiltins(t *testing.T) { } func TestHelpListsSorted(t *testing.T) { - stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands()) + stdout, _, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 0, code) lines := strings.Split(strings.TrimSpace(stdout), "\n") @@ -108,7 +109,7 @@ func TestHelpListsSorted(t *testing.T) { } func TestHelpIncludesDescriptions(t *testing.T) { - stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands()) + stdout, _, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 0, code) // Spot-check a few descriptions. @@ -119,13 +120,13 @@ func TestHelpIncludesDescriptions(t *testing.T) { } func TestHelpIncludesFooterHint(t *testing.T) { - stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands()) + stdout, _, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 0, code) assert.Contains(t, stdout, "Run 'help ' for more information on a specific command.") } func TestHelpColumnsAligned(t *testing.T) { - stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands()) + stdout, _, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 0, code) lines := strings.Split(strings.TrimSpace(stdout), "\n") @@ -235,26 +236,26 @@ func TestHelpAlwaysAvailableNoCommands(t *testing.T) { // --- Error handling --- func TestHelpUnknownCommandShowsError(t *testing.T) { - _, stderr, code := runScript(t, "help foo", "", interp.AllowAllCommands()) + _, stderr, code := runScript(t, "help foo", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 1, code) assert.Contains(t, stderr, "no help topics match 'foo'") } func TestHelpShowsCommandHelp(t *testing.T) { - stdout, _, code := runScript(t, "help echo", "", interp.AllowAllCommands()) + stdout, _, code := runScript(t, "help echo", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 0, code) assert.Contains(t, stdout, "echo: echo [-neE]") } func TestHelpFlagPrintsUsage(t *testing.T) { - stdout, _, code := runScript(t, "help --help", "", interp.AllowAllCommands()) + stdout, _, code := runScript(t, "help --help", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 1, code) assert.Contains(t, stdout, "Usage: help") assert.Contains(t, stdout, "Display help for builtin commands.") } func TestHelpUnknownFlagRejected(t *testing.T) { - _, stderr, code := runScript(t, "help --verbose", "", interp.AllowAllCommands()) + _, stderr, code := runScript(t, "help --verbose", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 1, code) assert.Contains(t, stderr, "help:") } @@ -262,21 +263,21 @@ func TestHelpUnknownFlagRejected(t *testing.T) { // --- Pipeline / composition --- func TestHelpInPipeline(t *testing.T) { - stdout, _, code := runScript(t, "help | grep echo", "", interp.AllowAllCommands()) + stdout, _, code := runScript(t, "help | grep echo", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 0, code) assert.Contains(t, stdout, "echo") assert.Contains(t, stdout, "write arguments to stdout") } func TestHelpExitCodeInScript(t *testing.T) { - stdout, _, code := runScript(t, "help; echo $?", "", interp.AllowAllCommands()) + stdout, _, code := runScript(t, "help; echo $?", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 0, code) // The last line before the footer should be "0" from echo $?. assert.True(t, strings.HasSuffix(strings.TrimSpace(stdout), "0")) } func TestHelpFailExitCodeInScript(t *testing.T) { - stdout, _, code := runScript(t, "help badarg; echo $?", "", interp.AllowAllCommands()) + stdout, _, code := runScript(t, "help badarg; echo $?", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 0, code) // overall script exits 0 because echo $? succeeds assert.True(t, strings.HasSuffix(strings.TrimSpace(stdout), "1")) } @@ -284,7 +285,7 @@ func TestHelpFailExitCodeInScript(t *testing.T) { // --- Help lists itself --- func TestHelpListsItself(t *testing.T) { - stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands()) + stdout, _, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 0, code) assert.Contains(t, stdout, "help") assert.Contains(t, stdout, "display help for commands") @@ -293,7 +294,7 @@ func TestHelpListsItself(t *testing.T) { // --- Empty stderr on success --- func TestHelpNoStderrOnSuccess(t *testing.T) { - _, stderr, code := runScript(t, "help", "", interp.AllowAllCommands()) + _, stderr, code := runScript(t, "help", "", interpoption.AllowAllCommands().(interp.RunnerOption)) assert.Equal(t, 0, code) assert.Empty(t, stderr) } diff --git a/builtins/tests/ip/helpers_test.go b/builtins/tests/ip/helpers_test.go index 8f934cfb..8b49eb07 100644 --- a/builtins/tests/ip/helpers_test.go +++ b/builtins/tests/ip/helpers_test.go @@ -15,6 +15,7 @@ import ( "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -31,7 +32,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts .. t.Fatal(err) } var outBuf, errBuf bytes.Buffer - allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...) + allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...) runner, err := interp.New(allOpts...) if err != nil { t.Fatal(err) diff --git a/builtins/tests/ip/ip_fuzz_test.go b/builtins/tests/ip/ip_fuzz_test.go index 26ebfaaf..53d0e3aa 100644 --- a/builtins/tests/ip/ip_fuzz_test.go +++ b/builtins/tests/ip/ip_fuzz_test.go @@ -28,6 +28,7 @@ import ( "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -44,7 +45,7 @@ func cmdRunCtxFuzz(ctx context.Context, t *testing.T, script string) (stdout, st return "", err.Error(), -1 } var outBuf, errBuf bytes.Buffer - runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()) + runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)) if err != nil { t.Fatalf("interp.New: %v", err) } diff --git a/builtins/tests/ps/ps_fuzz_test.go b/builtins/tests/ps/ps_fuzz_test.go index f3277c6f..679a8fb7 100644 --- a/builtins/tests/ps/ps_fuzz_test.go +++ b/builtins/tests/ps/ps_fuzz_test.go @@ -15,6 +15,7 @@ import ( "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -27,7 +28,7 @@ func runPS(t testing.TB, script string) (string, string, int) { return "", err.Error(), 1 } var outBuf, errBuf bytes.Buffer - runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()) + runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)) if err != nil { t.Fatal(err) } @@ -86,7 +87,7 @@ func FuzzPSPidList(f *testing.F) { return // unparseable, skip } var outBuf, errBuf bytes.Buffer - runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()) + runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)) if err != nil { t.Fatal(err) } @@ -133,7 +134,7 @@ func FuzzPSFlags(f *testing.F) { return } var outBuf, errBuf bytes.Buffer - runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()) + runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)) if err != nil { t.Fatal(err) } diff --git a/builtins/tests/sed/sed_test.go b/builtins/tests/sed/sed_test.go index 4eed69ea..658bb2bc 100644 --- a/builtins/tests/sed/sed_test.go +++ b/builtins/tests/sed/sed_test.go @@ -14,6 +14,7 @@ import ( "strings" "testing" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -31,7 +32,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts .. prog, err := parser.Parse(strings.NewReader(script), "") require.NoError(t, err) var outBuf, errBuf bytes.Buffer - allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...) + allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...) runner, err := interp.New(allOpts...) require.NoError(t, err) defer runner.Close() diff --git a/builtins/tests/tail/helpers_test.go b/builtins/tests/tail/helpers_test.go index 43d91b9f..7d826214 100644 --- a/builtins/tests/tail/helpers_test.go +++ b/builtins/tests/tail/helpers_test.go @@ -14,6 +14,7 @@ import ( "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -25,7 +26,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts .. t.Fatal(err) } var outBuf, errBuf bytes.Buffer - allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...) + allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...) runner, err := interp.New(allOpts...) if err != nil { t.Fatal(err) diff --git a/builtins/tests/wc/helpers_test.go b/builtins/tests/wc/helpers_test.go index 526f4ff6..656a2313 100644 --- a/builtins/tests/wc/helpers_test.go +++ b/builtins/tests/wc/helpers_test.go @@ -14,6 +14,7 @@ import ( "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -25,7 +26,7 @@ func runScriptCtx(ctx context.Context, t *testing.T, script, dir string, opts .. t.Fatal(err) } var outBuf, errBuf bytes.Buffer - allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...) + allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...) runner, err := interp.New(allOpts...) if err != nil { t.Fatal(err) diff --git a/builtins/tests/wc/wc_pentest_test.go b/builtins/tests/wc/wc_pentest_test.go index 13b6d341..fffb8c10 100644 --- a/builtins/tests/wc/wc_pentest_test.go +++ b/builtins/tests/wc/wc_pentest_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -37,7 +38,7 @@ func wcRunCtx(ctx context.Context, t *testing.T, script, dir string) (string, st opts := []interp.RunnerOption{ interp.StdIO(nil, &outBuf, &errBuf), interp.AllowedPaths([]string{dir}), - interp.AllowAllCommands(), + interpoption.AllowAllCommands().(interp.RunnerOption), } runner, err := interp.New(opts...) diff --git a/builtins/testutil/testutil.go b/builtins/testutil/testutil.go index 73d16fba..bd8fd7de 100644 --- a/builtins/testutil/testutil.go +++ b/builtins/testutil/testutil.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/require" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -61,7 +62,7 @@ func RunScriptCtx(ctx context.Context, t testing.TB, script, dir string, opts .. require.NoError(t, err) var outBuf, errBuf bytes.Buffer - allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()}, opts...) + allOpts := append([]interp.RunnerOption{interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...) runner, err := interp.New(allOpts...) require.NoError(t, err) defer runner.Close() @@ -133,7 +134,7 @@ func RunScriptDiscardCtx(ctx context.Context, t testing.TB, script, dir string, require.NoError(t, err) var errBuf bytes.Buffer - allOpts := append([]interp.RunnerOption{interp.StdIO(nil, io.Discard, &errBuf), interp.AllowAllCommands()}, opts...) + allOpts := append([]interp.RunnerOption{interp.StdIO(nil, io.Discard, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)}, opts...) runner, err := interp.New(allOpts...) require.NoError(t, err) defer runner.Close() diff --git a/cmd/rshell/main.go b/cmd/rshell/main.go index 3e20f1d1..c939b302 100644 --- a/cmd/rshell/main.go +++ b/cmd/rshell/main.go @@ -16,6 +16,7 @@ import ( "strings" "time" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" "github.com/spf13/cobra" "mvdan.cc/sh/v3/syntax" @@ -219,7 +220,7 @@ func execute(ctx context.Context, script, name string, opts executeOpts, stdin i runOpts = append(runOpts, interp.AllowedPaths(opts.allowedPaths)) } if opts.allowAllCommands { - runOpts = append(runOpts, interp.AllowAllCommands()) + runOpts = append(runOpts, interpoption.AllowAllCommands().(interp.RunnerOption)) } else if len(opts.allowedCommands) > 0 { runOpts = append(runOpts, interp.AllowedCommands(opts.allowedCommands)) } diff --git a/internal/interpoption/option.go b/internal/interpoption/option.go new file mode 100644 index 00000000..0c78d9aa --- /dev/null +++ b/internal/interpoption/option.go @@ -0,0 +1,18 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-present Datadog, Inc. + +// Package interpoption provides internal-only interpreter options that are not +// part of the public API. External consumers of the module cannot import this +// package due to Go's internal/ directory convention. +package interpoption + +// AllowAllCommands returns a value of type interp.RunnerOption that permits +// execution of any command (builtin or external), bypassing the AllowedCommands +// restriction. It is populated by interp.init(). +// +// Callers must type-assert the result: +// +// opt := interpoption.AllowAllCommands().(interp.RunnerOption) +var AllowAllCommands func() any diff --git a/interp/allowed_paths_internal_test.go b/interp/allowed_paths_internal_test.go index ee64f87f..a4469a0d 100644 --- a/interp/allowed_paths_internal_test.go +++ b/interp/allowed_paths_internal_test.go @@ -39,7 +39,7 @@ func runScriptInternal(t *testing.T, script, dir string, opts ...RunnerOption) ( var outBuf, errBuf bytes.Buffer allOpts := append([]RunnerOption{ StdIO(nil, &outBuf, &errBuf), - AllowAllCommands(), + allowAllCommandsOpt(), }, opts...) runner, err := New(allOpts...) @@ -151,7 +151,7 @@ func TestAllowedPathsExecSymlinkEscape(t *testing.T) { func TestRunRecoversPanic(t *testing.T) { var outBuf, errBuf bytes.Buffer - runner, err := New(StdIO(nil, &outBuf, &errBuf), AllowAllCommands()) + runner, err := New(StdIO(nil, &outBuf, &errBuf), allowAllCommandsOpt()) require.NoError(t, err) defer runner.Close() diff --git a/interp/allowed_paths_test.go b/interp/allowed_paths_test.go index 960bfb43..d435fcfc 100644 --- a/interp/allowed_paths_test.go +++ b/interp/allowed_paths_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -31,7 +32,7 @@ func runScript(t *testing.T, script, dir string, opts ...interp.RunnerOption) (s var outBuf, errBuf bytes.Buffer allOpts := append([]interp.RunnerOption{ interp.StdIO(nil, &outBuf, &errBuf), - interp.AllowAllCommands(), + interpoption.AllowAllCommands().(interp.RunnerOption), }, opts...) runner, err := interp.New(allOpts...) @@ -201,7 +202,7 @@ func TestAllowedPathsPinsRootBeforeRun(t *testing.T) { runner, err := interp.New( interp.StdIO(nil, &outBuf, &errBuf), interp.AllowedPaths([]string{allowed}), - interp.AllowAllCommands(), + interpoption.AllowAllCommands().(interp.RunnerOption), ) require.NoError(t, err) defer runner.Close() @@ -241,7 +242,7 @@ func TestAllowedPathsClose(t *testing.T) { dir := t.TempDir() runner, err := interp.New( interp.AllowedPaths([]string{dir}), - interp.AllowAllCommands(), + interpoption.AllowAllCommands().(interp.RunnerOption), ) require.NoError(t, err) diff --git a/interp/api.go b/interp/api.go index a7c0a546..3a30d3db 100644 --- a/interp/api.go +++ b/interp/api.go @@ -473,8 +473,7 @@ func AllowedPaths(paths []string) RunnerOption { // will not match bare command names and vice versa. Empty strings and empty // command names are rejected. // -// When not set (default), no commands are allowed unless [AllowAllCommands] is -// used. +// When not set (default), no commands are allowed. func AllowedCommands(names []string) RunnerOption { return func(r *Runner) error { m := make(map[string]bool, len(names)) @@ -504,10 +503,8 @@ func AllowedCommands(names []string) RunnerOption { } } -// AllowAllCommands permits execution of any command (builtin or external), -// bypassing the [AllowedCommands] restriction. This is intended for testing -// convenience. -func AllowAllCommands() RunnerOption { +// allowAllCommandsOpt is a convenience for tests within the interp package. +func allowAllCommandsOpt() RunnerOption { return func(r *Runner) error { r.allowAllCommands = true return nil diff --git a/interp/builtin_ip_pentest_test.go b/interp/builtin_ip_pentest_test.go index 870a084a..54190601 100644 --- a/interp/builtin_ip_pentest_test.go +++ b/interp/builtin_ip_pentest_test.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/require" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -28,7 +29,7 @@ func runWithCtx(ctx context.Context, t *testing.T, script string) (stdout, stder prog, err := parser.Parse(strings.NewReader(script), "") require.NoError(t, err) var outBuf, errBuf bytes.Buffer - runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()) + runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)) require.NoError(t, err) defer runner.Close() runErr := runner.Run(ctx, prog) diff --git a/interp/readonly_test.go b/interp/readonly_test.go index 898072ec..8eae6e13 100644 --- a/interp/readonly_test.go +++ b/interp/readonly_test.go @@ -22,7 +22,7 @@ func TestReadonlyVariableBlocksReassignment(t *testing.T) { r, err := New( StdIO(nil, &stdout, &stderr), Env("RO_VAR=original"), - AllowAllCommands(), + allowAllCommandsOpt(), ) require.NoError(t, err) t.Cleanup(func() { r.Close() }) diff --git a/interp/register_internal.go b/interp/register_internal.go new file mode 100644 index 00000000..f6737f19 --- /dev/null +++ b/interp/register_internal.go @@ -0,0 +1,17 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-present Datadog, Inc. + +package interp + +import "github.com/DataDog/rshell/internal/interpoption" + +func init() { + interpoption.AllowAllCommands = func() any { + return RunnerOption(func(r *Runner) error { + r.allowAllCommands = true + return nil + }) + } +} diff --git a/interp/start_time_test.go b/interp/start_time_test.go index c878796a..df3fba38 100644 --- a/interp/start_time_test.go +++ b/interp/start_time_test.go @@ -28,7 +28,7 @@ func parseScript(t *testing.T, src string) *syntax.File { // run. Uses New() directly (not newResetRunner) to check the initial zero-value // state before any Run or Reset call. func TestStartTimeZeroBeforeRun(t *testing.T) { - r, err := New(AllowAllCommands()) + r, err := New(allowAllCommandsOpt()) require.NoError(t, err) t.Cleanup(func() { r.Close() }) assert.True(t, r.startTime.IsZero(), "startTime should be zero before Run") @@ -37,7 +37,7 @@ func TestStartTimeZeroBeforeRun(t *testing.T) { // TestStartTimeSetByRun verifies that Run captures the current time into // startTime before executing any builtins. func TestStartTimeSetByRun(t *testing.T) { - r, err := New(AllowAllCommands()) + r, err := New(allowAllCommandsOpt()) require.NoError(t, err) t.Cleanup(func() { r.Close() }) @@ -54,7 +54,7 @@ func TestStartTimeSetByRun(t *testing.T) { // TestStartTimeUpdatesOnSubsequentRun verifies that each Run call captures a // fresh timestamp, so commands in different runs do not share a stale time. func TestStartTimeUpdatesOnSubsequentRun(t *testing.T) { - r, err := New(AllowAllCommands()) + r, err := New(allowAllCommandsOpt()) require.NoError(t, err) t.Cleanup(func() { r.Close() }) @@ -79,7 +79,7 @@ func TestStartTimeUpdatesOnSubsequentRun(t *testing.T) { // subshell() inherits the parent's startTime so builtins in subshells and // pipelines use the correct time reference. func TestStartTimePropagatedToSubshell(t *testing.T) { - r, err := New(AllowAllCommands()) + r, err := New(allowAllCommandsOpt()) require.NoError(t, err) t.Cleanup(func() { r.Close() }) @@ -96,7 +96,7 @@ func TestStartTimePropagatedToSubshell(t *testing.T) { // a runner that has been reset but not yet re-run does not expose the previous // run's timestamp. func TestStartTimeResetToZeroByReset(t *testing.T) { - r, err := New(AllowAllCommands()) + r, err := New(allowAllCommandsOpt()) require.NoError(t, err) t.Cleanup(func() { r.Close() }) diff --git a/interp/tests/cmdsubst_test.go b/interp/tests/cmdsubst_test.go index dd2db3f6..bd40c573 100644 --- a/interp/tests/cmdsubst_test.go +++ b/interp/tests/cmdsubst_test.go @@ -19,18 +19,19 @@ import ( "github.com/stretchr/testify/require" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) // cmdSubstRun runs a script with the given dir as working directory and allowed path. func cmdSubstRun(t *testing.T, script, dir string) (string, string, int) { t.Helper() - return cmdSubstRunWithOpts(t, script, dir, interp.AllowedPaths([]string{dir}), interp.AllowAllCommands()) + return cmdSubstRunWithOpts(t, script, dir, interp.AllowedPaths([]string{dir}), interpoption.AllowAllCommands().(interp.RunnerOption)) } func cmdSubstRunCtx(ctx context.Context, t *testing.T, script, dir string) (string, string, int) { t.Helper() - return cmdSubstRunCtxWithOpts(ctx, t, script, dir, interp.AllowedPaths([]string{dir}), interp.AllowAllCommands()) + return cmdSubstRunCtxWithOpts(ctx, t, script, dir, interp.AllowedPaths([]string{dir}), interpoption.AllowAllCommands().(interp.RunnerOption)) } func cmdSubstRunWithOpts(t *testing.T, script, dir string, opts ...interp.RunnerOption) (string, string, int) { diff --git a/interp/tests/if_clause_pentest_test.go b/interp/tests/if_clause_pentest_test.go index 9d588fbd..02b68117 100644 --- a/interp/tests/if_clause_pentest_test.go +++ b/interp/tests/if_clause_pentest_test.go @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/require" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -33,7 +34,7 @@ func ifRunCtx(ctx context.Context, t *testing.T, script string) (string, string, require.NoError(t, err) var outBuf, errBuf bytes.Buffer - runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interp.AllowAllCommands()) + runner, err := interp.New(interp.StdIO(nil, &outBuf, &errBuf), interpoption.AllowAllCommands().(interp.RunnerOption)) require.NoError(t, err) defer runner.Close() diff --git a/interp/tests/redir_devnull_pentest_test.go b/interp/tests/redir_devnull_pentest_test.go index 30f7db49..c563a9e9 100644 --- a/interp/tests/redir_devnull_pentest_test.go +++ b/interp/tests/redir_devnull_pentest_test.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/require" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -39,7 +40,7 @@ func pentestRedirRunCtx(ctx context.Context, t *testing.T, script, dir string) ( var outBuf, errBuf bytes.Buffer opts := []interp.RunnerOption{ interp.StdIO(nil, &outBuf, &errBuf), - interp.AllowAllCommands(), + interpoption.AllowAllCommands().(interp.RunnerOption), } if dir != "" { opts = append(opts, interp.AllowedPaths([]string{dir})) diff --git a/interp/tests/redir_devnull_test.go b/interp/tests/redir_devnull_test.go index 6b7c2d1a..f12ca6e6 100644 --- a/interp/tests/redir_devnull_test.go +++ b/interp/tests/redir_devnull_test.go @@ -18,19 +18,20 @@ import ( "github.com/stretchr/testify/require" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) // redirRun runs a script with the given dir as working directory and allowed path. func redirRun(t *testing.T, script, dir string) (string, string, int) { t.Helper() - return redirRunWithOpts(t, script, dir, interp.AllowedPaths([]string{dir}), interp.AllowAllCommands()) + return redirRunWithOpts(t, script, dir, interp.AllowedPaths([]string{dir}), interpoption.AllowAllCommands().(interp.RunnerOption)) } // redirRunNoAllowed runs a script with no allowed paths. func redirRunNoAllowed(t *testing.T, script, dir string) (string, string, int) { t.Helper() - return redirRunWithOpts(t, script, dir, interp.AllowAllCommands()) + return redirRunWithOpts(t, script, dir, interpoption.AllowAllCommands().(interp.RunnerOption)) } func redirRunWithOpts(t *testing.T, script, dir string, opts ...interp.RunnerOption) (string, string, int) { diff --git a/interp/tests/subshell_test.go b/interp/tests/subshell_test.go index 5402b966..74d5be11 100644 --- a/interp/tests/subshell_test.go +++ b/interp/tests/subshell_test.go @@ -12,18 +12,19 @@ import ( "github.com/stretchr/testify/assert" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) // subshellRun runs a script with the given dir as working directory and allowed path. func subshellRun(t *testing.T, script, dir string) (string, string, int) { t.Helper() - return cmdSubstRunWithOpts(t, script, dir, interp.AllowedPaths([]string{dir}), interp.AllowAllCommands()) + return cmdSubstRunWithOpts(t, script, dir, interp.AllowedPaths([]string{dir}), interpoption.AllowAllCommands().(interp.RunnerOption)) } func subshellRunCtx(ctx context.Context, t *testing.T, script, dir string) (string, string, int) { t.Helper() - return cmdSubstRunCtxWithOpts(ctx, t, script, dir, interp.AllowedPaths([]string{dir}), interp.AllowAllCommands()) + return cmdSubstRunCtxWithOpts(ctx, t, script, dir, interp.AllowedPaths([]string{dir}), interpoption.AllowAllCommands().(interp.RunnerOption)) } // --- Basic subshell --- diff --git a/interp/timeout_test.go b/interp/timeout_test.go index 4d49f689..4a983489 100644 --- a/interp/timeout_test.go +++ b/interp/timeout_test.go @@ -16,7 +16,7 @@ import ( func newTimeoutRunner(t *testing.T, opts ...RunnerOption) *Runner { t.Helper() - allOpts := append([]RunnerOption{AllowAllCommands()}, opts...) + allOpts := append([]RunnerOption{allowAllCommandsOpt()}, opts...) r, err := New(allOpts...) require.NoError(t, err) t.Cleanup(func() { _ = r.Close() }) diff --git a/tests/scenarios_test.go b/tests/scenarios_test.go index d104630b..a3d6ccb8 100644 --- a/tests/scenarios_test.go +++ b/tests/scenarios_test.go @@ -26,6 +26,7 @@ import ( "gopkg.in/yaml.v3" "mvdan.cc/sh/v3/syntax" + "github.com/DataDog/rshell/internal/interpoption" "github.com/DataDog/rshell/interp" ) @@ -204,13 +205,13 @@ func runScenario(t *testing.T, sc scenario) { opts = append(opts, interp.AllowedPaths(resolved)) } if sc.Input.AllowAllCommands != nil && *sc.Input.AllowAllCommands { - opts = append(opts, interp.AllowAllCommands()) + opts = append(opts, interpoption.AllowAllCommands().(interp.RunnerOption)) } else if len(sc.Input.AllowedCommands) > 0 { opts = append(opts, interp.AllowedCommands(sc.Input.AllowedCommands)) } else if sc.Input.AllowAllCommands == nil { // Default: allow all commands for backward compatibility with // existing scenarios that predate the allowedCommands feature. - opts = append(opts, interp.AllowAllCommands()) + opts = append(opts, interpoption.AllowAllCommands().(interp.RunnerOption)) } // When allow_all_commands is explicitly false and allowed_commands is // empty, no AllowedCommands/AllowAllCommands option is added, so the