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.
- 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
|