RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end config.mock_with :rspec do |mocks| mocks.verify_partial_doubles = true end config.before(:context, scan_assessment: true) do @base_id = described_class.scan_base @base_results = described_class.retrieve_results(@base_id) described_class.get_scans.each do |type| puts type instance_variable_set("@#{type}_id", described_class.scan_machine(type)) puts instance_variable_get("@#{type}_id") instance_variable_set("@#{type}_results", described_class.retrieve_results(instance_variable_get("@#{type}_id"))) end end end if ENV['SCAN_ASSESSMENT'] require 'yaml' require 'net/http' require 'json' require 'docker' require 'securerandom' require 'base64' require 'pp' Docker.url = ENV.fetch('DOCKER_HOST') { "unix:///var/run/docker.sock" } #************************************************************************* # @parent - for variants of base tests like serverspec # @verify_machine_ready - when True will wait for file /tmp/status in the container # under test content's to match /ready_to_test/ # @host_config - passed to Docker::Container.create param HostConfig # # Example: # # class DockerHostCisBenchmarks # extend AssessmentHelpers # @parent = 'serverspec' # @host_config = { # 'vulnerable': { 'Privileged': true }, # 'secure': { 'Privileged': true, 'cap-add': 'ALL', }, # } # @verify_machine_ready = true # end RSpec::Matchers.define :fail_with_msg do |expected| match do false end failure_message do expected.to_s end end class ResultsServer attr_accessor :container def host_port container.info["NetworkSettings"]["Ports"]["3000/tcp"].first["HostPort"] end end class BaseMachine attr_accessor :container end results_server = ResultsServer.new base_machine = BaseMachine.new RSpec.configure do |config| config.before(:suite) do results_server.container = Docker::Container.create( Image: ENV['TEST_RESULTS_SERVER_IMAGE'], HostConfig: { PublishAllPorts: true } ) results_server.container.start sleep 5 # sleep rather than wait since we are daemonizing a container results_server.container.refresh! # get more details base_machine.container = Docker::Container.create(Image: 'alpine:3.4', Cmd: '/bin/ash', Tty: true) base_machine.container.start end config.after(:suite) do results_server.container.stop results_server.container.delete(force: true) base_machine.container.stop base_machine.container.delete(force: true) end end AssessmentHelpers = Module.new do MAX_SLEEP_TIME = 30 SLEEP_INTERVAL = 3 define_method :get_scans do %w(vulnerable secure) end define_method :assessment_name do name.split(/(?=[A-Z])|(?<=[a-z])(\d+)/).map { |e| e.downcase }.join('-') end define_method :manifest_file do assessment_path = @parent.nil? ? assessment_name : "#{@parent}/variants/#{assessment_name}" "./sectests/#{assessment_path}/manifest.yml" end define_method :options do @options ||= YAML.load(File.read(manifest_file)) end define_method :image_name do "#{options['registry']}/#{options['name']}:#{options['version']}" end define_method :config_hash do return default_test_config if respond_to?(:default_test_config) options['default_config'].each_with_object({}) do |(k, v), h| h[k.to_sym] = v end end define_method :args do |target| ssh_vals = options['prog_args']['ssh_key'] ? load_ssh_args : {} format(options['prog_args'], config_hash.merge({target: target}).merge(ssh_vals)).split(' ') end define_method :load_ssh_args do { ssh_user: 'testuser', ssh_key: Base64.strict_encode64(File.read('./spec/support/ssh_key')) } end define_method :machine_ready? do |machine, image_name| if @verify_machine_ready config_status = machine.exec(['cat', '/tmp/status']).to_s unless config_status.match(/ready_to_test/) puts "machine:#{image_name} not ready_to_test current status:#{config_status}" return false end end true end define_method :machine_running? do |machine, image_name| if !machine.json['State']['Running'] puts "Target machine: #{image_name} failed to start." return false end true end define_method :start_target_machine do |image_name| elapsed_time = 0 target = image_name.split('-')[-1].to_sym h_config = nil h_config = @host_config[target] if @host_config machine = Docker::Container.create(Image: image_name, HostConfig: h_config ) machine.start sleep SLEEP_INTERVAL until (machine_running?(machine, image_name) && machine_ready?(machine, image_name)) || elapsed_time > MAX_SLEEP_TIME sleep SLEEP_INTERVAL # sleep rather than wait since we are daemonizing a container elapsed_time += SLEEP_INTERVAL end machine end define_method :log_machine_not_running do |machine, image_name| puts '*' * 80 puts "* target docker container for #{image_name} not running" puts '*' * 80 pp machine.json puts "******************** logs #{image_name} *****************************" machine.streaming_logs(stdout: true, stderr: true) { |stream, chunk| puts "#{stream}: #{chunk}" } end define_method :stop_target_machine do |machine| machine.stop machine.delete(force: true) machine.id end define_method :scan_machine do |type| image_name = "#{assessment_name}-#{type}" machine = start_target_machine(image_name) target_name = type target_name = @target_host if @target_host scan(target_name, machine.id) if machine_running?(machine, image_name) && machine_ready?(machine, image_name) log_machine_not_running(machine, image_name) unless machine_running?(machine, image_name) stop_target_machine(machine) end define_method :scan_base do assessment_id = scan('base', base_machine.container.id) assessment_id end define_method :scan do |target, target_id| assessment_id = target == 'base' ? SecureRandom.hex(32) : target_id env = [ "NORAD_ROOT=http://results:3000", %Q{ASSESSMENT_PATHS=[{"id":"#{target}", "assessment": "/results/#{assessment_id}"}]}, "NORAD_SECRET=1234" ] c = Docker::Container.create({ Image: image_name, Cmd: args(target), Env: env, HostConfig: { Links: ["#{target_id}:#{target}", "#{results_server.container.id}:results"] }, }) c.start c.wait(60 * 10) # Output container logs for debugging if ENV['ENABLE_LOGS'] c.stop c_state = c.json['State'] # Print the entire state regardless of error or not to aid in debugging puts "[DEBUG] Container #{image_name}'s Final State" puts '-------------------------' c_state.each do |key, value| puts "#{key}: #{value}" end puts "\n[DEBUG] Logs for target \"#{image_name}\" run against #{target}:" # Print logs regardless of ExitCode puts c.logs(stdout: true, stderr: true) end c.delete(force: true) assessment_id end define_method :retrieve_results do |id| url = "http://localhost:#{results_server.host_port}/results?assessment_id=#{id}" uri = URI(url) JSON.parse(Net::HTTP.get(uri)) end end end