inspec 4.1.4.preview → 4.2.0.preview

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,6 +14,73 @@ end
14
14
 
15
15
  module Inspec
16
16
  class Input
17
+ #===========================================================================#
18
+ # Class Input::Event
19
+ #===========================================================================#
20
+
21
+ # Information about how the input obtained its value.
22
+ # Each time it changes, an Input::Event is added to the #events array.
23
+ class Event
24
+ EVENT_PROPERTIES = [
25
+ :action, # :create, :set, :fetch
26
+ :provider, # Name of the plugin
27
+ :priority, # Priority of this plugin for resolving conflicts. 1-100, higher numbers win.
28
+ :value, # New value, if provided.
29
+ :file, # File containing the input-changing action, if known
30
+ :line, # Line in file containing the input-changing action, if known
31
+ :hit, # if action is :fetch, true if the remote source had the input
32
+ ].freeze
33
+
34
+ # Value has a special handler
35
+ EVENT_PROPERTIES.reject { |p| p == :value }.each do |prop|
36
+ attr_accessor prop
37
+ end
38
+
39
+ attr_reader :value
40
+
41
+ def initialize(properties = {})
42
+ @value_has_been_set = false
43
+
44
+ properties.each do |prop_name, prop_value|
45
+ if EVENT_PROPERTIES.include? prop_name
46
+ # OK, save the property
47
+ send((prop_name.to_s + '=').to_sym, prop_value)
48
+ else
49
+ raise "Unrecognized property to Input::Event: #{prop_name}"
50
+ end
51
+ end
52
+ end
53
+
54
+ def value=(the_val)
55
+ # Even if set to nil or false, it has indeed been set; note that fact.
56
+ @value_has_been_set = true
57
+ @value = the_val
58
+ end
59
+
60
+ def value_has_been_set?
61
+ @value_has_been_set
62
+ end
63
+
64
+ def diagnostic_string
65
+ to_h.reject { |_, val| val.nil? }.to_a.map { |pair| "#{pair[0]}: '#{pair[1]}'" }.join(', ')
66
+ end
67
+
68
+ def to_h
69
+ EVENT_PROPERTIES.each_with_object({}) do |prop, hash|
70
+ hash[prop] = send(prop)
71
+ end
72
+ end
73
+
74
+ def self.probe_stack
75
+ frames = caller_locations(2, 40)
76
+ frames.reject! { |f| f.path && f.path.include?('/lib/inspec/') }
77
+ frames.first
78
+ end
79
+ end
80
+
81
+ #===========================================================================#
82
+ # Class NO_VALUE_SET
83
+ #===========================================================================#
17
84
  # This special class is used to represent the value when an input has
18
85
  # not been assigned a value. This allows a user to explicitly assign nil
19
86
  # to an input.
@@ -62,8 +129,11 @@ module Inspec
62
129
  end
63
130
 
64
131
  class Input
65
- attr_accessor :name
132
+ #===========================================================================#
133
+ # Class Inspec::Input
134
+ #===========================================================================#
66
135
 
136
+ # Validation types for input values
67
137
  VALID_TYPES = %w{
68
138
  String
69
139
  Numeric
@@ -74,6 +144,21 @@ module Inspec
74
144
  Any
75
145
  }.freeze
76
146
 
147
+ # If you call `input` in a control file, the input will receive this priority.
148
+ # You can override that with a :priority option.
149
+ DEFAULT_PRIORITY_FOR_DSL_ATTRIBUTES = 20
150
+
151
+ # If you somehow manage to initialize an Input outside of the DSL,
152
+ # AND you don't provide an Input::Event, this is the priority you get.
153
+ DEFAULT_PRIORITY_FOR_UNKNOWN_CALLER = 10
154
+
155
+ # If you directly call value=, this is the priority assigned.
156
+ # This is the highest priority within InSpec core; though plugins
157
+ # are free to go higher.
158
+ DEFAULT_PRIORITY_FOR_VALUE_SET = 60
159
+
160
+ attr_reader :description, :events, :identifier, :name, :required, :title, :type
161
+
77
162
  def initialize(name, options = {})
78
163
  @name = name
79
164
  @opts = options
@@ -82,49 +167,164 @@ module Inspec
82
167
  if @opts.key?(:value)
83
168
  Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
84
169
  @opts.delete(:default)
