inspec 0.20.1 → 0.21.0

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