inspec-core 4.16.0 → 4.17.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inspec.rb +0 -1
  3. data/lib/inspec/backend.rb +7 -0
  4. data/lib/inspec/base_cli.rb +2 -0
  5. data/lib/inspec/cli.rb +3 -10
  6. data/lib/inspec/config.rb +3 -4
  7. data/lib/inspec/control_eval_context.rb +5 -3
  8. data/lib/inspec/dsl.rb +24 -1
  9. data/lib/inspec/errors.rb +0 -26
  10. data/lib/inspec/file_provider.rb +33 -43
  11. data/lib/inspec/formatters/base.rb +1 -0
  12. data/lib/inspec/impact.rb +2 -0
  13. data/lib/inspec/input.rb +410 -0
  14. data/lib/inspec/input_registry.rb +10 -1
  15. data/lib/inspec/objects.rb +3 -1
  16. data/lib/inspec/objects/input.rb +5 -387
  17. data/lib/inspec/objects/tag.rb +1 -1
  18. data/lib/inspec/plugin/v1/plugin_types/resource.rb +16 -5
  19. data/lib/inspec/plugin/v2/activator.rb +4 -8
  20. data/lib/inspec/plugin/v2/loader.rb +19 -3
  21. data/lib/inspec/profile.rb +1 -1
  22. data/lib/inspec/profile_context.rb +1 -1
  23. data/lib/inspec/reporters/json.rb +70 -88
  24. data/lib/inspec/resource.rb +1 -0
  25. data/lib/inspec/resources.rb +9 -2
  26. data/lib/inspec/resources/aide_conf.rb +4 -0
  27. data/lib/inspec/resources/apt.rb +19 -19
  28. data/lib/inspec/resources/etc_fstab.rb +4 -0
  29. data/lib/inspec/resources/etc_hosts.rb +4 -0
  30. data/lib/inspec/resources/firewalld.rb +4 -0
  31. data/lib/inspec/resources/json.rb +10 -3
  32. data/lib/inspec/resources/mssql_session.rb +1 -1
  33. data/lib/inspec/resources/platform.rb +18 -13
  34. data/lib/inspec/resources/postfix_conf.rb +6 -2
  35. data/lib/inspec/resources/security_identifier.rb +4 -0
  36. data/lib/inspec/resources/sys_info.rb +65 -4
  37. data/lib/inspec/resources/user.rb +1 -0
  38. data/lib/inspec/rule.rb +68 -6
  39. data/lib/inspec/runner.rb +6 -1
  40. data/lib/inspec/runner_rspec.rb +1 -0
  41. data/lib/inspec/shell.rb +8 -1
  42. data/lib/inspec/utils/pkey_reader.rb +1 -1
  43. data/lib/inspec/version.rb +1 -1
  44. data/lib/matchers/matchers.rb +2 -0
  45. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ff3612b0be834f75c0bd4e457652ee6736aaff5787519e28c8733c114ecc330
4
- data.tar.gz: 58b2c9c7ddcf0bd09ce2e4d9cdf34e67ca0786cb3b97453253e375ec3fe6c29d
3
+ metadata.gz: 353c4d150560f02cf75373bbb1389cbdb147d13dd12628d2060342393f1e2937
4
+ data.tar.gz: 95754aea44d3f38def5176f7811b5ef63e03feae21903d217ead5441656ba346
5
5
  SHA512:
6
- metadata.gz: d23d441a6db5edc44f754717a6e86b581065b8739d8ee68c657055f2ab053fcbecaed2b800938840a5d7fd8fb9879c27076862e9875258ae9c091778f5b8d99a
7
- data.tar.gz: 3edba9eccd97749bb2661f1ab9dde985fbba0f3524e349383967d8e6517df36adad8969be7a7129e05bd96cdd35a49516c9b754fdc92ac3840630f135b1f05fb
6
+ metadata.gz: 005a55cfb392666cbea9133b63cc26fc20e3e0ab4301ea90b076816b360d156ab7c137a0ff51a5f179dfd4b0135ae4e5556998e04e0b9715d8753c9bbcd04e68
7
+ data.tar.gz: 6dbb31259d5ea18b3b61f3dddc57ba320ed84689d00b4e20d5ff219aea2bbab30302acecb81c4e1ed24c6ea2b669d6ba36b6e775114f1a0d628fa2f13ffb902a
data/lib/inspec.rb CHANGED
@@ -28,4 +28,3 @@ require "inspec/base_cli"
28
28
  require "inspec/fetcher"
