inspec 0.19.3 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +5 -13
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +30 -2
  4. data/inspec.gemspec +1 -1
  5. data/lib/bundles/inspec-compliance.rb +1 -0
  6. data/lib/bundles/inspec-compliance/.kitchen.yml +21 -0
  7. data/lib/bundles/inspec-compliance/README.md +24 -0
  8. data/lib/bundles/inspec-compliance/bootstrap.sh +37 -0
  9. data/lib/bundles/inspec-compliance/cli.rb +2 -2
  10. data/lib/bundles/inspec-compliance/support.rb +36 -0
  11. data/lib/bundles/inspec-compliance/target.rb +3 -5
  12. data/lib/bundles/inspec-compliance/test/integration/default/cli.rb +56 -0
  13. data/lib/fetchers/url.rb +7 -2
  14. data/lib/inspec/backend.rb +1 -1
  15. data/lib/inspec/cli.rb +13 -13
  16. data/lib/inspec/plugins/fetcher.rb +1 -0
  17. data/lib/inspec/version.rb +1 -1
  18. data/lib/resources/file.rb +11 -23
  19. data/lib/resources/os.rb +10 -1
  20. data/lib/resources/package.rb +16 -0
  21. data/lib/resources/user.rb +14 -0
  22. data/lib/resources/xinetd.rb +39 -94
  23. data/lib/utils/filter.rb +184 -0
  24. data/lib/utils/hash_map.rb +37 -0
  25. data/test/functional/inspec_test.rb +23 -0
  26. data/test/helper.rb +5 -0
  27. data/test/resource/file_test.rb +3 -1
  28. data/test/unit/{fetchers.rb → fetchers_test.rb} +1 -0
  29. data/test/unit/mock/cmd/logins-x +4 -0
  30. data/test/unit/mock/cmd/swlist-l-product +1 -0
  31. data/test/unit/mock/profiles/resource-tiny/inspec.yml +10 -0
  32. data/test/unit/mock/profiles/resource-tiny/libraries/resource.rb +3 -0
  33. data/test/unit/resources/file_test.rb +21 -0
  34. data/test/unit/resources/package_test.rb +9 -0
  35. data/test/unit/resources/user_test.rb +6 -0
  36. data/test/unit/resources/xinetd_test.rb +3 -3
  37. data/test/unit/utils/filter_table_test.rb +125 -0
  38. metadata +46 -31
  39. data/lib/utils/detect.rb +0 -15
data/lib/resources/os.rb CHANGED
@@ -13,7 +13,7 @@ module Inspec::Resources
13
13
  "
14
14
 
15
15
  # reuse helper methods from backend
16
- %w{aix? redhat? debian? suse? bsd? solaris? linux? unix? windows?}.each do |os_family|
16
+ %w{aix? redhat? debian? suse? bsd? solaris? linux? unix? windows? hpux?}.each do |os_family|
17
17
  define_method(os_family.to_sym) do
18
18
  inspec.backend.os.send(os_family)
19
19
  end
@@ -25,6 +25,15 @@ module Inspec::Resources
25
25
  inspec.backend.os[name]
26
26
  end
27
27
 
28
+ def params
29
+ {
30
+ name: inspec.backend.os[:name],
31
+ family: inspec.backend.os[:family],
32
+ release: inspec.backend.os[:release],
33
+ arch: inspec.backend.os[:arch],
34
+ }
35
+ end
36
+
28
37
  def to_s
29
38
  'Operating System Detection'
30
39
  end
@@ -41,6 +41,8 @@ module Inspec::Resources
41
41
  @pkgman = BffPkg.new(inspec)
42
42
  elsif os.solaris?
43
43
  @pkgman = SolarisPkg.new(inspec)
44
+ elsif ['hpux'].include?(os[:family])
45
+ @pkgman = HpuxPkg.new(inspec)
44
46
  else
45
47
  return skip_resource 'The `package` resource is not supported on your OS yet.'
46
48
  end
@@ -168,6 +170,20 @@ module Inspec::Resources
168
170
  end
169
171
  end
170
172
 
173
+ class HpuxPkg < PkgManagement
174
+ def info(package_name)
175
+ cmd = inspec.command("swlist -l product | grep #{package_name}")
176
+ return nil if cmd.exit_status.to_i != 0
177
+ pkg = cmd.stdout.strip.split(' ')
178
+ {
179
+ name: pkg[0],
180
+ installed: true,
181
+ version: pkg[1],
182
+ type: 'pkg',
183
+ }
184
+ end
185
+ end
186
+
171
187
  # Determines the installed packages on Windows
