inspec 2.1.84 → 2.2.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -8
  3. data/README.md +1 -0
  4. data/docs/dev/filtertable-internals.md +353 -0
  5. data/docs/dev/filtertable-usage.md +533 -0
  6. data/docs/matchers.md +36 -36
  7. data/docs/profiles.md +2 -2
  8. data/docs/resources/apache.md.erb +1 -1
  9. data/docs/resources/aws_elb.md.erb +144 -0
  10. data/docs/resources/aws_elbs.md.erb +242 -0
  11. data/docs/resources/aws_flow_log.md.erb +118 -0
  12. data/docs/resources/aws_iam_groups.md.erb +34 -1
  13. data/docs/resources/crontab.md.erb +10 -6
  14. data/docs/resources/dh_params.md.erb +71 -65
  15. data/docs/resources/docker_service.md.erb +1 -1
  16. data/docs/resources/etc_fstab.md.erb +1 -1
  17. data/docs/resources/firewalld.md.erb +1 -1
  18. data/docs/resources/http.md.erb +1 -1
  19. data/docs/resources/iis_app.md.erb +1 -1
  20. data/docs/resources/inetd_conf.md.erb +1 -1
  21. data/docs/resources/nginx.md.erb +1 -1
  22. data/docs/resources/npm.md.erb +9 -1
  23. data/docs/resources/os.md.erb +21 -19
  24. data/docs/resources/shadow.md.erb +37 -31
  25. data/docs/resources/x509_certificate.md.erb +2 -2
  26. data/examples/custom-resource/README.md +3 -0
  27. data/examples/custom-resource/controls/example.rb +7 -0
  28. data/examples/custom-resource/inspec.yml +8 -0
  29. data/examples/custom-resource/libraries/batsignal.rb +20 -0
  30. data/examples/custom-resource/libraries/gordon.rb +21 -0
  31. data/lib/inspec/reporters/junit.rb +1 -0
  32. data/lib/inspec/resource.rb +8 -0
  33. data/lib/inspec/version.rb +1 -1
  34. data/lib/resource_support/aws.rb +3 -0
  35. data/lib/resources/aws/aws_elb.rb +81 -0
  36. data/lib/resources/aws/aws_elbs.rb +78 -0
  37. data/lib/resources/aws/aws_flow_log.rb +102 -0
  38. data/lib/resources/aws/aws_iam_groups.rb +1 -2
  39. data/lib/resources/aws/aws_iam_users.rb +65 -47
  40. data/lib/resources/npm.rb +15 -2
  41. data/lib/resources/package.rb +1 -1
  42. data/lib/utils/filter.rb +243 -85
  43. metadata +15 -2
@@ -0,0 +1,102 @@
1
+ class AwsFlowLog < Inspec.resource(1)
2
+ name 'aws_flow_log'
3
+ supports platform: 'aws'
4
+ desc 'This resource is used to test the attributes of a Flow Log.'
5
+ example <<~EOT
6
+ describe aws_flow_log('fl-9c718cf5') do
7
+ it { should exist }
8
+ end
9
+ EOT
10
+
11
+ include AwsSingularResourceMixin
12
+
13
+ def to_s
14
+ "AWS Flow Log #{id}"
15
+ end
16
+
17
+ def resource_type
18
+ case @resource_id
19
+ when /^eni/
20
+ @resource_type = 'eni'
21
+ when /^subnet/
22
+ @resource_type = 'subnet'
23
+ when /^vpc/
24
+ @resource_type = 'vpc'
25
+ end
26
+ end
27
+
28
+ def attached_to_eni?
29
+ resource_type.eql?('eni') ? true : false
30
+ end
31
+
32
+ def attached_to_subnet?
33
+ resource_type.eql?('subnet') ? true : false
34
+ end
35
+
36
+ def attached_to_vpc?
37
+ resource_type.eql?('vpc') ? true : false
38
+ end
39
+
40
+ attr_reader :log_group_name, :resource_id, :flow_log_id
41
+
42
+ private
43
+
44
+ def validate_params(raw_params)
45
+ validated_params = check_resource_param_names(
46
+ raw_params: raw_params,
47
+ allowed_params: [:flow_log_id, :subnet_id, :vpc_id],
48
+ allowed_scalar_name: :flow_log_id,
49
+ allowed_scalar_type: String,
50
+ )
51
+
52
+ if validated_params.empty?
53
+ raise ArgumentError,
54
+ 'aws_flow_log requires a parameter: flow_log_id, subnet_id, or vpc_id'
55
+ end
56
+
57
+ validated_params
58
+ end
59
+
60
+ def fetch_from_api
61
+ backend = BackendFactory.create(inspec_runner)
62
+
63
+ resp = backend.describe_flow_logs(filter_args)
64
+ flow_log = resp.to_h[:flow_logs].first
65
+ @exists = !flow_log.nil?
66
+ unless flow_log.nil?
67
+ @log_group_name = flow_log[:log_group_name]
68
+ @resource_id = flow_log[:resource_id]
69
+ @flow_log_id = flow_log[:flow_log_id]
70
+ end
71
+ end
72
+
73
+ def filter_args
74
+ if @flow_log_id
75
+ { filter: [{ name: 'flow-log-id', values: [@flow_log_id] }] }
76
+ elsif @subnet_id || @vpc_id
77
+ filter = @subnet_id || @vpc_id
78
+ { filter: [{ name: 'resource-id', values: [filter] }] }
79
+ end
80
+ end
81
+
82
+ def id
83
+ return @flow_log_id if @flow_log_id
84
+ return @subnet_id if @subnet_id
85
+ return @vpc_id if @vpc_id
86
+ end
87
+
88
+ def backend
89
+ BackendFactory.create(inspec_runner)
90
+ end
91
+
92
+ class Backend
93
+ class AwsClientApi < AwsBackendBase
94
+ AwsFlowLog::BackendFactory.set_default_backend(self)
95
+ self.aws_client_class = Aws::EC2::Client
96
+
97
+ def describe_flow_logs(query)
98
+ aws_service_client.describe_flow_logs(query)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -19,8 +19,7 @@ class AwsIamGroups < Inspec.resource(1)
19
19
 
