inspec 0.18.0 → 0.19.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -2
  3. data/README.md +27 -2
  4. data/docs/resources.rst +49 -3
  5. data/inspec.gemspec +1 -1
  6. data/lib/bundles/inspec-compliance/README.md +2 -1
  7. data/lib/bundles/inspec-compliance/api.rb +60 -102
  8. data/lib/bundles/inspec-compliance/cli.rb +133 -14
  9. data/lib/bundles/inspec-compliance/configuration.rb +43 -1
  10. data/lib/bundles/inspec-compliance/http.rb +80 -0
  11. data/lib/bundles/inspec-compliance.rb +1 -0
  12. data/lib/inspec/metadata.rb +40 -27
  13. data/lib/inspec/objects/test.rb +2 -1
  14. data/lib/inspec/resource.rb +1 -0
  15. data/lib/inspec/rspec_json_formatter.rb +1 -1
  16. data/lib/inspec/runner.rb +19 -13
  17. data/lib/inspec/runner_rspec.rb +1 -1
  18. data/lib/inspec/version.rb +1 -1
  19. data/lib/matchers/matchers.rb +32 -13
  20. data/lib/resources/grub_conf.rb +186 -0
  21. data/lib/resources/json.rb +1 -1
  22. data/lib/resources/service.rb +9 -3
  23. data/lib/utils/base_cli.rb +2 -1
  24. data/lib/utils/hash_map.rb +37 -0
  25. data/test/functional/inspec_compliance_test.rb +60 -0
  26. data/test/functional/inspec_exec_test.rb +49 -10
  27. data/test/helper.rb +3 -0
  28. data/test/integration/default/compare_matcher_spec.rb +21 -0
  29. data/test/unit/metadata_test.rb +49 -23
  30. data/test/unit/mock/cmd/systemctl-show-all-dbus +6 -0
  31. data/test/unit/mock/files/grub.conf +21 -0
  32. data/test/unit/mock/profiles/resource-tiny/inspec.yml +10 -0
  33. data/test/unit/mock/profiles/resource-tiny/libraries/resource.rb +3 -0
  34. data/test/unit/mock/profiles/supported_inspec/inspec.yml +2 -0
  35. data/test/unit/mock/profiles/unsupported_inspec/inspec.yml +2 -0
  36. data/test/unit/profile_test.rb +1 -1
  37. data/test/unit/resources/grub_conf_test.rb +29 -0
  38. data/test/unit/resources/service_test.rb +9 -0
  39. data/test/unit/utils/hash_map_test.rb +63 -0
  40. metadata +26 -5
@@ -0,0 +1,80 @@
1
+ # encoding: utf-8
2
+ # author: Christoph Hartmann
3
+ # author: Dominik Richter
4
+
5
+ require 'net/http'
6
+ require 'uri'
7
+
8
+ module Compliance
9
+ # implements a simple http abstraction on top of Net::HTTP
10
+ class HTTP
11
+ # generic get requires
12
+ def self.get(url, token, insecure, basic_auth = false)
13
+ uri = URI.parse(url)
14
+ req = Net::HTTP::Get.new(uri.path)
15
+
16
+ return send_request(uri, req, insecure) if token.nil?
17
+
18
+ if basic_auth
19
+ req.basic_auth(token, '')
20
+ else
21
+ req['Authorization'] = "Bearer #{token}"
22
+ end
23
+ send_request(uri, req, insecure)
24
+ end
25
+
26
+ # generic post request
27
+ def self.post(url, token, insecure, basic_auth = false)
28
+ # form request
29
+ uri = URI.parse(url)
30
+ req = Net::HTTP::Post.new(uri.path)
31
+ if basic_auth
32
+ req.basic_auth token, ''
33
+ else
34
+ req['Authorization'] = "Bearer #{token}"
35
+ end
36
+ req.form_data={}
37
+
38
+ send_request(uri, req, insecure)
39
+ end
40
+
41
+ # post a file
42
+ def self.post_file(url, token, file_path, insecure, basic_auth = false)
43
+ uri = URI.parse(url)
44
+ http = Net::HTTP.new(uri.host, uri.port)
45
+
46
+ # set connection flags
47
+ http.use_ssl = (uri.scheme == 'https')
48
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE if insecure
49
+
50
+ req = Net::HTTP::Post.new(uri.path)
51
+ if basic_auth
52
+ req.basic_auth token, ''
53
+ else
54
+ req['Authorization'] = "Bearer #{token}"
55
+ end
56
+
57
+ req.body_stream=File.open(file_path, 'rb')
58
+ req.add_field('Content-Length', File.size(file_path))
59
+ req.add_field('Content-Type', 'application/x-gtar')
60
+
61
+ boundary = 'INSPEC-PROFILE-UPLOAD'
62
+ req.add_field('session', boundary)
63
+ res=http.request(req)
64
+ res
65
+ end
66
+
67
+ # sends a http requests
68
+ def self.send_request(uri, req, insecure)
69
+ opts = {
70
+ use_ssl: uri.scheme == 'https',
71
+ }
72
+ opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if insecure
73
+
74
+ res = Net::HTTP.start(uri.host, uri.port, opts) {|http|
75
+ http.request(req)
76
+ }
77
+ res
78
+ end
79
+ end
80
+ end
@@ -7,6 +7,7 @@ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
7
7
 