172
188
  # Currently we use 'Get-WmiObject -Class Win32_Product' as a detection method
173
189
  # TODO: evaluate if alternative methods as proposed by Microsoft are still valid:
@@ -67,6 +67,8 @@ module Inspec::Resources
67
67
  @user_provider = AixUser.new(inspec)
68
68
  elsif os.solaris?
69
69
  @user_provider = SolarisUser.new(inspec)
70
+ elsif ['hpux'].include?(os[:family])
71
+ @user_provider = HpuxUser.new(inspec)
70
72
  else
71
73
  return skip_resource 'The `user` resource is not supported on your OS yet.'
72
74
  end
@@ -330,6 +332,18 @@ module Inspec::Resources
330
332
  end
331
333
  end
332
334
 
335
+ class HpuxUser < UnixUser
336
+ def meta_info(username)
337
+ hpuxuser = inspec.command("logins -x -l #{username}")
338
+ return nil if hpuxuser.exit_status != 0
339
+ user = hpuxuser.stdout.chomp.split(' ')
340
+ {
341
+ home: user[4],
342
+ shell: user[5],
343
+ }
344
+ end
345
+ end
346
+
333
347
  # we do not use 'finger' for MacOS, because it is harder to parse data with it
334
348
  # @see https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/fingerd.8.html
335
349
  # instead we use 'dscl' to request user data
@@ -3,9 +3,10 @@
3
3
  # author: Dominik Richter
4
4
 
5
5
  require 'utils/parser'
6
+ require 'utils/filter'
6
7
 
7
8
  module Inspec::Resources
8
- class XinetdConf < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
9
+ class XinetdConf < Inspec.resource(1)
9
10
  name 'xinetd_conf'
10
11
  desc 'Xinetd services configuration.'
11
12
  example "
@@ -20,112 +21,33 @@ module Inspec::Resources
20
21
 
21
22
  include XinetdParser
22
23
 
23
- def initialize(conf_path = '/etc/xinetd.conf', opts = {})
24
+ def initialize(conf_path = '/etc/xinetd.conf')
24
25
  @conf_path = conf_path
25
- @params = opts[:params] unless opts[:params].nil?
26
- @filters = opts[:filters] || ''
27
26
  @contents = {}
28
27
  end
29
28
 
30
29
  def to_s
31
- "Xinetd config #{@conf_path}"
32
- end
33
-
34
- def services(condition = nil)
35
- condition.nil? ? params['services'].keys : filter(service: condition)
36
- end
37
-
38
- def ids(condition = nil)
39
- condition.nil? ? services_field('id') : filter(id: condition)
40
- end
41
-
42
- def socket_types(condition = nil)
43
- condition.nil? ? services_field('socket_type') : filter(socket_type: condition)
44
- end
45
-
46
- def types(condition = nil)
47
- condition.nil? ? services_field('type') : filter(type: condition)
48
- end
49
-
50
- def wait(condition = nil)
51
- condition.nil? ? services_field('wait') : filter(wait: condition)
52
- end
53
-
54
- def disabled?
55
- filter(disable: 'no').services.empty?
56
- end
57
-
58
- def enabled?
59
- filter(disable: 'yes').services.empty?
30
+ "Xinetd config #{@conf_path}#{@filters}"
60
31
  end
61
32
 
62
33
  def params
63
- return @params if defined?(@params)
64
- return @params = {} if read_content.nil?
65
- flat_params = parse_xinetd(read_content)
66
- @params = { 'services' => {} }
67
- flat_params.each do |k, v|
68
- name = k[/^service (.+)$/, 1]
69
- if name.nil?
70
- @params[k] = v
71
- else
72
- @params['services'][name] = v
73
- end
74
- end
75
- @params
34
+ @params ||= read_params
76
35
  end
77
36
 
