Skip to content
This repository has been archived by the owner on Dec 18, 2019. It is now read-only.

Commit

Permalink
Add censor_cmd to helpers
Browse files Browse the repository at this point in the history
This adds a `censor_cmd` method to the helpers modules, and modifies `run_cmd` and most callers
of that to take a `secrets` array argument, the values of which will be censored in the log output.

A basic test for `censor_cmd` is provided, but it is beyond my ken to do a proper test of the
output from `run_cmd` in the tests.
  • Loading branch information
Anthony Hersey committed Nov 9, 2018
1 parent d5cd044 commit 3cf60b8
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 22 deletions.
38 changes: 24 additions & 14 deletions lib/deployinator/helpers.rb
Expand Up @@ -129,39 +129,41 @@ def runlog_thread_filename(name)
# This defaults to 5, but can be specified with the num_retries parameter
# If all the retries fail, an exception is thrown
# Between retries it will sleep for a given period, defaulting to 2 seconds
def run_cmd_with_retries(cmd, num_retries=5, sleep_seconds=2, timing_metric=nil)
def run_cmd_with_retries(cmd, num_retries=5, sleep_seconds=2, timing_metric=nil, secrets=[])
censored_cmd = censor_cmd(cmd, secrets)
for i in 1..num_retries
if i == num_retries then
result = run_cmd(cmd, timing_metric)
result = run_cmd(cmd, timing_metric, true, secrets)
else
result = run_cmd(cmd, timing_metric, false)
result = run_cmd(cmd, timing_metric, false, secrets)
end
if result[:exit_code] == 0
return result
else
retries_remaining = num_retries - i
unless i == num_retries
log_and_stream("`#{cmd}` failed, will retry #{retries_remaining} more times<br>")
log_and_stream("`#{censored_cmd}` failed, will retry #{retries_remaining} more times<br>")
sleep sleep_seconds
end
end
end

raise "Unable to execute `#{cmd}` after retrying #{num_retries} times"
raise "Unable to execute `#{censored_cmd}` after retrying #{num_retries} times"
end

def check_command_safety(command)
def check_command_safety(command, secrets = [])
if command.include? "\n"
censored_cmd = censor_cmd(command, secrets)
raise CommandContainsNewlinesError.new(command),
"Command contains newlines and may not execute as intended: #{command}"
"Command contains newlines and may not execute as intended: #{censored_cmd}"
end

command
end

def run_cmd_safely(command, timing_metric = nil, log_errors = true)
command = check_command_safety(command)
run_cmd(command, timing_metric, log_errors)
def run_cmd_safely(command, timing_metric = nil, log_errors = true, secrets=[])
command = check_command_safety(command, secrets = [])
run_cmd(command, timing_metric, log_errors, secrets)
end

def all_eof(files)
Expand All @@ -179,21 +181,22 @@ def log_and_stream_error(erroutput, log_errors = true)
# Run external command with timing information
# streams and logs the output of the command as well
# does not (currently) check exit status codes
def run_cmd(cmd, timing_metric=nil, log_errors=true)
def run_cmd(cmd, timing_metric=nil, log_errors=true, secrets=[])
ret = ""
error_message = ""
exit_code = 0
start = Time.now.to_i
timestamp = Time.now.to_s
censored_cmd = censor_cmd(cmd, secrets)
plugin_state = {
:cmd => cmd,
:cmd => censored_cmd,
:timing_metric => timing_metric,
:start_time => start,
:log_errors => log_errors,
:filename => @filename,
}
raise_event(:run_command_start, plugin_state)
log_and_stream "<div class='command'><h4>#{timestamp}: Running #{cmd}</h4>\n<p class='output'>"
log_and_stream "<div class='command'><h4>#{timestamp}: Running #{censored_cmd}</h4>\n<p class='output'>"
time = Benchmark.measure do
Open3.popen3(cmd) do |inn, out, err, wait_thr|
output = ""
Expand Down Expand Up @@ -247,7 +250,7 @@ def run_cmd(cmd, timing_metric=nil, log_errors=true)
end
# Log non-zero exits
if wait_thr.value.exitstatus != 0 then
log_and_stream("<span class='stderr'>DANGER! #{cmd} had an exit value of: #{wait_thr.value.exitstatus}</span><br>")
log_and_stream("<span class='stderr'>DANGER! #{censored_cmd} had an exit value of: #{wait_thr.value.exitstatus}</span><br>")
exit_code = wait_thr.value.exitstatus
end
end
Expand All @@ -261,6 +264,13 @@ def run_cmd(cmd, timing_metric=nil, log_errors=true)
return { :stdout => ret, :exit_code => exit_code }
end

# Strips out the secrets from the command and replaces with *******
def censor_cmd(cmd, secrets=[])
censored = cmd.dup
secrets.each { |secret| censored.gsub!(secret, "*******") }
return censored
end

def nicify_env(env)
env = "production" if env == "PROD"
env.downcase
Expand Down
14 changes: 7 additions & 7 deletions lib/deployinator/helpers/dsh.rb
Expand Up @@ -14,10 +14,10 @@ def group_option_for_dsh(groups)
groups.map {|group| "-g #{group} "}.join("")
end