8
8
  module Compliance
9
9
  autoload :Configuration, 'inspec-compliance/configuration'
10
+ autoload :HTTP, 'inspec-compliance/http'
10
11
  autoload :API, 'inspec-compliance/api'
11
12
  end
12
13
 
@@ -4,6 +4,8 @@
4
4
  # author: Christoph Hartmann
5
5
 
6
6
  require 'logger'
7
+ require 'rubygems/version'
8
+ require 'rubygems/requirement'
7
9
 
8
10
  module Inspec
9
11
  # Extract metadata.rb information
@@ -40,8 +42,10 @@ module Inspec
40
42
  # already.
41
43
  end
42
44
 
43
- def is_supported(os, entry)
44
- name, family, release = support_fields(entry)
45
+ def is_supported?(os, entry)
46
+ name = entry[:'os-name'] || entry[:os]
47
+ family = entry[:'os-family']
48
+ release = entry[:release]
45
49
 
46
50
  # return true if the backend matches the supported OS's
47
51
  # fields act as masks, i.e. any value configured for os-name, os-family,
@@ -66,34 +70,24 @@ module Inspec
66
70
  name_ok && family_ok && release_ok
67
71
  end
68
72
 
69
- def support_fields(entry)
70
- if entry.is_a?(Hash)
71
- try_support = self.class.symbolize_keys(entry)
72
- name = try_support[:'os-name'] || try_support[:os]
73
- family = try_support[:'os-family']
74
- release = try_support[:release]
75
- elsif entry.is_a?(String)
76
- @logger.warn(
77
- "Do not use deprecated `supports: #{entry}` syntax. Instead use "\
78
- "`supports: {os-family: #{entry}}`.")
79
- family = entry
80
- end
73
+ def inspec_requirement
74
+ inspec = params[:supports].find { |x| !x[:inspec].nil? } || {}
75
+ Gem::Requirement.create(inspec[:inspec])
76
+ end
81
77
 
82
- [name, family, release]
78
+ def supports_runtime?
79
+ running = Gem::Version.new(Inspec::VERSION)
80
+ inspec_requirement.satisfied_by?(running)
83
81
  end
84
82
 
85
83
  def supports_transport?(backend)
86
- # make sure the supports field is always an array
87
- supp = params[:supports]
88
- supp = supp.is_a?(Hash) ? [supp] : Array(supp)
89
-
90
84
  # with no supports specified, always return true, as there are no
91
85
  # constraints on the supported backend; it is equivalent to putting
92
86
  # all fields into accept-all mode
93
- return true if supp.empty?
87
+ return true if params[:supports].empty?
94
88
 
