inspec 0.20.1 → 0.21.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -2
  3. data/docs/dsl_inspec.rst +2 -2
  4. data/docs/resources.rst +9 -9
  5. data/docs/ruby_usage.rst +145 -0
  6. data/inspec.gemspec +1 -0
  7. data/lib/bundles/inspec-compliance/cli.rb +15 -2
  8. data/lib/inspec/cli.rb +23 -10
  9. data/lib/inspec/dsl.rb +0 -52
  10. data/lib/inspec/objects/or_test.rb +1 -0
  11. data/lib/inspec/objects/test.rb +4 -4
  12. data/lib/inspec/profile.rb +76 -61
  13. data/lib/inspec/profile_context.rb +12 -11
  14. data/lib/inspec/rspec_json_formatter.rb +93 -40
  15. data/lib/inspec/rule.rb +7 -29
  16. data/lib/inspec/runner.rb +15 -4
  17. data/lib/inspec/runner_mock.rb +1 -1
  18. data/lib/inspec/runner_rspec.rb +26 -24
  19. data/lib/inspec/version.rb +1 -1
  20. data/lib/matchers/matchers.rb +3 -3
  21. data/lib/resources/auditd_rules.rb +2 -2
  22. data/lib/resources/host.rb +1 -1
  23. data/lib/resources/interface.rb +1 -1
  24. data/lib/resources/kernel_parameter.rb +1 -1
  25. data/lib/resources/mount.rb +2 -1
  26. data/lib/resources/mysql_session.rb +1 -1
  27. data/lib/resources/os_env.rb +2 -2
  28. data/lib/resources/passwd.rb +33 -93
  29. data/lib/resources/port.rb +47 -3
  30. data/lib/resources/processes.rb +3 -3
  31. data/lib/resources/service.rb +33 -1
  32. data/lib/resources/user.rb +15 -15
  33. data/lib/utils/base_cli.rb +1 -3
  34. data/lib/utils/filter.rb +30 -7
  35. data/test/cookbooks/os_prepare/recipes/_upstart_service_centos.rb +4 -0
  36. data/test/functional/helper.rb +1 -0
  37. data/test/functional/inheritance_test.rb +1 -1
  38. data/test/functional/inspec_compliance_test.rb +4 -3
  39. data/test/functional/inspec_exec_json_test.rb +122 -0
  40. data/test/functional/inspec_exec_test.rb +23 -117
  41. data/test/functional/{inspec_json_test.rb → inspec_json_profile_test.rb} +13 -15
  42. data/test/functional/inspec_test.rb +15 -2
  43. data/test/helper.rb +5 -1
  44. data/test/integration/default/auditd_rules_spec.rb +3 -3
  45. data/test/integration/default/kernel_parameter_spec.rb +6 -6
  46. data/test/integration/default/service_spec.rb +4 -0
  47. data/test/resource/command_test.rb +9 -9
  48. data/test/resource/dsl_test.rb +1 -1
  49. data/test/resource/file_test.rb +17 -17
  50. data/test/unit/control_test.rb +1 -1
  51. data/test/unit/mock/cmd/hpux-netstat-inet +10 -0
  52. data/test/unit/mock/cmd/hpux-netstat-inet6 +11 -0
  53. data/test/unit/mock/profiles/skippy-profile-os/controls/one.rb +1 -1
  54. data/test/unit/profile_context_test.rb +2 -2
  55. data/test/unit/profile_test.rb +11 -14
  56. data/test/unit/resources/passwd_test.rb +13 -14
  57. data/test/unit/resources/port_test.rb +14 -0
  58. data/test/unit/resources/processes_test.rb +3 -3
  59. data/test/unit/resources/service_test.rb +103 -39
  60. data/test/unit/utils/filter_table_test.rb +35 -3
  61. metadata +25 -4
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Inspec
4
4
  class OrTest
5
+ attr_reader :tests
5
6
  def initialize(tests)
6
7
  @tests = tests
7
8
  end
@@ -48,7 +48,7 @@ module Inspec
48
48
 
49
49
  if @qualifier.length > 1
50
50
  last = @qualifier[-1]
51
- # preventing its(:to_i) as the value returned is always 0
51
+ # preventing its('to_i') as the value returned is always 0
52
52
  if last.length == 1 && last[0] != 'to_i'
53
53
  xres = last[0]
54
54
  else
@@ -63,10 +63,10 @@ module Inspec
63
63
  vars = variables.map(&:to_ruby).join("\n")
