inspec 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,9 @@
3
3
  # author: Christoph Hartmann
4
4
  require 'inspec/log'
5
5
  require 'inspec/rule'
6
- require 'inspec/dsl'
6
+ require 'inspec/resource'
7
+ require 'inspec/library_eval_context'
8
+ require 'inspec/control_eval_context'
7
9
  require 'inspec/require_loader'
8
10
  require 'securerandom'
9
11
  require 'inspec/objects/attribute'
@@ -14,7 +16,7 @@ module Inspec
14
16
  new(profile.name, backend, { 'profile' => profile })
15
17
  end
16
18
 
17
- attr_reader :attributes, :rules, :profile_id
19
+ attr_reader :attributes, :rules, :profile_id, :resource_registry
18
20
  def initialize(profile_id, backend, conf)
19
21
  if backend.nil?
20
22
  fail 'ProfileContext is initiated with a backend == nil. ' \
@@ -25,17 +27,35 @@ module Inspec
25
27
  @conf = conf.dup
26
28
  @rules = {}
27
29
  @subcontexts = []
28
- @dependencies = {}
29
- @dependencies = conf['profile'].locked_dependencies unless conf['profile'].nil?
30
30
  @require_loader = ::Inspec::RequireLoader.new
31
31
  @attributes = []
32
- reload_dsl
32
+ # A local resource registry that only contains resources defined
33
+ # in the transitive dependency tree of the loaded profile.
34
+ @resource_registry = Inspec::Resource.new_registry
35
+ @library_eval_context = Inspec::LibraryEvalContext.create(@resource_registry, @require_loader)
36
+ end
37
+
38
+ def dependencies
39
+ if @conf['profile'].nil?
40
+ {}
41
+ else
42
+ @conf['profile'].locked_dependencies
43
+ end
44
+ end
45
+
46
+ def to_resources_dsl
47
+ Inspec::Resource.create_dsl(@backend, @resource_registry)
48
+ end
49
+
50
+ def control_eval_context
51
+ @control_eval_context ||= begin
52
+ ctx = Inspec::ControlEvalContext.create(self, to_resources_dsl)
53
+ ctx.new(@backend, @conf, dependencies, @require_loader)
54
+ end
33
55
  end
34
56
 
35
57
  def reload_dsl
36
- resources_dsl = Inspec::Resource.create_dsl(@backend)
37
- ctx = create_context(resources_dsl, rule_context(resources_dsl))
38
- @profile_context = ctx.new(@backend, @conf, @dependencies, @require_loader)
58
+ @control_eval_context = nil
39
59
  end
40
60
 
41
61
  def all_rules
@@ -44,6 +64,12 @@ module Inspec
44
64
  ret
45
65
  end
46
66
 
67
+ def add_resources(context)
68
+ @resource_registry.merge!(context.resource_registry)
69
+ control_eval_context.add_resources(context)
70
+ reload_dsl
71
+ end
72
+
47
73
  def add_subcontext(context)
48
74
  @subcontexts << context
49
75
  end
@@ -65,21 +91,29 @@ module Inspec
65
91
  # load all files directly that are flat inside the libraries folder
66
92
  autoloads.each do |path|
67
93
  next unless path.end_with?('.rb')
68
- load(*@require_loader.load(path)) unless @require_loader.loaded?(path)
94
+ load_library_file(*@require_loader.load(path)) unless @require_loader.loaded?(path)
69
95
  end
70
-
71
96
  reload_dsl
72
97
  end
73
98
 
74
- def load(content, source = nil, line = nil)
99
+ def load_control_file(*args)
100
+ load_with_context(control_eval_context, *args)
101
+ end
102
+ alias load load_control_file
103
+
104
+ def load_library_file(*args)
105
+ load_with_context(@library_eval_context, *args)
106
+ end
107
+
108
+ def load_with_context(context, content, source = nil, line = nil)
75
109
  Inspec::Log.debug("Loading #{source || '<anonymous content>'} into #{self}")
76
110
  @current_load = { file: source }
77
111
  if content.is_a? Proc
78
- @profile_context.instance_eval(&content)
112
+ context.instance_eval(&content)
79
113
  elsif source.nil? && line.nil?
80
- @profile_context.instance_eval(content)
114
+ context.instance_eval(content)
81
115
  else
82
- @profile_context.instance_eval(content, source || 'unknown', line || 1)
116
+ context.instance_eval(content, source || 'unknown', line || 1)
83
117
  end
84
118
  end
85
119
 
@@ -121,131 +155,5 @@ module Inspec
121
155
  return rid.to_s if pid.to_s.empty?
