inspec 0.31.0 → 0.32.0

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.
@@ -18,9 +18,9 @@ module Inspec
18
18
  #
19
19
  class VendorIndex
20
20
  attr_reader :path
21
- def initialize(path)
22
- @path = path
23
- FileUtils.mkdir_p(path) unless File.directory?(path)
21
+ def initialize(path = nil)
22
+ @path = path || File.join(Dir.home, '.inspec', 'cache')
23
+ FileUtils.mkdir_p(@path) unless File.directory?(@path)
24
24
  end
25
25
 
26
26
  def add(name, source, path_from)
@@ -42,7 +42,14 @@ module Inspec
42
42
  path = base_path_for(name, source_url)
43
43
  if File.directory?(path)
44
44
  path
45
- elsif File.exist?("#{path}.tar.gz")
45
+ else
46
+ archive_entry_for(name, source_url)
47
+ end
48
+ end
49
+
50
+ def archive_entry_for(name, source_url)
51
+ path = base_path_for(name, source_url)
52
+ if File.exist?("#{path}.tar.gz")
46
53
  "#{path}.tar.gz"
47
54
  elsif File.exist?("#{path}.zip")
48
55
  "#{path}.zip"
@@ -1,8 +1,8 @@
1
1
  # encoding: utf-8
2
2
  # copyright: 2015, Dominik Richter
3
- # license: All rights reserved
4
3
  # author: Dominik Richter
5
4
  # author: Christoph Hartmann
5
+ require 'inspec/log'
6
6
 
7
7
  module Inspec::DSL
8
8
  def require_controls(id, &block)
@@ -32,28 +32,36 @@ module Inspec::DSL
32
32
  end
33
33
 
34
34
  def self.load_spec_files_for_profile(bind_context, opts, &block)
35
- # get all spec files
36
- target = opts[:dependencies].list[opts[:profile_id]] ||
37
- fail("Can't find profile #{opts[:profile_id].inspect}, please add it as a dependency.")
38
- profile = Inspec::Profile.for_target(target.path, opts)
39
- context = load_profile_context(opts[:profile_id], profile, opts)
35
+ dependencies = opts[:dependencies]
36
+ profile_id = opts[:profile_id]
37
+
38
+ dep_entry = dependencies.list[profile_id]
39
+ if dep_entry.nil?
40
+ fail <<EOF
41
+ Cannot load #{profile_id} since it is not listed as a dependency
42
+ of #{bind_context.profile_name}.
43
+
44
+ Dependencies available from this context are:
45
+
46
+ #{dependencies.list.keys.join("\n ")}
47
+ EOF
48
+ end
49
+
50
+ context = load_profile_context(dep_entry.profile, opts[:backend])
40
51
 
41
52
  # if we don't want all the rules, then just make 1 pass to get all rule_IDs
42
53
  # that we want to keep from the original
43
- filter_included_controls(context, opts, &block) if !opts[:include_all]
54
+ filter_included_controls(context, dep_entry.profile, &block) if !opts[:include_all]
44
55
 
45
56
  # interpret the block and skip/modify as required
46
57
  context.load(block) if block_given?
47
58
 
48
- # finally register all combined rules
49
- context.rules.values.each do |control|
50
- bind_context.register_control(control)
51
- end
59
+ bind_context.add_subcontext(context)
52
60
  end
53
61
 
54
- def self.filter_included_controls(context, opts, &block)
62
+ def self.filter_included_controls(context, profile, &block)
55
63
  mock = Inspec::Backend.create({ backend: 'mock' })
56
- include_ctx = Inspec::ProfileContext.new(opts[:profile_id], mock, opts[:conf])
64
+ include_ctx = Inspec::ProfileContext.for_profile(profile, mock)
57
65
  include_ctx.load(block) if block_given?
58
66
  # remove all rules that were not registered
59
67
  context.rules.keys.each do |id|
@@ -63,8 +71,8 @@ module Inspec::DSL
63
71
  end
64
72
  end
65
73
 