def run_dsh(groups, cmd, only_stdout=true, timing_metric=nil, log_errors=true, ignore_failure=false, &block)
def run_dsh(groups, cmd, only_stdout=true, timing_metric=nil, log_errors=true, ignore_failure=false, secrets=[], &block)
dsh_groups = group_option_for_dsh(groups)
ignore_failure = ignore_failure ? ignore_failure_command : ""
cmd_return = run_cmd(%Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} dsh #{dsh_groups} -r ssh -F #{dsh_fanout} "#{cmd}"#{ignore_failure}}, timing_metric, log_errors, &block)
cmd_return = run_cmd(%Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} dsh #{dsh_groups} -r ssh -F #{dsh_fanout} "#{cmd}"#{ignore_failure}}, timing_metric, log_errors, secrets, &block)
if only_stdout
cmd_return[:stdout]
else
Expand All @@ -26,20 +26,20 @@ def run_dsh(groups, cmd, only_stdout=true, timing_metric=nil, log_errors=true, i
end

# run dsh against a given host or array of hosts
def run_dsh_hosts(hosts, cmd, extra_opts='', only_stdout=true, timing_metric=nil, log_errors=true, ignore_failure=false, &block)
def run_dsh_hosts(hosts, cmd, extra_opts='', only_stdout=true, timing_metric=nil, log_errors=true, ignore_failure=false, secrets=[], &block)
hosts = [hosts] unless hosts.is_a?(Array)
ignore_failure = ignore_failure ? ignore_failure_command : ""
if extra_opts.length > 0
run_cmd %Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} 'dsh -m #{hosts.join(',')} -r ssh -F #{dsh_fanout} #{extra_opts} -- "#{cmd}"#{ignore_failure}'}, timing_metric, log_errors, &block
run_cmd %Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} 'dsh -m #{hosts.join(',')} -r ssh -F #{dsh_fanout} #{extra_opts} -- "#{cmd}"#{ignore_failure}'}, timing_metric, log_errors, secrets, &block
else
run_cmd %Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} 'dsh -m #{hosts.join(',')} -r ssh -F #{dsh_fanout} -- "#{cmd}" #{ignore_failure}'}, timing_metric, log_errors, &block
run_cmd %Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} 'dsh -m #{hosts.join(',')} -r ssh -F #{dsh_fanout} -- "#{cmd}" #{ignore_failure}'}, timing_metric, log_errors, secrets, &block
end
end

def run_dsh_extra(groups, cmd, extra_opts, only_stdout=true, timing_metric=nil, log_errors=true, ignore_failure=false, &block)
def run_dsh_extra(groups, cmd, extra_opts, only_stdout=true, timing_metric=nil, log_errors=true, ignore_failure=false, secrets=[], &block)
dsh_groups = group_option_for_dsh(groups)
ignore_failure = ignore_failure ? ignore_failure_command : ""
cmd_return = run_cmd(%Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} dsh #{dsh_groups} -r ssh #{extra_opts} -F #{dsh_fanout} "#{cmd}"#{ignore_failure} }, timing_metric, log_errors, &block)
cmd_return = run_cmd(%Q{ssh #{Deployinator.default_user}@#{Deployinator.deploy_host} dsh #{dsh_groups} -r ssh #{extra_opts} -F #{dsh_fanout} "#{cmd}"#{ignore_failure} }, timing_metric, log_errors, secrets, &block)
if only_stdout
cmd_return[:stdout]
else
Expand Down
23 changes: 22 additions & 1 deletion test/unit/helpers_test.rb
Expand Up @@ -24,7 +24,7 @@ def setup

# mock out the run_cmd. this should move.
Deployinator::Helpers.module_eval do
define_method "run_cmd" do |cmd, arg2 = nil, arg3 = nil|
define_method "run_cmd" do |cmd, arg2 = nil, arg3 = nil, arg4 = nil|
return { :stdout => cmd, :exit_code => 0 }
end
end
Expand Down Expand Up @@ -147,4 +147,25 @@ def test_run_cmd_safely
run_cmd_safely(unsafe_command)
end
end

def test_censor_cmd
cmd = "something between alice and bob"

censored = censor_cmd(cmd)
assert_equal("something between alice and bob", cmd, "cmd should be unchanged")
assert_equal("something between alice and bob", censored, "censored should be unchanged as no secrets")

censored = censor_cmd(cmd, ["alice"])
assert_equal("something between alice and bob", cmd, "cmd should be unchanged")
assert_equal("something between ******* and bob", censored, "censored should have alice censored")

censored = censor_cmd(cmd, ["alice", "bob"])
assert_equal("something between alice and bob", cmd, "cmd should be unchanged")
assert_equal("something between ******* and *******", censored, "censored should have alice censored")

cmd = "something between alice and bob, not between alice and charlie"
censored = censor_cmd(cmd, ["alice"])
assert_equal("something between alice and bob, not between alice and charlie", cmd, "cmd should be unchanged")
assert_equal("something between ******* and bob, not between ******* and charlie", censored, "censored should have alice censored twice")
end
end

0 comments on commit 3cf60b8

Please sign in to comment.