20
20
  # Underlying FilterTable implementation.
21
21
  filter = FilterTable.create
22
- filter.add_accessor(:entries)
23
- .add(:exists?) { |x| !x.entries.empty? }
22
+ filter.add(:group_names, field: :group_name)
24
23
  filter.connect(self, :table)
25
24
 
26
25
  def to_s
@@ -23,27 +23,73 @@ class AwsIamUsers < Inspec.resource(1)
23
23
 
24
24
  include AwsPluralResourceMixin
25
25
 
26
+ def self.lazy_get_login_profile(row, _criterion, table)
27
+ backend = BackendFactory.create(table.resource.inspec_runner)
28
+ begin
29
+ _login_profile = backend.get_login_profile(user_name: row[:user_name])
30
+ row[:has_console_password] = true
31
+ rescue Aws::IAM::Errors::NoSuchEntity
32
+ row[:has_console_password] = false
33
+ end
34
+ row[:has_console_password?] = row[:has_console_password]
35
+ end
36
+
37
+ def self.lazy_list_mfa_devices(row, _criterion, table)
38
+ backend = BackendFactory.create(table.resource.inspec_runner)
39
+ begin
40
+ aws_mfa_devices = backend.list_mfa_devices(user_name: row[:user_name])
41
+ row[:has_mfa_enabled] = !aws_mfa_devices.mfa_devices.empty?
42
+ rescue Aws::IAM::Errors::NoSuchEntity
43
+ row[:has_mfa_enabled] = false
44
+ end
45
+ row[:has_mfa_enabled?] = row[:has_mfa_enabled]
46
+ end
47
+
48
+ def self.lazy_list_user_policies(row, _criterion, table)
49
+ backend = BackendFactory.create(table.resource.inspec_runner)
50
+ row[:inline_policy_names] = backend.list_user_policies(user_name: row[:user_name]).policy_names
51
+ row[:has_inline_policies] = !row[:inline_policy_names].empty?
52
+ row[:has_inline_policies?] = row[:has_inline_policies]
53
+ end
54
+
55
+ def self.lazy_list_attached_policies(row, _criterion, table)
56
+ backend = BackendFactory.create(table.resource.inspec_runner)
57
+ attached_policies = backend.list_attached_user_policies(user_name: row[:user_name]).attached_policies
58
+ row[:has_attached_policies] = !attached_policies.empty?
59
+ row[:has_attached_policies?] = row[:has_attached_policies]
60
+ row[:attached_policy_names] = attached_policies.map { |p| p[:policy_name] }
61
+ row[:attached_policy_arns] = attached_policies.map { |p| p[:policy_arn] }
62
+ end
63
+
26
64
  filter = FilterTable.create
27
65
  filter.add_accessor(:where)
28
66
  .add_accessor(:entries)
29
- .add(:exists?) { |x| !x.entries.empty? }
30
- .add(:has_mfa_enabled?, field: :has_mfa_enabled)
31
- .add(:has_console_password?, field: :has_console_password)
32
- .add(:has_inline_policies?, field: :has_inline_policies)
33
- .add(:has_attached_policies?, field: :has_attached_policies)
67
+ # Summary methods
68
+ filter.add(:exists?) { |table| !table.params.empty? }
69
+ .add(:count) { |table| table.params.count }
70
+
71
+ # These are included on the initial fetch
72
+ filter.add(:usernames, field: :user_name)
73
+ .add(:username) { |res| res.entries.map { |row| row[:user_name] } } # We should deprecate this; plural resources get plural properties
34
74
  .add(:password_ever_used?, field: :password_ever_used?)
35
75
  .add(:password_never_used?, field: :password_never_used?)
36
76
  .add(:password_last_used_days_ago, field: :password_last_used_days_ago)