66
- def self.load_profile_context(id, profile, opts)
67
- ctx = Inspec::ProfileContext.new(id, opts[:backend], opts[:conf])
74
+ def self.load_profile_context(profile, backend)
75
+ ctx = Inspec::ProfileContext.for_profile(profile, backend)
68
76
  profile.libraries.each do |path, content|
69
77
  ctx.load(content.to_s, path, 1)
70
78
  ctx.reload_dsl
@@ -7,11 +7,5 @@ module Inspec
7
7
 
8
8
  # dependency resolution
9
9
  class CyclicDependencyError < Error; end
10
- class VersionConflict < Error
11
- attr_reader :conflicts
12
- def initialize(conflicts, msg = nil)
13
- super(msg)
14
- @conflicts = conflicts
15
- end
16
- end
10
+ class UnsatisfiedVersionSpecification < Error; end
17
11
  end
@@ -1,34 +1,11 @@
1
1
  # encoding: utf-8
2
- # Copyright 2015 Dominik Richter. All rights reserved.
3
2
  # author: Dominik Richter
4
3
  # author: Christoph Hartmann
5
4
 
6
- require 'rainbow/ext/string'
5
+ require 'mixlib/log'
7
6
 
8
7
  module Inspec
9
8
  class Log
10
- def initialize(opts = {})
11
- @quiet = opts[:quiet] || false
12
- end
13
-
14
- def show(msg)
15
- puts msg unless @quiet
16
- end
17
-
18
- def info(msg)
19
- show ' . '.color(:white) + msg
20
- end
21
-
22
- def error(msg)
23
- show ' ✖ '.color(:red).bright + msg
24
- end
25
-
26
- def warn(msg)
27
- show ' ! '.color(:yellow).bright + msg
28
- end
29
-
30
- def ok(msg)
31
- show ' ✔ '.color(:green).bright + msg
32
- end
9
+ extend Mixlib::Log
33
10
  end
34
11
  end
@@ -8,6 +8,7 @@ require 'inspec/polyfill'
8
8
  require 'inspec/fetcher'
9
9
  require 'inspec/source_reader'
10
10
  require 'inspec/metadata'
11
+ require 'inspec/dependencies/lockfile'
11
12
  require 'inspec/dependencies/dependency_set'
12
13
 
13
14
  module Inspec
@@ -52,6 +53,10 @@ module Inspec
52
53
  Metadata.finalize(@source_reader.metadata, @profile_id)
53
54
  end
54
55
 
56
+ def name
57
+ metadata.params[:name]
58
+ end
59
+
55
60
  def params
56
61
  @params ||= load_params
57
62
  end
@@ -211,6 +216,44 @@ module Inspec
211
216
  @locked_dependencies ||= load_dependencies
212
217
  end
213
218
 
219
+ def lockfile_exists?
220
+ File.exist?(lockfile_path)
221
+ end
222
+
223
+ def lockfile_path
224
+ File.join(cwd, 'inspec.lock')
225
+ end
226
+
227
+ #
228
+ # TODO(ssd): Relative path handling really needs to be carefully
229
+ # thought through, especially with respect to relative paths in
230
+ # tarballs.
231
+ #
232
+ def cwd
233
+ @target.is_a?(String) && File.directory?(@target) ? @target : './'
234
+ end
235
+
236
+ def lockfile
237
+ @lockfile ||= if lockfile_exists?
238
+ Inspec::Lockfile.from_file(lockfile_path)
239
+ else
240
+ generate_lockfile
241
+ end
242
+ end
243
+
244
+ #
245
+ # Generate an in-memory lockfile. This won't render the lock file
246
+ # to disk, it must be explicitly written to disk by the caller.
247
+ #
248
+ # @param vendor_path [String] Path to the on-disk vendor dir
249
+ # @return [Inspec::Lockfile]
250
+ #
251
+ def generate_lockfile(vendor_path = nil)
252
+ res = Inspec::DependencySet.new(cwd, vendor_path)
253
+ res.vendor(metadata.dependencies)
254
+ Inspec::Lockfile.from_dependency_set(res)
255
+ end
256
+
214
257
  private
