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.
@@ -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