inspec 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/aws_elb.md.erb +144 -0
- data/docs/resources/aws_elbs.md.erb +242 -0
- data/docs/resources/aws_flow_log.md.erb +118 -0
- data/docs/resources/aws_iam_groups.md.erb +34 -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/resource_support/aws.rb +3 -0
- data/lib/resources/aws/aws_elb.rb +81 -0
- data/lib/resources/aws/aws_elbs.rb +78 -0
- data/lib/resources/aws/aws_flow_log.rb +102 -0
- data/lib/resources/aws/aws_iam_groups.rb +1 -2
- data/lib/resources/aws/aws_iam_users.rb +65 -47
- data/lib/resources/npm.rb +15 -2
- data/lib/resources/package.rb +1 -1
- data/lib/utils/filter.rb +243 -85
- 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.
|
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
|
-
|
30
|
-
|
31
|
-
.add(:
|
32
|
-
|
33
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
.add(:
|
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
|
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
|
-
|
107
|
-
|
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
|
-
|
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
|
|