215
258
 
216
259
  # Create an archive name for this profile and an additional options
@@ -225,7 +268,7 @@ module Inspec
225
268
 
226
269
  name = params[:name] ||
227
270
  fail('Cannot create an archive without a profile name! Please '\
228
- 'specify the name in metadata or use --output to create the archive.')
271
+ 'specify the name in metadata or use --output to create the archive.')
229
272
  ext = opts[:zip] ? 'zip' : 'tar.gz'
230
273
  slug = name.downcase.strip.tr(' ', '-').gsub(/[^\w-]/, '_')
231
274
  Pathname.new(Dir.pwd).join("#{slug}.#{ext}")
@@ -239,34 +282,34 @@ module Inspec
239
282
  params
240
283
  end
241
284
 
285
+ #
286
+ # Returns a new runner for the current profile.
287
+ #
288
+ # @params [Symbol] The type of backend to use when constructing a
289
+ # new runner.
290
+ # @returns [Inspec::Runner]
291
+ #
292
+ def runner_for_profile(backend = :mock)
293
+ opts = @options.dup
294
+ opts[:ignore_supports] = true
295
+ r = Runner.new(id: @profile_id,
296
+ backend: backend,
297
+ test_collector: opts.delete(:test_collector))
298
+ r.add_profile(self, opts)
299
+ r
300
+ end
301
+
242
302
  def load_checks_params(params)
243
303
  params[:controls] = controls = {}
244
304
  params[:groups] = groups = {}
245
305
  prefix = @source_reader.target.prefix || ''
246
-
247
- if @runner_context.nil?
248
- # we're checking a profile, we don't care if it runs on the host machine
249
- opts = @options.dup
250
- opts[:ignore_supports] = true
251
- runner = Runner.new(
252
- id: @profile_id,
253
- backend: :mock,
254
- test_collector: opts.delete(:test_collector),
255
- )
256
- runner.add_profile(self, opts)
257
- runner.rules.values.each do |rule|
258
- f = load_rule_filepath(prefix, rule)
259
- load_rule(rule, f, controls, groups)
260
- end
261
- params[:attributes] = runner.attributes
262
- else
263
- # load from context
264
- @runner_context.rules.values.each do |rule|
265
- f = load_rule_filepath(prefix, rule)
266
- load_rule(rule, f, controls, groups)
267
- end
268
- params[:attributes] = @runner_context.attributes
306
+ # Load from the runner_context if it exists
307
+ runner = @runner_context || runner_for_profile
308
+ runner.all_rules.each do |rule|
309
+ f = load_rule_filepath(prefix, rule)
310
+ load_rule(rule, f, controls, groups)
269
311
  end
312
+ params[:attributes] = runner.attributes
270
313
  params
271
314
  end
272
315
 
@@ -297,10 +340,7 @@ module Inspec
297
340
  end
298
341
 
299
342
  def load_dependencies
300
- cwd = File.directory?(@target) ? @target : nil
301
- res = Inspec::DependencySet.new(cwd, nil)
302
- res.vendor(metadata.dependencies)
303
- res
343
+ Inspec::DependencySet.from_lockfile(lockfile, cwd, nil)
304
344
  end
305
345
  end
306
346
  end
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
  # author: Dominik Richter
3
3
  # author: Christoph Hartmann
4
-
4
+ require 'inspec/log'
5
5
  require 'inspec/rule'
6
6
  require 'inspec/dsl'
7
7
  require 'inspec/require_loader'
@@ -10,18 +10,21 @@ require 'inspec/objects/attribute'
10
10
 
11
11
  module Inspec
12
12
  class ProfileContext # rubocop:disable Metrics/ClassLength
13
- attr_reader :rules
14
- attr_reader :attributes
13
+ def self.for_profile(profile, backend)
14
+ new(profile.name, backend, { 'profile' => profile })
15
+ end
16
+
17
+ attr_reader :attributes, :rules, :profile_id
15
18
  def initialize(profile_id, backend, conf)