95
- found = supp.find do |entry|
96
- is_supported(backend.os, entry)
89
+ found = params[:supports].find do |entry|
90
+ is_supported?(backend.os, entry)
97
91
  end
98
92
 
99
93
  # finally, if we found a supported entry, we are good to go
@@ -132,32 +126,51 @@ module Inspec
132
126
  @missing_methods
133
127
  end
134
128
 
135
- def self.symbolize_keys(hash)
136
- hash.each_with_object({}) {|(k, v), h|
129
+ def self.symbolize_keys(obj)
130
+ return obj.map { |i| symbolize_keys(i) } if obj.is_a?(Array)
131
+ return obj unless obj.is_a?(Hash)
132
+
133
+ obj.each_with_object({}) {|(k, v), h|
137
134
  v = symbolize_keys(v) if v.is_a?(Hash)
135
+ v = symbolize_keys(v) if v.is_a?(Array)
138
136
  h[k.to_sym] = v
139
137
  }
140
138
  end
141
139
 
142
- def self.finalize(metadata, profile_id)
140
+ def self.finalize(metadata, profile_id, logger = nil)
143
141
  return nil if metadata.nil?
144
142
  param = metadata.params || {}
145
143
  param['name'] = profile_id.to_s unless profile_id.to_s.empty?
146
144
  param['version'] = param['version'].to_s unless param['version'].nil?
147
145
  metadata.params = symbolize_keys(param)
146
+
147
+ # consolidate supports field with legacy mode
148
+ metadata.params[:supports] =
149
+ case x = metadata.params[:supports]
150
+ when Hash then [x]
151
+ when Array then x
152
+ when nil then []
153
+ else
154
+ logger ||= Logger.new(nil)
155
+ logger.warn(
156
+ "Do not use deprecated `supports: #{x}` syntax. Instead use "\
157
+ "`supports: {os-family: #{x}}`.")
158
+ [{ :'os-family' => x }]
159
+ end
160
+
148
161
  metadata
149
162
  end
150
163
 
151
164
  def self.from_yaml(ref, contents, profile_id, logger = nil)
152
165
  res = Metadata.new(ref, logger)
153
166
  res.params = YAML.load(contents)
154
- finalize(res, profile_id)
167
+ finalize(res, profile_id, logger)
155
168
  end
156
169
 
157
170
  def self.from_ruby(ref, contents, profile_id, logger = nil)
158
171
  res = Metadata.new(ref, logger)
159
172
  res.instance_eval(contents, ref, 1)
160
- finalize(res, profile_id)
173
+ finalize(res, profile_id, logger)
161
174
  end
162
175
 
163
176
  def self.from_ref(ref, contents, profile_id, logger = nil)
@@ -48,7 +48,8 @@ module Inspec
48
48
 
49
49
  if @qualifier.length > 1
50
50
  last = @qualifier[-1]
51
- if last.length == 1
51
+ # preventing its(:to_i) as the value returned is always 0
52
+ if last.length == 1 && last[0] != 'to_i'
52
53
  xres = last[0]
53
54
  else
54
55
  res += '.' + ruby_qualifier(last)
@@ -62,6 +62,7 @@ require 'resources/etc_group'
62
62
  require 'resources/file'
63
63
  require 'resources/gem'
64
64
  require 'resources/group'
65
+ require 'resources/grub_conf'
65
66
  require 'resources/host'
66
67
  require 'resources/inetd_conf'
67
68
  require 'resources/interface'
@@ -38,7 +38,7 @@ class InspecRspecFormatter < RSpec::Core::Formatters::JsonFormatter
38
38
 
39
39
  def dump_summary(summary)
40
40
  super(summary)
41
- @output_hash[:profiles] = @profiles.map do |profile|
41
+ @output_hash[:profiles] = Array(@profiles).map do |profile|
42
42
  r = profile.params.dup
43
43
  r.delete(:rules)
44
44
  r
data/lib/inspec/runner.rb CHANGED
@@ -52,7 +52,26 @@ module Inspec
52
52
  add_profile(profile, options)
53
53
  end
54
54
 
55
+ def supports_profile?(profile)
56
+ return true if profile.metadata.nil?
57
+
58
+ if !profile.metadata.supports_runtime?
59
+ fail 'This profile requires InSpec version '\
60
+ "#{profile.metadata.inspec_requirement}. You are running "\
61
+ "InSpec v#{Inspec::VERSION}.\n"
62
+ end
63
+
64
+ if !profile.metadata.supports_transport?(@backend)
65
+ os_info = @backend.os[:family].to_s
66
+ fail "This OS/platform (#{os_info}) is not supported by this profile."
67
+ end
68
+
69
+ true
70
+ end
71
+
55
72
  def add_profile(profile, options = {})
73
+ return if !options[:ignore_supports] && !supports_profile?(profile)
74
+
56
75
  @test_collector.add_profile(profile)
57
76
  options[:metadata] = profile.metadata
58
77
 
@@ -86,19 +105,6 @@ module Inspec
86
105
  tests = [tests] unless tests.is_a? Array
87
106
  tests.each { |t| add_test_to_context(t, ctx) }
88
107
 
89
- # skip based on support checks in metadata
90
- meta = options[:metadata]
91
- if !options[:ignore_supports] && !meta.nil? &&
92
- !meta.supports_transport?(@backend)
93
- os_info = @backend.os[:family].to_s
94
- ctx.rules.values.each do |ctrl|
95
- ::Inspec::Rule.set_skip_rule(
96
- ctrl,
97
- "This OS/platform (#{os_info}) is not supported by this profile.",
98
- )
99
- end
100
- end
101
-
102
108
  # process the resulting rules
103
109
  filter_controls(ctx.rules, options[:controls]).each do |rule_id, rule|
104
110
  register_rule(rule_id, rule)
@@ -48,7 +48,7 @@ module Inspec
48
48
  # @return [nil]
49
49
  def add_test(example, rule_id, rule)
50
50
  set_rspec_ids(example, rule_id, rule)
51
- @tests.register(example)
51
+ @tests.example_groups.push(example)
52
52
  end
53
53
 
54
54
  # Retrieve the list of tests that have been added.
@@ -3,5 +3,5 @@
3
3
  # author: Christoph Hartmann
4
4
 
5
5
  module Inspec
6
- VERSION = '0.18.0'.freeze
6
+ VERSION = '0.19.0'.freeze
7
7
  end
@@ -226,7 +226,7 @@ end
226
226
  # - compare strings case-insensitive
227
227
  # - you expect a number (strings will be converted if possible)
228
228
  #
229
- RSpec::Matchers.define :cmp do |expected|
229
+ RSpec::Matchers.define :cmp do |first_expected|
230
230
 
231
231
  def integer?(value)
232
232
  !(value =~ /\A\d+\Z/).nil?
@@ -243,33 +243,52 @@ RSpec::Matchers.define :cmp do |expected|
243
243
  !(value =~ /\A0+\d+\Z/).nil?
244
244
  end
245
245
 
246
- match do |actual|
247
- actual = actual[0] if actual.is_a?(Array) && !expected.is_a?(Array) && actual.length == 1
246
+ def try_match(actual, op, expected) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
248
247
  # if actual and expected are strings
249
248
  if expected.is_a?(String) && actual.is_a?(String)
250
- actual.casecmp(expected) == 0
249
+ return actual.casecmp(expected) == 0 if op == :==
251
250
  elsif expected.is_a?(String) && integer?(expected) && actual.is_a?(Integer)
252
- expected.to_i == actual
251
+ return actual.method(op).call(expected.to_i)
253
252
  elsif expected.is_a?(Integer) && integer?(actual)
254
- expected == actual.to_i
253
+ return actual.to_i.method(op).call(expected)
255
254
  elsif expected.is_a?(Float) && float?(actual)
256
- expected == actual.to_f
255
+ return actual.to_f.method(op).call(expected)
257
256
  elsif octal?(expected) && actual.is_a?(Integer)
258
- expected.to_i(8) == actual
259
- # fallback to equal
260
- else
261
- actual == expected
257
+ return actual.method(op).call(expected.to_i(8))
258
+ end
259
+
260
+ # fallback to simple operation
261
+ actual.method(op).call(expected)
262
+
263
+ rescue NameError => _
264
+ false
265
+ rescue ArgumentError
266
+ false
267
+ end
268
+
269
+ match do |actual|
270
+ @operation ||= :==
271
+ @expected ||= first_expected
272
+ return actual === @expected if @operation == :=== # rubocop:disable Style/CaseEquality
273
+ actual = actual[0] if actual.is_a?(Array) && !@expected.is_a?(Array) && actual.length == 1
274
+ try_match(actual, @operation, @expected)
275
+ end
276
+
277
+ [:==, :<, :<=, :>=, :>, :===, :=~].each do |op|
278
+ chain(op) do |x|
279
+ @operation = op
280
+ @expected = x
262
281
  end
263
282
  end
264
283
 
265
284
  failure_message do |actual|
266
285
  actual = '0' + actual.to_s(8) if octal?(expected)
267
- "\nexpected: #{expected}\n got: #{actual}\n\n(compared using `cmp` matcher)\n"
286
+ "\nexpected: value #{@operation} #{expected}\n got: #{actual}\n\n(compared using `cmp` matcher)\n"
268
287
  end
269
288
 
270
289
  failure_message_when_negated do |actual|
271
290
  actual = '0' + actual.to_s(8) if octal?(expected)
272
- "\nexpected: value != #{expected}\n got: #{actual}\n\n(compared using `cmp` matcher)\n"
291
+ "\nexpected: value ! #{@operation} #{expected}\n got: #{actual}\n\n(compared using `cmp` matcher)\n"
273
292
  end
274
293
  end
275
294
 
@@ -0,0 +1,186 @@
1
+ # encoding: utf-8
2
+ # author: Thomas Cate
3
+ # license: All rights reserved
4
+
5
+ require 'utils/simpleconfig'
6
+
7
+ class GrubConfig < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
8
+ name 'grub_conf'
9
+ desc 'Use the grub_conf InSpec audit resource to test the boot config of Linux systems that use Grub.'
10
+ example "
11
+ describe grub_conf('/etc/grub.conf', 'default') do
12
+ its('kernel') { should include '/vmlinuz-2.6.32-573.7.1.el6.x86_64' }
13
+ its('initrd') { should include '/initramfs-2.6.32-573.el6.x86_64.img=1' }
14
+ its('default') { should_not eq '1' }
15
+ its('timeout') { should eq '5' }
16
+ end
17
+
18
+ also check specific kernels
19
+ describe grub_conf('/etc/grub.conf', 'CentOS (2.6.32-573.12.1.el6.x86_64)') do
20
+ its('kernel') { should include 'audit=1' }
21
+ end
22
+ "
23
+
24
+ def initialize(path = nil, kernel = nil)
25
+ family = inspec.os[:family]
26
+ case family
27
+ when 'redhat', 'fedora', 'centos'
28
+ release = inspec.os[:release].to_f
29
+ supported = true
30
+ if release < 7
31
+ @conf_path = path || '/etc/grub.conf'
32
+ @version = 'legacy'
33
+ else
34
+ @conf_path = path || '/boot/grub/grub.cfg'
35
+ @defaults_path = '/etc/default/grub'
36
+ @version = 'grub2'
37
+ end
38
+ when 'ubuntu'
39
+ @conf_path = path || '/boot/grub/grub.cfg'
40
+ @defaults_path = '/etc/default/grub'
41
+ @version = 'grub2'
42
+ supported = true
43
+ end
44
+ @kernel = kernel || 'default'
45
+ return skip_resource 'The `grub_config` resource is not supported on your OS yet.' if supported.nil?
46
+ end
47
+
48
+ def method_missing(name)
49
+ read_params[name.to_s]
50
+ end
51
+
52
+ def to_s
53
+ 'Grub Config'
54
+ end
55
+
56
+ private
57
+
58
+ ######################################################################
59
+ # Grub2 This is used by all supported versions of Ubuntu and Rhel 7+ #
60
+ ######################################################################
61
+
62
+ def grub2_parse_kernel_lines(content, conf)
63
+ # Find all "menuentry" lines and then parse them into arrays
64
+ menu_entry = 0
65
+ lines = content.split("\n")
66
+ kernel_opts = {}
67
+ kernel_opts['insmod'] = []
68
+ lines.each_with_index do |file_line, index|
69
+ next unless file_line =~ /(^|\s)menuentry\s.*/
70
+ lines.drop(index+1).each do |kernel_line|
71
+ next if kernel_line =~ /(^|\s)(menu|}).*/
72
+ if menu_entry == conf['GRUB_DEFAULT'].to_i && @kernel == 'default'
73
+ if kernel_line =~ /(^|\s)initrd.*/
74
+ kernel_opts['initrd'] = kernel_line.split(' ')[1]
75
+ end
76
+ if kernel_line =~ /(^|\s)linux.*/
77
+ kernel_opts['kernel'] = kernel_line.split
78
+ end
79
+ if kernel_line =~ /(^|\s)set root=.*/
80
+ kernel_opts['root'] = kernel_line.split('=')[1].tr('\'', '')
81
+ end
82
+ if kernel_line =~ /(^|\s)insmod.*/
83
+ kernel_opts['insmod'].push(kernel_line.split(' ')[1])
84
+ end
85
+ else
86
+ menu_entry += 1
87
+ break
88
+ end
89
+ end
90
+ end
91
+ kernel_opts
92
+ end
93
+
94
+ ###################################################################
95
+ # Grub1 aka legacy-grub config. Primarily used by Centos/Rhel 6.x #
96
+ ###################################################################
97
+
98
+ def parse_kernel_lines(content, conf)
99
+ # Find all "title" lines and then parse them into arrays
100
+ menu_entry = 0
101
+ lines = content.split("\n")
102
+ kernel_opts = {}
103
+ lines.each_with_index do |file_line, index|
104
+ next unless file_line =~ /^title.*/
105
+ current_kernel = file_line.split(' ', 2)[1]
106
+ lines.drop(index+1).each do |kernel_line|
107
+ if kernel_line =~ /^\s.*/
108
+ option_type = kernel_line.split(' ')[0]
109
+ line_options = kernel_line.split(' ').drop(1)
110
+ if (menu_entry == conf['default'].to_i && @kernel == 'default') || current_kernel == @kernel
111
+ if option_type == 'kernel'
112
+ kernel_opts['kernel'] = line_options
113
+ else
114
+ kernel_opts[option_type] = line_options[0]
115
+ end
116
+ end
117
+ else
118
+ menu_entry += 1
119
+ break
120
+ end
121
+ end
122
+ end
123
+ kernel_opts
124
+ end
125
+
126
+ def read_file(config_file)
127
+ file = inspec.file(config_file)
128
+
129
+ if !file.file? && !file.symlink?
130
+ skip_resource "Can't find file '#{@conf_path}'"
131
+ return @params = {}
132
+ end
133
+
134
+ content = file.content
135
+
136
+ if content.empty? && file.size > 0
137
+ skip_resource "Can't read file '#{@conf_path}'"
138
+ return @params = {}
139
+ end
140
+
141
+ content
142
+ end
143
+
144
+ def read_params
145
+ return @params if defined?(@params)
146
+
147
+ content = read_file(@conf_path)
148
+
149
+ if @version == 'legacy'
150
+ # parse the file
151
+ conf = SimpleConfig.new(
152
+ content,
153
+ multiple_values: true,
154
+ ).params
155
+ # convert single entry arrays into strings
156
+ conf.each do |key, value|
157
+ if value.size == 1
158
+ conf[key] = conf[key][0].to_s
159
+ end
160
+ end
161
+ kernel_opts = parse_kernel_lines(content, conf)
162
+ @params = conf.merge(kernel_opts)
163
+ end
164
+
165
+ if @version == 'grub2'
166
+ # read defaults
167
+ defaults = read_file(@defaults_path)
168
+
169
+ conf = SimpleConfig.new(
170
+ defaults,
171
+ multiple_values: true,
172
+ ).params
173
+
174
+ # convert single entry arrays into strings
175
+ conf.each do |key, value|
176
+ if value.size == 1
177
+ conf[key] = conf[key][0].to_s
178
+ end
179
+ end
180
+
181
+ kernel_opts = grub2_parse_kernel_lines(content, conf)
182
+ @params = conf.merge(kernel_opts)
183
+ end
184
+ @params
185
+ end
186
+ end
@@ -10,7 +10,7 @@ module Inspec::Resources
10
10
  desc 'Use the json InSpec audit resource to test data in a JSON file.'