122
156
  pid.to_s + '/' + rid.to_s
123
157
  end
124
-
125
- # Create the context for controls. This includes all components of the DSL,
126
- # including matchers and resources.
127
- #
128
- # @param [ResourcesDSL] resources_dsl which has all resources to attach
129
- # @return [RuleContext] the inner context of rules
130
- def rule_context(resources_dsl)
131
- require 'rspec/core/dsl'
132
- Class.new(Inspec::Rule) do
133
- include RSpec::Core::DSL
134
- include resources_dsl
135
- end
136
- end
137
-
138
- # Creates the heart of the profile context:
139
- # An instantiated object which has all resources registered to it
140
- # and exposes them to the a test file. The profile context serves as a
141
- # container for all profiles which are registered. Within the context
142
- # profiles get access to all DSL calls for creating tests and controls.
143
- #
144
- # @param outer_dsl [OuterDSLClass]
145
- # @return [ProfileContextClass]
146
- def create_context(resources_dsl, rule_class) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
147
- profile_context_owner = self
148
- profile_id = @profile_id
149
-
150
- # rubocop:disable Lint/NestedMethodDefinition
151
- Class.new do
152
- include Inspec::DSL
153
- include resources_dsl
154
-
155
- def initialize(backend, conf, dependencies, require_loader) # rubocop:disable Lint/NestedMethodDefinition, Lint/DuplicateMethods
156
- @backend = backend
157
- @conf = conf
158
- @dependencies = dependencies
159
- @require_loader = require_loader
160
- @skip_profile = false
161
- end
162
-
163
- # Save the toplevel require method to load all ruby dependencies.
164
- # It is used whenever the `require 'lib'` is not in libraries.
165
- alias_method :__ruby_require, :require
166
-
167
- def require(path)
168
- rbpath = path + '.rb'
169
- return __ruby_require(path) if !@require_loader.exists?(rbpath)
170
- return false if @require_loader.loaded?(rbpath)
171
-
172
- # This is equivalent to calling `require 'lib'` with lib on disk.
173
- # We cannot rely on libraries residing on disk however.
174
- # TODO: Sandboxing.
175
- content, path, line = @require_loader.load(rbpath)
176
- eval(content, TOPLEVEL_BINDING, path, line) # rubocop:disable Lint/Eval
177
- end
178
-
179
- define_method :title do |arg|
180
- profile_context_owner.set_header(:title, arg)
181
- end
182
-
183
- def to_s
184
- "Profile Context Run #{profile_name}"
185
- end
186
-
187
- define_method :profile_name do
188
- profile_id
189
- end
190
-
191
- define_method :control do |*args, &block|
192
- id = args[0]
193
- opts = args[1] || {}
194
- register_control(rule_class.new(id, profile_id, opts, &block))
195
- end
196
-
197
- define_method :describe do |*args, &block|
198
- loc = block_location(block, caller[0])
199
- id = "(generated from #{loc} #{SecureRandom.hex})"
200
-
201
- res = nil
202
- rule = rule_class.new(id, profile_id, {}) do
203
- res = describe(*args, &block)
204
- end
205
- register_control(rule, &block)
206
-
207
- res
208
- end
209
-
210
- define_method :add_subcontext do |context|
211
- profile_context_owner.add_subcontext(context)
212
- end
213
-
214
- define_method :register_control do |control, &block|
215
- ::Inspec::Rule.set_skip_rule(control, true) if @skip_profile
216
-
217
- profile_context_owner.register_rule(control, &block) unless control.nil?
218
- end
219
-
220
- # method for attributes; import attribute handling
221
- define_method :attribute do |name, options|
222
- profile_context_owner.register_attribute(name, options)
223
- end
224
-
225
- define_method :skip_control do |id|
226
- profile_context_owner.unregister_rule(id)
227
- end
228
-
229
- def only_if
230
- return unless block_given?
231
- @skip_profile ||= !yield
232
- end
233
-
234
- alias_method :rule, :control
235
- alias_method :skip_rule, :skip_control
236
-
237
- private
238
-
239
- def block_location(block, alternate_caller)
240
- if block.nil?
241
- alternate_caller[/^(.+:\d+):in .+$/, 1] || 'unknown'
242
- else
243
- path, line = block.source_location
244
- "#{File.basename(path)}:#{line}"
245
- end
246
- end
247
- end
248
- # rubocop:enable all
249
- end
250
158
  end
251
159
  end
@@ -3,17 +3,20 @@
3
3
  # license: All rights reserved
4
4
  # author: Dominik Richter
5
5
  # author: Christoph Hartmann
6
-
7
6
  require 'inspec/plugins'