16
19
  if backend.nil?
17
20
  fail 'ProfileContext is initiated with a backend == nil. ' \
18
21
  'This is a backend error which must be fixed upstream.'
19
22
  end
20
-
21
23
  @profile_id = profile_id
22
24
  @backend = backend
23
25
  @conf = conf.dup
24
26
  @rules = {}
27
+ @subcontexts = []
25
28
  @dependencies = {}
26
29
  @dependencies = conf['profile'].locked_dependencies unless conf['profile'].nil?
27
30
  @require_loader = ::Inspec::RequireLoader.new
@@ -35,6 +38,16 @@ module Inspec
35
38
  @profile_context = ctx.new(@backend, @conf, @dependencies, @require_loader)
36
39
  end
37
40
 
41
+ def all_rules
42
+ ret = @rules.values
43
+ ret += @subcontexts.map(&:all_rules).flatten
44
+ ret
45
+ end
46
+
47
+ def add_subcontext(context)
48
+ @subcontexts << context
49
+ end
50
+
38
51
  def load_libraries(libs)
39
52
  lib_prefix = 'libraries' + File::SEPARATOR
40
53
  autoloads = []
@@ -59,6 +72,7 @@ module Inspec
59
72
  end
60
73
 
61
74
  def load(content, source = nil, line = nil)
75
+ Inspec::Log.debug("Loading #{source || '<anonymous content>'} into #{self}")
62
76
  @current_load = { file: source }
63
77
  if content.is_a? Proc
64
78
  @profile_context.instance_eval(&content)
@@ -167,7 +181,11 @@ module Inspec
167
181
  end
168
182
 
169
183
  def to_s
170
- 'Profile Context Run'
184
+ "Profile Context Run #{profile_name}"
185
+ end
186
+
187
+ define_method :profile_name do
188
+ profile_id
171
189
  end
172
190
 
173
191
  define_method :control do |*args, &block|
@@ -185,9 +203,14 @@ module Inspec
185
203
  res = describe(*args, &block)
186
204
  end
187
205
  register_control(rule, &block)
206
+
188
207
  res
189
208
  end
190
209
 
210
+ define_method :add_subcontext do |context|
211
+ profile_context_owner.add_subcontext(context)
212
+ end
213
+
191
214
  define_method :register_control do |control, &block|
192
215
  ::Inspec::Rule.set_skip_rule(control, true) if @skip_profile
193
216
 
@@ -210,12 +210,7 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
210
210
  'passed' => ' ✔ ',
211
211
  'unknown' => ' ? ',
212
212
  'empty' => ' ',
213
- }.freeze
214
-
215
- TEST_INDICATORS = {
216
- 'failed' => ' fail: ',
217
- 'skipped' => ' skip: ',
218
- 'empty' => ' ',
213
+ 'small' => ' ',
219
214
  }.freeze
220
215
 
221
216
  MULTI_TEST_CONTROL_SUMMARY_MAX_LEN = 60
@@ -223,18 +218,20 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
223
218
  def initialize(*args)
224
219
  @colors = COLORS
225
220
  @indicators = INDICATORS
226
- @test_indicators = TEST_INDICATORS
227
221
 
228
222
  @format = '%color%indicator%id%summary'
229
223
  @current_control = nil
230
224
  @current_profile = nil
231
225
  @missing_controls = []
226
+ @anonymous_tests = []
232
227
  super(*args)
233
228
  end
234
229
 
235
230
  def close(_notification)
236
231
  flush_current_control
237
232
  output.puts('') unless @current_control.nil?
233
+ print_tests
234
+ output.puts('')
238
235
 
239
236
  @profiles_info.each do |_id, profile|
240
237
  next if profile[:already_printed]
@@ -274,13 +271,15 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
274
271
  summary_status = STATUS_TYPES['unknown']
275
272
  skips = []
276
273
  fails = []
274
+ passes = []
277
275
  @current_control[:results].each do |r|
