Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 30 additions & 22 deletions cmd/rshell/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,19 @@ func main() {

func run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
var (
command string
allowedPaths string
allowedCommands string
allowedServices string
allowAllCmds bool
timeout time.Duration
procPath string
journalDirs string
machineIDPath string
journalSocket string
managerSocket string
mode string
command string
allowedPaths string
allowedCommands string
allowedServices string
allowAllCmds bool
timeout time.Duration
procPath string
journalDirs string
machineIDPath string
journalSocket string
managerSocket string
mode string
disableDetailedTel bool
)

cmd := &cobra.Command{
Expand Down Expand Up @@ -113,8 +114,9 @@ func run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.
JournalControlSocket: journalSocket,
ManagerBusSocket: managerSocket,
},
systemdTargetSet: systemdTargetSet,
mode: parsedMode,
systemdTargetSet: systemdTargetSet,
mode: parsedMode,
disableDetailedTel: disableDetailedTel,
}

if commandSet {
Expand Down Expand Up @@ -173,6 +175,7 @@ func run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.
cmd.Flags().StringVar(&journalSocket, "systemd-journal-socket", "", "journald Varlink socket for an explicit systemd target")
cmd.Flags().StringVar(&managerSocket, "systemd-manager-socket", "", "system D-Bus socket for an explicit systemd target")
cmd.Flags().StringVar(&mode, "mode", "read-only", "shell execution mode: read-only (default) or remediation (enables file-target output redirections within :rw AllowedPaths roots and remediation-only builtins, including the restricted systemctl builtin)")
cmd.Flags().BoolVar(&disableDetailedTel, "disable-detailed-telemetry", false, "suppress the rshell.run.command and rshell.run.options.* tags on the top-level run telemetry span (on by default; set this when the raw command or effective sandbox configuration is too sensitive to report)")

if err := cmd.ExecuteContext(ctx); err != nil {
var status interp.ExitStatus
Expand Down Expand Up @@ -238,14 +241,15 @@ func rejectLongCommand(rawArgs []string) error {

// executeOpts holds options for the execute function.
type executeOpts struct {
allowedPaths []string
allowedCommands []string
allowedServices []interp.SystemdControlGrant
allowAllCommands bool
procPath string
systemdTarget interp.SystemdTargetConfig
systemdTargetSet bool
mode interp.Mode
allowedPaths []string
allowedCommands []string
allowedServices []interp.SystemdControlGrant
allowAllCommands bool
procPath string
systemdTarget interp.SystemdTargetConfig
systemdTargetSet bool
mode interp.Mode
disableDetailedTel bool
}

func execute(ctx context.Context, script, name string, opts executeOpts, stdin io.Reader, stdout, stderr io.Writer) error {
Expand All @@ -260,6 +264,7 @@ func execute(ctx context.Context, script, name string, opts executeOpts, stdin i
// Build runner options.
runOpts := []interp.RunnerOption{
interp.StdIO(stdin, stdout, stderr),
interp.Script(script),
}
if len(opts.allowedPaths) > 0 {
runOpts = append(runOpts, interp.AllowedPaths(opts.allowedPaths))
Expand All @@ -281,6 +286,9 @@ func execute(ctx context.Context, script, name string, opts executeOpts, stdin i
if opts.mode != "" {
runOpts = append(runOpts, interp.WithMode(opts.mode))
}
if opts.disableDetailedTel {
runOpts = append(runOpts, interp.DisableDetailedTelemetry())
}

runner, err := interp.New(runOpts...)
if err != nil {
Expand Down
94 changes: 94 additions & 0 deletions interp/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"io"
"os"
"path/filepath"
"sort"
"strings"
"sync/atomic"
"time"
Expand Down Expand Up @@ -102,6 +103,20 @@ type runnerConfig struct {
// Defaults to "/proc" when empty.
procPath string

// commandText holds the raw, unparsed shell script or command string this
// Runner will execute. It has no effect on parsing or execution: [Run]
// still only accepts a pre-parsed [syntax.Node]. It exists solely so the
// top-level "run" telemetry span can report the full source text. Set via
// [Script]; empty when the caller does not supply one.
commandText string

// disableDetailedTelemetry, when true, suppresses the rshell.run.command
// and rshell.run.options.* tags on the top-level "run" telemetry span.
// The rshell.version, rshell.run.exit_code, and other outcome tags set in
// [Runner.Run] are unaffected. Set via [DisableDetailedTelemetry]; false
// (tags emitted) by default.
disableDetailedTelemetry bool

// remediationMode enables remediation-only capabilities, including file-target
// output redirections within AllowedPaths and the restricted systemctl builtin.
remediationMode bool
Expand Down Expand Up @@ -591,6 +606,10 @@ func (s ExitStatus) Error() string { return fmt.Sprintf("exit status %d", s) }
func (r *Runner) Run(ctx context.Context, node syntax.Node) (retErr error) {
span, ctx := telemetry.StartSpanFromContext(ctx, "run")
span.SetTag("rshell.version", version.Version)
if !r.disableDetailedTelemetry {
span.SetTag("rshell.run.command", r.commandText)
r.setRunOptionTags(span)
}
defer func() {
span.SetTag("rshell.run.exit_code", int(r.exit.code))
span.SetTag("rshell.run.commands.total", r.totalCount)
Expand Down Expand Up @@ -700,6 +719,53 @@ func (r *Runner) Run(ctx context.Context, node syntax.Node) (retErr error) {
return nil
}

// setRunOptionTags records the effective [RunnerOption] configuration of r on
// the top-level "run" span. All fields read here are set once during [New]
// and never mutated afterwards, so this is safe to call without additional
// synchronization from within Run.
func (r *Runner) setRunOptionTags(span *telemetry.Span) {
mode := ModeReadOnly
if r.remediationMode {
mode = ModeRemediation
}
span.SetTag("rshell.run.options.mode", string(mode))
span.SetTag("rshell.run.options.max_execution_time", r.maxExecutionTime.String())

procPath := r.procPath
if procPath == "" {
procPath = "/proc"
}
span.SetTag("rshell.run.options.proc_path", procPath)
span.SetTag("rshell.run.options.host_prefix", r.hostPrefix)

pathEntries := make([]string, 0, len(r.sandbox.PathAccesses()))
for _, access := range r.sandbox.PathAccesses() {
suffix := "ro"
if access.ReadWrite {
suffix = "rw"
}
pathEntries = append(pathEntries, fmt.Sprintf("%s:%s", access.Path, suffix))
}
span.SetTag("rshell.run.options.allowed_paths", strings.Join(pathEntries, ","))

span.SetTag("rshell.run.options.allow_all_commands", r.allowAllCommands)
allowedCommands := make([]string, 0, len(r.allowedCommands))
for name := range r.allowedCommands {
allowedCommands = append(allowedCommands, name)
}
sort.Strings(allowedCommands)
span.SetTag("rshell.run.options.allowed_commands", strings.Join(allowedCommands, ","))

allowedServices := r.allowedSystemServicesList()
serviceEntries := make([]string, 0, len(allowedServices))
for _, op := range allowedServices {
serviceEntries = append(serviceEntries, fmt.Sprintf("%s:%s", op.Service, op.Action))
}
span.SetTag("rshell.run.options.allowed_system_services", strings.Join(serviceEntries, ","))

span.SetTag("rshell.run.options.systemd_target_configured", r.systemdTargetConfigured)
}

// MaxScriptBytes is the maximum allowed byte length of a shell script passed
// to [ParseScript]. Scripts larger than this are rejected before parsing to
// prevent the parser from allocating unbounded memory. Unlike other per-input
Expand Down Expand Up @@ -881,6 +947,34 @@ func ProcPath(path string) RunnerOption {
}
}

// Script attaches the raw, unparsed shell script or command string that this
// Runner is about to execute. It has no effect on parsing or execution — [Run]
// still requires a pre-parsed [syntax.Node], typically produced by
// [ParseScript] — and exists solely so the top-level "run" telemetry span can
// report the full source text (see [Runner.Run]).
//
// Callers that do not want the raw script recorded in telemetry should omit
// this option.
func Script(text string) RunnerOption {
return func(r *Runner) error {
r.commandText = text
return nil
}
}

// DisableDetailedTelemetry suppresses the rshell.run.command and
// rshell.run.options.* tags that [Runner.Run] would otherwise add to the
// top-level "run" telemetry span. Use this when the raw command text or
// effective sandbox configuration is too sensitive to forward to the
// telemetry backend; other span tags (rshell.version, exit code, command
// counts, outcome) are unaffected.
func DisableDetailedTelemetry() RunnerOption {
return func(r *Runner) error {
r.disableDetailedTelemetry = true
return nil
}
}

// Mode controls the execution mode of a Runner.
type Mode string

Expand Down
79 changes: 79 additions & 0 deletions interp/tracing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"sync"
"sync/atomic"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -84,6 +85,84 @@ func TestRunEmitsTracerSpan(t *testing.T) {
assert.Equal(t, float64(0), runSpan.Metrics["rshell.run.exit_code"])
}

// TestRunSpanCommandAndOptions verifies that the run span records the raw
// script text supplied via [Script], plus the effective [RunnerOption]
// configuration (mode, timeout, proc path, host prefix, allowed paths,
// allowed commands, and allowed system services).
func TestRunSpanCommandAndOptions(t *testing.T) {
tel, ct := newCapturingTelemetry(t)

dir := t.TempDir()
script := "echo hi"
r, err := New(
Script(script),
AllowedPaths([]string{dir + ":rw"}),
AllowedCommands([]string{"rshell:echo"}),
AllowedSystemServices([]SystemServiceControlGrant{
{Service: "foo.service", Actions: []SystemServiceAction{SystemServiceRead}},
}),
ProcPath("/custom/proc"),
HostPrefix(dir),
MaxExecutionTime(5*time.Second),
WithMode(ModeRemediation),
)
require.NoError(t, err)
t.Cleanup(func() { r.Close() })

traceID := newTestTraceID()
require.NoError(t, runWithTracedContext(t, r, traceID, script))
tel.Stop()

spans := ct.spansForTrace(t, traceID)
runSpan := findOneSpanByResource(spans, "run")
require.NotNil(t, runSpan, "expected a run span")

assert.Equal(t, script, runSpan.Meta["rshell.run.command"])
assert.Equal(t, "remediation", runSpan.Meta["rshell.run.options.mode"])
assert.Equal(t, "5s", runSpan.Meta["rshell.run.options.max_execution_time"])
assert.Equal(t, "/custom/proc", runSpan.Meta["rshell.run.options.proc_path"])
assert.Equal(t, dir, runSpan.Meta["rshell.run.options.host_prefix"])
assert.Equal(t, dir+":rw", runSpan.Meta["rshell.run.options.allowed_paths"])
assert.Equal(t, "false", runSpan.Meta["rshell.run.options.allow_all_commands"])
assert.Equal(t, "echo", runSpan.Meta["rshell.run.options.allowed_commands"])
assert.Equal(t, "foo.service:read", runSpan.Meta["rshell.run.options.allowed_system_services"])
assert.Equal(t, "false", runSpan.Meta["rshell.run.options.systemd_target_configured"])
}

// TestRunSpanDetailedTelemetryDisabled verifies that [DisableDetailedTelemetry]
// suppresses rshell.run.command and every rshell.run.options.* tag, while
// leaving unrelated tags such as rshell.version and rshell.run.exit_code
// intact.
func TestRunSpanDetailedTelemetryDisabled(t *testing.T) {
tel, ct := newCapturingTelemetry(t)

script := "echo hi"
r, err := New(
allowAllCommandsOpt(),
Script(script),
WithMode(ModeRemediation),
DisableDetailedTelemetry(),
)
require.NoError(t, err)
t.Cleanup(func() { r.Close() })

traceID := newTestTraceID()
require.NoError(t, runWithTracedContext(t, r, traceID, script))
tel.Stop()

spans := ct.spansForTrace(t, traceID)
runSpan := findOneSpanByResource(spans, "run")
require.NotNil(t, runSpan, "expected a run span")

_, hasCommand := runSpan.Meta["rshell.run.command"]
assert.False(t, hasCommand, "rshell.run.command should be suppressed")
for tag := range runSpan.Meta {
assert.NotContains(t, tag, "rshell.run.options.", "no rshell.run.options.* tag should be present")
}
assert.NotEmpty(t, runSpan.Meta["rshell.version"])
assert.Equal(t, float64(0), runSpan.Metrics["rshell.run.exit_code"])
}

// TestRunSpanOutcome verifies the outcome classification on the run span:
// any script completion (zero exit, non-zero exit, explicit exit N, or
// scripts that hit blocked/unknown commands along the way) is "success"
Expand Down
Loading