78
- def filter(conditions = {})
79
- res = params.dup
80
- filters = ''
81
- conditions.each do |k, v|
82
- v = v.to_s if v.is_a? Integer
83
- filters += " #{k} = #{v.inspect}"
84
- res['services'] = filter_by(res['services'], k.to_s, v)
85
- end
86
- XinetdConf.new(@conf_path, params: res, filters: filters)
87
- end
37
+ filter = FilterTable.create
38
+ filter.add_accessor(:where)
39
+ .add_accessor(:entries)
40
+ .add(:services, field: 'service')
41
+ .add(:ids, field: 'id')
42
+ .add(:socket_types, field: 'socket_type')
43
+ .add(:types, field: 'type')
44
+ .add(:wait, field: 'wait')
45
+ .add(:disabled?) { |x| x.where('disable' => 'no').services.empty? }
46
+ .add(:enabled?) { |x| x.where('disable' => 'yes').services.empty? }
47
+ .connect(self, :service_lines)
88
48
 
89
49
  private
90
50
 
91
- # Retrieve the provided field from all configured services.
92
- #
93
- # @param [String] field name, e.g. `socket_type`
94
- # @return [Array[String]] all values of this field across services
95
- def services_field(field)
96
- params['services'].values.compact.flatten
97
- .map { |x| x.params[field] }.flatten.compact
98
- end
99
-
100
- def match_condition(sth, condition)
101
- case sth
102
- # this does Regex-matching as well as string comparison
103
- when condition
104
- true
105
- else
106
- false
107
- end
108
- end
109
-
110
- # Filter services by a criteria. This allows for search queries for
111
- # certain values.
112
- #
113
- # @param [Hash] service collection
114
- # @param [String] search key you want to query
115
- # @param [Any] search value that the key should match
116
- # @return [Hash] filtered service collection
117
- def filter_by(services, k, v)
118
- if k == 'service'
119
- return Hash[services.find_all { |name, _| match_condition(v, name) }]
120
- end
121
- Hash[services.map { |name, service_arr|
122
- found = service_arr.find_all { |service|
123
- match_condition(service.params[k], v)
124
- }
125
- found.empty? ? nil : [name, found]
126
- }.compact]
127
- end
128
-
129
51
  def read_content(path = @conf_path)
130
52
  return @contents[path] if @contents.key?(path)
131
53
  file = inspec.file(path)
@@ -140,5 +62,28 @@ module Inspec::Resources
140
62
 
141
63
  @contents[path]
142
64
  end
65
+
66
+ def read_params
67
+ return {} if read_content.nil?
68
+ flat_params = parse_xinetd(read_content)
69
+ params = { 'services' => {} }
70
+
71
+ # parse services that were defined:
72
+ flat_params.each do |k, v|
73
+ name = k[/^service (.+)$/, 1]
74
+ if name.nil?
75
+ params[k] = v
76
+ else
77
+ params['services'][name] = v
78
+ # add the service identifier to its parameters
79
+ v.each { |service| service.params['service'] = name }
80
+ end
81
+ end
82
+ params
83
+ end
84
+
85
+ def service_lines
86
+ @services ||= params['services'].values.flatten.map(&:params)
87
+ end
143
88
  end
144
89
  end
