inspec 0.19.3 → 0.20.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.
- checksums.yaml +5 -13
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +30 -2
- data/inspec.gemspec +1 -1
- data/lib/bundles/inspec-compliance.rb +1 -0
- data/lib/bundles/inspec-compliance/.kitchen.yml +21 -0
- data/lib/bundles/inspec-compliance/README.md +24 -0
- data/lib/bundles/inspec-compliance/bootstrap.sh +37 -0
- data/lib/bundles/inspec-compliance/cli.rb +2 -2
- data/lib/bundles/inspec-compliance/support.rb +36 -0
- data/lib/bundles/inspec-compliance/target.rb +3 -5
- data/lib/bundles/inspec-compliance/test/integration/default/cli.rb +56 -0
- data/lib/fetchers/url.rb +7 -2
- data/lib/inspec/backend.rb +1 -1
- data/lib/inspec/cli.rb +13 -13
- data/lib/inspec/plugins/fetcher.rb +1 -0
- data/lib/inspec/version.rb +1 -1
- data/lib/resources/file.rb +11 -23
- data/lib/resources/os.rb +10 -1
- data/lib/resources/package.rb +16 -0
- data/lib/resources/user.rb +14 -0
- data/lib/resources/xinetd.rb +39 -94
- data/lib/utils/filter.rb +184 -0
- data/lib/utils/hash_map.rb +37 -0
- data/test/functional/inspec_test.rb +23 -0
- data/test/helper.rb +5 -0
- data/test/resource/file_test.rb +3 -1
- data/test/unit/{fetchers.rb → fetchers_test.rb} +1 -0
- data/test/unit/mock/cmd/logins-x +4 -0
- data/test/unit/mock/cmd/swlist-l-product +1 -0
- data/test/unit/mock/profiles/resource-tiny/inspec.yml +10 -0
- data/test/unit/mock/profiles/resource-tiny/libraries/resource.rb +3 -0
- data/test/unit/resources/file_test.rb +21 -0
- data/test/unit/resources/package_test.rb +9 -0
- data/test/unit/resources/user_test.rb +6 -0
- data/test/unit/resources/xinetd_test.rb +3 -3
- data/test/unit/utils/filter_table_test.rb +125 -0
- metadata +46 -31
- 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
|
data/lib/resources/package.rb
CHANGED
@@ -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:
|
data/lib/resources/user.rb
CHANGED
@@ -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
|
data/lib/resources/xinetd.rb
CHANGED
@@ -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)
|
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'
|
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
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
data/lib/utils/filter.rb
ADDED
@@ -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
|