inspec 0.35.0 → 1.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -2
  3. data/Gemfile +6 -0
  4. data/Rakefile +3 -55
  5. data/docs/README.md +20 -0
  6. data/docs/cli.rst +6 -0
  7. data/docs/dsl_inspec.md +245 -0
  8. data/docs/dsl_resource.md +93 -0
  9. data/docs/inspec_and_friends.md +102 -0
  10. data/docs/matchers.md +136 -0
  11. data/docs/plugin_kitchen_inspec.html.md +55 -0
  12. data/docs/profiles.md +271 -0
  13. data/docs/resources.rst +1 -1
  14. data/docs/shell.md +150 -0
  15. data/inspec.gemspec +1 -1
  16. data/lib/bundles/inspec-compliance/api.rb +28 -18
  17. data/lib/bundles/inspec-compliance/cli.rb +19 -27
  18. data/lib/fetchers/git.rb +4 -0
  19. data/lib/fetchers/local.rb +16 -1
  20. data/lib/fetchers/mock.rb +4 -0
  21. data/lib/fetchers/url.rb +40 -12
  22. data/lib/inspec/base_cli.rb +4 -0
  23. data/lib/inspec/cli.rb +6 -8
  24. data/lib/inspec/control_eval_context.rb +8 -0
  25. data/lib/inspec/dependencies/{vendor_index.rb → cache.rb} +5 -4
  26. data/lib/inspec/dependencies/dependency_set.rb +8 -14
  27. data/lib/inspec/dependencies/requirement.rb +10 -20
  28. data/lib/inspec/dependencies/resolver.rb +2 -2
  29. data/lib/inspec/dsl.rb +9 -0
  30. data/lib/inspec/fetcher.rb +1 -1
  31. data/lib/inspec/objects/test.rb +8 -2
  32. data/lib/inspec/plugins/fetcher.rb +11 -12
  33. data/lib/inspec/plugins/resource.rb +3 -0
  34. data/lib/inspec/profile.rb +60 -14
  35. data/lib/inspec/profile_context.rb +28 -7
  36. data/lib/inspec/resource.rb +17 -2
  37. data/lib/inspec/rspec_json_formatter.rb +80 -35
  38. data/lib/inspec/runner.rb +42 -18
  39. data/lib/inspec/shell.rb +5 -16
  40. data/lib/inspec/version.rb +1 -1
  41. data/lib/resources/apache_conf.rb +1 -1
  42. data/lib/resources/gem.rb +1 -0
  43. data/lib/resources/oneget.rb +1 -0
  44. data/lib/resources/os.rb +1 -1
  45. data/lib/resources/package.rb +3 -1
  46. data/lib/resources/pip.rb +1 -1
  47. data/lib/resources/ssl.rb +9 -11
  48. metadata +15 -15
  49. data/docs/dsl_inspec.rst +0 -259
  50. data/docs/dsl_resource.rst +0 -90
  51. data/docs/inspec_and_friends.rst +0 -85
  52. data/docs/matchers.rst +0 -137
  53. data/docs/profiles.rst +0 -169
  54. data/docs/readme.rst +0 -105
  55. data/docs/shell.rst +0 -130
  56. data/docs/template.rst +0 -51
@@ -17,7 +17,8 @@ module Inspec
17
17
  'attributes' => attributes })
18
18
  end
19
19
 
20
- attr_reader :attributes, :rules, :profile_id, :resource_registry
20
+ attr_reader :attributes, :profile_id, :resource_registry, :backend
21
+ attr_accessor :rules
21
22
  def initialize(profile_id, backend, conf)
22
23
  if backend.nil?
23
24
  fail 'ProfileContext is initiated with a backend == nil. ' \
@@ -27,7 +28,8 @@ module Inspec
27
28
  @backend = backend
28
29
  @conf = conf.dup
29
30
  @rules = {}
30
- @subcontexts = []
31
+ @control_subcontexts = []
32
+ @lib_subcontexts = []
31
33
  @require_loader = ::Inspec::RequireLoader.new
32
34
  @attributes = []
33
35
  # A local resource registry that only contains resources defined
@@ -45,7 +47,7 @@ module Inspec
45
47
  end
46
48
 
47
49
  def to_resources_dsl
48
- Inspec::Resource.create_dsl(@backend, @resource_registry)
50
+ Inspec::Resource.create_dsl(self)
49
51
  end