@@ -0,0 +1,184 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Stephan Renatus
4
+ # author: Christoph Hartmann
5
+
6
+ module FilterTable
7
+ module Show; end
8
+
9
+ class Trace
10
+ def initialize
11
+ @chain = []
12
+ end
13
+
14
+ %w{== != >= > < <= =~ !~}.each do |m|
15
+ define_method m.to_sym do |*args|
16
+ res = Trace.new
17
+ @chain.push([[m.to_sym] + args, res])
18
+ res
19
+ end
20
+ end
21
+
22
+ def method_missing(*args)
23
+ res = Trace.new
24
+ @chain.push([args, res])
25
+ res
26
+ end
27
+
28
+ def self.to_ruby(trace)
29
+ chain = trace.instance_variable_get(:@chain)
30
+ return '' if chain.empty?
31
+ ' ' + chain.map do |el|
32
+ m = el[0][0]
33
+ args = el[0].drop(1)
34
+ nxt = to_ruby(el[1])
35
+ next m.to_s + nxt if args.empty?
36
+ next m.to_s + ' ' + args[0].inspect + nxt if args.length == 1
37
+ m.to_s + '(' + args.map(&:inspect).join(', ') + ')' + nxt
38
+ end.join(' ')
39
+ end
40
+ end
41
+
42
+ class Table
43
+ attr_reader :params
44
+ def initialize(resource, params, filters)
45
+ @resource = resource
46
+ @params = params
47
+ @filters = filters
48
+ end
49
+
50
+ def where(conditions = {}, &block)
51
+ return self if !conditions.is_a?(Hash)
52
+ return self if conditions.empty? && !block_given?
53
+
54
+ filters = ''
55
+ table = @params
56
+ conditions.each do |field, condition|
57
+ filters += " #{field} == #{condition.inspect}"
58
+ table = filter_lines(table, field, condition)
59
+ end
60
+
61
+ if block_given?
62
+ table = table.find_all { |e| new_entry(e, '').instance_eval(&block) }
63
+ src = Trace.new
64
+ src.instance_eval(&block)
65
+ filters += Trace.to_ruby(src)
66
+ end
67
+
68
+ self.class.new(@resource, table, @filters + filters)
69
+ end
70
+
71
+ def new_entry(*_)
72
+ fail "#{self.class} must not be used on its own. It must be inherited "\
73
+ 'and the #new_entry method must be implemented. This is an internal '\
74
+ 'error and should not happen.'
75
+ end
76
+
77
+ def entries
78
+ f = @resource.to_s + @filters.to_s + ' one entry'
79
+ @params.map do |line|
80
+ new_entry(line, f)
81
+ end
82
+ end
83
+
84
+ def get_fields(*fields)
85
+ @params.map do |line|
86
+ fields.map { |f| line[f] }
87
+ end.flatten
88
+ end
89
+
90
+ def to_s
91
+ @resource.to_s + @filters
92
+ end
93
+
94
+ alias inspect to_s
95
+
96
+ private
97
+
98
+ def filter_lines(table, field, condition)
99
+ table.find_all do |line|
100
+ next unless line.key?(field)
101
+ case line[field]
102
+ when condition
103
+ true
104
+ else
105
+ false
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ class Factory
112
+ def initialize
113
+ @accessors = []
114
+ @fields = {}
115
+ @blocks = {}
116
+ end
117
+
118
+ def connect(resource, table_accessor) # rubocop:disable Metrics/AbcSize
119
+ # create the table structure
120
+ fields = @fields
121
+ blocks = @blocks
122
+ struct_fields = fields.values
123
+
124
+ # the struct to hold single items from the #entries method
125
+ entry_struct = Struct.new(*struct_fields.map(&:to_sym)) do
126
+ attr_accessor :__filter
127
+ def to_s # rubocop:disable Lint/NestedMethodDefinition
128
+ @__filter || super
129
+ end
130
+ end unless struct_fields.empty?
131
+
132
+ # the main filter table
133
+ table = Class.new(Table) {
134
+ fields.each do |method, field_name|
135
+ block = blocks[method]
136
+ define_method method.to_sym do |condition = Show, &cond_block|
137
+ return block.call(self) unless block.nil?
138
+ return where(nil).get_fields(field_name) if condition == Show && !block_given?
139
+ where({ field_name => condition }, &cond_block)
140
+ end
141
+ end
142
+
143
+ define_method :new_entry do |hashmap, filter = ''|
144
+ return entry_struct.new if hashmap.nil?
145
+ res = entry_struct.new(*struct_fields.map { |x| hashmap[x] })
146
+ res.__filter = filter
147
+ res
148
+ end
149
+ }
150
+
151
+ # define all access methods with the parent resource
152
+ accessors = @accessors + @fields.keys
153
+ accessors.each do |method_name|
154
+ resource.send(:define_method, method_name.to_sym) do |*args, &block|
155
+ filter = table.new(self, method(table_accessor).call, ' with')
156
+ filter.method(method_name.to_sym).call(*args, &block)
157
+ end
158
+ end
159
+ end
160
+
161
+ def add_accessor(method_name)
162
+ if method_name.nil?
163
+ throw RuntimeError, "Called filter.add_delegator for resource #{@resource} with method name nil!"
164
+ end
165
+ @accessors.push(method_name)
166
+ self
167
+ end
168
+
169
+ def add(method_name, opts = {}, &block)
170
+ if method_name.nil?
171
+ throw RuntimeError, "Called filter.add for resource #{@resource} with method name nil!"
172
+ end
173
+
174
+ field_name = opts[:field] || method_name
175
+ @fields[method_name.to_sym] = field_name
176
+ @blocks[method_name.to_sym] = block
177
+ self
178
+ end
179
+ end
180
+
181
+ def self.create
182
+ Factory.new
183
+ end
184
+ end
@@ -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