Skip to content

Commit

Permalink
[Feature] Command Argument STDOUT/capistrano.log Hiding (#430)
Browse files Browse the repository at this point in the history
- Module Redaction under SSHKit, so it's available everywhere we need it to be
- Simplified redact and moved it to abstact.rb so we can use it where we need
- with_redaction method available for SSHKit::Command
- Added .with_redaction to netssh backends, log_command_start
- Fixed tests, added Array, Hash, etc
- Rewrote README
  • Loading branch information
NorseGaud authored and mattbrictson committed Jun 30, 2018
1 parent a3b8c4a commit 6af6cad
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ appear at the top.
## [Unreleased][]

* Your contribution here!
* [#430](https://github.com/capistrano/sshkit/pull/430): [Feature] Command Argument STDOUT/capistrano.log Hiding - [@NorseGaud](https://github.com/NorseGaud)

## [1.16.1][] (2018-05-20)

Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,32 @@ SSHKit.config.output = SSHKit::Formatter::Pretty.new(output)
SSHKit.config.output = SSHKit::Formatter::SimpleText.new(File.open('log/deploy.log', 'wb'))
```

#### Output & Log Redaction

If necessary, redact() can be used on a section of your execute arguments to hide it from both STDOUT and the capistrano.log. It supports the majority of data types.

```ruby
# Example from capistrano-postgresql gem
execute(:psql, fetch(:pg_system_db), '-c', %Q{"CREATE USER \\"#{fetch(:pg_username)}\\" PASSWORD}, redact("'#{fetch(:pg_password)}'"), %Q{;"})
```
Once wrapped, sshkit logging will replace the actual pg_password with a [REDACTED] value:

```
# STDOUT
00:00 postgresql:create_database_user
01 sudo -i -u postgres psql -d postgres -c "CREATE USER \"db_admin_user\" PASSWORD [REDACTED] ;"
01 CREATE ROLE
✔ 01 user@localhost 0.099s
# capistrano.log
INFO [59dbd2ba] Running /usr/bin/env sudo -i -u postgres psql -d postgres -c "CREATE USER \"db_admin_user\" PASSWORD [REDACTED] ;" as user@localhost
DEBUG [59dbd2ba] Command: ( export PATH="$HOME/.gem/ruby/2.5.0/bin:$PATH" ; /usr/bin/env sudo -i -u postgres psql -d postgres -c "CREATE USER \"db_admin_user\" PASSWORD [REDACTED] ;" )
DEBUG [529b623c] CREATE ROLE
```

Yet, the created database user will have the value from `fetch(:pg_password)`.

#### Output Colors

By default, SSHKit will color the output using ANSI color escape sequences
Expand Down
3 changes: 3 additions & 0 deletions lib/sshkit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ def reset_configuration!

end

# Used for redaction of a certain argument
module Redaction end

end

require_relative 'sshkit/all'
4 changes: 4 additions & 0 deletions lib/sshkit/backends/abstract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ def initialize(host, &block)
@group = nil
end

def redact(arg) # Used in execute_command to hide redact() args a user passes in
arg.to_s.extend(Redaction) # to_s due to our inability to extend Integer, etc
end

def make(commands=[])
execute :make, commands
end
Expand Down
8 changes: 1 addition & 7 deletions lib/sshkit/backends/local.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,24 @@ def download!(remote, local=nil, _options = {})
private

def execute_command(cmd)
output.log_command_start(cmd)

output.log_command_start(cmd.with_redaction)
cmd.started = Time.now

Open3.popen3(cmd.to_command) do |stdin, stdout, stderr, wait_thr|
stdout_thread = Thread.new do
while (line = stdout.gets) do
cmd.on_stdout(stdin, line)
output.log_command_data(cmd, :stdout, line)
end
end

stderr_thread = Thread.new do
while (line = stderr.gets) do
cmd.on_stderr(stdin, line)
output.log_command_data(cmd, :stderr, line)
end
end

stdout_thread.join
stderr_thread.join

cmd.exit_status = wait_thr.value.to_i

output.log_command_exit(cmd)
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/sshkit/backends/netssh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def transfer_summarizer(action, options = {})
end

def execute_command(cmd)
output.log_command_start(cmd)
output.log_command_start(cmd.with_redaction)
cmd.started = true
exit_status = nil
with_ssh do |ssh|
Expand Down
2 changes: 1 addition & 1 deletion lib/sshkit/backends/printer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Backend
class Printer < Abstract

def execute_command(cmd)
output.log_command_start(cmd)
output.log_command_start(cmd.with_redaction)
end

alias :upload! :execute
Expand Down
7 changes: 7 additions & 0 deletions lib/sshkit/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,13 @@ def to_command
end
end

def with_redaction
new_args = args.map{|arg| arg.is_a?(Redaction) ? '[REDACTED]' : arg }
redacted_cmd = dup
redacted_cmd.instance_variable_set(:@args, new_args)
redacted_cmd
end

def to_s
if should_map?
[SSHKit.config.command_map[command.to_sym], *Array(args)].join(' ')
Expand Down
32 changes: 31 additions & 1 deletion test/functional/backends/test_netssh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,43 @@ def test_simple_netssh
], command_lines
end

def test_redaction
# Be sure redaction in the logs is showing [REDACTED]
Netssh.new(a_host) do
execute :echo, 'password:', redact('PASSWORD')
execute :echo, 'password:', redact(10000)
execute :echo, 'password:', redact(['test1','test2'])
execute :echo, 'password:', redact({:test => 'test_value'})
end.run
command_lines = @output.lines.select { |line| line.start_with?('Command:') }
assert_equal [
"Command: /usr/bin/env echo password: [REDACTED]\n",
"Command: /usr/bin/env echo password: [REDACTED]\n",
"Command: /usr/bin/env echo password: [REDACTED]\n",
"Command: /usr/bin/env echo password: [REDACTED]\n"
], command_lines
# Be sure the actual command executed without *REDACTED*
Netssh.new(a_host) do
file_name = 'test.file'
execute :touch, redact("'#{file_name}'") # Test and be sure single quotes are included in actual command; expected /usr/bin/env touch 'test.file'
execute :ls, 'test.file'
end.run
ls_lines = @output.lines.select { |line| line.start_with?("\ttest.file") }
assert_equal [
"\ttest.file\n"
], ls_lines
# Cleanup
Netssh.new(a_host) do
execute :rm, ' -f test.file'
end.run
end

def test_group_netssh
Netssh.new(a_host) do
as user: :root, group: :admin do
execute :touch, 'restart.txt'
end
end.run

command_lines = @output.lines.select { |line| line.start_with?('Command:') }
assert_equal [
"Command: if ! sudo -u root whoami > /dev/null; then echo \"You cannot switch to user 'root' using sudo, please check the sudoers file\" 1>&2; false; fi\n",
Expand Down

0 comments on commit 6af6cad

Please sign in to comment.