50
52
 
51
53
  def control_eval_context
@@ -65,20 +67,34 @@ module Inspec
65
67
  @conf['profile'].supports_os?
66
68
  end
67
69
 
68
- def all_rules
70
+ def all_controls
69
71
  ret = @rules.values
70
- ret += @subcontexts.map(&:all_rules).flatten
72
+ ret += @control_subcontexts.map(&:all_rules).flatten
71
73
  ret
72
74
  end
75
+ alias all_rules all_controls
76
+
77
+ def subcontext_by_name(name)
78
+ found = @lib_subcontexts.find { |c| c.profile_id == name }
79
+ if !found
80
+ @lib_subcontexts.each do |c|
81
+ found = c.subcontext_by_name(name)
82
+ break if found
83
+ end
84
+ end
85
+
86
+ found
87
+ end
73
88
 
74
89
  def add_resources(context)
75
90
  @resource_registry.merge!(context.resource_registry)
76
91
  control_eval_context.add_resources(context)
92
+ @lib_subcontexts << context
77
93
  reload_dsl
78
94
  end
79
95
 
80
96
  def add_subcontext(context)
81
- @subcontexts << context
97
+ @control_subcontexts << context
82
98
  end
83
99
 
84
100
  def load_libraries(libs)
@@ -132,7 +148,12 @@ module Inspec
132
148
 
133
149
  def register_rule(r)
134
150
  # get the full ID
135
- r.instance_variable_set(:@__file, current_load[:file])
151
+ file = if @current_load.nil?
152
+ 'unknown'
153
+ else
154
+ @current_load[:file] || 'unknown'
155
+ end
156
+ r.instance_variable_set(:@__file, file)
136
157
  r.instance_variable_set(:@__group_title, current_load[:title])
137
158
 
138
159
  # add the rule to the registry
@@ -6,6 +6,8 @@
6
6
  require 'inspec/plugins'
7
7
 
8
8
  module Inspec
9
+ class ProfileNotFound < StandardError; end
10
+
9
11
  class Resource
10
12
  def self.default_registry
11
13
  @default_registry ||= {}
@@ -25,9 +27,22 @@ module Inspec
25
27
  #
26
28
  # @param backend [BackendRunner] exposing the target to resources
27
29
  # @return [ResourcesDSL]
28
- def self.create_dsl(backend, my_registry = registry)
29
- # need the local name, to use it in the module creation further down
30
+ def self.create_dsl(profile_context)
31
+ backend = profile_context.backend
32
+ my_registry = profile_context.resource_registry
33
+
30
34
  Module.new do
35
+ define_method :resource_class do |profile_name, resource_name|
36
+ inner_context = if profile_name == profile_context.profile_id
37
+ profile_context
38
+ else
39
+ profile_context.subcontext_by_name(profile_name)
40
+ end
41
+
42
+ fail ProfileNotFound, "Cannot find profile named: #{profile_name}" if inner_context.nil?
43
+ inner_context.resource_registry[resource_name]
44
+ end
45
+
31
46
  my_registry.each do |id, r|
32
47
  define_method id.to_sym do |*args|
33
48
  r.new(backend, id.to_s, *args)
@@ -36,11 +36,8 @@ class InspecRspecMiniJson < RSpec::Core::Formatters::JsonFormatter
36
36
  # Called after stop has been called and the run is complete.
37
37
  def dump_summary(summary)
38
38
  @output_hash[:version] = Inspec::VERSION
