inspec 4.16.0 → 4.17.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|