require 'rspec' require 'rspec/core/rake_task' require 'net/ssh' require 'specinfra' require 'norad_spec_runner/sec_test' require 'norad_spec_runner/linux_sec_test' module NoradSpecRunner # Class to run Rspec tests over SSH class RemoteTask < Task SSH_TIMEOUT = 10 attr_reader :host, :username, :sshkey, :ssh_port, :obj, :platform, :results_file, :cisco_enable_pw, :tests_parent_dir, :disable_sudo def initialize(encoded_key, options) @host = options.fetch(:host) @sshkey = options.fetch(:sshkey) @cisco_enable_pw = options.fetch(:cisco_enable_pw, '') @disable_sudo = options.fetch(:disable_sudo, 'true') # Decode the key and store File.open(@sshkey, "w") do |f| f.write Base64.decode64(encoded_key) end @ssh_port = options.fetch(:port, nil) @username = options.fetch(:username, nil) @tests_parent_dir = ENV.fetch('TESTS_PARENT_DIR', '/') tests = options.fetch(:tests) sub_tests = options.fetch(:sub_tests) detect_os = options.fetch(:detect_os, false) @obj = RSpec::Core::RakeTask.new(@host) do |_| true end @results_file = options.fetch(:results_file) @obj.pattern = detect_os ? autodetect_test_pattern(tests) : tests @obj.rspec_opts = build_rspec_opts(options) end def run pstderr = STDERR.dup ftmp = Tempfile.open('eout') FileUtils.touch results_file return if platform.nil? && unable_to_ssh? ENV['AUDIT_HOST'] = host ENV['AUDIT_USERNAME'] = username ENV['AUDIT_SSHKEY'] = sshkey ENV['AUDIT_SSH_PORT'] = ssh_port ENV['AUDIT_CISCO_ENABLE_PASSWORD'] = cisco_enable_pw ENV['AUDIT_DISABLE_SUDO'] = disable_sudo.to_s # Capture STDERR for SSH related errors STDERR.reopen(ftmp) obj.run_task(true) rescue SystemExit => e ftmp.rewind err = ftmp.read ftmp.close p err # We land here even on successful run (SystemExit exception), only report error if stderr is not empty if err =~ /Please set sudo password to Specinfra.configuration.sudo_password|set :request_pty, true/ write_error_to_results_file "SSH user #{username} requires SUDO permission with NOPASSWD: option set." elsif not err.empty? write_error_to_results_file err end rescue Exception => e # Unknown exception! write_error_to_results_file e.message ensure STDERR.reopen pstderr end private def write_error_to_results_file(error) File.open(results_file, "w") do |f| f.write "!! NORAD SPECS SSH ERROR !!\nError: #{error}\n" end end def start_ssh_session Net::SSH.start(host, username, port: ssh_port, timeout: SSH_TIMEOUT, auth_methods: ['publickey'], keys: [sshkey] ) end def autodetect_test_pattern(tests) pattern = select_spec_for_os(tests) puts "Trying to run #{pattern}" pattern end def select_spec_for_os(test_name) # Explicitly need this test case first if target_is_ios_device?(start_ssh_session) test_klass = SecTest elsif target_is_linux?(start_ssh_session) test_klass = LinuxSecTest else test_klass = SecTest end test_klass.new(tests_parent_dir, test_name, platform).test_to_run end def target_is_ios_device?(ssh_session) out = ssh_session.exec!('show version') if out =~ /Cisco ((?:\w+\s?){1,6}) Software.+Version ([[:alnum:][:punct:]]+)/ @platform = $1.gsub(/\s/, '_').downcase end !@platform.nil? end def target_is_linux?(ssh_session) spec_infra_backend = Specinfra::Backend::Ssh.new spec_infra_backend.set_config(:ssh, ssh_session) spec_infra_backend.set_config(:ssh_options, { user: username }) Specinfra::Helper::DetectOs.subclasses.each do |c| detector = c.new(spec_infra_backend) res = detector.detect if res && res[:family] res[:arch] ||= spec_infra_backend.run_command('uname -m').stdout.strip @platform = res[:family] # we can optionally add release here if we want break end end !@platform.nil? end # Check if given host/ip is reachable and we can ssh as root # If not, then create empty log file for that host and return false. def unable_to_ssh? session = start_ssh_session session.exec('ls') session.close() false rescue Net::SSH::AuthenticationFailed, Net::SSH::ConnectionTimeout, Net::SSH::Timeout, Net::SSH::Disconnect, Net::SSH::Exception, Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e p e write_error_to_results_file e.message true end def build_rspec_opts(options) rspec_opts = [ '--format json', "-o #{@results_file}"] options.fetch(:tags).split(',').each do |tag| rspec_opts << "--tag=#{tag}" end # --tag and --example do not work together so # if there is a tag disable example option if options.fetch(:tags) == '' rspec_opts << "-e #{sub_tests}" end rspec_opts.join(' ') end end end