Class: Sxn::Security::SecureCommandExecutor

Inherits:
Object
  • Object
show all
Defined in:
lib/sxn/security/secure_command_executor.rb

Overview

SecureCommandExecutor provides secure command execution with strict controls. It prevents shell interpolation by using Process.spawn with arrays, whitelists allowed commands, cleans environment variables, and logs all executions.

Examples:

executor = SecureCommandExecutor.new("/path/to/project")
result = executor.execute(["bundle", "install"], env: {"RAILS_ENV" => "development"})
puts result.success? # => true/false
puts result.stdout   # => command output

Defined Under Namespace

Classes: CommandResult

Constant Summary collapse

ALLOWED_COMMANDS =

Whitelist of allowed commands with their expected paths Commands are mapped to either:

  • String: exact path to executable

  • Array: list of possible paths (first existing one is used)

  • Symbol: special handling required

{
  # Ruby/Rails commands
  "bundle" => %w[bundle /usr/local/bin/bundle /opt/homebrew/bin/bundle],
  "gem" => %w[gem /usr/local/bin/gem /opt/homebrew/bin/gem],
  "ruby" => %w[ruby /usr/local/bin/ruby /opt/homebrew/bin/ruby],
  "rails" => :rails_command, # Special handling for bin/rails vs rails

  # Node.js commands
  "npm" => %w[npm /usr/local/bin/npm /opt/homebrew/bin/npm],
  "yarn" => %w[yarn /usr/local/bin/yarn /opt/homebrew/bin/yarn],
  "pnpm" => %w[pnpm /usr/local/bin/pnpm /opt/homebrew/bin/pnpm],
  "node" => %w[node /usr/local/bin/node /opt/homebrew/bin/node],

  # Git commands
  "git" => %w[git /usr/bin/git /usr/local/bin/git /opt/homebrew/bin/git],

  # Database commands
  "psql" => %w[psql /usr/local/bin/psql /opt/homebrew/bin/psql],
  "mysql" => %w[mysql /usr/local/bin/mysql /opt/homebrew/bin/mysql],
  "sqlite3" => %w[sqlite3 /usr/bin/sqlite3 /usr/local/bin/sqlite3],

  # Development tools
  "make" => %w[make /usr/bin/make],
  "curl" => %w[curl /usr/bin/curl /usr/local/bin/curl],
  "wget" => %w[wget /usr/bin/wget /usr/local/bin/wget],

  # Project-specific executables (resolved relative to project)
  "bin/rails" => :project_executable,
  "bin/setup" => :project_executable,
  "bin/dev" => :project_executable,
  "bin/test" => :project_executable,
  "./bin/rails" => :project_executable,
  "./bin/setup" => :project_executable
}.freeze
SAFE_ENV_VARS =

Environment variables that are safe to preserve

%w[
  PATH
  HOME
  USER
  LANG
  LC_ALL
  TZ
  TMPDIR
  RAILS_ENV
  NODE_ENV
  BUNDLE_GEMFILE
  GEM_HOME
  GEM_PATH
  RBENV_VERSION
  NVM_DIR
  NVM_BIN
  SSL_CERT_FILE
  SSL_CERT_DIR
].freeze
MAX_TIMEOUT =

Maximum command execution timeout (in seconds)

300

Instance Method Summary collapse

Constructor Details

#initialize(project_root, logger: nil) ⇒ SecureCommandExecutor

Returns a new instance of SecureCommandExecutor.

Parameters:

  • project_root (String)

    The absolute path to the project root directory

  • logger (Logger) (defaults to: nil)

    Optional logger for audit trail



120
121
122
123
124
125
126
# File 'lib/sxn/security/secure_command_executor.rb', line 120

def initialize(project_root, logger: nil)
  @project_root = File.realpath(project_root)
  @logger = logger || Sxn.logger
  @command_whitelist = build_command_whitelist
rescue Errno::ENOENT
  raise ArgumentError, "Project root does not exist: #{project_root}"
end

Instance Method Details

#allowed_commandsArray<String>

Returns the list of allowed commands

Returns:

  • (Array<String>)

    List of allowed command names



188
189
190
# File 'lib/sxn/security/secure_command_executor.rb', line 188

def allowed_commands
  @command_whitelist.keys.sort
end

#command_allowed?(command) ⇒ Boolean

Checks if a command is allowed without executing it

Parameters:

  • command (Array<String>)

    Command and arguments as an array

Returns:

  • (Boolean)

    true if the command is whitelisted



174
175
176
177
178
179
180
181
182
183
# File 'lib/sxn/security/secure_command_executor.rb', line 174

def command_allowed?(command)
  return false unless command.is_a?(Array) && !command.empty?

  begin
    validate_and_resolve_command(command)
    true
  rescue CommandExecutionError
    false
  end
end

#execute(command, env: {}, timeout: 30, chdir: nil) ⇒ CommandResult

Executes a command securely with strict controls

Parameters:

  • command (Array<String>)

    Command and arguments as an array

  • env (Hash) (defaults to: {})

    Environment variables to set

  • timeout (Integer) (defaults to: 30)

    Maximum execution time in seconds

  • chdir (String) (defaults to: nil)

    Directory to run command in (must be within project)

Returns:

Raises:



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/sxn/security/secure_command_executor.rb', line 136

def execute(command, env: {}, timeout: 30, chdir: nil)
  raise ArgumentError, "Command must be an array" unless command.is_a?(Array)
  raise ArgumentError, "Command cannot be empty" if command.empty?
  raise ArgumentError, "Timeout must be positive" unless timeout.positive? && timeout <= MAX_TIMEOUT

  validated_command = validate_and_resolve_command(command)
  safe_env = build_safe_environment(env)
  work_dir = chdir ? validate_work_directory(chdir) : @project_root

  start_time = Time.now
  audit_log("EXEC_START", validated_command, work_dir, safe_env.keys)

  begin
    result = execute_with_timeout(validated_command, safe_env, work_dir, timeout)
    duration = Time.now - start_time

    audit_log("EXEC_COMPLETE", validated_command, work_dir, {
                exit_status: result.exit_status,
                duration: duration,
                success: result.success?
              })

    CommandResult.new(result.exit_status, result.stdout, result.stderr, validated_command, duration)
  rescue StandardError => e
    duration = Time.now - start_time
    audit_log("EXEC_ERROR", validated_command, work_dir, {
                error: e.class.name,
                message: e.message,
                duration: duration
              })
    raise CommandExecutionError, "Command execution failed: #{e.message}"
  end
end