inspec 0.32.0 → 0.33.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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|