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.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/etc/deprecations.json +30 -30
- data/inspec.gemspec +1 -1
- data/lib/inspec/cli.rb +5 -1
- data/lib/inspec/control_eval_context.rb +32 -6
- data/lib/inspec/dependencies/requirement.rb +1 -0
- data/lib/inspec/dependencies/resolver.rb +2 -0
- data/lib/inspec/dsl.rb +1 -1
- data/lib/inspec/impact.rb +1 -1
- data/lib/inspec/input_registry.rb +187 -46
- data/lib/inspec/objects/input.rb +276 -65
- data/lib/inspec/profile.rb +23 -16
- data/lib/inspec/profile_context.rb +9 -13
- data/lib/inspec/rspec_extensions.rb +5 -1
- data/lib/inspec/runner.rb +12 -52
- data/lib/inspec/version.rb +1 -1
- data/lib/resources/mssql_session.rb +1 -1
- data/lib/resources/port.rb +4 -0
- metadata +12 -6
data/lib/inspec/objects/input.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
206
|
+
self.class.infer_event(options) # Sets options[:event]
|
87
207
|
end
|
88
208
|
end
|
89
|
-
|
90
|
-
|
209
|
+
events << options[:event] if options.key? :event
|
210
|
+
|
211
|
+
enforce_type_restriction!
|
91
212
|
end
|
92
213
|
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
277
|
+
winning_event.value # May still be nil
|
104
278
|
end
|
105
279
|
end
|
106
280
|
|
107
|
-
|
108
|
-
|
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
|
112
|
-
|
299
|
+
def value
|
300
|
+
enforce_required_validation!
|
301
|
+
current_value
|
113
302
|
end
|
114
303
|
|
115
|
-
def
|
116
|
-
|
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
|
-
|
122
|
-
|
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('#{
|
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 #{
|
344
|
+
"Input #{name} with #{current_value}"
|
141
345
|
end
|
142
346
|
|
347
|
+
#--------------------------------------------------------------------------#
|
348
|
+
# Validation
|
349
|
+
#--------------------------------------------------------------------------#
|
350
|
+
|
143
351
|
private
|
144
352
|
|
145
|
-
def
|
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
|
-
|
150
|
-
if
|
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 =
|
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
|
158
|
-
|
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
|
-
|
164
|
-
if !VALID_TYPES.include?(
|
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 =
|
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
|
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
|
data/lib/inspec/profile.rb
CHANGED
@@ -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
|
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] = @
|
605
|
+
params[:inputs] = Inspec::InputRegistry.list_inputs_for_profile(@profile_id)
|
599
606
|
params
|
600
607
|
end
|
601
608
|
|