require 'spec_helper' require 'puppet/provider/exec' require 'puppet_spec/compiler' require 'puppet_spec/files' describe Puppet::Provider::Exec do include PuppetSpec::Compiler include PuppetSpec::Files describe "#extractexe" do it "should return the first element of an array" do expect(subject.extractexe(['one', 'two'])).to eq('one') end { # double-quoted commands %q{"/has whitespace"} => "/has whitespace", %q{"/no/whitespace"} => "/no/whitespace", # singe-quoted commands %q{'/has whitespace'} => "/has whitespace", %q{'/no/whitespace'} => "/no/whitespace", # combinations %q{"'/has whitespace'"} => "'/has whitespace'", %q{'"/has whitespace"'} => '"/has whitespace"', %q{"/has 'special' characters"} => "/has 'special' characters", %q{'/has "special" characters'} => '/has "special" characters', # whitespace split commands %q{/has whitespace} => "/has", %q{/no/whitespace} => "/no/whitespace", }.each do |base_command, exe| ['', ' and args', ' "and args"', " 'and args'"].each do |args| command = base_command + args it "should extract #{exe.inspect} from #{command.inspect}" do expect(subject.extractexe(command)).to eq(exe) end end end end context "when handling sensitive data" do before :each do Puppet[:log_level] = 'debug' end let(:supersecret) { 'supersecret' } let(:path) do if Puppet::Util::Platform.windows? # The `apply_compiled_manifest` helper doesn't add the `path` fact, so # we can't reference that in our manifest. Windows PATHs can contain # double quotes and trailing backslashes, which confuse HEREDOC # interpolation below. So sanitize it: ENV['PATH'].split(File::PATH_SEPARATOR).map do |dir| dir.gsub(/"/, '\"').gsub(/\\$/, '') end.join(File::PATH_SEPARATOR) else ENV['PATH'] end end def ruby_exit_0 "ruby -e 'exit 0'" end def echo_from_ruby_exit_0(message) # Escape double quotes due to HEREDOC interpolation below "ruby -e 'puts \"#{message}\"; exit 0'".gsub(/"/, '\"') end def echo_from_ruby_exit_1(message) # Escape double quotes due to HEREDOC interpolation below "ruby -e 'puts \"#{message}\"; exit 1'".gsub(/"/, '\"') end context "when validating the command" do it "redacts the arguments if the command is relative" do expect { apply_compiled_manifest(<<-MANIFEST) exec { 'echo': command => Sensitive.new('echo #{supersecret}') } MANIFEST }.to raise_error do |err| expect(err).to be_a(Puppet::Error) expect(err.message).to match(/'echo' is not qualified and no path was specified. Please qualify the command or specify a path./) expect(err.message).to_not match(/#{supersecret}/) end end it "redacts the arguments if the command is a directory" do dir = tmpdir('exec') apply_compiled_manifest(<<-MANIFEST) exec { 'echo': command => Sensitive.new('#{dir} #{supersecret}'), } MANIFEST expect(@logs).to include(an_object_having_attributes(level: :err, message: /'#{dir}' is a directory, not a file/)) expect(@logs).to_not include(an_object_having_attributes(message: /#{supersecret}/)) end it "redacts the arguments if the command isn't executable" do file = tmpfile('exec') Puppet::FileSystem.touch(file) Puppet::FileSystem.chmod(0644, file) apply_compiled_manifest(<<-MANIFEST) exec { 'echo': command => Sensitive.new('#{file} #{supersecret}'), } MANIFEST # Execute permission works differently on Windows, but execute will fail since the # file doesn't have a valid extension and isn't a valid executable. The raised error # will be Errno::EIO, which is not useful. The Windows execute code needs to raise # Puppet::Util::Windows::Error so the Win32 error message is preserved. pending("PUP-3561 Needs to raise a meaningful Puppet::Error") if Puppet::Util::Platform.windows? expect(@logs).to include(an_object_having_attributes(level: :err, message: /'#{file}' is not executable/)) expect(@logs).to_not include(an_object_having_attributes(message: /#{supersecret}/)) end it "redacts the arguments if the relative command cannot be resolved using the path parameter" do file = File.basename(tmpfile('exec')) dir = tmpdir('exec') apply_compiled_manifest(<<-MANIFEST) exec { 'echo': command => Sensitive.new('#{file} #{supersecret}'), path => "#{dir}", } MANIFEST expect(@logs).to include(an_object_having_attributes(level: :err, message: /Could not find command '#{file}'/)) expect(@logs).to_not include(an_object_having_attributes(message: /#{supersecret}/)) end end it "redacts the command on success" do command = echo_from_ruby_exit_0(supersecret) apply_compiled_manifest(<<-MANIFEST) exec { 'true': command => Sensitive.new("#{command}"), path => "#{path}", } MANIFEST expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing '[redacted]'", source: /Exec\[true\]/)) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing: '[redacted]'", source: "Puppet")) expect(@logs).to include(an_object_having_attributes(level: :notice, message: "executed successfully")) expect(@logs).to_not include(an_object_having_attributes(message: /#{supersecret}/)) end it "redacts the command on failure" do command = echo_from_ruby_exit_1(supersecret) apply_compiled_manifest(<<-MANIFEST) exec { 'false': command => Sensitive.new("#{command}"), path => "#{path}", } MANIFEST expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing '[redacted]'", source: /Exec\[false\]/)) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing: '[redacted]'", source: "Puppet")) expect(@logs).to include(an_object_having_attributes(level: :err, message: "[command redacted] returned 1 instead of one of [0]")) expect(@logs).to_not include(an_object_having_attributes(message: /#{supersecret}/)) end context "when handling checks" do let(:onlyifsecret) { "onlyifsecret" } let(:unlesssecret) { "unlesssecret" } it "redacts command and onlyif outputs" do onlyif = echo_from_ruby_exit_0(onlyifsecret) apply_compiled_manifest(<<-MANIFEST) exec { 'true': command => Sensitive.new("#{ruby_exit_0}"), onlyif => Sensitive.new("#{onlyif}"), path => "#{path}", } MANIFEST expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing check '[redacted]'")) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing '[redacted]'", source: /Exec\[true\]/)) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing: '[redacted]'", source: "Puppet")) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "[output redacted]")) expect(@logs).to include(an_object_having_attributes(level: :notice, message: "executed successfully")) expect(@logs).to_not include(an_object_having_attributes(message: /#{onlyifsecret}/)) end it "redacts the command that would have been executed but didn't due to onlyif" do command = echo_from_ruby_exit_0(supersecret) onlyif = echo_from_ruby_exit_1(onlyifsecret) apply_compiled_manifest(<<-MANIFEST) exec { 'true': command => Sensitive.new("#{command}"), onlyif => Sensitive.new("#{onlyif}"), path => "#{path}", } MANIFEST expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing check '[redacted]'")) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing: '[redacted]'", source: "Puppet")) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "[output redacted]")) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "'[command redacted]' won't be executed because of failed check 'onlyif'")) expect(@logs).to_not include(an_object_having_attributes(message: /#{supersecret}/)) expect(@logs).to_not include(an_object_having_attributes(message: /#{onlyifsecret}/)) end it "redacts command and unless outputs" do unlesscmd = echo_from_ruby_exit_1(unlesssecret) apply_compiled_manifest(<<-MANIFEST) exec { 'true': command => Sensitive.new("#{ruby_exit_0}"), unless => Sensitive.new("#{unlesscmd}"), path => "#{path}", } MANIFEST expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing check '[redacted]'")) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing '[redacted]'", source: /Exec\[true\]/)) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing: '[redacted]'", source: "Puppet")) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "[output redacted]")) expect(@logs).to include(an_object_having_attributes(level: :notice, message: "executed successfully")) expect(@logs).to_not include(an_object_having_attributes(message: /#{unlesssecret}/)) end it "redacts the command that would have been executed but didn't due to unless" do command = echo_from_ruby_exit_0(supersecret) unlesscmd = echo_from_ruby_exit_0(unlesssecret) apply_compiled_manifest(<<-MANIFEST) exec { 'true': command => Sensitive.new("#{command}"), unless => Sensitive.new("#{unlesscmd}"), path => "#{path}", } MANIFEST expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing check '[redacted]'")) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "Executing: '[redacted]'", source: "Puppet")) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "[output redacted]")) expect(@logs).to include(an_object_having_attributes(level: :debug, message: "'[command redacted]' won't be executed because of failed check 'unless'")) expect(@logs).to_not include(an_object_having_attributes(message: /#{supersecret}/)) expect(@logs).to_not include(an_object_having_attributes(message: /#{unlesssecret}/)) end end end end