inspec-core 2.1.84 → 2.2.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 (on newer systems passwords should be stored in `/etc/shadow` )
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 one (or more) users and associated user information to be tested:
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
- or with a single query:
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
- or with a filter:
39
+ Use the `.where` method to find properties that match a value:
40
40
 
41
- describe shadow.filter(min_days: '0', max_days: '99999') do
42
- its('count') { should eq 1 }
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
- ## Matchers
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` matcher tests if the username exists `/etc/shadow`:
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` matcher 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.
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` matcher tests the last time a password was changed:
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` matcher tests the minimum number of days a password must exist, before it may be changed:
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` matcher tests the maximum number of days after which a password must be changed:
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` matcher tests the number of days a user is warned about an expiring password:
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` matcher tests the number of days a user must be inactive before the user account is disabled:
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` matcher tests the number of days a user account has been disabled:
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
- ### validity_in_days (Float)
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
- ### not_before and not_after (Time)
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,3 @@
1
+ # Example Custom Resource Profile
2
+
3
+ This example shows the implementation of an InSpec profile with custom resources.
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ describe gordon do
4
+ its('crime_rate') { should be < 5 }
5
+ it { should have_a_fabulous_mustache }
6
+ end
7
+
@@ -0,0 +1,8 @@
1
+ name: custom_resource_example
2
+ title: InSpec Profile
3
+ maintainer: The Authors
4
+ copyright: The Authors
5
+ copyright_email: you@example.com
6
+ license: Apache-2.0
7
+ summary: An InSpec Compliance Profile
8
+ version: 0.1.0
@@ -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?
@@ -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
@@ -4,5 +4,5 @@
4
4
  # author: Christoph Hartmann
5
5
 
6
6
  module Inspec
7
- VERSION = '2.1.84'
7
+ VERSION = '2.2.10'
8
8
  end
@@ -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
- cmd = inspec.command("npm ls -g --json #{@package_name}")
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',
@@ -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 -like "#{package_name}" -or $_.PSChildName -like "#{package_name}" } |
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
 
@@ -4,7 +4,8 @@
4
4
  # author: Christoph Hartmann
5
5
 
6
6
  module FilterTable
7
- module Show; end
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 :params, :resource
86
- def initialize(resource, params, filters)
87
- @resource = resource
88
- @params = params
89
- @params = [] if @params.nil?
90
- @filters = filters
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
- filters = ''
98
- table = @params
99
- conditions.each do |field, condition|
100
- filters += " #{field} == #{condition.inspect}"
101
- table = filter_lines(table, field, condition)
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
- table = table.find_all { |e| new_entry(e, '').instance_eval(&block) }
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
- src.instance_eval(&block)
108
- filters += Trace.to_ruby(src)
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(@resource, table, @filters + filters)
137
+ self.class.new(resource, filtered_raw_data, new_criteria_string)
112
138
  end
113
139
 
114
- def new_entry(*_)
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 #new_entry method must be implemented. This is an internal '\
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
- f = @resource.to_s + @filters.to_s + ' one entry'
122
- @params.map do |line|
123
- new_entry(line, f)
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 get_field(field)
128
- @params.map do |line|
129
- line[field]
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
- @resource.to_s + @filters
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 filter_lines(table, field, condition)
163
- m = case condition
164
- when Float then method(:matches_float)
165
- when Integer then method(:matches_int)
166
- when Regexp then method(:matches_regex)
167
- else method(:matches)
168
- end
169
-
170
- table.find_all do |line|
171
- next unless line.key?(field)
172
- m.call(line[field], condition)
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
- Connector = Struct.new(:field_name, :block, :opts)
258
+ CustomPropertyType = Struct.new(:field_name, :block, :opts)
179
259
 
180
260
  def initialize
181
- @accessors = []
182
- @connectors = {}
183
- @resource = nil
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 connect(resource, table_accessor) # rubocop:disable Metrics/AbcSize
187
- # create the table structure
188
- connectors = @connectors
189
- struct_fields = connectors.values.map(&:field_name)
190
- connector_blocks = connectors.map do |method, c|
191
- [method.to_sym, create_connector(c)]
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 struct to hold single items from the #entries method
195
- entry_struct = Struct.new(*struct_fields.map(&:to_sym)) do
196
- attr_accessor :__filter
197
- def to_s
198
- @__filter || super
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
- # the main filter table
203
- table = Class.new(Table) {
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
- define_method :new_entry do |hashmap, filter = ''|
209
- return entry_struct.new if hashmap.nil?
210
- res = entry_struct.new(*struct_fields.map { |x| hashmap[x] })
211
- res.__filter = filter
212
- res
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
- accessors = @accessors + @connectors.keys
223
- accessors.each do |method_name|
224
- resource.send(:define_method, method_name.to_sym) do |*args, &block|
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
- filter = table.new(self, method(table_accessor).call, ' with')
227
- filter.method(method_name.to_sym).call(*args, &block)
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(resource, e)
345
+ FilterTable::ExceptionCatcher.new(resource_class, e)
230
346
  end
231
347
  end
232
348
  end
233
349
  end
234
350
 
235
- def add_accessor(method_name)
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
- throw RuntimeError, "Called filter.add_delegator for resource #{@resource} with method name nil!"
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
- def add(method_name, opts = {}, &block)
244
- if method_name.nil?
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
- @connectors[method_name.to_sym] =
249
- Connector.new(opts[:field] || method_name, block, opts)
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
- private
385
+ alias add register_custom_property
386
+ alias register_column register_custom_property
387
+ alias register_custom_matcher register_custom_property
254
388
 
255
- def create_connector(c)
256
- return ->(cond = Show) { c.block.call(self, cond) } if !c.block.nil?
389
+ private
257
390
 
258
- lambda { |condition = Show, &cond_block|
259
- if condition == Show && !block_given?
260
- r = where(nil).get_field(c.field_name)
261
- r = r.flatten.uniq.compact if c.opts[:style] == :simple
262
- r
263
- else
264
- where({ c.field_name => condition }, &cond_block)
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