8
7
 
9
8
  module Inspec
10
9
  class Resource
11
- class Registry
12
- # empty class for namespacing resource classes in the registry
10
+ def self.default_registry
11
+ @default_registry ||= {}
13
12
  end
14
13
 
15
14
  def self.registry
16
- @registry ||= {}
15
+ @registry ||= default_registry
16
+ end
17
+
18
+ def self.new_registry
19
+ default_registry.dup
17
20
  end
18
21
 
19
22
  # Creates the inner DSL which includes all resources for
@@ -22,9 +25,8 @@ module Inspec
22
25
  #
23
26
  # @param backend [BackendRunner] exposing the target to resources
24
27
  # @return [ResourcesDSL]
25
- def self.create_dsl(backend)
28
+ def self.create_dsl(backend, my_registry = registry)
26
29
  # need the local name, to use it in the module creation further down
27
- my_registry = registry
28
30
  Module.new do
29
31
  my_registry.each do |id, r|
30
32
  define_method id.to_sym do |*args|
@@ -41,10 +43,14 @@ module Inspec
41
43
  # @param [int] version the resource version to use
42
44
  # @return [Resource] base class for creating a new resource
43
45
  def self.resource(version)
46
+ validate_resource_dsl_version!(version)
47
+ Inspec::Plugins::Resource
48
+ end
49
+
50
+ def self.validate_resource_dsl_version!(version)
44
51
  if version != 1
45
52
  fail 'Only resource version 1 is supported!'
46
53
  end
47
- Inspec::Plugins::Resource
48
54
  end
49
55
  end
50
56
 
@@ -164,10 +164,32 @@ class InspecRspecJson < InspecRspecMiniJson
164
164
  [info[:name], info]
165
165
  end
166
166
 
167
+ #
168
+ # TODO(ssd+vj): We should probably solve this by either ensuring the example has
169
+ # the profile_id of the top level profile when it is included as a dependency, or
170
+ # by registrying all dependent profiles with the formatter. The we could remove
171
+ # this heuristic matching.
172
+ #
173
+ def example2profile(example, profiles)
174
+ profiles.values.find { |p| profile_contains_example?(p, example) }
175
+ end
176
+
177
+ def profile_contains_example?(profile, example)
178
+ # Heuristic for finding the profile an example came from:
179
+ # Case 1: The profile_id on the example matches the name of the profile
180
+ # Case 2: The profile contains a control that matches the id of the example
181
+ if profile[:name] == example[:profile_id]
182
+ true
183
+ elsif profile[:controls] && profile[:controls].key?(example[:id])
184
+ true
185
+ else
186
+ false
187
+ end
188
+ end
189
+
167
190
  def example2control(example, profiles)
168
- profile = profiles[example[:profile_id]]
169
- return nil if profile.nil? || profile[:controls].nil?
170
- profile[:controls][example[:id]]
191
+ p = example2profile(example, profiles)
192
+ p[:controls][example[:id]] if p && p[:controls]
171
193
  end
172
194
 
173
195
  def format_example(example)
@@ -240,7 +262,7 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
240
262
  print_line(
241
263
  color: '', indicator: @indicators['empty'], id: '', profile: '',
242
264
  summary: 'No tests executed.'
243
- )
265
+ ) if @current_control.nil?
244
266
  output.puts('')
245
267
  end
246
268
 
@@ -348,7 +370,7 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
348
370
  test_status = x[:status_type]
349
371
  test_color = @colors[test_status]
350
372
  indicator = @indicators[x[:status]]
351
- indicator = @indicators['empty'] if all.length == 1 || indicator.nil?
373
+ indicator = @indicators['empty'] if indicator.nil?
352
374
  msg = x[:message] || x[:skip_message] || x[:code_desc]
