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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/etc/deprecations.json +9 -0
  3. data/lib/bundles/inspec-supermarket/cli.rb +1 -1
  4. data/lib/inspec/backend.rb +9 -8
  5. data/lib/inspec/base_cli.rb +12 -170
  6. data/lib/inspec/cli.rb +17 -16
  7. data/lib/inspec/config.rb +391 -0
  8. data/lib/inspec/control_eval_context.rb +2 -1
  9. data/lib/inspec/dsl.rb +2 -2
  10. data/lib/inspec/errors.rb +5 -0
  11. data/lib/inspec/plugin/v1/plugin_types/resource.rb +1 -1
  12. data/lib/inspec/plugin/v2/activator.rb +24 -3
  13. data/lib/inspec/plugin/v2/loader.rb +1 -1
  14. data/lib/inspec/plugin/v2/registry.rb +8 -12
  15. data/lib/inspec/profile.rb +3 -2
  16. data/lib/inspec/profile_vendor.rb +2 -1
  17. data/lib/inspec/rspec_extensions.rb +2 -2
  18. data/lib/inspec/runner.rb +5 -5
  19. data/lib/inspec/shell.rb +1 -1
  20. data/lib/inspec/ui.rb +2 -2
  21. data/lib/inspec/version.rb +1 -1
  22. data/lib/plugins/inspec-habitat/lib/inspec-habitat/profile.rb +1 -1
  23. data/lib/plugins/inspec-init/lib/inspec-init/cli.rb +3 -28
  24. data/lib/plugins/inspec-init/lib/inspec-init/cli_plugin.rb +245 -0
  25. data/lib/plugins/inspec-init/lib/inspec-init/cli_profile.rb +49 -0
  26. data/lib/plugins/inspec-init/lib/inspec-init/renderer.rb +43 -31
  27. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/Gemfile +12 -0
  28. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/LICENSE +2 -0
  29. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/README.md +28 -0
  30. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/Rakefile +40 -0
  31. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/inspec-plugin-template.gemspec +45 -0
  32. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/lib/inspec-plugin-template.rb +16 -0
  33. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/lib/inspec-plugin-template/cli_command.rb +64 -0
  34. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/lib/inspec-plugin-template/plugin.rb +55 -0
  35. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/lib/inspec-plugin-template/version.rb +10 -0
  36. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/fixtures/README.md +24 -0
  37. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/functional/README.md +12 -0
  38. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/functional/inspec_plugin_template_test.rb +110 -0
  39. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/helper.rb +26 -0
  40. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/unit/README.md +17 -0
  41. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/unit/cli_args_test.rb +67 -0
  42. data/lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/test/unit/plugin_def_test.rb +51 -0
  43. data/lib/plugins/inspec-init/{lib/inspec-init/templates → templates}/profiles/os/README.md +0 -0
  44. data/lib/plugins/inspec-init/{lib/inspec-init/templates → templates}/profiles/os/controls/example.rb +0 -0
  45. data/lib/plugins/inspec-init/{lib/inspec-init/templates → templates}/profiles/os/inspec.yml +0 -0
  46. data/lib/plugins/inspec-init/{lib/inspec-init/templates → templates}/profiles/os/libraries/.gitkeep +0 -0
  47. data/lib/plugins/inspec-init/test/functional/inspec_init_plugin_test.rb +173 -0
  48. data/lib/plugins/inspec-init/test/functional/{inspec_init_test.rb → inspec_init_profile_test.rb} +7 -7
  49. data/lib/resources/filesystem.rb +40 -12
  50. metadata +31 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 190e8714166fd805239f3a7cf9bd5b830af9a7abffb671001361dd62da963e26
4
- data.tar.gz: 4f9d5fae927f80c5b136614fb581e308d43c2d1dabaa1e36238690891c499598
3
+ metadata.gz: 540ae71a1d966748d37cddcb1f171f7921e34f9bfcc6781702934756904476f9
4
+ data.tar.gz: 52a9a03513892e4e266eaba4a24eee0cee01749c6b96d99845c335616544b962
5
5
  SHA512:
6
- metadata.gz: 80a02ea7b5bd63fbf69cea562a5e951afbdab37de14623861fb8b907ba9738af0c83be270e48e32b1bb797c4d4634bb03e671203b33fc49d9e42aba97d789d79
7
- data.tar.gz: ccd6167f75157dd5ce47f0678846a5837e22cb1b023fd559aa80878b235e1efb712403a0503a1c10fc825654c5dd93f960fd5fe337a9d1cae2986617334073bc
6
+ metadata.gz: b13e81a88b36ee456597ffcacb701e84c1702d402f80e8ac1f2c9e135af14c3990394f366230969d6ca6968dad4b973cb09ada2a919e16789282e77cfcf38348
7
+ data.tar.gz: 1a27aa7f35b92b4b5b974123bee5ef33ab0d942e6bc90a478521927a771a564d92c9b1063f359a7d4d457b1e401229e96a5938cb62dd38b7fa6e10f57cab8778
@@ -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
  }
@@ -30,7 +30,7 @@ module Supermarket
30
30
  desc 'exec PROFILE', 'execute a Supermarket profile'
31
31
  exec_options
32
32
  def exec(*tests)
33
- o = opts(:exec).dup
33
+ o = config
34
34
  diagnose(o)
35
35
  configure_logger(o)
36
36
 
@@ -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 [Hash] config for the transport backend
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
- conf = Train.target_config(config)
45
- name = Train.validate_backend(conf)
46
- transport = Train.create(name, conf)
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 '#{name}'."
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 '#{name}'."
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 '#{name}' backend: #{e.message}"
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 '#{name}' backend: #{e.message}"
91
+ raise "Transport error, can't connect to '#{transport_name}' backend: #{e.message}"
91
92
  end
92
93
  end
93
94
  end
@@ -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 :json_config, type: :string,
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.default_options
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(opts)
290
- return unless opts['diagnose']
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 merged_opts(type = nil)
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 big gross, but this configures the
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.log_location
417
- o.log_location
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.log_level)
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.log_level)
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 = opts.dup
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(target: 'mock://')
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 = opts.dup
85
+ o = config
85
86
  diagnose(o)
86
- o[:backend] = Inspec::Backend.create(target: 'mock://')
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 = opts.dup
137
+ o = config
137
138
  configure_logger(o)
138
139
  o[:logger] = Logger.new(STDOUT)
139
- o[:logger].level = get_log_level(o.log_level)
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 = opts.dup
158
+ o = config
158
159
  diagnose(o)
159
160
 
160
161
  o[:logger] = Logger.new(STDOUT)
161
- o[:logger].level = get_log_level(o.log_level)
162
- o[:backend] = Inspec::Backend.create(target: 'mock://')
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 = opts(:exec).dup
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 = opts(:detect).dup
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.detect(params: res, indent: 0, color: 36)
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 = opts(:shell).dup
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.log_level)
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 opts['format'] == 'json'
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