170
+ end
171
+ end
172
+
173
+ # Array of Input::Event objects. These compete with one another to determine
174
+ # the value of the input when value() is called, as well as providing a
175
+ # debugging record of when and how the value changed.
176
+ @events = []
177
+ events.push make_creation_event(options)
178
+
179
+ update(options)
180
+ end
181
+
182
+ def set_events
183
+ events.select { |e| e.action == :set }
184
+ end
185
+
186
+ def diagnostic_string
187
+ "Input #{name}, with history:\n" +
188
+ events.map(&:diagnostic_string).map { |line| " #{line}" }.join("\n")
189
+ end
190
+
191
+ #--------------------------------------------------------------------------#
192
+ # Managing Value
193
+ #--------------------------------------------------------------------------#
194
+
195
+ def update(options)
196
+ _update_set_metadata(options)
197
+ normalize_type_restriction!
198
+
199
+ # Values are set by passing events in; but we can also infer an event.
200
+ if options.key?(:value) || options.key?(:default)
201
+ if options.key?(:event)
202
+ if options.key?(:value) || options.key?(:default)
203
+ Inspec::Log.warn "Do not provide both an Event and a value as an option to attribute('#{name}') - using value from event"
204
+ end
85
205
  else
86
- @opts[:value] = @opts.delete(:default)
206
+ self.class.infer_event(options) # Sets options[:event]
87
207
  end
88
208
  end
89
- @value = @opts[:value]
90
- validate_value_type(@value) if @opts.key?(:type) && @opts.key?(:value)
209
+ events << options[:event] if options.key? :event
210
+
211
+ enforce_type_restriction!
91
212
  end
92
213
 
93
- def value=(new_value)
94
- validate_value_type(new_value) if @opts.key?(:type)
95
- @value = new_value
214
+ # We can determine a value:
215
+ # 1. By event.value (preferred)
216
+ # 2. By options[:value]
217
+ # 3. By options[:default] (deprecated)
218
+ def self.infer_event(options)
219
+ # Don't rely on this working; you really should be passing a proper Input::Event
220
+ # with the context information you have.
221
+ location = Input::Event.probe_stack
222
+ event = Input::Event.new(
223
+ action: :set,
224
+ provider: options[:provider] || :unknown,
225
+ priority: options[:priority] || Inspec::Input::DEFAULT_PRIORITY_FOR_UNKNOWN_CALLER,
226
+ file: location.path,
227
+ line: location.lineno,
228
+ )
229
+
230
+ if options.key?(:default)
231
+ Inspec.deprecate(:attrs_value_replaces_default, "attribute name: '#{name}'")
232
+ if options.key?(:value)
233
+ Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
234
+ options.delete(:default)
235
+ else
236
+ options[:value] = options.delete(:default)
237
+ end
238
+ end
239
+ event.value = options[:value] if options.key?(:value)
240
+ options[:event] = event
96
241
  end
97
242
 
98
- def value
99
- if @value.nil?
100
- validate_required(@value) if @opts[:required] == true
101
- @value = value_or_dummy
243
+ private
244
+
245
+ def _update_set_metadata(options)
246
+ # Basic metadata
247
+ @title = options[:title] if options.key?(:title)
248
+ @description = options[:description] if options.key?(:description)
249
+ @required = options[:required] if options.key?(:required)
250
+ @identifier = options[:identifier] if options.key?(:identifier) # TODO: determine if this is ever used
251
+ @type = options[:type] if options.key?(:type)
252
+ end
253
+
254
+ def make_creation_event(options)
255
+ loc = options[:location] || Event.probe_stack
256
+ Input::Event.new(
257
+ action: :create,
258
+ provider: options[:provider],
259
+ file: loc.path,
260
+ line: loc.lineno,
261
+ )
262
+ end
263
+
264
+ # Determine the current winning value, but don't validate it
265
+ def current_value
266
+ # Examine the events to determine highest-priority value. Tie-break
267
+ # by using the last one set.
268
+ events_that_set_a_value = events.select(&:value_has_been_set?)
269
+ winning_priority = events_that_set_a_value.map(&:priority).max
270
+ winning_events = events_that_set_a_value.select { |e| e.priority == winning_priority }
271
+ winning_event = winning_events.last # Last for tie-break
272
+
273
+ if winning_event.nil?
274
+ # No value has been set - return special no value object
275
+ NO_VALUE_SET.new(name)
102
276
  else
103
- @value
277
+ winning_event.value # May still be nil
104
278
  end
105
279
  end
106
280
 
107
- def title
108
- @opts[:title]
281
+ public
282
+
283
+ def value=(new_value, priority = DEFAULT_PRIORITY_FOR_VALUE_SET)
284
+ # Inject a new Event with the new value.
285
+ location = Event.probe_stack
286
+ events << Event.new(
287
+ action: :set,
288
+ provider: :value_setter,
289
+ priority: priority,
290
+ value: new_value,
291
+ file: location.path,
292
+ line: location.lineno,
293
+ )
294
+ enforce_type_restriction!
295
+
296
+ new_value
109
297
  end
110
298
 
111
- def description
112
- @opts[:description]
299
+ def value
300
+ enforce_required_validation!
301
+ current_value
113
302
  end
114
303
 