39
- @output_hash[:summary] = {
39
+ @output_hash[:statistics] = {
40
40
  duration: summary.duration,
41
- example_count: summary.example_count,
42
- failure_count: summary.failure_count,
43
- skip_count: summary.pending_count,
44
41
  }
45
42
  end
46
43
 
@@ -86,7 +83,7 @@ class InspecRspecMiniJson < RSpec::Core::Formatters::JsonFormatter
86
83
  end
87
84
  end
88
85
 
89
- class InspecRspecJson < InspecRspecMiniJson
86
+ class InspecRspecJson < InspecRspecMiniJson # rubocop:disable Metrics/ClassLength
90
87
  RSpec::Core::Formatters.register self, :start, :stop, :dump_summary
91
88
  attr_writer :backend
92
89
 
@@ -108,7 +105,7 @@ class InspecRspecJson < InspecRspecMiniJson
108
105
  def start(_notification)
109
106
  # Note that the default profile may have no name - therefore
110
107
  # the hash may have a valid nil => entry.
111
- @profiles_info ||= Hash[@profiles.map { |x| profile_info(x) }]
108
+ @profiles_info = @profiles.map(&:info!).map(&:dup)
112
109
  end
113
110
 
114
111
  def dump_one_example(example, control)
@@ -133,20 +130,59 @@ class InspecRspecJson < InspecRspecMiniJson
133
130
  @output_hash[:other_checks] = missing
134
131
  end
135
132
 
136
- def dump_summary(summary)
137
- super(summary)
133
+ def controls_summary
134
+ failed = 0
135
+ skipped = 0
136
+ passed = 0
137
+ critical = 0
138
+ major = 0
139
+ minor = 0
140
+
141
+ @control_tests.each do |control|
142
+ next if control[:id].start_with? '(generated from '
143
+ next unless control[:results]
144
+ if control[:results].any? { |r| r[:status] == 'failed' }
145
+ failed += 1
146
+ if control[:impact] >= 0.7
147
+ critical += 1
148
+ elsif control[:impact] >= 0.4
149
+ major += 1
150
+ else
151
+ minor += 1
152
+ end
153
+ elsif control[:results].any? { |r| r[:status] == 'skipped' }
154
+ skipped += 1
155
+ else
156
+ passed += 1
157
+ end
158
+ end
159
+
160
+ total = failed + passed + skipped
161
+
162
+ { 'total' => total,
163
+ 'failed' => {
164
+ 'total' => failed,
165
+ 'critical' => critical,
166
+ 'major' => major,
167
+ 'minor' => minor,
168
+ },
169
+ 'skipped' => skipped,
170
+ 'passed' => passed }
171
+ end
172
+
173
+ def tests_summary
138
174
  total = 0
139
175
  failed = 0
140
176
  skipped = 0
141
177
  passed = 0
142
178
 
143
- @profiles_info.each do |_name, profile|
144
- total += profile[:controls].length
145
- profile[:controls].each do |_control_name, control|
146
- next unless control[:results]
147
- if control[:results].any? { |r| r[:status] == 'failed' }
179
+ all_tests = @anonymous_tests + @control_tests
180
+ all_tests.each do |control|
181
+ next unless control[:results]
182
+ control[:results].each do |result|
183
+ if result[:status] == 'failed'
148
184
  failed += 1
149
- elsif control[:results].any? { |r| r[:status] == 'skipped' }
185
+ elsif result[:status] == 'skipped'
150
186
  skipped += 1
151
187
  else
152
188
  passed += 1
@@ -154,16 +190,11 @@ class InspecRspecJson < InspecRspecMiniJson
154
190
  end
155
191
  end
156
192
 
157
- # TODO: provide this information in the output
193
+ { 'total' => total, 'failed' => failed, 'skipped' => skipped, 'passed' => passed }
158
194
  end
159
195
 
160
196
  private
161
197
 
162
- def profile_info(profile)
163
- info = profile.info.dup
164
- [info[:name], info]
165
- end
166
-
167
198
  #
168
199
  # TODO(ssd+vj): We should probably solve this by either ensuring the example has
169
200
  # the profile_id of the top level profile when it is included as a dependency, or
@@ -171,7 +202,7 @@ class InspecRspecJson < InspecRspecMiniJson
171
202
  # this heuristic matching.
172
203
  #
173
204
  def example2profile(example, profiles)
174
- profiles.values.find { |p| profile_contains_example?(p, example) }
205
+ profiles.find { |p| profile_contains_example?(p, example) }
175
206
  end
176
207
 
177
208
  def profile_contains_example?(profile, example)
@@ -180,7 +211,7 @@ class InspecRspecJson < InspecRspecMiniJson
180
211
  # Case 2: The profile contains a control that matches the id of the example
181
212
  if profile[:name] == example[:profile_id]
182
213
  true
183
- elsif profile[:controls] && profile[:controls].key?(example[:id])
214
+ elsif profile[:controls] && profile[:controls].any? { |x| x[:id] == example[:id] }
184
215
  true
185
216
  else
186
217
  false
@@ -188,8 +219,9 @@ class InspecRspecJson < InspecRspecMiniJson
188
219
  end
189
220
 
190
221
  def example2control(example, profiles)
191
- p = example2profile(example, profiles)
192
- p[:controls][example[:id]] if p && p[:controls]
222
+ profile = example2profile(example, profiles)
223
+ return nil unless profile && profile[:controls]
224
+ profile[:controls].find { |x| x[:id] == example[:id] }
193
225
  end
194
226
 
195
227
  def format_example(example)
@@ -246,16 +278,17 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
246
278
  @current_profile = nil
247
279
  @missing_controls = []
248
280
  @anonymous_tests = []
281
+ @control_tests = []
249
282
  super(*args)
250
283
  end
251
284
 
252
- def close(_notification)
285
+ def close(_notification) # rubocop:disable Metrics/AbcSize
253
286
  flush_current_control
254
287
  output.puts('') unless @current_control.nil?
255
288
  print_tests
256
289
  output.puts('')
257
290
 
258
- @profiles_info.each do |_id, profile|
291
+ @profiles_info.each do |profile|
259
292
  next if profile[:already_printed]
260
293
  @current_profile = profile
261
294
  next unless print_current_profile
@@ -266,13 +299,20 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
266
299
  output.puts('')
267
300
  end
268
301
 
269
- res = @output_hash[:summary]
270
- passed = res[:example_count] - res[:failure_count] - res[:skip_count]
271
- s = format('Summary: %s%d successful%s, %s%d failures%s, %s%d skipped%s',
272
- COLORS['passed'], passed, COLORS['reset'],
273
- COLORS['failed'], res[:failure_count], COLORS['reset'],
274
- COLORS['skipped'], res[:skip_count], COLORS['reset'])
275
- output.puts(s)
302
+ controls_res = controls_summary
303
+ tests_res = tests_summary
304
+
305
+ s = format('Profile Summary: %s%d successful%s, %s%d failures%s, %s%d skipped%s',
306
+ COLORS['passed'], controls_res['passed'], COLORS['reset'],
307
+ COLORS['failed'], controls_res['failed']['total'], COLORS['reset'],
308
+ COLORS['skipped'], controls_res['skipped'], COLORS['reset'])
309
+ output.puts(s) if controls_res['total'] > 0
310
+
311
+ s = format('Test Summary: %s%d successful%s, %s%d failures%s, %s%d skipped%s',
312
+ COLORS['passed'], tests_res['passed'], COLORS['reset'],
313
+ COLORS['failed'], tests_res['failed'], COLORS['reset'],
314
+ COLORS['skipped'], tests_res['skipped'], COLORS['reset'])
315
+ output.puts(s) if !@anonymous_tests.empty? || @current_control.nil?
276
316
  end
277
317
 
278
318
  private
@@ -371,7 +411,11 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
371
411
  test_color = @colors[test_status]
372
412
  indicator = @indicators[x[:status]]
373
413
  indicator = @indicators['empty'] if indicator.nil?
374
- msg = x[:message] || x[:skip_message] || x[:code_desc]
414
+ if x[:message]
415
+ msg = x[:code_desc] + "\n" + x[:message]
416
+ else
417
+ msg = x[:skip_message] || x[:code_desc]
418
+ end
375
419
  print_line(
376
420
  color: test_color,
377
421
  indicator: @indicators['small'] + indicator,
@@ -414,7 +458,7 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
414
458
  return if @current_control.nil?
415
459
 
416
460
  prev_profile = @current_profile
417
- @current_profile = @profiles_info[@current_control[:profile_id]]
461
+ @current_profile = @profiles_info.find { |i| i[:id] == @current_control[:profile_id] }
418
462
  print_current_profile if prev_profile != @current_profile
419
463
 
420
464
  fails, skips, passes, summary_indicator = current_control_infos
@@ -425,6 +469,7 @@ class InspecRspecCli < InspecRspecJson # rubocop:disable Metrics/ClassLength
425
469
  if control_id.start_with? '(generated from '
426
470
  @anonymous_tests.push(@current_control)
427
471
  else
472
+ @control_tests.push(@current_control)
428
473
  print_line(
429
474
  color: @colors[summary_indicator] || '',
430
475
  indicator: @indicators[summary_indicator] || @indicators['unknown'],
data/lib/inspec/runner.rb CHANGED
@@ -11,6 +11,7 @@ require 'inspec/profile_context'
11
11
  require 'inspec/profile'
12
12
  require 'inspec/metadata'
13
13
  require 'inspec/secrets'
14
+ require 'inspec/dependencies/cache'
14
15
  # spec requirements
15
16
 
16
17
  module Inspec
@@ -32,7 +33,6 @@ module Inspec
32
33
  extend Forwardable
33
34
 
34
35
  def_delegator :@test_collector, :report
35
- def_delegator :@test_collector, :reset
36
36
 
37
37
  attr_reader :backend, :rules, :attributes
38
38
  def initialize(conf = {})
@@ -42,7 +42,8 @@ module Inspec
42
42
  @target_profiles = []
43
43
  @controls = @conf[:controls] || []
44
44
  @ignore_supports = @conf[:ignore_supports]
45
-
45
+ @create_lockfile = @conf[:create_lockfile]
46
+ @cache = Inspec::Cache.new(@conf[:cache])
46
47
  @test_collector = @conf.delete(:test_collector) || begin
47
48
  require 'inspec/runner_rspec'
48
49
  RunnerRspec.new(@conf)
@@ -64,12 +65,20 @@ module Inspec
64
65
  @test_collector.backend = @backend
65
66
  end
66
67
 
67
- def run(with = nil)
68
- Inspec::Log.debug "Starting run with targets: #{@target_profiles.map(&:to_s)}"
68
+ def reset
69
+ @test_collector.reset
70
+ @target_profiles.each do |profile|
71
+ profile.runner_context.rules = {}
72
+ end
73
+ @rules = []
74
+ end
75
+
76
+ def load
69
77
  all_controls = []
70
78
 
71
79
  @target_profiles.each do |profile|
72
80
  @test_collector.add_profile(profile)
81
+ write_lockfile(profile) if @create_lockfile
73
82
  profile.locked_dependencies
74
83
  profile.load_libraries
75
84
  @attributes |= profile.runner_context.attributes
@@ -79,7 +88,27 @@ module Inspec
79
88
  all_controls.each do |rule|
80
89
  register_rule(rule)
81
90
  end
91
+ end
92
+
93
+ def run(with = nil)
94
+ Inspec::Log.debug "Starting run with targets: #{@target_profiles.map(&:to_s)}"
95
+ load
96
+ run_tests(with)
97
+ end
82
98
 
99
+ def write_lockfile(profile)
100
+ return false if !profile.writable?
101
+
102
+ if profile.lockfile_exists?
103
+ Inspec::Log.debug "Using existing lockfile #{profile.lockfile_path}"
104
+ else
105
+ Inspec::Log.debug "Creating lockfile: #{profile.lockfile_path}"
106
+ lockfile = profile.generate_lockfile
107
+ File.write(profile.lockfile_path, lockfile.to_yaml)
108
+ end
109
+ end
110
+
111
+ def run_tests(with = nil)
83
112
  @test_collector.run(with)
84
113
  end
85
114
 
@@ -126,6 +155,7 @@ module Inspec
126
155
  #
127
156
  def add_target(target, _opts = [])
128
157
  profile = Inspec::Profile.for_target(target,
158
+ cache: @cache,
129
159
  backend: @backend,
130
160
  controls: @controls,
131
161
  attributes: @conf[:attributes])
@@ -133,20 +163,6 @@ module Inspec
133
163
  @target_profiles << profile if supports_profile?(profile)
134
164
  end
135
165
 
136
- #
137
- # This is used by inspec-shell and inspec-detect. This should
138
- # probably be cleaned up a bit.
139
- #
140
- # @params [Hash] Options
141
- # @returns [Inspec::ProfileContext]
142
- #
143
- def create_context(options = {})
144
- meta = options[:metadata]
145
- profile_id = nil
146
- profile_id = meta.params[:name] unless meta.nil?
147
- Inspec::ProfileContext.new(profile_id, @backend, @conf.merge(options))
148
- end
149
-
150
166
  def supports_profile?(profile)
151
167
  return true if @ignore_supports
152
168
 
@@ -180,6 +196,14 @@ module Inspec
180
196
  new_tests
181
197
  end
182
198
 
199
+ def eval_with_virtual_profile(command)
200
+ require 'fetchers/mock'
201
+ add_target({ 'inspec.yml' => 'name: inspec-shell' })
202
+ our_profile = @target_profiles.first
203
+ ctx = our_profile.runner_context
204
+ ctx.load(command)
205
+ end
206
+
183
207
  private
184
208
 
185
209
  def block_source_info(block)