class Inspec::Runner
“‘
r.run
r.add_target(“url/to/some/profile”)
r.add_target(“/path/to/some/profile”)
r = Inspec::Runner.new()
“`
and then call the run method:
Users are expected to insantiate a runner, add targets to be run,
entry point to the application.
Inspec::Runner coordinates the running of tests and is the main
def add_resource(method_name, arg, opts, block)
def add_resource(method_name, arg, opts, block) case method_name when "describe" opts = { backend: @test_collector.backend }.merge opts @test_collector.example_group(*arg, opts, &block) when "expect" block.example_group when "describe.one" tests = arg.map do |x| @test_collector.example_group(x[1][0], block_source_info(x[2]), &x[2]) end return nil if tests.empty? successful_tests = tests.find_all(&:run) # Return all tests if none succeeds; we will just report full failure return tests if successful_tests.empty? successful_tests else raise "A rule was registered with #{method_name.inspect}," \ "which isn't understood and cannot be processed." end end
def add_target(target, _opts = [])
@eturns [Inspec::ProfileContext]
@params _opts [Hash] Unused, but still here to avoid breaking kitchen-inspec
@params target [String] A path or URL to a profile or raw test.
the ProfileContext, the Profile, and Inspec::DSL
TODO: Deduplicate/clarify the loading code that exists in here,
responsible for actually running the tests.
registered with the @test_collector which is ultimately
query the profile for the full list of rules. Those rules are
Once the we've loaded all of the tests files in the profile, we
called using similar code in Inspec::DSL.
loaded on-demand when include_content or required_content are
If the profile depends on other profiles, those profiles will be
into the ProfileContext.
(libraries, tests, and inputs) from the Profile are loaded
target we generate a Profile and a ProfileContext. The content
A target is a path or URL that points to a profile. Using this
run when the user calls the run method.
add_target allows the user to add a target whose tests will be
def add_target(target, _opts = []) profile = Inspec::Profile.for_target(target, vendor_cache: @cache, backend: @backend, controls: @controls, tags: @tags, runner_conf: @conf) raise "Could not resolve #{target} to valid input." if profile.nil? @target_profiles << profile if supports_profile?(profile) end
def all_rules
places we read it off of the profile context. To keep the API's
In some places we read the rules off of the runner, in other
def all_rules @rules end
def attributes
def attributes Inspec.deprecate(:rename_attributes_to_inputs, "Don't call runner.attributes, call runner.inputs") inputs end
def block_source_info(block)
def block_source_info(block) return {} if block.nil? || !block.respond_to?(:source_location) opts = {} file_path, line = block.source_location opts["file_path"] = file_path opts["line_number"] = line opts end
def configure_transport
def configure_transport @backend = Inspec::Backend.create(@conf) @test_collector.backend = @backend end
def eval_with_virtual_profile(command)
def eval_with_virtual_profile(command) require "inspec/fetcher/mock" add_target({ "inspec.yml" => "name: inspec-shell" }) our_profile = @target_profiles.first ctx = our_profile.runner_context # Load local profile dependencies. This is used in inspec shell # to provide access to local profiles that add resources. @depends.each do |dep| # support for windows paths dep = dep.tr("\\", "/") Inspec::Profile.for_path(dep, { profile_context: ctx }).load_libraries end ctx.load(command) end
def get_check_example(method_name, arg, block)
def get_check_example(method_name, arg, block) opts = block_source_info(block) return nil if arg.empty? resource = arg[0] # check to see if we are using a filtertable object resource = resource.resource if resource.is_a? FilterTable::Table if resource.respond_to?(:resource_skipped?) && resource.resource_skipped? return rspec_skipped_block(arg, opts, resource.resource_exception_message) end if resource.respond_to?(:resource_failed?) && resource.resource_failed? return rspec_failed_block(arg, opts, resource.resource_exception_message) end # If neither skipped nor failed then add the resource add_resource(method_name, arg, opts, block) end
def initialize(conf = {})
def initialize(conf = {}) @rules = [] # If we were handed a Hash config (by audit cookbook or kitchen-inspec), # upgrade it to a proper config. This handles a lot of config finalization, # like reporter parsing. @conf = conf.is_a?(Hash) ? Inspec::Config.new(conf) : conf @conf[:logger] ||= Logger.new(nil) @target_profiles = [] @controls = @conf[:controls] || [] @tags = @conf[:tags] || [] @depends = @conf[:depends] || [] @create_lockfile = @conf[:create_lockfile] @cache = Inspec::Cache.new(@conf[:vendor_cache]) @test_collector = @conf.delete(:test_collector) || begin RunnerRspec.new(@conf) end if @conf[:waiver_file] @conf[:waiver_file].each do |file| unless File.file?(file) raise Inspec::Exceptions::WaiversFileDoesNotExist, "Waiver file #{file} does not exist." end end end # About reading inputs: # @conf gets passed around a lot, eventually to # Inspec::InputRegistry.register_external_inputs. # # @conf may contain the key :attributes or :inputs, which is to be a Hash # of values passed in from the Runner API. # This is how kitchen-inspec and the audit_cookbook pass in inputs. # # @conf may contain the key :attrs or :input_file, which is to be an Array # of file paths, each a YAML file. This how --input-file works. configure_transport end
def load
def load all_controls = [] @target_profiles.each do |profile| @test_collector.add_profile(profile) next unless profile.supports_platform? write_lockfile(profile) if @create_lockfile profile.locked_dependencies profile.load_gem_dependencies profile_context = profile.load_libraries profile_context.dependencies.list.values.each do |requirement| unless requirement.profile.supports_platform? Inspec::Log.warn "Skipping profile: '#{requirement.profile.name}'" \ " on unsupported platform: '#{@backend.platform.name}/#{@backend.platform.release}'." next end @test_collector.add_profile(requirement.profile) end begin tests = profile.collect_tests all_controls += tests unless tests.nil? rescue Inspec::Exceptions::ProfileLoadFailed => e Inspec::Log.error "Failed to load profile #{profile.name}: #{e}" profile.set_status_message e.to_s next end end controls_count = 0 control_checks_count_map = {} all_controls.each do |rule| unless rule.nil? register_rule(rule) total_checks = 0 control_describe_checks = ::Inspec::Rule.prepare_checks(rule) examples = control_describe_checks.flat_map do |m, a, b| get_check_example(m, a, b) end.compact examples.map { |example| total_checks += example.examples.count } unless control_describe_checks.empty? # controls with empty tests are avoided # checks represent tests within control controls_count += 1 if control_checks_count_map[rule.to_s].nil? control_checks_count_map[rule.to_s] = control_checks_count_map[rule.to_s].to_i + total_checks end end end # this sets data via runner-rspec into base RSpec formatter object, which gets used up within streaming plugins @test_collector.set_controls_count(controls_count) @test_collector.set_control_checks_count_map(control_checks_count_map) end
def register_rule(rule)
def register_rule(rule) Inspec::Log.debug "Registering rule #{rule}" @rules << rule checks = ::Inspec::Rule.prepare_checks(rule) examples = checks.flat_map do |m, a, b| get_check_example(m, a, b) end.compact examples.each { |e| @test_collector.add_test(e, rule) } end
def register_rules(ctx)
def register_rules(ctx) new_tests = false ctx.rules.each do |rule_id, rule| next if block_given? && !(yield rule_id, rule) new_tests = true register_rule(rule) end new_tests end
def render_output(run_data)
def render_output(run_data) return if @conf["reporter"].nil? @conf["reporter"].each do |reporter| result = Inspec::Reporters.render(reporter, run_data, @conf["enhanced_outcomes"]) raise Inspec::ReporterError, "Error generating reporter '#{reporter[0]}'" if result == false end end
def report
def report Inspec::Reporters.report(@conf["reporter"].first, @run_data) end
def reset
def reset @test_collector.reset @target_profiles.each do |profile| profile.runner_context.rules = {} end @rules = [] end
def rspec_failed_block(arg, opts, message)
def rspec_failed_block(arg, opts, message) @test_collector.example_group(*arg, opts) do # Send custom `it` block to RSpec it "" do # Raising here to fail the test and get proper formatting raise Inspec::Exceptions::ResourceFailed, message end end end
def rspec_skipped_block(arg, opts, message)
def rspec_skipped_block(arg, opts, message) @test_collector.example_group(*arg, opts) do # Send custom `it` block to RSpec it message end end
def run(with = nil)
def run(with = nil) Inspec::Log.debug "Starting run with targets: #{@target_profiles.map(&:to_s)}" load run_tests(with) end
def run_tests(with = nil)
def run_tests(with = nil) @run_data = @test_collector.run(with) # dont output anything if we want a report render_output(@run_data) unless @conf["report"] @test_collector.exit_code end
def supports_profile?(profile)
def supports_profile?(profile) unless profile.supports_runtime? raise "This profile requires #{Inspec::Dist::PRODUCT_NAME} version "\ "#{profile.metadata.inspec_requirement}. You are running "\ "#{Inspec::Dist::PRODUCT_NAME} v#{Inspec::VERSION}.\n" end true end
def tests
def tests @test_collector.tests end
def write_lockfile(profile)
def write_lockfile(profile) return false unless profile.writable? if profile.lockfile_exists? Inspec::Log.debug "Using existing lockfile #{profile.lockfile_path}" else Inspec::Log.debug "Creating lockfile: #{profile.lockfile_path}" lockfile = profile.generate_lockfile File.write(profile.lockfile_path, lockfile.to_yaml) end end