115
- def ruby_var_identifier
116
- @opts[:identifier] || 'attr_' + @name.downcase.strip.gsub(/\s+/, '-').gsub(/[^\w-]/, '')
304
+ def has_value?
305
+ !current_value.is_a? NO_VALUE_SET
117
306
  end
118
307
 
308
+ #--------------------------------------------------------------------------#
309
+ # Marshalling
310
+ #--------------------------------------------------------------------------#
311
+
119
312
  def to_hash
120
- {
121
- name: @name,
122
- options: @opts,
123
- }
313
+ as_hash = { name: name, options: {} }
314
+ [:description, :title, :identifier, :type, :required, :value].each do |field|
315
+ val = send(field)
316
+ next if val.nil?
317
+ as_hash[:options][field] = val
318
+ end
319
+ as_hash
320
+ end
321
+
322
+ def ruby_var_identifier
323
+ identifier || 'attr_' + name.downcase.strip.gsub(/\s+/, '-').gsub(/[^\w-]/, '')
124
324
  end
125
325
 
126
326
  def to_ruby
127
- res = ["#{ruby_var_identifier} = attribute('#{@name}',{"]
327
+ res = ["#{ruby_var_identifier} = attribute('#{name}',{"]
128
328
  res.push " title: '#{title}'," unless title.to_s.empty?
129
329
  res.push " value: #{value.inspect}," unless value.to_s.empty?
130
330
  # to_ruby may generate code that is to be used by older versions of inspec.
@@ -136,37 +336,78 @@ module Inspec
136
336
  res.join("\n")
137
337
  end
138
338
 
339
+ #--------------------------------------------------------------------------#
340
+ # Value Type Coercion
341
+ #--------------------------------------------------------------------------#
342
+
139
343
  def to_s
140
- "Input #{@name} with #{@value}"
344
+ "Input #{name} with #{current_value}"
141
345
  end
142
346
 
347
+ #--------------------------------------------------------------------------#
348
+ # Validation
349
+ #--------------------------------------------------------------------------#
350
+
143
351
  private
144
352
 
145
- def validate_required(value)
353
+ def enforce_required_validation!
354
+ return unless required
146
355
  # skip if we are not doing an exec call (archive/vendor/check)
147
356
  return unless Inspec::BaseCLI.inspec_cli_command == :exec
148
357
 
149
- # value will be set already if a secrets file was passed in
150
- if (!@opts.key?(:default) && value.nil?) || (@opts[:default].nil? && value.nil?)
358
+ proposed_value = current_value
359
+ if proposed_value.nil? || proposed_value.is_a?(NO_VALUE_SET)
151
360
  error = Inspec::Input::RequiredError.new
152
- error.input_name = @name
361
+ error.input_name = name
153
362
  raise error, "Input '#{error.input_name}' is required and does not have a value."
154
363
  end
155
364
  end
156
365
 
157
- def validate_type(type)
158
- type = type.capitalize
366
+ def enforce_type_restriction! # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
367
+ return unless type
368
+ return unless has_value?
369
+
370
+ type_req = type
371
+ return if type_req == 'Any'
372
+
373
+ proposed_value = current_value
374
+
375
+ invalid_type = false
376
+ if type_req == 'Regexp'
377
+ invalid_type = true if !valid_regexp?(proposed_value)
378
+ elsif type_req == 'Numeric'
379
+ invalid_type = true if !valid_numeric?(proposed_value)
380
+ elsif type_req == 'Boolean'
381
+ invalid_type = true if ![true, false].include?(proposed_value)
382
+ elsif proposed_value.is_a?(Module.const_get(type_req)) == false
383
+ # TODO: why is this case here?
384
+ invalid_type = true
385
+ end
386
+
387
+ if invalid_type == true
388
+ error = Inspec::Input::ValidationError.new
389
+ error.input_name = @name
390
+ error.input_value = proposed_value
391
+ error.input_type = type_req
392
+ raise error, "Input '#{error.input_name}' with value '#{error.input_value}' does not validate to type '#{error.input_type}'."
393
+ end
394
+ end
395
+
396
+ def normalize_type_restriction!
397
+ return unless type
398
+
399
+ type_req = type.capitalize
159
400
  abbreviations = {
160
401
  'Num' => 'Numeric',
161
402
  'Regex' => 'Regexp',
162
403
  }
163
- type = abbreviations[type] if abbreviations.key?(type)
164
- if !VALID_TYPES.include?(type)
404
+ type_req = abbreviations[type_req] if abbreviations.key?(type_req)
405
+ if !VALID_TYPES.include?(type_req)
165
406
  error = Inspec::Input::TypeError.new
166
- error.input_type = type
407
+ error.input_type = type_req
167
408
  raise error, "Type '#{error.input_type}' is not a valid input type."
