inspec-core 3.5.0 → 3.6.2
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/etc/deprecations.json +9 -0
- data/lib/bundles/inspec-supermarket/cli.rb +1 -1
- data/lib/inspec/backend.rb +9 -8
- data/lib/inspec/base_cli.rb +12 -170
- data/lib/inspec/cli.rb +17 -16
- data/lib/inspec/config.rb +391 -0
- data/lib/inspec/control_eval_context.rb +2 -1
- data/lib/inspec/dsl.rb +2 -2
- data/lib/inspec/errors.rb +5 -0
- data/lib/inspec/plugin/v1/plugin_types/resource.rb +1 -1
- data/lib/inspec/plugin/v2/activator.rb +24 -3
- data/lib/inspec/plugin/v2/loader.rb +1 -1
- data/lib/inspec/plugin/v2/registry.rb +8 -12
- data/lib/inspec/profile.rb +3 -2
- data/lib/inspec/profile_vendor.rb +2 -1
- data/lib/inspec/rspec_extensions.rb +2 -2
- data/lib/inspec/runner.rb +5 -5
- data/lib/inspec/shell.rb +1 -1
- data/lib/inspec/ui.rb +2 -2
- data/lib/inspec/version.rb +1 -1
- data/lib/plugins/inspec-habitat/lib/inspec-habitat/profile.rb +1 -1
- data/lib/plugins/inspec-init/lib/inspec-init/cli.rb +3 -28
- data/lib/plugins/inspec-init/lib/inspec-init/cli_plugin.rb +245 -0
- data/lib/plugins/inspec-init/lib/inspec-init/cli_profile.rb +49 -0
- data/lib/plugins/inspec-init/lib/inspec-init/renderer.rb +43 -31
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/Gemfile +12 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/LICENSE +2 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/README.md +28 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/Rakefile +40 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/inspec-plugin-template.gemspec +45 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/lib/inspec-plugin-template.rb +16 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/lib/inspec-plugin-template/cli_command.rb +64 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/lib/inspec-plugin-template/plugin.rb +55 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/lib/inspec-plugin-template/version.rb +10 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/fixtures/README.md +24 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/functional/README.md +12 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/functional/inspec_plugin_template_test.rb +110 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/helper.rb +26 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/unit/README.md +17 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/unit/cli_args_test.rb +67 -0
- data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/unit/plugin_def_test.rb +51 -0
- data/lib/plugins/inspec-init/{lib/inspec-init/templates → templates}/profiles/os/README.md +0 -0
- data/lib/plugins/inspec-init/{lib/inspec-init/templates → templates}/profiles/os/controls/example.rb +0 -0
- data/lib/plugins/inspec-init/{lib/inspec-init/templates → templates}/profiles/os/inspec.yml +0 -0
- data/lib/plugins/inspec-init/{lib/inspec-init/templates → templates}/profiles/os/libraries/.gitkeep +0 -0
- data/lib/plugins/inspec-init/test/functional/inspec_init_plugin_test.rb +173 -0
- data/lib/plugins/inspec-init/test/functional/{inspec_init_test.rb → inspec_init_profile_test.rb} +7 -7
- data/lib/resources/filesystem.rb +40 -12
- metadata +31 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 540ae71a1d966748d37cddcb1f171f7921e34f9bfcc6781702934756904476f9
|
4
|
+
data.tar.gz: 52a9a03513892e4e266eaba4a24eee0cee01749c6b96d99845c335616544b962
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b13e81a88b36ee456597ffcacb701e84c1702d402f80e8ac1f2c9e135af14c3990394f366230969d6ca6968dad4b973cb09ada2a919e16789282e77cfcf38348
|
7
|
+
data.tar.gz: 1a27aa7f35b92b4b5b974123bee5ef33ab0d942e6bc90a478521927a771a564d92c9b1063f359a7d4d457b1e401229e96a5938cb62dd38b7fa6e10f57cab8778
|
data/etc/deprecations.json
CHANGED
@@ -5,6 +5,15 @@
|
|
5
5
|
"attrs_value_replaces_default": {
|
6
6
|
"action": "ignore",
|
7
7
|
"prefix": "The 'default' option for attributes is being replaced by 'value' - please use it instead."
|
8
|
+
},
|
9
|
+
"cli_option_json_config": {
|
10
|
+
"action": "ignore",
|
11
|
+
"prefix": "The --json-config option is being replaced by the --config option.",
|
12
|
+
"comment": "See #3661"
|
13
|
+
},
|
14
|
+
"filesystem_property_size": {
|
15
|
+
"action": "ignore",
|
16
|
+
"comment": "See #3778"
|
8
17
|
}
|
9
18
|
}
|
10
19
|
}
|
data/lib/inspec/backend.rb
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
# author: Christoph Hartmann
|
5
5
|
|
6
6
|
require 'train'
|
7
|
+
require 'inspec/config'
|
7
8
|
|
8
9
|
module Inspec
|
9
10
|
module Backend
|
@@ -38,19 +39,19 @@ module Inspec
|
|
38
39
|
|
39
40
|
# Create the transport backend with aggregated resources.
|
40
41
|
#
|
41
|
-
# @param [
|
42
|
+
# @param [Inspec::Config] config for the transport backend
|
42
43
|
# @return [TransportBackend] enriched transport instance
|
43
44
|
def self.create(config) # rubocop:disable Metrics/AbcSize
|
44
|
-
|
45
|
-
|
46
|
-
transport = Train.create(
|
45
|
+
train_credentials = config.unpack_train_credentials
|
46
|
+
transport_name = Train.validate_backend(train_credentials)
|
47
|
+
transport = Train.create(transport_name, train_credentials)
|
47
48
|
if transport.nil?
|
48
|
-
raise "Can't find transport backend '#{
|
49
|
+
raise "Can't find transport backend '#{transport_name}'."
|
49
50
|
end
|
50
51
|
|
51
52
|
connection = transport.connection
|
52
53
|
if connection.nil?
|
53
|
-
raise "Can't connect to transport backend '#{
|
54
|
+
raise "Can't connect to transport backend '#{transport_name}'."
|
54
55
|
end
|
55
56
|
|
56
57
|
# Set caching settings. We always want to enable caching for
|
@@ -85,9 +86,9 @@ module Inspec
|
|
85
86
|
|
86
87
|
cls.new
|
87
88
|
rescue Train::ClientError => e
|
88
|
-
raise "Client error, can't connect to '#{
|
89
|
+
raise "Client error, can't connect to '#{transport_name}' backend: #{e.message}"
|
89
90
|
rescue Train::TransportError => e
|
90
|
-
raise "Transport error, can't connect to '#{
|
91
|
+
raise "Transport error, can't connect to '#{transport_name}' backend: #{e.message}"
|
91
92
|
end
|
92
93
|
end
|
93
94
|
end
|
data/lib/inspec/base_cli.rb
CHANGED
@@ -75,8 +75,9 @@ module Inspec
|
|
75
75
|
desc: 'Whether to use disable sspi authentication, defaults to false (WinRM).'
|
76
76
|
option :winrm_basic_auth, type: :boolean,
|
77
77
|
desc: 'Whether to use basic authentication, defaults to false (WinRM).'
|
78
|
-
option :
|
78
|
+
option :config, type: :string,
|
79
79
|
desc: 'Read configuration from JSON file (`-` reads from stdin).'
|
80
|
+
option :json_config, type: :string, hide: true
|
80
81
|
option :proxy_command, type: :string,
|
81
82
|
desc: 'Specifies the command to use to connect to the server'
|
82
83
|
option :bastion_host, type: :string,
|
@@ -118,92 +119,7 @@ module Inspec
|
|
118
119
|
desc: 'Exit with code 101 if any tests fail, and 100 if any are skipped (default). If disabled, exit 0 on skips and 1 for failures.'
|
119
120
|
end
|
120
121
|
|
121
|
-
def self.
|
122
|
-
{
|
123
|
-
exec: {
|
124
|
-
'reporter' => ['cli'],
|
125
|
-
'show_progress' => false,
|
126
|
-
'color' => true,
|
127
|
-
'create_lockfile' => true,
|
128
|
-
'backend_cache' => true,
|
129
|
-
},
|
130
|
-
shell: {
|
131
|
-
'reporter' => ['cli'],
|
132
|
-
},
|
133
|
-
}
|
134
|
-
end
|
135
|
-
|
136
|
-
def self.parse_reporters(opts) # rubocop:disable Metrics/AbcSize
|
137
|
-
# default to cli report for ad-hoc runners
|
138
|
-
opts['reporter'] = ['cli'] if opts['reporter'].nil?
|
139
|
-
|
140
|
-
# parse out cli to proper report format
|
141
|
-
if opts['reporter'].is_a?(Array)
|
142
|
-
reports = {}
|
143
|
-
opts['reporter'].each do |report|
|
144
|
-
reporter_name, target = report.split(':', 2)
|
145
|
-
if target.nil? || target.strip == '-'
|
146
|
-
reports[reporter_name] = { 'stdout' => true }
|
147
|
-
else
|
148
|
-
reports[reporter_name] = {
|
149
|
-
'file' => target,
|
150
|
-
'stdout' => false,
|
151
|
-
}
|
152
|
-
reports[reporter_name]['target_id'] = opts['target_id'] if opts['target_id']
|
153
|
-
end
|
154
|
-
end
|
155
|
-
opts['reporter'] = reports
|
156
|
-
end
|
157
|
-
|
158
|
-
# add in stdout if not specified
|
159
|
-
if opts['reporter'].is_a?(Hash)
|
160
|
-
opts['reporter'].each do |reporter_name, config|
|
161
|
-
opts['reporter'][reporter_name] = {} if config.nil?
|
162
|
-
opts['reporter'][reporter_name]['stdout'] = true if opts['reporter'][reporter_name].empty?
|
163
|
-
opts['reporter'][reporter_name]['target_id'] = opts['target_id'] if opts['target_id']
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
validate_reporters(opts['reporter'])
|
168
|
-
opts
|
169
|
-
end
|
170
|
-
|
171
|
-
def self.validate_reporters(reporters)
|
172
|
-
return if reporters.nil?
|
173
|
-
|
174
|
-
valid_types = [
|
175
|
-
'automate',
|
176
|
-
'cli',
|
177
|
-
'documentation',
|
178
|
-
'html',
|
179
|
-
'json',
|
180
|
-
'json-automate',
|
181
|
-
'json-min',
|
182
|
-
'json-rspec',
|
183
|
-
'junit',
|
184
|
-
'progress',
|
185
|
-
'yaml',
|
186
|
-
]
|
187
|
-
|
188
|
-
reporters.each do |k, v|
|
189
|
-
raise NotImplementedError, "'#{k}' is not a valid reporter type." unless valid_types.include?(k)
|
190
|
-
|
191
|
-
next unless k == 'automate'
|
192
|
-
%w{token url}.each do |option|
|
193
|
-
raise Inspec::ReporterError, "You must specify a automate #{option} via the json-config." if v[option].nil?
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
# check to make sure we are only reporting one type to stdout
|
198
|
-
stdout = 0
|
199
|
-
reporters.each_value do |v|
|
200
|
-
stdout += 1 if v['stdout'] == true
|
201
|
-
end
|
202
|
-
|
203
|
-
raise ArgumentError, 'The option --reporter can only have a single report outputting to stdout.' if stdout > 1
|
204
|
-
end
|
205
|
-
|
206
|
-
def self.detect(params: {}, indent: 0, color: 39)
|
122
|
+
def self.format_platform_info(params: {}, indent: 0, color: 39)
|
207
123
|
str = ''
|
208
124
|
params.each { |item, info|
|
209
125
|
data = info
|
@@ -286,86 +202,12 @@ module Inspec
|
|
286
202
|
false
|
287
203
|
end
|
288
204
|
|
289
|
-
def diagnose(
|
290
|
-
|
291
|
-
puts "InSpec version: #{Inspec::VERSION}"
|
292
|
-
puts "Train version: #{Train::VERSION}"
|
293
|
-
puts 'Command line configuration:'
|
294
|
-
pp options
|
295
|
-
puts 'JSON configuration file:'
|
296
|
-
pp options_json
|
297
|
-
puts 'Merged configuration:'
|
298
|
-
pp opts
|
299
|
-
puts
|
300
|
-
end
|
301
|
-
|
302
|
-
def opts(type = nil)
|
303
|
-
o = merged_opts(type)
|
304
|
-
|
305
|
-
# Due to limitations in Thor it is not possible to set an argument to be
|
306
|
-
# both optional and its value to be mandatory. E.g. the user supplying
|
307
|
-
# the --password argument is optional and not always required, but
|
308
|
-
# whenever it is used, it requires a value. Handle options that were
|
309
|
-
# defined above and require a value here:
|
310
|
-
%w{password sudo-password}.each do |v|
|
311
|
-
id = v.tr('-', '_').to_sym
|
312
|
-
next unless o[id] == -1
|
313
|
-
raise ArgumentError, "Please provide a value for --#{v}. For example: --#{v}=hello."
|
314
|
-
end
|
315
|
-
|
316
|
-
# Infer `--sudo` if using `--sudo-password` without `--sudo`
|
317
|
-
if o[:sudo_password] && !o[:sudo]
|
318
|
-
o[:sudo] = true
|
319
|
-
warn 'WARN: `--sudo-password` used without `--sudo`. Adding `--sudo`.'
|
320
|
-
end
|
321
|
-
|
322
|
-
# check for compliance settings
|
323
|
-
if o['compliance']
|
324
|
-
require 'plugins/inspec-compliance/lib/inspec-compliance/api'
|
325
|
-
InspecPlugins::Compliance::API.login(o['compliance'])
|
326
|
-
end
|
327
|
-
|
328
|
-
o
|
205
|
+
def diagnose(_ = nil)
|
206
|
+
config.diagnose
|
329
207
|
end
|
330
208
|
|
331
|
-
def
|
332
|
-
opts
|
333
|
-
|
334
|
-
# start with default options if we have any
|
335
|
-
opts = BaseCLI.default_options[type] unless type.nil? || BaseCLI.default_options[type].nil?
|
336
|
-
opts['type'] = type unless type.nil?
|
337
|
-
Inspec::BaseCLI.inspec_cli_command = type
|
338
|
-
|
339
|
-
# merge in any options from json-config
|
340
|
-
json_config = options_json
|
341
|
-
opts.merge!(json_config)
|
342
|
-
|
343
|
-
# merge in any options defined via thor
|
344
|
-
opts.merge!(options)
|
345
|
-
|
346
|
-
# parse reporter options
|
347
|
-
opts = BaseCLI.parse_reporters(opts) if %i(exec shell).include?(type)
|
348
|
-
|
349
|
-
Thor::CoreExt::HashWithIndifferentAccess.new(opts)
|
350
|
-
end
|
351
|
-
|
352
|
-
def options_json
|
353
|
-
conffile = options['json_config']
|
354
|
-
@json ||= conffile ? read_config(conffile) : {}
|
355
|
-
end
|
356
|
-
|
357
|
-
def read_config(file)
|
358
|
-
if file == '-'
|
359
|
-
puts 'WARN: reading JSON config from standard input' if STDIN.tty?
|
360
|
-
config = STDIN.read
|
361
|
-
else
|
362
|
-
config = File.read(file)
|
363
|
-
end
|
364
|
-
|
365
|
-
JSON.parse(config)
|
366
|
-
rescue JSON::ParserError => e
|
367
|
-
puts "Failed to load JSON configuration: #{e}\nConfig was: #{config.inspect}"
|
368
|
-
exit 1
|
209
|
+
def config
|
210
|
+
@config ||= Inspec::Config.new(options) # 'options' here is CLI opts from Thor
|
369
211
|
end
|
370
212
|
|
371
213
|
# get the log level
|
@@ -409,12 +251,12 @@ module Inspec
|
|
409
251
|
|
410
252
|
def configure_logger(o)
|
411
253
|
#
|
412
|
-
# TODO(ssd): This is a
|
254
|
+
# TODO(ssd): This is a bit gross, but this configures the
|
413
255
|
# logging singleton Inspec::Log. Eventually it would be nice to
|
414
256
|
# move internal debug logging to use this logging singleton.
|
415
257
|
#
|
416
|
-
loc = if o
|
417
|
-
o
|
258
|
+
loc = if o['log_location']
|
259
|
+
o['log_location']
|
418
260
|
elsif suppress_log_output?(o)
|
419
261
|
STDERR
|
420
262
|
else
|
@@ -422,14 +264,14 @@ module Inspec
|
|
422
264
|
end
|
423
265
|
|
424
266
|
Inspec::Log.init(loc)
|
425
|
-
Inspec::Log.level = get_log_level(o
|
267
|
+
Inspec::Log.level = get_log_level(o['log_level'])
|
426
268
|
|
427
269
|
o[:logger] = Logger.new(loc)
|
428
270
|
# output json if we have activated the json formatter
|
429
271
|
if o['log-format'] == 'json'
|
430
272
|
o[:logger].formatter = Logger::JSONFormatter.new
|
431
273
|
end
|
432
|
-
o[:logger].level = get_log_level(o
|
274
|
+
o[:logger].level = get_log_level(o['log_level'])
|
433
275
|
end
|
434
276
|
end
|
435
277
|
end
|
data/lib/inspec/cli.rb
CHANGED
@@ -15,6 +15,7 @@ require 'inspec/plugin/v2'
|
|
15
15
|
require 'inspec/runner_mock'
|
16
16
|
require 'inspec/env_printer'
|
17
17
|
require 'inspec/schema'
|
18
|
+
require 'inspec/config'
|
18
19
|
|
19
20
|
class Inspec::InspecCLI < Inspec::BaseCLI
|
20
21
|
class_option :log_level, aliases: :l, type: :string,
|
@@ -45,12 +46,12 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
45
46
|
desc: 'A list of controls to include. Ignore all other tests.'
|
46
47
|
profile_options
|
47
48
|
def json(target)
|
48
|
-
o =
|
49
|
+
o = config
|
49
50
|
diagnose(o)
|
50
51
|
o['log_location'] = STDERR
|
51
52
|
configure_logger(o)
|
52
53
|
|
53
|
-
o[:backend] = Inspec::Backend.create(
|
54
|
+
o[:backend] = Inspec::Backend.create(Inspec::Config.mock)
|
54
55
|
o[:check_mode] = true
|
55
56
|
o[:vendor_cache] = Inspec::Cache.new(o[:vendor_cache])
|
56
57
|
|
@@ -81,9 +82,9 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
81
82
|
option :format, type: :string
|
82
83
|
profile_options
|
83
84
|
def check(path) # rubocop:disable Metrics/AbcSize
|
84
|
-
o =
|
85
|
+
o = config
|
85
86
|
diagnose(o)
|
86
|
-
o[:backend] = Inspec::Backend.create(
|
87
|
+
o[:backend] = Inspec::Backend.create(Inspec::Config.mock)
|
87
88
|
o[:check_mode] = true
|
88
89
|
o[:vendor_cache] = Inspec::Cache.new(o[:vendor_cache])
|
89
90
|
|
@@ -133,10 +134,10 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
133
134
|
option :overwrite, type: :boolean, default: false,
|
134
135
|
desc: 'Overwrite existing vendored dependencies and lockfile.'
|
135
136
|
def vendor(path = nil)
|
136
|
-
o =
|
137
|
+
o = config
|
137
138
|
configure_logger(o)
|
138
139
|
o[:logger] = Logger.new(STDOUT)
|
139
|
-
o[:logger].level = get_log_level(o
|
140
|
+
o[:logger].level = get_log_level(o[:log_level])
|
140
141
|
|
141
142
|
vendor_deps(path, o)
|
142
143
|
end
|
@@ -154,12 +155,12 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
154
155
|
option :ignore_errors, type: :boolean, default: false,
|
155
156
|
desc: 'Ignore profile warnings.'
|
156
157
|
def archive(path)
|
157
|
-
o =
|
158
|
+
o = config
|
158
159
|
diagnose(o)
|
159
160
|
|
160
161
|
o[:logger] = Logger.new(STDOUT)
|
161
|
-
o[:logger].level = get_log_level(o
|
162
|
-
o[:backend] = Inspec::Backend.create(
|
162
|
+
o[:logger].level = get_log_level(o[:log_level])
|
163
|
+
o[:backend] = Inspec::Backend.create(Inspec::Config.mock)
|
163
164
|
|
164
165
|
# Force vendoring with overwrite when archiving
|
165
166
|
vendor_options = o.dup
|
@@ -254,7 +255,7 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
254
255
|
EOT
|
255
256
|
exec_options
|
256
257
|
def exec(*targets)
|
257
|
-
o =
|
258
|
+
o = config
|
258
259
|
diagnose(o)
|
259
260
|
configure_logger(o)
|
260
261
|
|
@@ -273,14 +274,14 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
273
274
|
target_options
|
274
275
|
option :format, type: :string
|
275
276
|
def detect
|
276
|
-
o =
|
277
|
+
o = config
|
277
278
|
o[:command] = 'platform.params'
|
278
279
|
(_, res) = run_command(o)
|
279
280
|
if o['format'] == 'json'
|
280
281
|
puts res.to_json
|
281
282
|
else
|
282
283
|
headline('Platform Details')
|
283
|
-
puts Inspec::BaseCLI.
|
284
|
+
puts Inspec::BaseCLI.format_platform_info(params: res, indent: 0, color: 36)
|
284
285
|
end
|
285
286
|
rescue ArgumentError, RuntimeError, Train::UserError => e
|
286
287
|
$stderr.puts e.message
|
@@ -301,13 +302,13 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
301
302
|
option :distinct_exit, type: :boolean, default: true,
|
302
303
|
desc: 'Exit with code 100 if any tests fail, and 101 if any are skipped but none failed (default). If disabled, exit 0 on skips and 1 for failures.'
|
303
304
|
def shell_func
|
304
|
-
o =
|
305
|
+
o = config
|
305
306
|
diagnose(o)
|
306
307
|
o[:debug_shell] = true
|
307
308
|
|
308
309
|
log_device = suppress_log_output?(o) ? nil : STDOUT
|
309
310
|
o[:logger] = Logger.new(log_device)
|
310
|
-
o[:logger].level = get_log_level(o
|
311
|
+
o[:logger].level = get_log_level(o[:log_level])
|
311
312
|
|
312
313
|
if o[:command].nil?
|
313
314
|
runner = Inspec::Runner.new(o)
|
@@ -346,7 +347,7 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
346
347
|
desc 'version', 'prints the version of this tool'
|
347
348
|
option :format, type: :string
|
348
349
|
def version
|
349
|
-
if
|
350
|
+
if config['format'] == 'json'
|
350
351
|
v = { version: Inspec::VERSION }
|
351
352
|
puts v.to_json
|
352
353
|
else
|
@@ -363,7 +364,7 @@ class Inspec::InspecCLI < Inspec::BaseCLI
|
|
363
364
|
private
|
364
365
|
|
365
366
|
def run_command(opts)
|
366
|
-
runner = Inspec::Runner.new(opts)
|
367
|
+
runner = Inspec::Runner.new(Inspec::Config.new(opts))
|
367
368
|
res = runner.eval_with_virtual_profile(opts[:command])
|
368
369
|
runner.load
|
369
370
|
|
@@ -0,0 +1,391 @@
|
|
1
|
+
# Represents InSpec configuration. Merges defaults, config file options,
|
2
|
+
# and CLI arguments.
|
3
|
+
|
4
|
+
require 'pp'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
module Inspec
|
8
|
+
class Config
|
9
|
+
# These are options that apply to any transport
|
10
|
+
GENERIC_CREDENTIALS = %w{
|
11
|
+
backend
|
12
|
+
sudo
|
13
|
+
sudo_password
|
14
|
+
sudo_command
|
15
|
+
sudo_options
|
16
|
+
shell
|
17
|
+
shell_options
|
18
|
+
shell_command
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
extend Forwardable
|
22
|
+
|
23
|
+
# Many parts of InSpec expect to treat the Config as a Hash
|
24
|
+
def_delegators :@final_options, :each, :delete, :[], :[]=, :key?
|
25
|
+
attr_reader :final_options
|
26
|
+
|
27
|
+
# This makes it easy to make a config with a mock backend.
|
28
|
+
def self.mock(opts = {})
|
29
|
+
Inspec::Config.new({ backend: :mock }.merge(opts), StringIO.new('{}'))
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(cli_opts = {}, cfg_io = nil, command_name = nil)
|
33
|
+
@command_name = command_name || (ARGV.empty? ? nil : ARGV[0].to_sym)
|
34
|
+
@defaults = Defaults.for_command(@command_name)
|
35
|
+
|
36
|
+
@cli_opts = cli_opts.dup
|
37
|
+
cfg_io = resolve_cfg_io(@cli_opts, cfg_io)
|
38
|
+
@cfg_file_contents = read_cfg_file_io(cfg_io)
|
39
|
+
|
40
|
+
@merged_options = merge_options
|
41
|
+
@final_options = finalize_options
|
42
|
+
end
|
43
|
+
|
44
|
+
def diagnose
|
45
|
+
return unless self[:diagnose]
|
46
|
+
puts "InSpec version: #{Inspec::VERSION}"
|
47
|
+
puts "Train version: #{Train::VERSION}"
|
48
|
+
puts 'Command line configuration:'
|
49
|
+
pp @cli_opts
|
50
|
+
puts 'JSON configuration file:'
|
51
|
+
pp @cfg_file_contents
|
52
|
+
puts 'Merged configuration:'
|
53
|
+
pp @merged_options
|
54
|
+
puts
|
55
|
+
end
|
56
|
+
|
57
|
+
#-----------------------------------------------------------------------#
|
58
|
+
# Train Credential Handling
|
59
|
+
#-----------------------------------------------------------------------#
|
60
|
+
|
61
|
+
# Returns a Hash with Symbol keys as follows:
|
62
|
+
# backend: machine name of the Train transport needed
|
63
|
+
# If present, any of the GENERIC_CREDENTIALS.
|
64
|
+
# All other keys are specific to the backend.
|
65
|
+
#
|
66
|
+
# The credentials are gleaned from:
|
67
|
+
# * the Train transport defaults. Train handles this on transport creation,
|
68
|
+
# so this method doesn't load defaults.
|
69
|
+
# * individual InSpec CLI options (which in many cases may have the
|
70
|
+
# transport name prefixed, which is stripped before being added
|
71
|
+
# to the creds hash)
|
72
|
+
# * the --target CLI option, which is interpreted:
|
73
|
+
# - as an arbitrary URI, which is parsed by Train.unpack_target_from_uri
|
74
|
+
|
75
|
+
def unpack_train_credentials
|
76
|
+
# Internally, use indifferent access while we build the creds
|
77
|
+
credentials = Thor::CoreExt::HashWithIndifferentAccess.new({})
|
78
|
+
|
79
|
+
# Helper methods prefixed with _utc_ (Unpack Train Credentials)
|
80
|
+
|
81
|
+
credentials.merge!(_utc_generic_credentials)
|
82
|
+
|
83
|
+
_utc_determine_backend(credentials)
|
84
|
+
credentials.merge!(Train.unpack_target_from_uri(final_options[:target] || '')) # TODO: this will be replaced with the credset work
|
85
|
+
transport_name = credentials[:backend].to_s
|
86
|
+
_utc_merge_transport_options(credentials, transport_name)
|
87
|
+
|
88
|
+
# Convert to all-Symbol keys
|
89
|
+
credentials.each_with_object({}) do |(option, value), creds|
|
90
|
+
creds[option.to_sym] = value
|
91
|
+
creds
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def _utc_merge_transport_options(credentials, transport_name)
|
98
|
+
# Ask Train for the names of the transport options
|
99
|
+
transport_options = Train.options(transport_name).keys.map(&:to_s)
|
100
|
+
|
101
|
+
# If there are any options with those (unprefixed) names, merge them in.
|
102
|
+
unprefixed_transport_options = final_options.select do |option_name, _value|
|
103
|
+
transport_options.include? option_name # e.g., 'host'
|
104
|
+
end
|
105
|
+
credentials.merge!(unprefixed_transport_options)
|
106
|
+
|
107
|
+
# If there are any prefixed options, merge them in, stripping the prefix.
|
108
|
+
transport_prefix = transport_name.downcase.tr('-', '_') + '_'
|
109
|
+
transport_options.each do |bare_option_name|
|
110
|
+
prefixed_option_name = transport_prefix + bare_option_name.to_s
|
111
|
+
if final_options.key?(prefixed_option_name)
|
112
|
+
credentials[bare_option_name.to_s] = final_options[prefixed_option_name]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# fetch any info that applies to all transports (like sudo information)
|
118
|
+
def _utc_generic_credentials
|
119
|
+
@final_options.select { |option, _value| GENERIC_CREDENTIALS.include?(option) }
|
120
|
+
end
|
121
|
+
|
122
|
+
def _utc_determine_backend(credentials)
|
123
|
+
return if credentials.key?(:backend)
|
124
|
+
|
125
|
+
# Default to local
|
126
|
+
unless @final_options.key?(:target)
|
127
|
+
credentials[:backend] = 'local'
|
128
|
+
return
|
129
|
+
end
|
130
|
+
|
131
|
+
# Look into target
|
132
|
+
%r{^(?<transport_name>[a-z_\-0-9]+)://.*$} =~ final_options[:target]
|
133
|
+
unless transport_name
|
134
|
+
raise ArgumentError, "Could not recognize a backend from the target #{final_options[:target]} - use a URI format with the backend name as the URI schema. Example: 'ssh://somehost.com' or 'transport://credset' or 'transport://' if credentials are provided outside of InSpec."
|
135
|
+
end
|
136
|
+
credentials[:backend] = transport_name.to_s # these are indeed stored in Train as Strings.
|
137
|
+
end
|
138
|
+
|
139
|
+
#-----------------------------------------------------------------------#
|
140
|
+
# Reading Config Files
|
141
|
+
#-----------------------------------------------------------------------#
|
142
|
+
|
143
|
+
# Regardless of our situation, end up with a readable IO object
|
144
|
+
def resolve_cfg_io(cli_opts, cfg_io)
|
145
|
+
raise(ArgumentError, 'Inspec::Config must use an IO to read from') if cfg_io && !cfg_io.respond_to?(:read)
|
146
|
+
cfg_io ||= check_for_piped_config(cli_opts)
|
147
|
+
return cfg_io if cfg_io
|
148
|
+
|
149
|
+
path = determine_cfg_path(cli_opts)
|
150
|
+
|
151
|
+
cfg_io = File.open(path) if path
|
152
|
+
cfg_io || StringIO.new('{ "version": "1.1" }')
|
153
|
+
end
|
154
|
+
|
155
|
+
def check_for_piped_config(cli_opts)
|
156
|
+
cli_opt = cli_opts[:config] || cli_opts[:json_config]
|
157
|
+
Inspec.deprecate(:cli_option_json_config, '') if cli_opts.key?(:json_config)
|
158
|
+
|
159
|
+
return nil unless cli_opt
|
160
|
+
return nil unless cli_opt == '-'
|
161
|
+
# This warning is here so that if a user invokes inspec with --config=-,
|
162
|
+
# they will have an explanation for why it appears to hang.
|
163
|
+
Inspec::Log.warn 'Reading JSON config from standard input' if STDIN.tty?
|
164
|
+
STDIN
|
165
|
+
end
|
166
|
+
|
167
|
+
def determine_cfg_path(cli_opts)
|
168
|
+
path = cli_opts[:config] || cli_opts[:json_config]
|
169
|
+
Inspec.deprecate(:cli_option_json_config, '') if cli_opts.key?(:json_config)
|
170
|
+
|
171
|
+
if path.nil?
|
172
|
+
default_path = File.join(Inspec.config_dir, 'config.json')
|
173
|
+
path = default_path if File.exist?(default_path)
|
174
|
+
elsif !File.exist?(path)
|
175
|
+
raise ArgumentError, "Could not read configuration file at #{path}"
|
176
|
+
end
|
177
|
+
path
|
178
|
+
end
|
179
|
+
|
180
|
+
def read_cfg_file_io(cfg_io)
|
181
|
+
contents = cfg_io.read
|
182
|
+
begin
|
183
|
+
@cfg_file_contents = JSON.parse(contents)
|
184
|
+
validate_config_file_contents!
|
185
|
+
rescue JSON::ParserError => e
|
186
|
+
raise Inspec::ConfigError::MalformedJson, "Failed to load JSON configuration: #{e}\nConfig was: #{contents}"
|
187
|
+
end
|
188
|
+
@cfg_file_contents
|
189
|
+
end
|
190
|
+
|
191
|
+
def file_version
|
192
|
+
@cfg_file_contents['version'] || :legacy
|
193
|
+
end
|
194
|
+
|
195
|
+
def legacy_file?
|
196
|
+
file_version == :legacy
|
197
|
+
end
|
198
|
+
|
199
|
+
def config_file_cli_options
|
200
|
+
if legacy_file?
|
201
|
+
# Assume everything in the file is a CLI option
|
202
|
+
@cfg_file_contents
|
203
|
+
else
|
204
|
+
@cfg_file_contents['cli_options'] || {}
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def config_file_reporter_options
|
209
|
+
# This is assumed to be top-level in both legacy and 1.1.
|
210
|
+
# Technically, you could sneak it in the 1.1 cli opts area.
|
211
|
+
@cfg_file_contents.key?('reporter') ? { 'reporter' => @cfg_file_contents['reporter'] } : {}
|
212
|
+
end
|
213
|
+
|
214
|
+
#-----------------------------------------------------------------------#
|
215
|
+
# Validation
|
216
|
+
#-----------------------------------------------------------------------#
|
217
|
+
def validate_config_file_contents!
|
218
|
+
version = @cfg_file_contents['version']
|
219
|
+
|
220
|
+
# Assume legacy format, which is unconstrained
|
221
|
+
return unless version
|
222
|
+
|
223
|
+
unless version == '1.1'
|
224
|
+
raise Inspec::ConfigError::Invalid, "Unsupported config file version '#{version}' - currently supported versions: 1.1"
|
225
|
+
end
|
226
|
+
|
227
|
+
valid_fields = %w{version cli_options credentials compliance reporter}.sort
|
228
|
+
@cfg_file_contents.keys.each do |seen_field|
|
229
|
+
unless valid_fields.include?(seen_field)
|
230
|
+
raise Inspec::ConfigError::Invalid, "Unrecognized top-level configuration field #{seen_field}. Recognized fields: #{valid_fields.join(', ')}"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def validate_reporters!(reporters)
|
236
|
+
return if reporters.nil?
|
237
|
+
# TODO: move this into a reporter plugin type system
|
238
|
+
valid_types = [
|
239
|
+
'automate',
|
240
|
+
'cli',
|
241
|
+
'documentation',
|
242
|
+
'html',
|
243
|
+
'json',
|
244
|
+
'json-automate',
|
245
|
+
'json-min',
|
246
|
+
'json-rspec',
|
247
|
+
'junit',
|
248
|
+
'progress',
|
249
|
+
'yaml',
|
250
|
+
]
|
251
|
+
|
252
|
+
reporters.each do |reporter_name, reporter_config|
|
253
|
+
raise NotImplementedError, "'#{reporter_name}' is not a valid reporter type." unless valid_types.include?(reporter_name)
|
254
|
+
|
255
|
+
next unless reporter_name == 'automate'
|
256
|
+
%w{token url}.each do |option|
|
257
|
+
raise Inspec::ReporterError, "You must specify a automate #{option} via the config file." if reporter_config[option].nil?
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# check to make sure we are only reporting one type to stdout
|
262
|
+
stdout_reporters = 0
|
263
|
+
reporters.each_value do |reporter_config|
|
264
|
+
stdout_reporters += 1 if reporter_config['stdout'] == true
|
265
|
+
end
|
266
|
+
|
267
|
+
raise ArgumentError, 'The option --reporter can only have a single report outputting to stdout.' if stdout_reporters > 1
|
268
|
+
end
|
269
|
+
|
270
|
+
#-----------------------------------------------------------------------#
|
271
|
+
# Merging Options
|
272
|
+
#-----------------------------------------------------------------------#
|
273
|
+
def merge_options
|
274
|
+
options = Thor::CoreExt::HashWithIndifferentAccess.new({})
|
275
|
+
|
276
|
+
# Lowest precedence: default, which may vary by command
|
277
|
+
options.merge!(@defaults)
|
278
|
+
|
279
|
+
# Middle precedence: merge in any CLI options defined from the config file
|
280
|
+
options.merge!(config_file_cli_options)
|
281
|
+
# Reporter options may be defined top-level.
|
282
|
+
options.merge!(config_file_reporter_options)
|
283
|
+
|
284
|
+
# Highest precedence: merge in any options defined via the CLI
|
285
|
+
options.merge!(@cli_opts)
|
286
|
+
|
287
|
+
options
|
288
|
+
end
|
289
|
+
|
290
|
+
#-----------------------------------------------------------------------#
|
291
|
+
# Finalization
|
292
|
+
#-----------------------------------------------------------------------#
|
293
|
+
def finalize_options
|
294
|
+
options = @merged_options.dup
|
295
|
+
|
296
|
+
finalize_set_top_level_command(options)
|
297
|
+
finalize_parse_reporters(options)
|
298
|
+
finalize_handle_sudo(options)
|
299
|
+
finalize_compliance_login(options)
|
300
|
+
|
301
|
+
Thor::CoreExt::HashWithIndifferentAccess.new(options)
|
302
|
+
end
|
303
|
+
|
304
|
+
def finalize_set_top_level_command(options)
|
305
|
+
options[:type] = @command_name
|
306
|
+
Inspec::BaseCLI.inspec_cli_command = @command_name # TODO: move to a more relevant location
|
307
|
+
end
|
308
|
+
|
309
|
+
def finalize_parse_reporters(options) # rubocop:disable Metrics/AbcSize
|
310
|
+
# default to cli report for ad-hoc runners
|
311
|
+
options['reporter'] = ['cli'] if options['reporter'].nil?
|
312
|
+
|
313
|
+
# parse out cli to proper report format
|
314
|
+
if options['reporter'].is_a?(Array)
|
315
|
+
reports = {}
|
316
|
+
options['reporter'].each do |report|
|
317
|
+
reporter_name, destination = report.split(':', 2)
|
318
|
+
if destination.nil? || destination.strip == '-'
|
319
|
+
reports[reporter_name] = { 'stdout' => true }
|
320
|
+
else
|
321
|
+
reports[reporter_name] = {
|
322
|
+
'file' => destination,
|
323
|
+
'stdout' => false,
|
324
|
+
}
|
325
|
+
reports[reporter_name]['target_id'] = options['target_id'] if options['target_id']
|
326
|
+
end
|
327
|
+
end
|
328
|
+
options['reporter'] = reports
|
329
|
+
end
|
330
|
+
|
331
|
+
# add in stdout if not specified
|
332
|
+
if options['reporter'].is_a?(Hash)
|
333
|
+
options['reporter'].each do |reporter_name, config|
|
334
|
+
options['reporter'][reporter_name] = {} if config.nil?
|
335
|
+
options['reporter'][reporter_name]['stdout'] = true if options['reporter'][reporter_name].empty?
|
336
|
+
options['reporter'][reporter_name]['target_id'] = options['target_id'] if options['target_id']
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
validate_reporters!(options['reporter'])
|
341
|
+
options
|
342
|
+
end
|
343
|
+
|
344
|
+
def finalize_handle_sudo(options)
|
345
|
+
# Due to limitations in Thor it is not possible to set an argument to be
|
346
|
+
# both optional and its value to be mandatory. E.g. the user supplying
|
347
|
+
# the --password argument is optional and not always required, but
|
348
|
+
# whenever it is used, it requires a value. Handle options that were
|
349
|
+
# defined in such a way and require a value here:
|
350
|
+
%w{password sudo-password}.each do |option_name|
|
351
|
+
snake_case_option_name = option_name.tr('-', '_').to_s
|
352
|
+
next unless options[snake_case_option_name] == -1 # Thor sets -1 for missing value - see #1918
|
353
|
+
raise ArgumentError, "Please provide a value for --#{option_name}. For example: --#{option_name}=hello."
|
354
|
+
end
|
355
|
+
|
356
|
+
# Infer `--sudo` if using `--sudo-password` without `--sudo`
|
357
|
+
if options['sudo_password'] && !options['sudo']
|
358
|
+
options['sudo'] = true
|
359
|
+
Inspec::Log.warn '`--sudo-password` used without `--sudo`. Adding `--sudo`.'
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def finalize_compliance_login(options)
|
364
|
+
# check for compliance settings
|
365
|
+
# This is always a hash, comes from config file, not CLI opts
|
366
|
+
if options.key?('compliance')
|
367
|
+
require 'plugins/inspec-compliance/lib/inspec-compliance/api'
|
368
|
+
InspecPlugins::Compliance::API.login(options['compliance'])
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
class Defaults
|
373
|
+
DEFAULTS = {
|
374
|
+
exec: {
|
375
|
+
'reporter' => ['cli'],
|
376
|
+
'show_progress' => false,
|
377
|
+
'color' => true,
|
378
|
+
'create_lockfile' => true,
|
379
|
+
'backend_cache' => true,
|
380
|
+
},
|
381
|
+
shell: {
|
382
|
+
'reporter' => ['cli'],
|
383
|
+
},
|
384
|
+
}.freeze
|
385
|
+
|
386
|
+
def self.for_command(command_name)
|
387
|
+
DEFAULTS[command_name] || {}
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|