Class: Inspec::Runner

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/inspec/runner.rb

Overview

Inspec::Runner coordinates the running of tests and is the main entry point to the application.

Users are expected to insantiate a runner, add targets to be run, and then call the run method:

“‘ r = Inspec::Runner.new() r.add_target(“/path/to/some/profile”) r.add_target(“url/to/some/profile”) r.run “`

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(conf = {}) ⇒ Runner

Returns a new instance of Runner.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/inspec/runner.rb', line 48

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]
    Inspec.with_feature("inspec-waivers") {
      @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

Instance Attribute Details

#backendObject (readonly)

Returns the value of attribute backend.



38
39
40
# File 'lib/inspec/runner.rb', line 38

def backend
  @backend
end

#rulesObject (readonly)

Returns the value of attribute rules.



38
39
40
# File 'lib/inspec/runner.rb', line 38

def rules
  @rules
end

#target_profilesObject

Returns the value of attribute target_profiles.



39
40
41
# File 'lib/inspec/runner.rb', line 39

def target_profiles
  @target_profiles
end

#test_collectorObject

Returns the value of attribute test_collector.



41
42
43
# File 'lib/inspec/runner.rb', line 41

def test_collector
  @test_collector
end

Instance Method Details

#add_target(target, _opts = []) ⇒ Object

add_target allows the user to add a target whose tests will be run when the user calls the run method.

A target is a path or URL that points to a profile. Using this target we generate a Profile and a ProfileContext. The content (libraries, tests, and inputs) from the Profile are loaded into the ProfileContext.

If the profile depends on other profiles, those profiles will be loaded on-demand when include_content or required_content are called using similar code in Inspec::DSL.

Once the we’ve loaded all of the tests files in the profile, we query the profile for the full list of rules. Those rules are registered with the @test_collector which is ultimately responsible for actually running the tests.

TODO: Deduplicate/clarify the loading code that exists in here, the ProfileContext, the Profile, and Inspec::DSL



280
281
282
283
284
285
286
287
288
289
290
# File 'lib/inspec/runner.rb', line 280

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

#all_rulesObject

In some places we read the rules off of the runner, in other places we read it off of the profile context. To keep the API’s the same, we provide an #all_rules method here as well.



305
306
307
# File 'lib/inspec/runner.rb', line 305

def all_rules
  @rules
end

#attributesObject



43
44
45
46
# File 'lib/inspec/runner.rb', line 43

def attributes
  Inspec.deprecate(:rename_attributes_to_inputs, "Don't call runner.attributes, call runner.inputs")
  inputs
end

#configure_transportObject



94
95
96
97
# File 'lib/inspec/runner.rb', line 94

def configure_transport
  backend = Inspec::Backend.create(@conf)
  set_backend(backend)
end

#eval_with_virtual_profile(command) ⇒ Object



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/inspec/runner.rb', line 320

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

#loadObject



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/inspec/runner.rb', line 112

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
    # TODO: InSpec 8: Replace with Profile OnLoad event handling
    profile.locked_dependencies # Only need to do this once, this recurses down
    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
      # TODO: InSpec 8: Replace with Profile OnLoad event handling
      requirement.profile.load_gem_dependencies
      requirement.profile.load_libraries
      @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.descendant_filtered_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

#register_rules(ctx) ⇒ Object



309
310
311
312
313
314
315
316
317
318
# File 'lib/inspec/runner.rb', line 309

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

#render_output(run_data) ⇒ Object



220
221
222
223
224
225
226
227
228
# File 'lib/inspec/runner.rb', line 220

def render_output(run_data)
  return if @conf["reporter"].nil?

  @conf["reporter"].each do |reporter|
    enhanced_outcome_flag = @conf["enhanced_outcomes"]
    result = Inspec::Reporters.render(reporter, run_data, enhanced_outcome_flag)
    raise Inspec::ReporterError, "Error generating reporter '#{reporter[0]}'" if result == false
  end
end

#reportObject



230
231
232
# File 'lib/inspec/runner.rb', line 230

def report
  Inspec::Reporters.report(@conf["reporter"].first, @run_data)
end

#resetObject



104
105
106
107
108
109
110
# File 'lib/inspec/runner.rb', line 104

def reset
  @test_collector.reset
  @target_profiles.each do |profile|
    profile.runner_context.rules = {}
  end
  @rules = []
end

#run(with = nil) ⇒ Object



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/inspec/runner.rb', line 176

def run(with = nil)
  product_dist_name = Inspec::Dist::PRODUCT_NAME
  if Inspec::Dist::EXEC_NAME == "inspec"
    if Inspec::Telemetry::RunContextProbe.guess_run_context == "test-kitchen"
      product_dist_name = "Chef Workstation"
      configure_licensing_config_for_kitchen(@conf)
      # Persist the license key in file when passed via test-kitchen
      ChefLicensing.fetch_and_persist if @conf[:chef_license_key]
    end
    ChefLicensing.check_software_entitlement!
  end

  # Validate if profiles are signed and verified
  # Additional check is required to provide error message in case of inspec exec command (exec command can use multiple profiles as well)
  # Only runs this block when preview flag CHEF_PREVIEW_MANDATORY_PROFILE_SIGNING is set
  Inspec.with_feature("inspec-mandatory-profile-signing") {
    unless @conf.allow_unsigned_profiles?
      verify_target_profiles_if_signed(@target_profiles)
    end
  }

  Inspec::Log.debug "Starting run with targets: #{@target_profiles.map(&:to_s)}"
  Inspec::Telemetry.run_starting(runner: self, conf: @conf)
  load
  run_tests(with)
rescue ChefLicensing::LicenseKeyFetcher::LicenseKeyNotFetchedError
  Inspec::Log.error "#{product_dist_name} cannot execute without valid licenses."
  Inspec::UI.new.exit(:license_not_set)
rescue ChefLicensing::SoftwareNotEntitled
  Inspec::Log.error "License is not entitled to use #{product_dist_name}."
  Inspec::UI.new.exit(:license_not_entitled)
rescue ChefLicensing::Error => e
  Inspec::Log.error e.message
  Inspec::UI.new.exit(:usage_error)
end

#run_tests(with = nil) ⇒ Object



246
247
248
249
250
251
252
# File 'lib/inspec/runner.rb', line 246

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"]
  Inspec::Telemetry.run_ending(runner: self, run_data: @run_data, conf: @conf)
  @test_collector.exit_code
end

#set_backend(new_backend) ⇒ Object



99
100
101
102
# File 'lib/inspec/runner.rb', line 99

def set_backend(new_backend)
  @backend = new_backend
  @test_collector.backend = @backend
end

#supports_profile?(profile) ⇒ Boolean

Returns:

  • (Boolean)


292
293
294
295
296
297
298
299
300
# File 'lib/inspec/runner.rb', line 292

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

#testsObject



90
91
92
# File 'lib/inspec/runner.rb', line 90

def tests
  @test_collector.tests
end

#verify_target_profiles_if_signed(target_profiles) ⇒ Object



212
213
214
215
216
217
218
# File 'lib/inspec/runner.rb', line 212

def verify_target_profiles_if_signed(target_profiles)
  unsigned_profiles = []
  target_profiles.each do |profile|
    unsigned_profiles << profile.name unless profile.verify_if_signed
  end
  raise Inspec::ProfileSignatureRequired, "Signature required for profile/s: #{unsigned_profiles.join(", ")}. Please provide a signed profile. Or set CHEF_ALLOW_UNSIGNED_PROFILES in the environment. Or use `--allow-unsigned-profiles` flag with InSpec CLI. " unless unsigned_profiles.empty?
end

#write_lockfile(profile) ⇒ Object



234
235
236
237
238
239
240
241
242
243
244
# File 'lib/inspec/runner.rb', line 234

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