278
276
  i = STATUS_TYPES[r[:status_type]]
279
277
  summary_status = i if i > summary_status
280
278
  fails.push(r) if i > 0
279
+ passes.push(r) if i == STATUS_TYPES['passed']
281
280
  skips.push(r) if i == STATUS_TYPES['skipped']
282
281
  end
283
- [fails, skips, STATUS_TYPES.key(summary_status)]
282
+ [fails, skips, passes, STATUS_TYPES.key(summary_status)]
284
283
  end
285
284
 
286
285
  def current_control_title
@@ -344,20 +343,42 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
344
343
  lines.gsub(/\n/, "\n" + indentation)
345
344
  end
346
345
 
347
- def print_fails_and_skips(all, color)
346
+ def print_results(all)
348
347
  all.each do |x|
349
- indicator = @test_indicators[x[:status]]
350
- indicator = @test_indicators['empty'] if all.length == 1 || indicator.nil?
348
+ test_status = x[:status_type]
349
+ test_color = @colors[test_status]
350
+ indicator = @indicators[x[:status]]
351
+ indicator = @indicators['empty'] if all.length == 1 || indicator.nil?
351
352
  msg = x[:message] || x[:skip_message] || x[:code_desc]
352
353
  print_line(
353
- color: color,
354
- indicator: indicator,
355
- summary: format_lines(msg, @test_indicators['empty']),
354
+ color: test_color,
355
+ indicator: @indicators['small'] + indicator,
356
+ summary: format_lines(msg, @indicators['empty']),
356
357
  id: nil, profile: nil
357
358
  )
358
359
  end
359
360
  end
360
361
 
362
+ def print_tests
363
+ @anonymous_tests.each do |control|
364
+ control_result = control[:results]
365
+ title = control_result[0][:code_desc].split[0..1].join(' ')
366
+ puts ' ' + title
367
+ control_result.each do |test|
368
+ control_id = ''
369
+ test_result = test[:code_desc].split[2..-1].join(' ')
370
+ status_indicator = test[:status_type]
371
+ print_line(
372
+ color: @colors[status_indicator] || '',
373
+ indicator: @indicators['small'] + @indicators[status_indicator] || @indicators['unknown'],
374
+ summary: format_lines(test_result, @indicators['empty']),
375
+ id: control_id,
376
+ profile: control[:profile_id],
377
+ )
378
+ end
379
+ end
380
+ end
381
+
361
382
  def flush_current_control
362
383
  return if @current_control.nil?
363
384
 
@@ -365,22 +386,24 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
365
386
  @current_profile = @profiles_info[@current_control[:profile_id]]
366
387
  print_current_profile if prev_profile != @current_profile
367
388
 
368
- fails, skips, summary_indicator = current_control_infos
389
+ fails, skips, passes, summary_indicator = current_control_infos
369
390
  summary = current_control_summary(fails, skips)
370
391
 
371
392
  control_id = @current_control[:id].to_s
372
393
  control_id += ': '
373
- control_id = '' if control_id.start_with? '(generated from '
374
-
375
- print_line(
376
- color: @colors[summary_indicator] || '',
377
- indicator: @indicators[summary_indicator] || @indicators['unknown'],
378
- summary: format_lines(summary, @indicators['empty']),
379
- id: control_id,
380
- profile: @current_control[:profile_id],
381
- )
394
+ if control_id.start_with? '(generated from '
395
+ @anonymous_tests.push(@current_control)
396
+ else
397
+ print_line(
398
+ color: @colors[summary_indicator] || '',
399
+ indicator: @indicators[summary_indicator] || @indicators['unknown'],
400
+ summary: format_lines(summary, @indicators['empty']),
401
+ id: control_id,
402
+ profile: @current_control[:profile_id],
403
+ )
382
404
 
383
- print_fails_and_skips(fails + skips, @colors[summary_indicator] || '')
405
+ print_results(fails + skips + passes)
406
+ end
384
407
  end
385
408
 
386
409
  def print_target(before, after)