11
11
  example "
12
12
  describe json('policyfile.lock.json') do
13
- its('cookbook_locks.omnibus.version') { should eq('2.2.0') }
13
+ its(['cookbook_locks','omnibus','version']) { should eq('2.2.0') }
14
14
  end
15
15
  "
16
16
 
@@ -94,7 +94,7 @@ module Inspec::Resources
94
94
  return skip_resource 'The `service` resource is not supported on your OS yet.' if @service_mgmt.nil?
95
95
  end
96
96
 
97
- def select_service_mgmt # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
97
+ def select_service_mgmt # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
98
98
  os = inspec.os
99
99
  family = os[:family]
100
100
 
@@ -135,8 +135,14 @@ module Inspec::Resources
135
135
  WindowsSrv.new(inspec)
136
136
  elsif %w{freebsd}.include?(family)
137
137
  BSDInit.new(inspec, service_ctl)
138
- elsif %w{arch opensuse}.include?(family)
138
+ elsif %w{arch}.include?(family)
139
139
  Systemd.new(inspec, service_ctl)
140
+ elsif %w{suse opensuse}.include?(family)
141
+ if inspec.os[:release].to_i >= 12
142
+ Systemd.new(inspec, service_ctl)
143
+ else
144
+ SysV.new(inspec, service_ctl || '/sbin/service')
145
+ end
140
146
  elsif %w{aix}.include?(family)
