Class: Sxn::Security::SecureCommandExecutor
- Inherits:
-
Object
- Object
- Sxn::Security::SecureCommandExecutor
- 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.
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
-
#allowed_commands ⇒ Array<String>
Returns the list of allowed commands.
-
#command_allowed?(command) ⇒ Boolean
Checks if a command is allowed without executing it.
-
#execute(command, env: {}, timeout: 30, chdir: nil) ⇒ CommandResult
Executes a command securely with strict controls.
-
#initialize(project_root, logger: nil) ⇒ SecureCommandExecutor
constructor
A new instance of SecureCommandExecutor.
Constructor Details
#initialize(project_root, logger: nil) ⇒ SecureCommandExecutor
Returns a new instance of SecureCommandExecutor.
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_commands ⇒ Array<String>
Returns the list of allowed commands
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
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
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., duration: duration }) raise CommandExecutionError, "Command execution failed: #{e.message}" end end |