inspec-core 2.1.84 → 2.2.10
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 +4 -4
- data/CHANGELOG.md +31 -8
- data/README.md +1 -0
- data/docs/dev/filtertable-internals.md +353 -0
- data/docs/dev/filtertable-usage.md +533 -0
- data/docs/matchers.md +36 -36
- data/docs/profiles.md +2 -2
- data/docs/resources/apache.md.erb +1 -1
- data/docs/resources/crontab.md.erb +10 -6
- data/docs/resources/dh_params.md.erb +71 -65
- data/docs/resources/docker_service.md.erb +1 -1
- data/docs/resources/etc_fstab.md.erb +1 -1
- data/docs/resources/firewalld.md.erb +1 -1
- data/docs/resources/http.md.erb +1 -1
- data/docs/resources/iis_app.md.erb +1 -1
- data/docs/resources/inetd_conf.md.erb +1 -1
- data/docs/resources/nginx.md.erb +1 -1
- data/docs/resources/npm.md.erb +9 -1
- data/docs/resources/os.md.erb +21 -19
- data/docs/resources/shadow.md.erb +37 -31
- data/docs/resources/x509_certificate.md.erb +2 -2
- data/examples/custom-resource/README.md +3 -0
- data/examples/custom-resource/controls/example.rb +7 -0
- data/examples/custom-resource/inspec.yml +8 -0
- data/examples/custom-resource/libraries/batsignal.rb +20 -0
- data/examples/custom-resource/libraries/gordon.rb +21 -0
- data/lib/inspec/reporters/junit.rb +1 -0
- data/lib/inspec/resource.rb +8 -0
- data/lib/inspec/version.rb +1 -1
- data/lib/resources/npm.rb +15 -2
- data/lib/resources/package.rb +1 -1
- data/lib/utils/filter.rb +243 -85
- metadata +9 -2
@@ -8,7 +8,7 @@ platform: linux
|
|
8
8
|
Use the `shadow` InSpec audit resource to test the contents of `/etc/shadow`, which contains password details that are only readable by the `root` user. The format for `/etc/shadow` includes:
|
9
9
|
|
10
10
|
* A username
|
11
|
-
* The password for that user
|
11
|
+
* The hashed password for that user
|
12
12
|
* The last time a password was changed
|
13
13
|
* The minimum number of days a password must exist, before it may be changed
|
14
14
|
* The maximum number of days after which a password must be changed
|
@@ -24,22 +24,26 @@ These entries are defined as a colon-delimited row in the file, one row per user
|
|
24
24
|
|
25
25
|
## Syntax
|
26
26
|
|
27
|
-
A `shadow` resource block declares
|
27
|
+
A `shadow` resource block declares user properties to be tested:
|
28
28
|
|
29
29
|
describe shadow do
|
30
30
|
its('user') { should_not include 'forbidden_user' }
|
31
31
|
end
|
32
32
|
|
33
|
-
|
33
|
+
Properties can be used as a single query:
|
34
34
|
|
35
35
|
describe shadow.user('root') do
|
36
36
|
its('count') { should eq 1 }
|
37
37
|
end
|
38
38
|
|
39
|
-
|
39
|
+
Use the `.where` method to find properties that match a value:
|
40
40
|
|
41
|
-
describe shadow.
|
42
|
-
its('
|
41
|
+
describe shadow.where { min_days == '0' } do
|
42
|
+
its ('user') { should include 'nfs' }
|
43
|
+
end
|
44
|
+
|
45
|
+
describe shadow.where { password =~ /[x|!|*]/ } do
|
46
|
+
its('count') { should eq 0 }
|
43
47
|
end
|
44
48
|
|
45
49
|
The following properties are available:
|
@@ -54,8 +58,6 @@ The following properties are available:
|
|
54
58
|
* `expiry_date`
|
55
59
|
* `reserved`
|
56
60
|
|
57
|
-
Properties can be used as a single query or can be joined together with the `.filter` method.
|
58
|
-
|
59
61
|
<br>
|
60
62
|
|
61
63
|
## Examples
|
@@ -77,31 +79,17 @@ The following examples show how to use this InSpec audit resource.
|
|
77
79
|
|
78
80
|
<br>
|
79
81
|
|
80
|
-
##
|
81
|
-
|
82
|
-
For a full list of available matchers, please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
|
83
|
-
|
84
|
-
### count
|
85
|
-
|
86
|
-
The `count` matcher tests the number of times the named user appears in `/etc/shadow`:
|
87
|
-
|
88
|
-
its('count') { should eq 1 }
|
89
|
-
|
90
|
-
This matcher is best used in conjunction with filters. For example:
|
91
|
-
|
92
|
-
describe shadow.user('dannos') do
|
93
|
-
its('count') { should eq 1 }
|
94
|
-
end
|
82
|
+
## Properties
|
95
83
|
|
96
84
|
### user
|
97
85
|
|
98
|
-
The `user`
|
86
|
+
The `user` property tests if the username exists `/etc/shadow`:
|
99
87
|
|
100
88
|
its('user') { should eq 'root' }
|
101
89
|
|
102
90
|
### password
|
103
91
|
|
104
|
-
The `password`
|
92
|
+
The `password` property returns the encrypted password string from the shadow file. The returned string may not be an encrypted password, but rather a `*` or similar which indicates that direct logins are not allowed.
|
105
93
|
|
106
94
|
For example:
|
107
95
|
|
@@ -109,38 +97,56 @@ For example:
|
|
109
97
|
|
110
98
|
### last_change
|
111
99
|
|
112
|
-
The `last_change`
|
100
|
+
The `last_change` property tests the last time a password was changed:
|
113
101
|
|
114
102
|
its('last_change') { should be_empty }
|
115
103
|
|
116
104
|
### min_days
|
117
105
|
|
118
|
-
The `min_days`
|
106
|
+
The `min_days` property tests the minimum number of days a password must exist, before it may be changed:
|
119
107
|
|
120
108
|
its('min_days') { should eq 0 }
|
121
109
|
|
122
110
|
### max_days
|
123
111
|
|
124
|
-
The `max_days`
|
112
|
+
The `max_days` property tests the maximum number of days after which a password must be changed:
|
125
113
|
|
126
114
|
its('max_days') { should eq 90 }
|
127
115
|
|
128
116
|
### warn_days
|
129
117
|
|
130
|
-
The `warn_days`
|
118
|
+
The `warn_days` property tests the number of days a user is warned about an expiring password:
|
131
119
|
|
132
120
|
its('warn_days') { should eq 7 }
|
133
121
|
|
134
122
|
### inactive_days
|
135
123
|
|
136
|
-
The `inactive_days`
|
124
|
+
The `inactive_days` property tests the number of days a user must be inactive before the user account is disabled:
|
137
125
|
|
138
126
|
its('inactive_days') { should be_empty }
|
139
127
|
|
140
128
|
### expiry_date
|
141
129
|
|
142
|
-
The `expiry_date`
|
130
|
+
The `expiry_date` property tests the number of days a user account has been disabled:
|
143
131
|
|
144
132
|
its('expiry_date') { should be_empty }
|
145
133
|
|
134
|
+
### count
|
135
|
+
|
136
|
+
The `count` property tests the number of times the named property appears:
|
137
|
+
|
138
|
+
describe shadow.user('root') do
|
139
|
+
its('count') { should eq 1 }
|
140
|
+
end
|
141
|
+
|
142
|
+
This property is best used in conjunction with filters. For example:
|
146
143
|
|
144
|
+
describe shadow.where { password =~ /[x|!|*]/ } do
|
145
|
+
its('count') { should eq 0 }
|
146
|
+
end
|
147
|
+
|
148
|
+
<br>
|
149
|
+
|
150
|
+
## Matchers
|
151
|
+
|
152
|
+
For a full list of available matchers, please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
|
@@ -92,7 +92,7 @@ sign the certificate.
|
|
92
92
|
end
|
93
93
|
|
94
94
|
|
95
|
-
###
|
95
|
+
### validity\_in\_days (Float)
|
96
96
|
|
97
97
|
The `validity_in_days` property can be used to check that certificates are not in
|
98
98
|
danger of expiring soon.
|
@@ -101,7 +101,7 @@ danger of expiring soon.
|
|
101
101
|
its('validity_in_days') { should be > 30 }
|
102
102
|
end
|
103
103
|
|
104
|
-
###
|
104
|
+
### not\_before and not\_after (Time)
|
105
105
|
|
106
106
|
The `not_before` and `not_after` properties expose the start and end dates of certificate
|
107
107
|
validity. They are exposed as ruby Time class so that date arithmetic can be easily performed.
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Batsignal < Inspec.resource(1)
|
2
|
+
name 'batsignal'
|
3
|
+
|
4
|
+
example "
|
5
|
+
describe batsignal do
|
6
|
+
its('number_of_sightings)') { should eq '4' }
|
7
|
+
end
|
8
|
+
"
|
9
|
+
|
10
|
+
def number_of_sightings
|
11
|
+
local_command_call
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def local_command_call
|
17
|
+
# call out to a core resource
|
18
|
+
inspec.command('echo 4').stdout.to_i
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Gordon < Inspec.resource(1)
|
2
|
+
name 'gordon'
|
3
|
+
|
4
|
+
example "
|
5
|
+
describe gordon do
|
6
|
+
its('crime_rate') { should be < 2 }
|
7
|
+
it { should have_a_fabulous_mustache }
|
8
|
+
end
|
9
|
+
"
|
10
|
+
|
11
|
+
def crime_rate
|
12
|
+
# call out ot another custom resource
|
13
|
+
inspec.batsignal.number_of_sightings
|
14
|
+
end
|
15
|
+
|
16
|
+
def has_a_fabulous_mustache?
|
17
|
+
# always true
|
18
|
+
true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -27,6 +27,7 @@ module Inspec::Reporters
|
|
27
27
|
profile_xml.add_attribute('name', profile[:name])
|
28
28
|
profile_xml.add_attribute('tests', count_profile_tests(profile))
|
29
29
|
profile_xml.add_attribute('failed', count_profile_failed_tests(profile))
|
30
|
+
profile_xml.add_attribute('failures', count_profile_failed_tests(profile))
|
30
31
|
|
31
32
|
profile[:controls].each do |control|
|
32
33
|
next if control[:results].nil?
|
data/lib/inspec/resource.rb
CHANGED
@@ -50,8 +50,16 @@ module Inspec
|
|
50
50
|
define_method id.to_sym do |*args|
|
51
51
|
r.new(backend, id.to_s, *args)
|
52
52
|
end
|
53
|
+
|
54
|
+
# confirm backend custom resources have access to other custom resources
|
55
|
+
next if backend.respond_to?(id)
|
56
|
+
backend.class.send(:define_method, id.to_sym) do |*args|
|
57
|
+
r.new(backend, id.to_s, *args)
|
58
|
+
end
|
53
59
|
end
|
54
60
|
|
61
|
+
# attach backend so we have access to all resources and
|
62
|
+
# the train connection object
|
55
63
|
define_method :inspec do
|
56
64
|
backend
|
57
65
|
end
|
data/lib/inspec/version.rb
CHANGED
data/lib/resources/npm.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
require 'shellwords'
|
4
|
+
|
3
5
|
module Inspec::Resources
|
4
6
|
class NpmPackage < Inspec.resource(1)
|
5
7
|
name 'npm'
|
@@ -10,17 +12,28 @@ module Inspec::Resources
|
|
10
12
|
describe npm('bower') do
|
11
13
|
it { should be_installed }
|
12
14
|
end
|
15
|
+
|
16
|
+
describe npm('tar', path: '/path/to/project') do
|
17
|
+
it { should be_installed }
|
18
|
+
end
|
13
19
|
"
|
14
20
|
|
15
|
-
def initialize(package_name)
|
21
|
+
def initialize(package_name, opts = {})
|
16
22
|
@package_name = package_name
|
23
|
+
@location = opts[:path]
|
17
24
|
@cache = nil
|
18
25
|
end
|
19
26
|
|
20
27
|
def info
|
21
28
|
return @info if defined?(@info)
|
22
29
|
|
23
|
-
|
30
|
+
if @location
|
31
|
+
npm = "cd #{Shellwords.escape @location} && npm"
|
32
|
+
else
|
33
|
+
npm = 'npm -g'
|
34
|
+
end
|
35
|
+
|
36
|
+
cmd = inspec.command("#{npm} ls --json #{@package_name}")
|
24
37
|
@info = {
|
25
38
|
name: @package_name,
|
26
39
|
type: 'npm',
|
data/lib/resources/package.rb
CHANGED
@@ -270,7 +270,7 @@ module Inspec::Resources
|
|
270
270
|
# Find the package
|
271
271
|
cmd = inspec.command <<-EOF.gsub(/^\s*/, '')
|
272
272
|
Get-ItemProperty (@("#{search_paths.join('", "')}") | Where-Object { Test-Path $_ }) |
|
273
|
-
Where-Object { $_.DisplayName -
|
273
|
+
Where-Object { $_.DisplayName -match "^\\s*#{package_name}\\s*$" -or $_.PSChildName -match "^\\s*#{package_name}\\s*$" } |
|
274
274
|
Select-Object -Property DisplayName,DisplayVersion | ConvertTo-Json
|
275
275
|
EOF
|
276
276
|
|
data/lib/utils/filter.rb
CHANGED
@@ -4,7 +4,8 @@
|
|
4
4
|
# author: Christoph Hartmann
|
5
5
|
|
6
6
|
module FilterTable
|
7
|
-
|
7
|
+
# This is used as a sentinel value in custom property filtering
|
8
|
+
module NoCriteriaProvided; end
|
8
9
|
|
9
10
|
class ExceptionCatcher
|
10
11
|
def initialize(original_resource, original_exception)
|
@@ -82,60 +83,139 @@ module FilterTable
|
|
82
83
|
end
|
83
84
|
|
84
85
|
class Table
|
85
|
-
attr_reader :
|
86
|
-
def initialize(
|
87
|
-
@
|
88
|
-
@
|
89
|
-
@
|
90
|
-
@
|
86
|
+
attr_reader :raw_data, :resource_instance, :criteria_string
|
87
|
+
def initialize(resource_instance, raw_data, criteria_string)
|
88
|
+
@resource_instance = resource_instance
|
89
|
+
@raw_data = raw_data
|
90
|
+
@raw_data = [] if @raw_data.nil?
|
91
|
+
@criteria_string = criteria_string
|
92
|
+
@populated_lazy_columns = {}
|
91
93
|
end
|
92
94
|
|
95
|
+
# Filter the raw data based on criteria (as method params) or by evaling a
|
96
|
+
# block; then construct a new Table of the same class as ourselves,
|
97
|
+
# wrapping the filtered data, and return it.
|
93
98
|
def where(conditions = {}, &block)
|
94
99
|
return self if !conditions.is_a?(Hash)
|
95
100
|
return self if conditions.empty? && !block_given?
|
96
101
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
+
# Initialize the details of the new Table.
|
103
|
+
new_criteria_string = criteria_string
|
104
|
+
filtered_raw_data = raw_data
|
105
|
+
|
106
|
+
# If we were provided params, interpret them as criteria to be evaluated
|
107
|
+
# against the raw data. Criteria are assumed to be hash keys.
|
108
|
+
conditions.each do |raw_field_name, desired_value|
|
109
|
+
raise(ArgumentError, "'#{raw_field_name}' is not a recognized criterion - expected one of #{list_fields.join(', ')}'") unless field?(raw_field_name)
|
110
|
+
populate_lazy_field(raw_field_name, desired_value) if is_field_lazy?(raw_field_name)
|
111
|
+
new_criteria_string += " #{raw_field_name} == #{desired_value.inspect}"
|
112
|
+
filtered_raw_data = filter_raw_data(filtered_raw_data, raw_field_name, desired_value)
|
102
113
|
end
|
103
114
|
|
115
|
+
# If we were given a block, make a special Struct for each row, that has an accessor
|
116
|
+
# for each field declared using `register_custom_property`, then instance-eval the block
|
117
|
+
# against the struct.
|
104
118
|
if block_given?
|
105
|
-
|
119
|
+
# Perform the filtering.
|
120
|
+
filtered_raw_data = filtered_raw_data.find_all { |row_as_hash| create_eval_context_for_row(row_as_hash, '').instance_eval(&block) }
|
121
|
+
# Try to interpret the block for updating the stringification.
|
106
122
|
src = Trace.new
|
107
|
-
|
108
|
-
|
123
|
+
# Swallow any exceptions raised here.
|
124
|
+
# See https://github.com/chef/inspec/issues/2929
|
125
|
+
begin
|
126
|
+
src.instance_eval(&block)
|
127
|
+
rescue # rubocop: disable Lint/HandleExceptions
|
128
|
+
# Yes, robocop, ignoring all exceptions is normally
|
129
|
+
# a bad idea. Here, an exception just means we don't
|
130
|
+
# understand what was in a `where` block, so we can't
|
131
|
+
# meaningfully sytringify it. We still have a decent
|
132
|
+
# default stringification.
|
133
|
+
end
|
134
|
+
new_criteria_string += Trace.to_ruby(src)
|
109
135
|
end
|
110
136
|
|
111
|
-
self.class.new(
|
137
|
+
self.class.new(resource, filtered_raw_data, new_criteria_string)
|
112
138
|
end
|
113
139
|
|
114
|
-
def
|
140
|
+
def create_eval_context_for_row(*_)
|
115
141
|
raise "#{self.class} must not be used on its own. It must be inherited "\
|
116
|
-
'and the #
|
142
|
+
'and the #create_eval_context_for_row method must be implemented. This is an internal '\
|
117
143
|
'error and should not happen.'
|
118
144
|
end
|
119
145
|
|
146
|
+
def resource
|
147
|
+
resource_instance
|
148
|
+
end
|
149
|
+
|
150
|
+
def params
|
151
|
+
# TODO: consider deprecating
|
152
|
+
raw_data
|
153
|
+
end
|
154
|
+
|
120
155
|
def entries
|
121
|
-
|
122
|
-
|
123
|
-
|
156
|
+
row_criteria_string = resource.to_s + criteria_string + ' one entry'
|
157
|
+
raw_data.map do |row|
|
158
|
+
create_eval_context_for_row(row, row_criteria_string)
|
124
159
|
end
|
125
160
|
end
|
126
161
|
|
127
|
-
def
|
128
|
-
|
129
|
-
|
162
|
+
def get_column_values(field)
|
163
|
+
raw_data.map do |row|
|
164
|
+
row[field]
|
130
165
|
end
|
131
166
|
end
|
132
167
|
|
168
|
+
def list_fields
|
169
|
+
@__fields_in_raw_data ||= raw_data.reduce([]) do |fields, row|
|
170
|
+
fields.concat(row.keys).uniq
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def field?(proposed_field)
|
175
|
+
# Currently we only know about a field if it is present in a at least one row of the raw data.
|
176
|
+
# If we have no rows in the raw data, assume all fields are acceptable (and rely on failing to match on value, nil)
|
177
|
+
return true if raw_data.empty?
|
178
|
+
list_fields.include?(proposed_field) || is_field_lazy?(proposed_field)
|
179
|
+
end
|
180
|
+
|
133
181
|
def to_s
|
134
|
-
|
182
|
+
resource.to_s + criteria_string
|
135
183
|
end
|
136
184
|
|
137
185
|
alias inspect to_s
|
138
186
|
|
187
|
+
def populate_lazy_field(field_name, criterion)
|
188
|
+
return unless is_field_lazy?(field_name)
|
189
|
+
return if field_populated?(field_name)
|
190
|
+
raw_data.each do |row|
|
191
|
+
next if row.key?(field_name) # skip row if pre-existing data is present
|
192
|
+
callback_for_lazy_field(field_name).call(row, criterion, self)
|
193
|
+
end
|
194
|
+
mark_lazy_field_populated(field_name)
|
195
|
+
end
|
196
|
+
|
197
|
+
def is_field_lazy?(sought_field_name)
|
198
|
+
custom_properties_schema.values.any? do |property_struct|
|
199
|
+
sought_field_name == property_struct.field_name && \
|
200
|
+
property_struct.opts[:lazy]
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def callback_for_lazy_field(field_name)
|
205
|
+
return unless is_field_lazy?(field_name)
|
206
|
+
custom_properties_schema.values.find do |property_struct|
|
207
|
+
property_struct.field_name == field_name
|
208
|
+
end.opts[:lazy]
|
209
|
+
end
|
210
|
+
|
211
|
+
def field_populated?(field_name)
|
212
|
+
@populated_lazy_columns[field_name]
|
213
|
+
end
|
214
|
+
|
215
|
+
def mark_lazy_field_populated(field_name)
|
216
|
+
@populated_lazy_columns[field_name] = true
|
217
|
+
end
|
218
|
+
|
139
219
|
private
|
140
220
|
|
141
221
|
def matches_float(x, y)
|
@@ -159,111 +239,189 @@ module FilterTable
|
|
159
239
|
x === y # rubocop:disable Style/CaseEquality
|
160
240
|
end
|
161
241
|
|
162
|
-
def
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
next unless
|
172
|
-
|
242
|
+
def filter_raw_data(current_raw_data, field, desired_value)
|
243
|
+
method_ref = case desired_value
|
244
|
+
when Float then method(:matches_float)
|
245
|
+
when Integer then method(:matches_int)
|
246
|
+
when Regexp then method(:matches_regex)
|
247
|
+
else method(:matches)
|
248
|
+
end
|
249
|
+
|
250
|
+
current_raw_data.find_all do |row|
|
251
|
+
next unless row.key?(field)
|
252
|
+
method_ref.call(row[field], desired_value)
|
173
253
|
end
|
174
254
|
end
|
175
255
|
end
|
176
256
|
|
177
257
|
class Factory
|
178
|
-
|
258
|
+
CustomPropertyType = Struct.new(:field_name, :block, :opts)
|
179
259
|
|
180
260
|
def initialize
|
181
|
-
@
|
182
|
-
@
|
183
|
-
|
261
|
+
@filter_methods = [:where, :entries, :raw_data]
|
262
|
+
@custom_properties = {}
|
263
|
+
register_custom_matcher(:exist?) { |table| !table.raw_data.empty? }
|
264
|
+
register_custom_property(:count) { |table| table.raw_data.count }
|
265
|
+
|
266
|
+
@resource = nil # TODO: this variable is never initialized
|
184
267
|
end
|
185
268
|
|
186
|
-
def
|
187
|
-
#
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
269
|
+
def install_filter_methods_on_resource(resource_class, raw_data_fetcher_method_name) # rubocop: disable Metrics/AbcSize, Metrics/MethodLength
|
270
|
+
# A context in which you can access the fields as accessors
|
271
|
+
non_block_struct_fields = @custom_properties.values.reject(&:block).map(&:field_name)
|
272
|
+
row_eval_context_type = Struct.new(*non_block_struct_fields.map(&:to_sym)) do
|
273
|
+
attr_accessor :criteria_string
|
274
|
+
attr_accessor :filter_table
|
275
|
+
def to_s
|
276
|
+
@criteria_string || super
|
277
|
+
end
|
278
|
+
end unless non_block_struct_fields.empty?
|
279
|
+
|
280
|
+
properties_to_define = @custom_properties.map do |method_name, custom_property_structure|
|
281
|
+
{ method_name: method_name, method_body: create_custom_property_body(custom_property_structure) }
|
192
282
|
end
|
193
283
|
|
194
|
-
# the
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
284
|
+
# Define the filter table subclass
|
285
|
+
custom_properties = @custom_properties # We need a local var, not an instance var, for a closure below
|
286
|
+
table_class = Class.new(Table) {
|
287
|
+
# Install each custom property onto the FilterTable subclass
|
288
|
+
properties_to_define.each do |property_info|
|
289
|
+
define_method property_info[:method_name], &property_info[:method_body]
|
199
290
|
end
|
200
|
-
end unless struct_fields.empty?
|
201
291
|
|
202
|
-
|
203
|
-
|
204
|
-
connector_blocks.each do |x|
|
205
|
-
define_method x[0], &x[1]
|
292
|
+
define_method :custom_properties_schema do
|
293
|
+
custom_properties
|
206
294
|
end
|
207
295
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
296
|
+
# Install a method that can wrap all the fields into a context with accessors
|
297
|
+
define_method :create_eval_context_for_row do |row_as_hash, criteria_string = ''|
|
298
|
+
return row_eval_context_type.new if row_as_hash.nil?
|
299
|
+
context = row_eval_context_type.new(*non_block_struct_fields.map { |field| row_as_hash[field] })
|
300
|
+
context.criteria_string = criteria_string
|
301
|
+
context.filter_table = self
|
302
|
+
context
|
213
303
|
end
|
214
304
|
}
|
215
305
|
|
306
|
+
# Now that the table class is defined and the row eval context struct is defined,
|
307
|
+
# extend the row eval context struct to support triggering population of lazy fields
|
308
|
+
# in where blocks. To do that, we'll need a reference to the table (which
|
309
|
+
# knows which fields are populated, and how to populate them) and we'll need to
|
310
|
+
# override the getter method for each lazy field, so it will trigger
|
311
|
+
# population if needed. Keep in mind we don't have to adjust the constructor
|
312
|
+
# args of the row struct; also the Struct class will already have provided
|
313
|
+
# a setter for each field.
|
314
|
+
@custom_properties.values.each do |property_info|
|
315
|
+
next unless property_info.opts[:lazy]
|
316
|
+
field_name = property_info.field_name.to_sym
|
317
|
+
row_eval_context_type.send(:define_method, field_name) do
|
318
|
+
unless filter_table.field_populated?(field_name)
|
319
|
+
filter_table.populate_lazy_field(field_name, NoCriteriaProvided) # No access to criteria here
|
320
|
+
# OK, the underlying raw data has the value in the first row
|
321
|
+
# (because we would trigger population only on the first row)
|
322
|
+
# We could just return the value, but we need to set it on this Struct in case it is referenced multiple times
|
323
|
+
# in the where block.
|
324
|
+
self[field_name] = filter_table.raw_data[0][field_name]
|
325
|
+
end
|
326
|
+
# Now return the value using the Struct getter, whether newly populated or not
|
327
|
+
self[field_name]
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
216
331
|
# Define all access methods with the parent resource
|
217
332
|
# These methods will be configured to return an `ExceptionCatcher` object
|
218
333
|
# that will always return the original exception, but only when called
|
219
334
|
# upon. This will allow method chains in `describe` statements to pass the
|
220
335
|
# `instance_eval` when loaded and only throw-and-catch the exception when
|
221
336
|
# the tests are run.
|
222
|
-
|
223
|
-
|
224
|
-
|
337
|
+
methods_to_install_on_resource_class = @filter_methods + @custom_properties.keys
|
338
|
+
methods_to_install_on_resource_class.each do |method_name|
|
339
|
+
resource_class.send(:define_method, method_name.to_sym) do |*args, &block|
|
225
340
|
begin
|
226
|
-
|
227
|
-
|
341
|
+
# self here is the resource instance
|
342
|
+
filter_table_instance = table_class.new(self, method(raw_data_fetcher_method_name).call, ' with')
|
343
|
+
filter_table_instance.method(method_name.to_sym).call(*args, &block)
|
228
344
|
rescue Inspec::Exceptions::ResourceFailed, Inspec::Exceptions::ResourceSkipped => e
|
229
|
-
FilterTable::ExceptionCatcher.new(
|
345
|
+
FilterTable::ExceptionCatcher.new(resource_class, e)
|
230
346
|
end
|
231
347
|
end
|
232
348
|
end
|
233
349
|
end
|
234
350
|
|
235
|
-
|
351
|
+
alias connect install_filter_methods_on_resource
|
352
|
+
|
353
|
+
# TODO: This should almost certainly be privatized. Every FilterTable client should get :entries and :where;
|
354
|
+
# InSpec core resources do not define anything else, other than azure_generic_resource, which is likely a mis-use.
|
355
|
+
def register_filter_method(method_name)
|
236
356
|
if method_name.nil?
|
237
|
-
|
357
|
+
# TODO: @resource is never initialized
|
358
|
+
throw RuntimeError, "Called filter.add_accessor for resource #{@resource} with method name nil!"
|
359
|
+
end
|
360
|
+
if @filter_methods.include? method_name.to_sym
|
361
|
+
# TODO: issue deprecation warning?
|
362
|
+
else
|
363
|
+
@filter_methods.push(method_name.to_sym)
|
238
364
|
end
|
239
|
-
@accessors.push(method_name)
|
240
365
|
self
|
241
366
|
end
|
242
367
|
|
243
|
-
|
244
|
-
|
368
|
+
alias add_accessor register_filter_method
|
369
|
+
|
370
|
+
def register_custom_property(property_name, opts = {}, &property_implementation)
|
371
|
+
if property_name.nil?
|
372
|
+
# TODO: @resource is never initialized
|
245
373
|
throw RuntimeError, "Called filter.add for resource #{@resource} with method name nil!"
|
246
374
|
end
|
247
375
|
|
248
|
-
@
|
249
|
-
|
376
|
+
if @custom_properties.key?(property_name.to_sym)
|
377
|
+
# TODO: issue deprecation warning?
|
378
|
+
else
|
379
|
+
@custom_properties[property_name.to_sym] =
|
380
|
+
CustomPropertyType.new(opts[:field] || property_name, property_implementation, opts)
|
381
|
+
end
|
250
382
|
self
|
251
383
|
end
|
252
384
|
|
253
|
-
|
385
|
+
alias add register_custom_property
|
386
|
+
alias register_column register_custom_property
|
387
|
+
alias register_custom_matcher register_custom_property
|
254
388
|
|
255
|
-
|
256
|
-
return ->(cond = Show) { c.block.call(self, cond) } if !c.block.nil?
|
389
|
+
private
|
257
390
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
391
|
+
# This provides the implementation for methods requested using
|
392
|
+
# register_custom_property(:some_method_name, opts, &block)
|
393
|
+
# Some usage in the wild involves people passing a desired value to the generated method, like:
|
394
|
+
# things.ids(23)
|
395
|
+
# I'm calling this the 'filter_criterion_value'. I speculate that a default value is
|
396
|
+
# provided here so that users can meaningfully query for nil.
|
397
|
+
def create_custom_property_body(custom_property_struct)
|
398
|
+
if !custom_property_struct.block.nil?
|
399
|
+
# If the custom method provided its own block, rely on it.
|
400
|
+
lambda do |filter_criteria_value = NoCriteriaProvided|
|
401
|
+
# Here self is an instance of the FilterTable subclass that wraps the raw data.
|
402
|
+
# Call the block with two args - the table instance, and any filter criteria value.
|
403
|
+
custom_property_struct.block.call(self, filter_criteria_value)
|
265
404
|
end
|
266
|
-
|
405
|
+
else
|
406
|
+
# No block definition, so the property was registered using (field: :some_field)
|
407
|
+
# This does however support passing a block to this method, and filtering using it, like Enumerable#select.
|
408
|
+
lambda do |filter_criteria_value = NoCriteriaProvided, &cond_block|
|
409
|
+
if filter_criteria_value == NoCriteriaProvided && !block_given?
|
410
|
+
# No second-order block given. Just return an array of the values in the selected column.
|
411
|
+
result = where(nil)
|
412
|
+
if custom_property_struct.opts[:lazy]
|
413
|
+
result.populate_lazy_field(custom_property_struct.field_name, filter_criteria_value)
|
414
|
+
end
|
415
|
+
result = where(nil).get_column_values(custom_property_struct.field_name) # TODO: the where(nil). is likely unneeded
|
416
|
+
result = result.flatten.uniq.compact if custom_property_struct.opts[:style] == :simple
|
417
|
+
result
|
418
|
+
else
|
419
|
+
# A secondary block was provided. Rely on where() to execute the block, while also filtering on any single value
|
420
|
+
# Suspected bug: if filter_criteria_value == NoCriteriaProvided, this is unlikely to match - see hash condition handling in where() above.
|
421
|
+
where(custom_property_struct.field_name => filter_criteria_value, &cond_block)
|
422
|
+
end
|
423
|
+
end
|
424
|
+
end
|
267
425
|
end
|
268
426
|
end
|
269
427
|
|