64
64
  vars += "\n" unless vars.empty?
65
65
  res, xtra = describe_chain
66
- itsy = xtra.nil? ? 'it' : 'its(' + xtra.to_sym.inspect + ')'
66
+ itsy = xtra.nil? ? 'it' : 'its(' + xtra.to_s.inspect + ')'
67
67
  naughty = @negated ? '_not' : ''
68
- xpect = defined?(@expectation) ? expectation.inspect : ''
69
- format("%sdescribe %s do\n %s { should%s %s %s }\nend",
68
+ xpect = defined?(@expectation) ? expectation.inspect+' ' : ''
69
+ format("%sdescribe %s do\n %s { should%s %s %s}\nend",
70
70
  vars, res, itsy, naughty, matcher, xpect)
71
71
  end
72
72
 
@@ -11,7 +11,6 @@ require 'inspec/metadata'
11
11
  module Inspec
12
12
  class Profile # rubocop:disable Metrics/ClassLength
13
13
  extend Forwardable
14
- attr_reader :path
15
14
 
16
15
  def self.resolve_target(target, opts)
17
16
  # Fetchers retrieve file contents
@@ -35,6 +34,7 @@ module Inspec
35
34
  end
36
35
 
37
36
  attr_reader :source_reader
37
+ attr_accessor :runner_context
38
38
  def_delegator :@source_reader, :tests
39
39
  def_delegator :@source_reader, :libraries
40
40
  def_delegator :@source_reader, :metadata
@@ -46,6 +46,7 @@ module Inspec
46
46
  @logger = @options[:logger] || Logger.new(nil)
47
47
  @source_reader = source_reader
48
48
  @profile_id = @options[:id]
49
+ @runner_context = nil
49
50
  Metadata.finalize(@source_reader.metadata, @profile_id)
50
51
  end
51
52
 
@@ -55,24 +56,16 @@ module Inspec
55
56
 
56
57
  def info
57
58
  res = params.dup
58
- rules = {}
59
- res[:rules].each do |gid, group|
60
- next if gid.to_s.empty?
61
- rules[gid] = { title: gid, rules: {} }
62
- group.each do |id, rule|
63
- next if id.to_s.empty?
64
- data = rule.dup
65
- data.delete(:checks)
66
- data[:impact] ||= 0.5
67
- data[:impact] = 1.0 if data[:impact] > 1.0
68
- data[:impact] = 0.0 if data[:impact] < 0.0
69
- rules[gid][:rules][id] = data
70
- # TODO: temporarily flatten the group down; replace this with
71
- # proper hierarchy later on
72
- rules[gid][:title] = data[:group_title]
73
- end
59
+ controls = res[:controls].map do |id, rule|
60
+ next if id.to_s.empty?
61
+ data = rule.dup
62
+ data.delete(:checks)
63
+ data[:impact] ||= 0.5
64
+ data[:impact] = 1.0 if data[:impact] > 1.0
65
+ data[:impact] = 0.0 if data[:impact] < 0.0
66
+ [id, data]
74
67
  end
75
- res[:rules] = rules
68
+ res[:controls] = Hash[controls.compact]
76
69
  res
77
70
  end
78
71
 
@@ -137,7 +130,7 @@ module Inspec
137
130
  warn.call(@target, 0, 0, nil, 'Profile uses deprecated `test` directory, rename it to `controls`.')
138
131
  end
139
132
 
140
- count = rules_count
133
+ count = controls_count
141
134
  result[:summary][:controls] = count
142
135
  if count == 0
143
136
  warn.call(nil, nil, nil, nil, 'No controls or tests were defined.')
@@ -146,18 +139,15 @@ module Inspec
146
139
  end
147
140
 
148
141
  # iterate over hash of groups
149
- params[:rules].each { |group, controls|
150
- @logger.info "Verify all controls in #{group}"
151
- controls.each { |id, control|
152
- sfile, sline = control[:source_location]
153
- error.call(sfile, sline, nil, id, 'Avoid controls with empty IDs') if id.nil? or id.empty?
154
- next if id.start_with? '(generated '
155
- warn.call(sfile, sline, nil, id, "Control #{id} has no title") if control[:title].to_s.empty?
156
- warn.call(sfile, sline, nil, id, "Control #{id} has no description") if control[:desc].to_s.empty?
157
- warn.call(sfile, sline, nil, id, "Control #{id} has impact > 1.0") if control[:impact].to_f > 1.0
158
- warn.call(sfile, sline, nil, id, "Control #{id} has impact < 0.0") if control[:impact].to_f < 0.0
159
- warn.call(sfile, sline, nil, id, "Control #{id} has no tests defined") if control[:checks].nil? or control[:checks].empty?
160
- }
142
+ params[:controls].each { |id, control|
143
+ sfile, sline = control[:source_location]
144
+ error.call(sfile, sline, nil, id, 'Avoid controls with empty IDs') if id.nil? or id.empty?
145
+ next if id.start_with? '(generated '
146
+ warn.call(sfile, sline, nil, id, "Control #{id} has no title") if control[:title].to_s.empty?
147
+ warn.call(sfile, sline, nil, id, "Control #{id} has no description") if control[:desc].to_s.empty?
148
+ warn.call(sfile, sline, nil, id, "Control #{id} has impact > 1.0") if control[:impact].to_f > 1.0
149
+ warn.call(sfile, sline, nil, id, "Control #{id} has impact < 0.0") if control[:impact].to_f < 0.0
150
+ warn.call(sfile, sline, nil, id, "Control #{id} has no tests defined") if control[:checks].nil? or control[:checks].empty?
161
151
  }
162
152
 
163
153
  # profile is valid if we could not find any error
@@ -167,8 +157,8 @@ module Inspec
167
157
  result
168
158
  end
169
159
 
170
- def rules_count
171
- params[:rules].values.map { |hm| hm.values.length }.inject(:+) || 0
160
+ def controls_count
161
+ params[:controls].values.length
172
162
  end
173
163
 
174
164
  # generates a archive of a folder profile
@@ -233,38 +223,63 @@ module Inspec
233
223
  def load_params
234
224
  params = @source_reader.metadata.params
235
225
  params[:name] = @profile_id unless @profile_id.nil?
236
- params[:rules] = rules = {}
226
+ load_checks_params(params)
227
+ @profile_id ||= params[:name]
228
+ params
229
+ end
230
+
231
+ def load_checks_params(params)
232
+ params[:controls] = controls = {}
233
+ params[:groups] = groups = {}
237
234
  prefix = @source_reader.target.prefix || ''
238
235
 
239
- # we're checking a profile, we don't care if it runs on the host machine
240
- opts = @options.dup
241
- opts[:ignore_supports] = true
242
- runner = Runner.new(
243
- id: @profile_id,
244
- backend: :mock,
245
- test_collector: opts.delete(:test_collector),
246
- )
247
- runner.add_profile(self, opts)
248
-
249
- runner.rules.each do |id, rule|
250
- file = rule.instance_variable_get(:@__file)
251
- file = file[prefix.length..-1] if file.start_with?(prefix)
252
- rules[file] ||= {}
253
- rules[file][id] = {
254
- title: rule.title,
255
- desc: rule.desc,
256
- impact: rule.impact,
257
- refs: rule.ref,
258
- tags: rule.tag,
259
- checks: Inspec::Rule.checks(rule),
260
- code: rule.instance_variable_get(:@__code),
261
- source_location: rule.instance_variable_get(:@__source_location),
262
- group_title: rule.instance_variable_get(:@__group_title),
263
- }
236
+ if @runner_context.nil?
237
+ # we're checking a profile, we don't care if it runs on the host machine
238
+ opts = @options.dup
239
+ opts[:ignore_supports] = true
240
+ runner = Runner.new(
241
+ id: @profile_id,
242
+ backend: :mock,
243
+ test_collector: opts.delete(:test_collector),
244
+ )
245
+ runner.add_profile(self, opts)
246
+ runner.rules.values.each do |rule|
247
+ f = load_rule_filepath(prefix, rule)
248
+ load_rule(rule, f, controls, groups)
249
+ end
250
+ else
251
+ # load from context
252
+ @runner_context.rules.values.each do |rule|
253
+ f = load_rule_filepath(prefix, rule)
254
+ load_rule(rule, f, controls, groups)
255
+ end
264
256
  end
257
+ end
265
258
 
266
- @profile_id ||= params[:name]
267
- params
259
+ def load_rule_filepath(prefix, rule)
260
+ file = rule.instance_variable_get(:@__file)
261
+ file = file[prefix.length..-1] if file.start_with?(prefix)
262
+ file
263
+ end
264
+
265
+ def load_rule(rule, file, controls, groups)
266
+ id = Inspec::Rule.rule_id(rule)
267
+ controls[id] = {
268
+ title: rule.title,
269
+ desc: rule.desc,
270
+ impact: rule.impact,
271
+ refs: rule.ref,
272
+ tags: rule.tag,
273
+ checks: Inspec::Rule.checks(rule),
274
+ code: rule.instance_variable_get(:@__code),
275
+ source_location: rule.instance_variable_get(:@__source_location),
276
+ }
277
+
278
+ groups[file] ||= {
279
+ title: rule.instance_variable_get(:@__group_title),
280
+ controls: [],
281
+ }
282
+ groups[file][:controls].push(id)
268
283
  end
269
284
  end
270
285
  end
@@ -41,24 +41,19 @@ module Inspec
41
41
  end
42
42
 
43
43
  def unregister_rule(id)
44
- full_id = Inspec::Rule.full_id(@profile_id, id)
45
- @rules[full_id] = nil
44
+ @rules.delete(full_id(@profile_id, id))
46
45
  end
47
46
 
48
47
  def register_rule(r)
49
48
  # get the full ID
50
49
  r.instance_variable_set(:@__file, @current_load[:file])
51
50
  r.instance_variable_set(:@__group_title, @current_load[:title])
52
- full_id = Inspec::Rule.full_id(@profile_id, r)
53
- if full_id.nil?
54
- # TODO: error
55
- return
56
- end
57
51
 
58
52
  # add the rule to the registry
59
- existing = @rules[full_id]
53
+ fid = full_id(Inspec::Rule.profile_id(r), Inspec::Rule.rule_id(r))
54
+ existing = @rules[fid]
60
55
  if existing.nil?
61
- @rules[full_id] = r
56
+ @rules[fid] = r
62
57
  else
63
58
  Inspec::Rule.merge(existing, r)
64
59
  end
@@ -70,6 +65,11 @@ module Inspec
70
65
 
71
66
  private
72
67
 
68
+ def full_id(pid, rid)
69
+ return rid.to_s if pid.to_s.empty?
70
+ pid.to_s + '/' + rid.to_s
71
+ end
72
+
73
73
  # Create the context for controls. This includes all components of the DSL,
74
74
  # including matchers and resources.
75
75
  #
@@ -93,6 +93,7 @@ module Inspec
93
93
  # @return [ProfileContextClass]
94
94
  def create_context(resources_dsl, rule_class) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
95
95
  profile_context_owner = self
96
+ profile_id = @profile_id
96
97
 
97
98
  # rubocop:disable Lint/NestedMethodDefinition
98
99
  Class.new do
@@ -116,7 +117,7 @@ module Inspec
116
117
  define_method :control do |*args, &block|
117
118
  id = args[0]
118
119
  opts = args[1] || {}
119
- register_control(rule_class.new(id, opts, &block))
120
+ register_control(rule_class.new(id, profile_id, opts, &block))
120
121
  end
121
122
 
122
123
  define_method :describe do |*args, &block|
@@ -124,7 +125,7 @@ module Inspec
124
125
  id = "(generated from #{loc} #{SecureRandom.hex})"
125
126
 
126
127
  res = nil
127
- rule = rule_class.new(id, {}) do
128
+ rule = rule_class.new(id, profile_id, {}) do
128
129
  res = describe(*args, &block)
129
130
  end
130
131
  register_control(rule, &block)
@@ -5,43 +5,46 @@
5
5
  require 'rspec/core'
6
6
  require 'rspec/core/formatters/json_formatter'
7
7
 
8
- # Extend the basic RSpec JSON Formatter
9
- # to give us an ID in its output
10
- # TODO: remove once RSpec has IDs in stable (probably v3.3/v4.0)
11
- module RSpec::Core::Formatters
12
- class JsonFormatter
13
- private
14
-
15
- def format_example(example)
16
- {
17
- description: example.description,
18
- full_description: example.full_description,
19
- status: example.execution_result.status.to_s,
20
- file_path: example.metadata['file_path'],
21
- line_number: example.metadata['line_number'],
22
- run_time: example.execution_result.run_time,
23
- pending_message: example.execution_result.pending_message,
24
- id: example.metadata[:id],
25
- impact: example.metadata[:impact],
26
- }
27
- end
8
+ # Vanilla RSpec JSON formatter with a slight extension to show example IDs.
9
+ # TODO: Remove these lines when RSpec includes the ID natively
10
+ class InspecRspecVanilla < RSpec::Core::Formatters::JsonFormatter
11
+ RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
12
+
13
+ private
14
+
15
+ def format_example(example)
16
+ res = super(example)
17
+ res[:id] = example.metadata[:id]
18
+ res
28
19
  end
29
20
  end
30
21
 
31
- class InspecRspecFormatter < RSpec::Core::Formatters::JsonFormatter
22
+ # Minimal JSON formatter for inspec. Only contains limited information about
23
+ # examples without any extras.
24
+ class InspecRspecMiniJson < RSpec::Core::Formatters::JsonFormatter
32
25
  RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
33
26
 
34
- def add_profile(profile)
35
- @profiles ||= []
36
- @profiles.push(profile)
27
+ def dump_summary(summary)
28
+ @output_hash[:version] = Inspec::VERSION
29
+ @output_hash[:summary] = {
30
+ duration: summary.duration,
31
+ example_count: summary.example_count,
32
+ failure_count: summary.failure_count,
33
+ skip_count: summary.pending_count,
34
+ }
37
35
  end
38
36
 
39
- def dump_summary(summary)
40
- super(summary)
41
- @output_hash[:profiles] = Array(@profiles).map do |profile|
42
- r = profile.params.dup
43
- r.delete(:rules)
44
- r
37
+ def stop(notification)
38
+ @output_hash[:controls] = notification.examples.map do |example|
39
+ format_example(example).tap do |hash|
40
+ e = example.exception
41
+ next unless e
42
+ hash[:message] = e.message
43
+
44
+ next if e.is_a? RSpec::Expectations::ExpectationNotMetError
45
+ hash[:exception] = e.class.name
46
+ hash[:backtrace] = e.backtrace
47
+ end
45
48
  end
46
49
  end
47
50
 
@@ -50,21 +53,71 @@ class InspecRspecFormatter < RSpec::Core::Formatters::JsonFormatter
50
53
  def format_example(example)
51
54
  res = {
52
55
  id: example.metadata[:id],
53
- title: example.metadata[:title],
54
- desc: example.metadata[:desc],
55
- code: example.metadata[:code],
56
- impact: example.metadata[:impact],
57
56
  status: example.execution_result.status.to_s,
58
57
  code_desc: example.full_description,
59
- ref: example.metadata['file_path'],
60
- ref_line: example.metadata['line_number'],
61
- run_time: example.execution_result.run_time,
62
- start_time: example.execution_result.started_at.to_s,
63
58
  }
64
59
 
65
- # pending messages are embedded in the resources description
66
- res[:pending] = example.metadata[:description] if res[:status] == 'pending'
60
+ unless (pid = example.metadata[:profile_id]).nil?
61
+ res[:profile_id] = pid
62
+ end
63
+
64
+ if res[:status] == 'pending'
65
+ res[:status] = 'skipped'
66
+ res[:skip_message] = example.metadata[:description]
67
+ res[:resource] = example.metadata[:described_class].to_s
68
+ end
67
69
 
68
70
  res
69
71
  end
70
72
  end
73
+
74
+ class InspecRspecJson < InspecRspecMiniJson
75
+ RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
76
+
77
+ def add_profile(profile)
78
+ @profiles ||= []
79
+ @profiles.push(profile)
80
+ end
81
+
82
+ def dump_one_example(example, profiles, missing)
83
+ profile = profiles[example[:profile_id]]
84
+ return missing.push(example) if profile.nil? || profile[:controls].nil?
85
+
86
+ control = profile[:controls][example[:id]]
87
+ return missing.push(example) if control.nil?
88
+
89
+ control[:results] ||= []
90
+ example.delete(:id)
91
+ example.delete(:profile_id)
92
+ control[:results].push(example)
93
+ end
94
+
95
+ def profile_info(profile)
96
+ info = profile.info.dup
97
+ [info[:name], info]
98
+ end
99
+
100
+ def dump_summary(summary)
101
+ super(summary)
102
+ @profiles ||= []
103
+ examples = @output_hash.delete(:controls)
104
+ profiles = Hash[@profiles.map { |x| profile_info(x) }]
105
+ missing = []
106
+
107
+ examples.each do |example|
108
+ dump_one_example(example, profiles, missing)
109
+ end
110
+
111
+ @output_hash[:profiles] = profiles
112
+ @output_hash[:other_checks] = missing
113
+ end
114
+
115
+ private
116
+
117
+ def format_example(example)
118
+ super(example).tap do |res|
119
+ res[:run_time] = example.execution_result.run_time
120
+ res[:start_time] = example.execution_result.started_at.to_s
121
+ end
122
+ end
123
+ end