353
375
  print_line(
354
376
  color: test_color,
@@ -364,9 +386,18 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
364
386
  control_result = control[:results]
365
387
  title = control_result[0][:code_desc].split[0..1].join(' ')
366
388
  puts ' ' + title
389
+ # iterate over all describe blocks in anonoymous control block
367
390
  control_result.each do |test|
368
391
  control_id = ''
369
- test_result = test[:code_desc].split[2..-1].join(' ')
392
+ # display exceptions
393
+ unless test[:exception].nil?
394
+ test_result = test[:message]
395
+ else
396
+ # determine title
397
+ test_result = test[:code_desc].split[2..-1].join(' ')
398
+ # show error message
399
+ test_result += "\n" + test[:message] unless test[:message].nil?
400
+ end
370
401
  status_indicator = test[:status_type]
371
402
  print_line(
372
403
  color: @colors[status_indicator] || '',
data/lib/inspec/rule.rb CHANGED
@@ -33,6 +33,10 @@ module Inspec
33
33
  instance_eval(&block) if block_given?
34
34
  end
35
35
 
36
+ def to_s
37
+ Inspec::Rule.rule_id(self)
38
+ end
39
+
36
40
  def id(*_)
37
41
  # never overwrite the ID
38
42
  @id
data/lib/inspec/runner.rb CHANGED
@@ -14,13 +14,34 @@ require 'inspec/secrets'
14
14
  # spec requirements
15
15
 
16
16
  module Inspec
17
+ #
18
+ # Inspec::Runner coordinates the running of tests and is the main
19
+ # entry point to the application.
20
+ #
21
+ # Users are expected to insantiate a runner, add targets to be run,
22
+ # and then call the run method:
23
+ #
24
+ # ```
25
+ # r = Inspec::Runner.new()
26
+ # r.add_target("/path/to/some/profile")
27
+ # r.add_target("http://url/to/some/profile")
28
+ # r.run
29
+ # ```
30
+ #
17
31
  class Runner # rubocop:disable Metrics/ClassLength
18
32
  extend Forwardable
33
+
34
+ def_delegator :@test_collector, :report
35
+ def_delegator :@test_collector, :reset
36
+
19
37
  attr_reader :backend, :rules, :attributes
20
38
  def initialize(conf = {})
21
39
  @rules = []
22
40
  @conf = conf.dup
23
41
  @conf[:logger] ||= Logger.new(nil)
42
+ @target_profiles = []
43
+ @controls = @conf[:controls] || []
44
+ @ignore_supports = @conf[:ignore_supports]
24
45
 
25
46
  @test_collector = @conf.delete(:test_collector) || begin
26
47
  require 'inspec/runner_rspec'
@@ -38,19 +59,31 @@ module Inspec
38
59
  @test_collector.tests
39
60
  end
40
61
 
41
- def normalize_map(hm)
42
- res = {}
43
- hm.each {|k, v|
44
- res[k.to_s] = v
45
- }
46
- res
47
- end
48
-
49
62
  def configure_transport
50
63
  @backend = Inspec::Backend.create(@conf)
51
64
  @test_collector.backend = @backend
52
65
  end
53
66
 
67
+ def run(with = nil)
68
+ Inspec::Log.debug "Starting run with targets: #{@target_profiles.map(&:to_s)}"
69
+ Inspec::Log.debug "Backend is #{@backend}"
70
+ all_controls = []
71
+
72
+ @target_profiles.each do |profile|
73
+ @test_collector.add_profile(profile)
74
+ profile.locked_dependencies
75
+ profile.load_libraries
76
+ @attributes |= profile.runner_context.attributes
77
+ all_controls += profile.collect_tests
78
+ end
79
+
80
+ all_controls.each do |rule|
81
+ register_rule(rule)
82
+ end
83
+
84
+ @test_collector.run(with)
85
+ end
86
+
54
87
  # determine all attributes before the execution, fetch data from secrets backend
55
88
  def load_attributes(options)
56
89
  attributes = {}
@@ -66,15 +99,54 @@ module Inspec
66
99
  options['attributes'] = attributes
67
100
  end
68
101
 
69
- # Returns the profile context used the profile at this target.
70
- def add_target(target, options = {})
71
- profile = Inspec::Profile.for_target(target, options)
102
+ #
103
+ # add_target allows the user to add a target whose tests will be
104
+ # run when the user calls the run method.
105
+ #
106
+ # A target is a path or URL that points to a profile. Using this
107
+ # target we generate a Profile and a ProfileContext. The content
108
+ # (libraries, tests, and attributes) from the Profile are loaded
109
+ # into the ProfileContext.
110
+ #
111
+ # If the profile depends on other profiles, those profiles will be
112
+ # loaded on-demand when include_content or required_content are
113
+ # called using similar code in Inspec::DSL.
114
+ #
115
+ # Once the we've loaded all of the tests files in the profile, we
116
+ # query the profile for the full list of rules. Those rules are
117
+ # registered with the @test_collector which is ultimately
118
+ # responsible for actually running the tests.
119
+ #
120
+ # TODO: Deduplicate/clarify the loading code that exists in here,
121
+ # the ProfileContext, the Profile, and Inspec::DSL
122
+ #
123
+ # @params target [String] A path or URL to a profile or raw test.
124
+ # @params _opts [Hash] Unused, but still here to avoid breaking kitchen-inspec
125
+ #
126
+ # @eturns [Inspec::ProfileContext]
127
+ #
128
+ def add_target(target, _opts = [])
129
+ profile = Inspec::Profile.for_target(target, backend: @backend, controls: @controls)
72
130
  fail "Could not resolve #{target} to valid input." if profile.nil?
73
- add_profile(profile, options)
131
+ @target_profiles << profile if supports_profile?(profile)
132
+ end
133
+
134
+ #
135
+ # This is used by inspec-shell and inspec-detect. This should
136
+ # probably be cleaned up a bit.
137
+ #
138
+ # @params [Hash] Options
139
+ # @returns [Inspec::ProfileContext]
140
+ #
141
+ def create_context(options = {})
142
+ meta = options[:metadata]
143
+ profile_id = nil
144
+ profile_id = meta.params[:name] unless meta.nil?
145
+ Inspec::ProfileContext.new(profile_id, @backend, @conf.merge(options))
74
146
  end
75
147
 
76
148
  def supports_profile?(profile)
77
- return true if profile.metadata.nil?
149
+ return true if profile.metadata.nil? || @ignore_supports
78
150
 
79
151
  if !profile.metadata.supports_runtime?
80
152
  fail 'This profile requires InSpec version '\
@@ -90,61 +162,6 @@ module Inspec
90
162
  true
91
163
  end
92
164
 
93
- # Returns the profile context used to initialize this profile.
94
- def add_profile(profile, options = {})
95
- return if !options[:ignore_supports] && !supports_profile?(profile)
96
-
97
- @test_collector.add_profile(profile)
98
- options[:metadata] = profile.metadata
99
- options[:profile] = profile
100
-
101
- libs = profile.libraries.map do |k, v|
102
- { ref: k, content: v }
103
- end
104
-
105
- tests = profile.tests.map do |ref, content|
106
- r = profile.source_reader.target.abs_path(ref)
107
- { ref: r, content: content }
108
- end
109
-
110
- add_content(tests, libs, options)
111
- end
112
-
113
- def create_context(options = {})
114
- meta = options[:metadata]
115
- profile_id = nil
116
- profile_id = meta.params[:name] unless meta.nil?
117
- Inspec::ProfileContext.new(profile_id, @backend, @conf.merge(options))
118
- end
119
-
120
- # Returns the profile context used to evaluate the given content.
121
- # Calling this method again will use a different context each time.
122
- def add_content(tests, libs, options = {})
123
- return if tests.nil? || tests.empty?
124
-
125
- # load all libraries
126
- ctx = create_context(options)
127
- ctx.load_libraries(libs.map { |x| [x[:content], x[:ref], x[:line]] })
128
-
129
- # hand the context to the profile for further evaluation
130
- unless (profile = options[:profile]).nil?
131
- profile.runner_context = ctx
132
- end
133
-
134
- # evaluate the test content
135
- Array(tests).each { |t| add_test_to_context(t, ctx) }
136
-
137
- # merge and collect all attributes
138
- @attributes |= ctx.attributes
139
-
140
- # process the resulting rules
141
- filter_controls(ctx.all_rules, options[:controls]).each do |rule|
142
- register_rule(rule)
143
- end
144
-
145
- ctx
146
- end
147
-
148
165
  # In some places we read the rules off of the runner, in other
149
166
  # places we read it off of the profile context. To keep the API's
150
167
  # the same, we provide an #all_rules method here as well.
@@ -162,26 +179,8 @@ module Inspec
162
179
  new_tests
163
180
  end
164
181
 
165
- def_delegator :@test_collector, :run
166
- def_delegator :@test_collector, :report
167
- def_delegator :@test_collector, :reset
168
-
169
182
  private
170
183
 
171
- def add_test_to_context(test, ctx)
172
- content = test[:content]
173
- return if content.nil? || content.empty?
174
- ctx.load(content, test[:ref], test[:line])
175
- end
176
-
177
- def filter_controls(controls_array, include_list)
178
- return controls_array if include_list.nil? || include_list.empty?
179
- controls_array.select do |c|
180
- id = ::Inspec::Rule.rule_id(c)
181
- include_list.include?(id)
182
- end
183
- end
184
-
185
184
  def block_source_info(block)
186
185
  return {} if block.nil? || !block.respond_to?(:source_location)
187
186
  opts = {}
@@ -226,6 +225,7 @@ module Inspec
226
225
  end
227
226
 
228
227
  def register_rule(rule)
228
+ Inspec::Log.debug "Registering rule #{rule}"
229
229
  @rules << rule
230
230
  checks = ::Inspec::Rule.prepare_checks(rule)
231
231
  examples = checks.map do |m, a, b|