inspec-core 3.5.0 → 3.6.2

Sign up to get free protection for your applications and to get access to all the features.
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