29
29
  require "inspec/source_reader"
30
30
  require "inspec/resource"
31
- require "inspec/resources"
@@ -4,6 +4,7 @@ require "train"
4
4
  require "inspec/config"
5
5
  require "inspec/version"
6
6
  require "inspec/resource"
7
+ require "inspec/dsl" # for method_missing_resource
7
8
 
8
9
  module Inspec
9
10
  module Backend
@@ -77,6 +78,12 @@ module Inspec
77
78
  connection
78
79
  end
79
80
 
81
+ def method_missing(id, *args, &blk)
82
+ Inspec::DSL.method_missing_resource(self, id, *args)
83
+ rescue LoadError
84
+ super
85
+ end
86
+
80
87
  Inspec::Resource.registry.each do |id, r|
81
88
  define_method id.to_sym do |*args|
82
89
  r.new(self, id.to_s, *args)
@@ -137,6 +137,8 @@ module Inspec
137
137
  desc: "Specify one or more inputs directly on the command line, as --input NAME=VALUE"
138
138
  option :input_file, type: :array,
139
139
  desc: "Load one or more input files, a YAML file with values for the profile to use"
140
+ option :waiver_file, type: :array,
141
+ desc: "Load one or more waiver files."
140
142
  option :attrs, type: :array,
141
143
  desc: "Legacy name for --input-file - deprecated."
142
144
  option :create_lockfile, type: :boolean,
data/lib/inspec/cli.rb CHANGED
@@ -64,7 +64,6 @@ class Inspec::InspecCLI < Inspec::BaseCLI
64
64
  desc: "A list of controls to include. Ignore all other tests."
65
65
  profile_options
66
66
  def json(target)
67
- require "inspec/resources"
68
67
  require "json"
69
68
 
70
69
  o = config
@@ -103,8 +102,6 @@ class Inspec::InspecCLI < Inspec::BaseCLI
103
102
  option :format, type: :string
104
103
  profile_options
105
104
  def check(path) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
106
- require "inspec/resources"
107
-
108
105
  o = config
109
106
  diagnose(o)
110
107
  o["log_location"] ||= STDERR if o["format"] == "json"
@@ -157,8 +154,6 @@ class Inspec::InspecCLI < Inspec::BaseCLI
157
154
  option :overwrite, type: :boolean, default: false,
158
155
  desc: "Overwrite existing vendored dependencies and lockfile."
159
156
  def vendor(path = nil)
160
- require "inspec/resources"
161
-
162
157
  o = config
163
158
  configure_logger(o)
164
159
  o[:logger] = Logger.new($stdout)
@@ -180,8 +175,6 @@ class Inspec::InspecCLI < Inspec::BaseCLI
180
175
  option :ignore_errors, type: :boolean, default: false,
181
176
  desc: "Ignore profile warnings."
182
177
  def archive(path)
183
- require "inspec/resources"
184
-
185
178
  o = config
186
179
  diagnose(o)
187
180
 
@@ -208,9 +201,9 @@ class Inspec::InspecCLI < Inspec::BaseCLI
208
201
  pretty_handle_exception(e)
209
202
  end
210
203
 
211
- desc "exec LOCATIONS", "run all test files at the specified LOCATIONS."
212
- # TODO: find a way for Thor not to butcher the formatting of this
213
- long_desc <<~EOT
204
+ desc "exec LOCATIONS", <<~EOT
205
+ Run all test files at the specified LOCATIONS.
206
+
214
207
  Loads the given profile(s) and fetches their dependencies if needed. Then
215
208
  connects to the target and executes any controls contained in the profiles.
216
209
  One or more reporters are used to generate output.
data/lib/inspec/config.rb CHANGED
@@ -203,8 +203,7 @@ module Inspec
203
203
 
