inspec 4.16.0 → 4.17.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/inspec.rb +0 -1
- data/lib/inspec/backend.rb +7 -0
- data/lib/inspec/base_cli.rb +2 -0
- data/lib/inspec/cli.rb +3 -10
- data/lib/inspec/config.rb +3 -4
- data/lib/inspec/control_eval_context.rb +5 -3
- data/lib/inspec/dsl.rb +24 -1
- data/lib/inspec/errors.rb +0 -26
- data/lib/inspec/file_provider.rb +33 -43
- data/lib/inspec/formatters/base.rb +1 -0
- data/lib/inspec/impact.rb +2 -0
- data/lib/inspec/input.rb +410 -0
- data/lib/inspec/input_registry.rb +10 -1
- data/lib/inspec/objects.rb +3 -1
- data/lib/inspec/objects/input.rb +5 -387
- data/lib/inspec/objects/tag.rb +1 -1
- data/lib/inspec/plugin/v1/plugin_types/resource.rb +16 -5
- data/lib/inspec/plugin/v2/activator.rb +4 -8
- data/lib/inspec/plugin/v2/loader.rb +19 -3
- data/lib/inspec/profile.rb +1 -1
- data/lib/inspec/profile_context.rb +1 -1
- data/lib/inspec/reporters/json.rb +70 -88
- data/lib/inspec/resource.rb +1 -0
- data/lib/inspec/resources.rb +9 -2
- data/lib/inspec/resources/aide_conf.rb +4 -0
- data/lib/inspec/resources/apt.rb +19 -19
- data/lib/inspec/resources/etc_fstab.rb +4 -0
- data/lib/inspec/resources/etc_hosts.rb +4 -0
- data/lib/inspec/resources/firewalld.rb +4 -0
- data/lib/inspec/resources/json.rb +10 -3
- data/lib/inspec/resources/mssql_session.rb +1 -1
- data/lib/inspec/resources/platform.rb +18 -13
- data/lib/inspec/resources/postfix_conf.rb +6 -2
- data/lib/inspec/resources/security_identifier.rb +4 -0
- data/lib/inspec/resources/sys_info.rb +65 -4
- data/lib/inspec/resources/user.rb +1 -0
- data/lib/inspec/rule.rb +68 -6
- data/lib/inspec/runner.rb +6 -1
- data/lib/inspec/runner_rspec.rb +1 -0
- data/lib/inspec/shell.rb +8 -1
- data/lib/inspec/utils/pkey_reader.rb +1 -1
- data/lib/inspec/version.rb +1 -1
- data/lib/matchers/matchers.rb +2 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/functional/help_test.rb +23 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/functional/helper.rb +62 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/functional/install_test.rb +368 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/functional/list_test.rb +101 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/functional/search_test.rb +129 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/functional/uninstall_test.rb +63 -0
- data/lib/plugins/inspec-plugin-manager-cli/test/functional/update_test.rb +84 -0
- metadata +11 -3
- data/lib/plugins/inspec-plugin-manager-cli/test/functional/inspec-plugin_test.rb +0 -845
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a2c29f41f20501e3eb5820dd06a6ff60cec6f4c5af386f820c819f8e58651a81
|
4
|
+
data.tar.gz: b7eb018efcf58fcc215e5188f29c3207be4cc3046da1683bef3f58de3fb42c1d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b7b9738ff7701f7a4d1a61797c2353c06fc437102b5202f7e83857c4ba40189c2ab0d2b17ec34649bb18fa4a6b851040b6f0be6f3c719cfc62f01bfe67af111
|
7
|
+
data.tar.gz: c291eae358770b3af3ebca51acaf38105c609867084ed94262b1079f3a903ce6365d6e3e60c446ef51b1e74d8b23351ba9056dea18b3f421e78223b82ea55718
|
data/lib/inspec.rb
CHANGED
data/lib/inspec/backend.rb
CHANGED
@@ -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)
|
data/lib/inspec/base_cli.rb
CHANGED
@@ -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",
|
212
|
-
|
213
|
-
|
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
|
-
|
225
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
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
|
data/lib/inspec/file_provider.rb
CHANGED
@@ -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
|
-
|
162
|
-
@files = @files.find_all { |x| !x.full_name.empty? && x.full_name.squeeze("/") !~ %r{\.{2}(?:/|\z)} }
|
158
|
+
here = Pathname.new(".")
|
163
159
|
|
164
|
-
|
165
|
-
|
160
|
+
walk_tar(@path) do |entries|
|
161
|
+
entries.each do |entry|
|
162
|
+
name = entry.full_name
|
166
163
|
|
167
|
-
|
168
|
-
|
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
|
-
|
176
|
-
|
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
|
-
|
186
|
-
|
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]
|
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
data/lib/inspec/input.rb
ADDED
@@ -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
|