inspec 0.18.0 → 0.19.0

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