204
204
  def _utc_find_credset_name(_credentials, transport_name)
205
205
  return nil unless final_options[:target]
206
-
207
- match = final_options[:target].match(%r{^#{transport_name}://(?<credset_name>[\w\d\-]+)$})
206
+ match = final_options[:target].match(%r{^#{transport_name}://(?<credset_name>[\w\-]+)$})
208
207
  match ? match[:credset_name] : nil
209
208
  end
210
209
 
@@ -221,8 +220,8 @@ module Inspec
221
220
 
222
221
  path = determine_cfg_path(cli_opts)
223
222
 
224
- cfg_io = File.open(path) if path
225
- cfg_io || StringIO.new('{ "version": "1.1" }')
223
+ ver = KNOWN_VERSIONS.max
224
+ path ? File.open(path) : StringIO.new({ "version" => ver }.to_json)
226
225
  end
227
226
 
228
227
  def check_for_piped_config(cli_opts)
@@ -66,9 +66,11 @@ module Inspec
66
66
  # We still haven't called it, so do so now.
67
67
  send(method_name, *arguments, &block)
68
68
  else
69
- # If we couldn't find a plugin to match, maybe something up above has it,
70
- # or maybe it is just a unknown method error.
71
- super
69
+ begin
70
+ Inspec::DSL.method_missing_resource(inspec, method_name, *arguments)
71
+ rescue LoadError
72
+ super
73
+ end
72
74
  end
73
75
  end
74
76
 
data/lib/inspec/dsl.rb CHANGED
@@ -3,6 +3,8 @@ require "inspec/log"
3
3
  require "inspec/plugin/v2"
4
4
 
5
5
  module Inspec::DSL
6
+ attr_accessor :backend
7
+
6
8
  def require_controls(id, &block)
7
9
  opts = { profile_id: id, include_all: false, backend: @backend, conf: @conf, dependencies: @dependencies }
8
10
  ::Inspec::DSL.load_spec_files_for_profile(self, opts, &block)
@@ -25,6 +27,23 @@ module Inspec::DSL
25
27
  add_resource(target_name, res)
26
28
  end
27
29
 
30
+ ##
31
+ # Try to load and instantiate a missing resource or raise LoadError
32
+ # if unable. Requiring the resource registers it and generates a
33
+ # method for it so you should only hit this once per missing
34
+ # resource.
35
+
36
+ def self.method_missing_resource(backend, id, *arguments)
37
+ begin
38
+ require "inspec/resources/#{id}"
39
+ rescue LoadError
40
+ require "resources/aws/#{id}"
41
+ end
42
+
43
+ klass = Inspec::Resource.registry[id.to_s]
44
+ klass.new(backend, id, *arguments)
45
+ end
46
+
28
47
  # Support for Outer Profile DSL plugins
29
48
  # This is called when an unknown method is encountered
30
49
  # "bare" in a control file - outside of a control or describe block.
@@ -44,7 +63,11 @@ module Inspec::DSL
44
63
  # We still haven't called it, so do so now.
45
64
  send(method_name, *arguments, &block)
46
65
  else
47
- super
66
+ begin
67
+ Inspec::DSL.method_missing_resource(backend, method_name, *arguments)
68
+ rescue LoadError
69
+ super
70
+ end
48
71
  end
49
72
  end
50
73
 
data/lib/inspec/errors.rb CHANGED
@@ -14,31 +14,5 @@ module Inspec
14
14
  class ConfigError::MalformedJson < ConfigError; end
15
15
  class ConfigError::Invalid < ConfigError; end
16
16
 
17
- class Input
18
- class Error < Inspec::Error; end
19
- class ValidationError < Error
20
- attr_accessor :input_name
21
- attr_accessor :input_value
22
- attr_accessor :input_type
23
- end
24
- class TypeError < Error
25
- attr_accessor :input_type
26
- end
27
- class RequiredError < Error
28
- attr_accessor :input_name
29
- end
30
- end
31
-
32
- class InputRegistry
33
- class Error < Inspec::Error; end
34
- class ProfileLookupError < Error
35
- attr_accessor :profile_name
36
- end
37
- class InputLookupError < Error
38
- attr_accessor :profile_name
39
- attr_accessor :input_name
40
- end
41
- end
42
-
43
17
  class UserInteractionRequired < Error; end
44
18
  end
@@ -51,7 +51,7 @@ module Inspec
51
51
  def relative_provider
52
52
  RelativeFileProvider.new(self)
53
53
  end
54
- end
54
+ end # class FileProvider
55
55
 
56
56
  class MockProvider < FileProvider
57
57
  attr_reader :files
@@ -89,7 +89,7 @@ module Inspec
89
89
 
90
90
  File.binread(file)
91
91
  end
92
- end
92
+ end # class DirProvider
93
93
 
94
94
  class ZipProvider < FileProvider
95
95
  attr_reader :files
@@ -146,7 +146,7 @@ module Inspec
146
146
  end
147
147
  res
148
148
  end
149
- end
149
+ end # class ZipProvider
150
150
 
151
151
  class TarProvider < FileProvider
152
152
  attr_reader :files
@@ -154,42 +154,48 @@ module Inspec
154
154
  def initialize(path)
155
155
  @path = path
156
156
  @contents = {}
157
- @files = []
158
- walk_tar(@path) do |tar|
159
- @files = tar.find_all(&:file?)
160
157
 
161
- # delete all entries with no name
162
- @files = @files.find_all { |x| !x.full_name.empty? && x.full_name.squeeze("/") !~ %r{\.{2}(?:/|\z)} }
158
+ here = Pathname.new(".")
163
159
 
164
- # delete all entries that have a PaxHeader
165
- @files = @files.delete_if { |x| x.full_name.include?("PaxHeader/") }
160
+ walk_tar(@path) do |entries|
161
+ entries.each do |entry|
162
+ name = entry.full_name
166
163
 
167
- # replace all items of the array simply with the relative filename of the file
168
- @files.map! { |x| Pathname.new(x.full_name).relative_path_from(Pathname.new(".")).to_s }
164
+ # rubocop:disable Layout/MultilineOperationIndentation
165
+ # rubocop:disable Style/ParenthesesAroundCondition
166
+ next unless (entry.file? && # duh
167
+ !name.empty? && # for empty filenames?
168
+ name !~ %r{\.\.(?:/|\z)} && # .. (to avoid attacks?)
169
+ !name.include?("PaxHeader/"))
170
+
171
+ path = Pathname.new(name).relative_path_from(here).to_s
172
+
173
+ @contents[path] = begin # not ||= in a tarball, last one wins
174
+ res = entry.read
175
+ try = res.dup
176
+ try.force_encoding Encoding::UTF_8
177
+ res = try if try.valid_encoding?
178
+ res
179
+ end
180
+ end
181
+
182
+ @files = @contents.keys
169
183
  end
170
184
  end
171
185
 
172
186
  def extract(destination_path = ".")
173
187
  FileUtils.mkdir_p(destination_path)
174
188
 
175
- walk_tar(@path) do |files|
176
- files.each do |file|
177
- next unless @files.include?(file.full_name)
178
-
179
- final_path = File.join(destination_path, file.full_name)
180
-
181
- # This removes the top level directory (and any other files) to ensure
182
- # extracted files do not conflict.
183
- FileUtils.remove_entry(final_path) if File.exist?(final_path)
189
+ @contents.each do |path, body|
190
+ full_path = File.join(destination_path, path)
184
191
 
185
- FileUtils.mkdir_p(File.dirname(final_path))
186
- File.open(final_path, "wb") { |f| f.write(file.read) }
187
- end
192
+ FileUtils.mkdir_p(File.dirname(full_path))
193
+ File.open(full_path, "wb") { |f| f.write(body) }
188
194
  end
189
195
  end
190
196
 
191
197
  def read(file)
192
- @contents[file] ||= read_from_tar(file)
198
+ @contents[file]
193
199
  end
194
200
 
195
201
  private
@@ -200,23 +206,7 @@ module Inspec
200
206
  ensure
201
207
  tar_file.close
202
208
  end
203
-
204
- def read_from_tar(file)
205
- return nil unless @files.include?(file)
206
-
207
- res = nil
208
- # NB `TarReader` includes `Enumerable` beginning with Ruby 2.x
209
- walk_tar(@path) do |tar|
210
- tar.each do |entry|
211
- next unless entry.file? && [file, "./#{file}"].include?(entry.full_name)
212
-
213
- res = entry.read
214
- break
215
- end
216
- end
217
- res
218
- end
219
- end
209
+ end # class TarProvider
220
210
 
221
211
  class RelativeFileProvider
222
212
  BLACKLIST_FILES = [
@@ -318,5 +308,5 @@ module Inspec
318
308
  b = File.dirname(new_pre + "b")
319
309
  get_prefix([a, b])
320
310
  end
321
- end
311
+ end # class RelativeFileProvider
322
312
  end
@@ -158,6 +158,7 @@ module Inspec::Formatters
158
158
  start_time: example.execution_result.started_at.to_datetime.rfc3339.to_s,
159
159
  resource_title: example.metadata[:described_class] || example.metadata[:example_group][:description],
160
160
  expectation_message: format_expectation_message(example),
161
+ waiver_data: example.metadata[:waiver_data],
161
162
  }
162
163
 
163
164
  unless (pid = example.metadata[:profile_id]).nil?
data/lib/inspec/impact.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require "inspec/errors"
2
+
1
3
  # Impact scores based off CVSS 3.0
2
4
  module Inspec::Impact
3
5
  IMPACT_SCORES = {
@@ -0,0 +1,410 @@
1
+ require "inspec/utils/deprecation"
2
+
3
+ # For backwards compatibility during the rename (see #3802),
4
+ # maintain the Inspec::Attribute namespace for people checking for
5
+ # Inspec::Attribute::DEFAULT_ATTRIBUTE
6
+ module Inspec
7
+ class Attribute
8
+ # This only exists to create the Inspec::Attribute::DEFAULT_ATTRIBUTE symbol with a class
9
+ class DEFAULT_ATTRIBUTE; end # rubocop: disable Naming/ClassAndModuleCamelCase
10
+ end
11
+ end
12
+
13
+ module Inspec
14
+ class Input
15
+
16
+ class Error < Inspec::Error; end
17
+ class ValidationError < Error
18
+ attr_accessor :input_name
19
+ attr_accessor :input_value
20
+ attr_accessor :input_type
21
+ end
22
+ class TypeError < Error
23
+ attr_accessor :input_type
24
+ end
25
+ class RequiredError < Error
26
+ attr_accessor :input_name
27
+ end
28
+
29
+ #===========================================================================#
30
+ # Class Input::Event
31
+ #===========================================================================#
32
+
33
+ # TODO: break this out to its own file under inspec/input?
34
+ # Information about how the input obtained its value.
35
+ # Each time it changes, an Input::Event is added to the #events array.
36
+ class Event
37
+ EVENT_PROPERTIES = [
38
+ :action, # :create, :set, :fetch
39
+ :provider, # Name of the plugin
40
+ :priority, # Priority of this plugin for resolving conflicts. 1-100, higher numbers win.
41
+ :value, # New value, if provided.
42
+ :file, # File containing the input-changing action, if known
43
+ :line, # Line in file containing the input-changing action, if known
44
+ :hit, # if action is :fetch, true if the remote source had the input
45
+ ].freeze
46
+
47
+ # Value has a special handler
48
+ EVENT_PROPERTIES.reject { |p| p == :value }.each do |prop|
49
+ attr_accessor prop
50
+ end
51
+
52
+ attr_reader :value
53
+
54
+ def initialize(properties = {})
55
+ @value_has_been_set = false
56
+
57
+ properties.each do |prop_name, prop_value|
58
+ if EVENT_PROPERTIES.include? prop_name
59
+ # OK, save the property
60
+ send((prop_name.to_s + "=").to_sym, prop_value)
61
+ else
62
+ raise "Unrecognized property to Input::Event: #{prop_name}"
63
+ end
64
+ end
65
+ end
66
+
67
+ def value=(the_val)
68
+ # Even if set to nil or false, it has indeed been set; note that fact.
69
+ @value_has_been_set = true
70
+ @value = the_val
71
+ end
72
+
73
+ def value_has_been_set?
74
+ @value_has_been_set
75
+ end
76
+
77
+ def diagnostic_string
78
+ to_h.reject { |_, val| val.nil? }.to_a.map { |pair| "#{pair[0]}: '#{pair[1]}'" }.join(", ")
79
+ end
80
+
81
+ def to_h
82
+ EVENT_PROPERTIES.each_with_object({}) do |prop, hash|
83
+ hash[prop] = send(prop)
84
+ end
85
+ end
86
+
87
+ def self.probe_stack
88
+ frames = caller_locations(2, 40)
89
+ frames.reject! { |f| f.path && f.path.include?("/lib/inspec/") }
90
+ frames.first
91
+ end
92
+ end # class Event
93
+
94
+ #===========================================================================#
95
+ # Class NO_VALUE_SET
96
+ #===========================================================================#
97
+ # This special class is used to represent the value when an input has
98
+ # not been assigned a value. This allows a user to explicitly assign nil
99
+ # to an input.
100
+ class NO_VALUE_SET # rubocop: disable Naming/ClassAndModuleCamelCase
101
+ def initialize(name)
102
+ @name = name
103
+
104
+ # output warn message if we are in a exec call
105
+ if Inspec::BaseCLI.inspec_cli_command == :exec
106
+ Inspec::Log.warn(
107
+ "Input '#{@name}' does not have a value. "\
108
+ "Use --input-file to provide a value for '#{@name}' or specify a "\
109
+ "value with `attribute('#{@name}', value: 'somevalue', ...)`."
110
+ )
111
+ end
112
+ end
113
+
114
+ def method_missing(*_)
115
+ self
116
+ end
117
+
118
+ def respond_to_missing?(_, _)
119
+ true
120
+ end
121
+
122
+ def to_s
123
+ "Input '#{@name}' does not have a value. Skipping test."
124
+ end
125
+
126
+ def is_a?(klass)
127
+ if klass == Inspec::Attribute::DEFAULT_ATTRIBUTE
128
+ Inspec.deprecate(:rename_attributes_to_inputs, "Don't check for `is_a?(Inspec::Attribute::DEFAULT_ATTRIBUTE)`, check for `Inspec::Input::NO_VALUE_SET")
129
+ true # lie for backward compatibility
130
+ else
131
+ super(klass)
132
+ end
133
+ end
134
+
135
+ def kind_of?(klass)
136
+ if klass == Inspec::Attribute::DEFAULT_ATTRIBUTE
137
+ Inspec.deprecate(:rename_attributes_to_inputs, "Don't check for `kind_of?(Inspec::Attribute::DEFAULT_ATTRIBUTE)`, check for `Inspec::Input::NO_VALUE_SET")
138
+ true # lie for backward compatibility
139
+ else
140
+ super(klass)
141
+ end
142
+ end
143
+ end # class NO_VALUE_SET
144
+
145
+ #===========================================================================#
146
+ # Class Inspec::Input
147
+ #===========================================================================#
148
+
149
+ # Validation types for input values
150
+ VALID_TYPES = %w{
151
+ String
152
+ Numeric
153
+ Regexp
154
+ Array
155
+ Hash
156
+ Boolean
157
+ Any
158
+ }.freeze
159
+
160
+ # TODO: this is not used anywhere?
161
+ # If you call `input` in a control file, the input will receive this priority.
162
+ # You can override that with a :priority option.
163
+ DEFAULT_PRIORITY_FOR_DSL_ATTRIBUTES = 20
164
+
165
+ # If you somehow manage to initialize an Input outside of the DSL,
166
+ # AND you don't provide an Input::Event, this is the priority you get.
167
+ DEFAULT_PRIORITY_FOR_UNKNOWN_CALLER = 10
168
+
169
+ # If you directly call value=, this is the priority assigned.
170
+ # This is the highest priority within InSpec core; though plugins
171
+ # are free to go higher.
172
+ DEFAULT_PRIORITY_FOR_VALUE_SET = 60
173
+
174
+ attr_reader :description, :events, :identifier, :name, :required, :title, :type
175
+
176
+ def initialize(name, options = {})
177
+ @name = name
178
+ @opts = options
179
+ if @opts.key?(:default)
180
+ Inspec.deprecate(:attrs_value_replaces_default, "input name: '#{name}'")
181
+ if @opts.key?(:value)
182
+ Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
183
+ @opts.delete(:default)
184
+ end
185
+ end
186
+
187
+ # Array of Input::Event objects. These compete with one another to determine
188
+ # the value of the input when value() is called, as well as providing a
189
+ # debugging record of when and how the value changed.
190
+ @events = []
191
+ events.push make_creation_event(options)
192
+
193
+ update(options)
194
+ end
195
+
196
+ # TODO: is this here just for testing?
197
+ def set_events
198
+ events.select { |e| e.action == :set }
199
+ end
200
+
201
+ def diagnostic_string
202
+ "Input #{name}, with history:\n" +
203
+ events.map(&:diagnostic_string).map { |line| " #{line}" }.join("\n")
204
+ end
205
+
206
+ #--------------------------------------------------------------------------#
207
+ # Managing Value
208
+ #--------------------------------------------------------------------------#
209
+
210
+ def update(options)
211
+ _update_set_metadata(options)
212
+ normalize_type_restriction!
213
+
214
+ # Values are set by passing events in; but we can also infer an event.
215
+ if options.key?(:value) || options.key?(:default)
216
+ if options.key?(:event)
217
+ if options.key?(:value) || options.key?(:default)
218
+ Inspec::Log.warn "Do not provide both an Event and a value as an option to attribute('#{name}') - using value from event"
219
+ end
220
+ else
221
+ self.class.infer_event(options) # Sets options[:event]
222
+ end
223
+ end
224
+ events << options[:event] if options.key? :event
225
+
226
+ enforce_type_restriction!
227
+ end
228
+
229
+ # We can determine a value:
230
+ # 1. By event.value (preferred)
231
+ # 2. By options[:value]
232
+ # 3. By options[:default] (deprecated)
233
+ def self.infer_event(options)
234
+ # Don't rely on this working; you really should be passing a proper Input::Event
235
+ # with the context information you have.
236
+ location = Input::Event.probe_stack
237
+ event = Input::Event.new(
238
+ action: :set,
239
+ provider: options[:provider] || :unknown,
240
+ priority: options[:priority] || Inspec::Input::DEFAULT_PRIORITY_FOR_UNKNOWN_CALLER,
241
+ file: location.path,
242
+ line: location.lineno
243
+ )
244
+
245
+ if options.key?(:default)
246
+ Inspec.deprecate(:attrs_value_replaces_default, "attribute name: '#{name}'")
247
+ if options.key?(:value)
248
+ Inspec::Log.warn "Input #{@name} created using both :default and :value options - ignoring :default"
249
+ options.delete(:default)
250
+ else
251
+ options[:value] = options.delete(:default)
252
+ end
253
+ end
254
+ event.value = options[:value] if options.key?(:value)
255
+ options[:event] = event
256
+ end
257
+
258
+ private
259
+
260
+ def _update_set_metadata(options)
261
+ # Basic metadata
262
+ @title = options[:title] if options.key?(:title)
263
+ @description = options[:description] if options.key?(:description)
264
+ @required = options[:required] if options.key?(:required)
265
+ @identifier = options[:identifier] if options.key?(:identifier) # TODO: determine if this is ever used
266
+ @type = options[:type] if options.key?(:type)
267
+ end
268
+
269
+ def make_creation_event(options)
270
+ loc = options[:location] || Event.probe_stack
271
+ Input::Event.new(
272
+ action: :create,
273
+ provider: options[:provider],
274
+ file: loc.path,
275
+ line: loc.lineno
276
+ )
277
+ end
278
+
279
+ # Determine the current winning value, but don't validate it
280
+ def current_value
281
+ # Examine the events to determine highest-priority value. Tie-break
282
+ # by using the last one set.
283
+ events_that_set_a_value = events.select(&:value_has_been_set?)
284
+ winning_priority = events_that_set_a_value.map(&:priority).max
285
+ winning_events = events_that_set_a_value.select { |e| e.priority == winning_priority }
286
+ winning_event = winning_events.last # Last for tie-break
287
+
288
+ if winning_event.nil?
289
+ # No value has been set - return special no value object
290
+ NO_VALUE_SET.new(name)
291
+ else
292
+ winning_event.value # May still be nil
293
+ end
294
+ end
295
+
296
+ public
297
+
298
+ def value=(new_value, priority = DEFAULT_PRIORITY_FOR_VALUE_SET)
299
+ # Inject a new Event with the new value.
300
+ location = Event.probe_stack
301
+ events << Event.new(
302
+ action: :set,
303
+ provider: :value_setter,
304
+ priority: priority,
305
+ value: new_value,
306
+ file: location.path,
307
+ line: location.lineno
308
+ )
309
+ enforce_type_restriction!
310
+ end
311
+
312
+ def value
313
+ enforce_required_validation!
314
+ current_value
315
+ end
316
+
317
+ def has_value?
318
+ !current_value.is_a? NO_VALUE_SET
319
+ end
320
+
321
+ #--------------------------------------------------------------------------#
322
+ # Value Type Coercion
323
+ #--------------------------------------------------------------------------#
324
+
325
+ def to_s
326
+ "Input #{name} with #{current_value}"
327
+ end
328
+
329
+ #--------------------------------------------------------------------------#
330
+ # Validation
331
+ #--------------------------------------------------------------------------#
332
+
333
+ private
334
+
335
+ def enforce_required_validation!
336
+ return unless required
337
+ # skip if we are not doing an exec call (archive/vendor/check)
338
+ return unless Inspec::BaseCLI.inspec_cli_command == :exec
339
+
340
+ proposed_value = current_value
341
+ if proposed_value.nil? || proposed_value.is_a?(NO_VALUE_SET)
342
+ error = Inspec::Input::RequiredError.new
343
+ error.input_name = name
344
+ raise error, "Input '#{error.input_name}' is required and does not have a value."
345
+ end
346
+ end
347
+
348
+ def enforce_type_restriction! # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
349
+ return unless type
350
+ return unless has_value?
351
+
352
+ type_req = type
353
+ return if type_req == "Any"
354
+
355
+ proposed_value = current_value
356
+
357
+ invalid_type = false
358
+ if type_req == "Regexp"
359
+ invalid_type = true unless valid_regexp?(proposed_value)
360
+ elsif type_req == "Numeric"
361
+ invalid_type = true unless valid_numeric?(proposed_value)
362
+ elsif type_req == "Boolean"
363
+ invalid_type = true unless [true, false].include?(proposed_value)
364
+ elsif proposed_value.is_a?(Module.const_get(type_req)) == false
365
+ # TODO: why is this case here?
366
+ invalid_type = true
367
+ end
368
+
369
+ if invalid_type == true
370
+ error = Inspec::Input::ValidationError.new
371
+ error.input_name = @name
372
+ error.input_value = proposed_value
373
+ error.input_type = type_req
374
+ raise error, "Input '#{error.input_name}' with value '#{error.input_value}' does not validate to type '#{error.input_type}'."
375
+ end
376
+ end
377
+
378
+ def normalize_type_restriction!
379
+ return unless type
380
+
381
+ type_req = type.capitalize
382
+ abbreviations = {
383
+ "Num" => "Numeric",
384
+ "Regex" => "Regexp",
385
+ }
386
+ type_req = abbreviations[type_req] if abbreviations.key?(type_req)
387
+ unless VALID_TYPES.include?(type_req)
388
+ error = Inspec::Input::TypeError.new
389
+ error.input_type = type_req
390
+ raise error, "Type '#{error.input_type}' is not a valid input type."
391
+ end
392
+ @type = type_req
393
+ end
394
+
395
+ def valid_numeric?(value)
396
+ Float(value)
397
+ true
398
+ rescue
399
+ false
400
+ end
401
+
402
+ def valid_regexp?(value)
403
+ # check for invalid regex syntax
404
+ Regexp.new(value)
405
+ true
406
+ rescue
407
+ false
408
+ end
409
+ end
410
+ end