168
409
  end
169
- type
410
+ @type = type_req
170
411
  end
171
412
 
172
413
  def valid_numeric?(value)
@@ -177,41 +418,11 @@ module Inspec
177
418
  end
178
419
 
179
420
  def valid_regexp?(value)
180
- # check for invalid regex syntex
421
+ # check for invalid regex syntax
181
422
  Regexp.new(value)
182
423
  true
183
424
  rescue
184
425
  false
185
426
  end
186
-
187
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
188
- def validate_value_type(value)
189
- type = validate_type(@opts[:type])
190
- return if type == 'Any'
191
-
192
- invalid_type = false
193
- if type == 'Regexp'
194
- invalid_type = true if !value.is_a?(String) || !valid_regexp?(value)
195
- elsif type == 'Numeric'
196
- invalid_type = true if !valid_numeric?(value)
197
- elsif type == 'Boolean'
198
- invalid_type = true if ![true, false].include?(value)
199
- elsif value.is_a?(Module.const_get(type)) == false
200
- invalid_type = true
201
- end
202
-
203
- if invalid_type == true
204
- error = Inspec::Input::ValidationError.new
205
- error.input_name = @name
206
- error.input_value = value
207
- error.input_type = type
208
- raise error, "Input '#{error.input_name}' with value '#{error.input_value}' does not validate to type '#{error.input_type}'."
209
- end
210
- end
211
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
212
-
213
- def value_or_dummy
214
- @opts.key?(:value) ? @opts[:value] : Inspec::Input::NO_VALUE_SET.new(@name)
215
- end
216
427
  end
217
428
  end
@@ -81,7 +81,7 @@ module Inspec
81
81
  end
82
82
 
83
83
  attr_reader :source_reader, :backend, :runner_context, :check_mode
84
- attr_accessor :parent_profile, :profile_name
84
+ attr_accessor :parent_profile, :profile_id, :profile_name
85
85
  def_delegator :@source_reader, :tests
86
86
  def_delegator :@source_reader, :libraries
87
87
  def_delegator :@source_reader, :metadata
@@ -118,25 +118,32 @@ module Inspec
118
118
  @runtime_profile = RuntimeProfile.new(self)
119
119
  @backend.profile = @runtime_profile
120
120
 
121
+ # The AttributeRegistry is in charge of keeping track of inputs;
122
+ # it is the single source of truth. Now that we have a profile object,
123
+ # we can create any inputs that were provided by various mechanisms.
124
+ options[:runner_conf] ||= Inspec::Config.cached
125
+
126
+ if options[:runner_conf].key?(:attrs)
127
+ Inspec.deprecate(:rename_attributes_to_inputs, 'Use --input-file on the command line instead of --attrs.')
128
+ options[:runner_conf][:input_file] = options[:runner_conf].delete(:attrs)
129
+ end
130
+
131
+ Inspec::InputRegistry.bind_profile_inputs(
132
+ # Every input only exists in the context of a profile
133
+ metadata.params[:name], # TODO: test this with profile aliasing
134
+ # Remaining args are possible sources of inputs
135
+ cli_input_files: options[:runner_conf][:input_file], # From CLI --input-file
136
+ profile_metadata: metadata,
137
+ # TODO: deprecation checks here
138
+ runner_api: options[:runner_conf][:attributes], # This is the route the audit_cookbook and kitchen-inspec take
139
+ )
140
+
121
141
  @runner_context =
122
142
  options[:profile_context] ||
123
- Inspec::ProfileContext.for_profile(self, @backend, @input_values)
143
+ Inspec::ProfileContext.for_profile(self, @backend)
124
144
 
125
145
  @supports_platform = metadata.supports_platform?(@backend)
126
146
  @supports_runtime = metadata.supports_runtime?
127
- register_metadata_inputs
128
- end
129
-
130
- def register_metadata_inputs # TODO: deprecate
131
- if metadata.params.key?(:attributes) && metadata.params[:attributes].is_a?(Array)
132
- metadata.params[:attributes].each do |attribute|
133
- attr_dup = attribute.dup
134
- name = attr_dup.delete(:name)
135
- @runner_context.register_input(name, attr_dup)
136
- end
137
- elsif metadata.params.key?(:attributes)
138
- Inspec::Log.warn 'Inputs must be defined as an Array. Skipping current definition.'
139
- end
140
147
  end
141
148
 
142
149
  def name
@@ -595,7 +602,7 @@ module Inspec
595
602
  f = load_rule_filepath(prefix, rule)
596
603
  load_rule(rule, f, controls, groups)
597
604
  end
598
- params[:inputs] = @runner_context.inputs
605
+ params[:inputs] = Inspec::InputRegistry.list_inputs_for_profile(@profile_id)
599
606
  params
600
607
  end
601
608