141
147
  SrcMstr.new(inspec)
142
148
  elsif %w{amazon}.include?(family)
@@ -214,7 +220,7 @@ module Inspec::Resources
214
220
  running = params['SubState'] == 'running'
215
221
  # test via systemctl --quiet is-enabled
216
222
  # ActiveState values eg.g inactive, active
217
- enabled = params['UnitFileState'] == 'enabled'
223
+ enabled = %w{enabled static}.include? params['UnitFileState']
218
224
 
219
225
  {
220
226
  name: params['Id'],
@@ -69,7 +69,8 @@ module Inspec
69
69
  targets.each { |target| runner.add_target(target, opts) }
70
70
  exit runner.run
71
71
  rescue RuntimeError => e
72
- puts e.message
72
+ $stderr.puts e.message
73
+ exit 1
73
74
  end
74
75
 
75
76
  def diagnose
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ class HashMap
6
+ class << self
7
+ def [](hash, *keys)
8
+ return hash if keys.empty? || hash.nil?
9
+ key = keys.shift
10
+ if hash.is_a?(Array)
11
+ map = hash.map { |i| [i, key] }
12
+ else
13
+ map = hash[key]
14
+ end
15
+ [map, *keys]
16
+ rescue NoMethodError => _
17
+ nil
18
+ end
19
+ end
20
+ end
21
+
22
+ class StringMap
23
+ class << self
24
+ def [](hash, *keys)
25
+ return hash if keys.empty? || hash.nil?
26
+ key = keys.shift
27
+ if hash.is_a?(Array)
28
+ map = hash.map { |i| [i, key] }
29
+ else
30
+ map = hash[key]
31
+ end
32
+ [map, *keys]
33
+ rescue NoMethodError => _
34
+ nil
35
+ end
36
+ end
37
+ end