37
- .add(:usernames, field: :user_name)
38
- .add(:username) { |res| res.entries.map { |row| row[:user_name] } } # We should deprecate this; plural resources get plural properties
39
- # Next three are needed to declare fields for use by the de-duped set
40
- filter.add(:dupe_inline_policy_names, field: :inline_policy_names_source)
41
- .add(:dupe_attached_policy_names, field: :attached_policy_names_source)
42
- .add(:dupe_attached_policy_arns, field: :attached_policy_arns_source)
43
- # These three are now able to access the above three in .entries
44
- filter.add(:inline_policy_names) { |obj| obj.dupe_inline_policy_names.flatten.uniq }
45
- .add(:attached_policy_names) { |obj| obj.dupe_attached_policy_names.flatten.uniq }
46
- .add(:attached_policy_arns) { |obj| obj.dupe_attached_policy_arns.flatten.uniq }
77
+
78
+ # Remaining properties / criteria are handled lazily, grouped by fetcher
79
+ filter.add(:has_console_password?, field: :has_console_password?, lazy: method(:lazy_get_login_profile))
80
+ .add(:has_console_password, field: :has_console_password, lazy: method(:lazy_get_login_profile))
81
+
82
+ filter.add(:has_mfa_enabled?, field: :has_mfa_enabled?, lazy: method(:lazy_list_mfa_devices))
83
+ .add(:has_mfa_enabled, field: :has_mfa_enabled, lazy: method(:lazy_list_mfa_devices))
84
+
85
+ filter.add(:has_inline_policies?, field: :has_inline_policies?, lazy: method(:lazy_list_user_policies))
86
+ .add(:has_inline_policies, field: :has_inline_policies, lazy: method(:lazy_list_user_policies))
87
+ .add(:inline_policy_names, field: :inline_policy_names, style: :simple, lazy: method(:lazy_list_user_policies))
88
+
89
+ filter.add(:has_attached_policies?, field: :has_attached_policies?, lazy: method(:lazy_list_attached_policies))
90
+ .add(:has_attached_policies, field: :has_attached_policies, lazy: method(:lazy_list_attached_policies))
91
+ .add(:attached_policy_names, field: :attached_policy_names, style: :simple, lazy: method(:lazy_list_attached_policies))
92
+ .add(:attached_policy_arns, field: :attached_policy_arns, style: :simple, lazy: method(:lazy_list_attached_policies))
47
93
  filter.connect(self, :table)
48
94
 
49
95
  def validate_params(raw_params)
@@ -66,45 +112,17 @@ class AwsIamUsers < Inspec.resource(1)
66
112
  table
67
113
  end
68
114
 
69
- def fetch_from_api # rubocop: disable Metrics/AbcSize
115
+ def fetch_from_api
70
116
  backend = BackendFactory.create(inspec_runner)
71
117
  @table = fetch_from_api_paginated(backend)
72
118
 
73
- # TODO: lazy columns - https://github.com/chef/inspec-aws/issues/100
74
119
  @table.each do |user|
75
- # Some of these throw exceptions to indicate empty results;
76
- # others return empty arrays
77
- begin
78
- _login_profile = backend.get_login_profile(user_name: user[:user_name])
79
- user[:has_console_password] = true
80
- rescue Aws::IAM::Errors::NoSuchEntity
81
- user[:has_console_password] = false
82
- end
83
- user[:has_console_password?] = user[:has_console_password]
84
-
85
- begin
86
- aws_mfa_devices = backend.list_mfa_devices(user_name: user[:user_name])
87
- user[:has_mfa_enabled] = !aws_mfa_devices.mfa_devices.empty?
88
- rescue Aws::IAM::Errors::NoSuchEntity
89
- user[:has_mfa_enabled] = false
90
- end
91
- user[:has_mfa_enabled?] = user[:has_mfa_enabled]
92
-
93
- user[:inline_policy_names_source] = backend.list_user_policies(user_name: user[:user_name]).policy_names
94
- user[:has_inline_policies] = !user[:inline_policy_names_source].empty?
95
- user[:has_inline_policies?] = user[:has_inline_policies]
96
-
97
- attached_policies = backend.list_attached_user_policies(user_name: user[:user_name]).attached_policies
98
- user[:has_attached_policies] = !attached_policies.empty?
99
- user[:has_attached_policies?] = user[:has_attached_policies]
100
- user[:attached_policy_names_source] = attached_policies.map { |p| p[:policy_name] }
101
- user[:attached_policy_arns_source] = attached_policies.map { |p| p[:policy_arn] }
102
-
103
120
  password_last_used = user[:password_last_used]
104
121
  user[:password_ever_used?] = !password_last_used.nil?
105
122
  user[:password_never_used?] = password_last_used.nil?
106
- next unless user[:password_ever_used?]
107
- user[:password_last_used_days_ago] = ((Time.now - password_last_used) / (24*60*60)).to_i
123
+ if user[:password_ever_used?]
124
+ user[:password_last_used_days_ago] = ((Time.now - password_last_used) / (24*60*60)).to_i
125
+ end
108
126
  end
109
127
  @table
110
128
  end
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
- 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
 
data/lib/utils/filter.rb CHANGED
@@ -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