rubocop-betterment 1.15.0 → 2.0.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b851af1c0666fffea3f63f36504e7acf1c6d6324e58caba76bcea50ac752f50
4
- data.tar.gz: 6dcbd29396beb9c7310f6ff9082ae9494219741fb17b6306ea976747445e337e
3
+ metadata.gz: 43b05fbd08387b00251df695ed3c9de3bbd38592195ef2dbea370f637ca2acdd
4
+ data.tar.gz: 55dfe59749ca808f49b442d5a0634b959c0492db4386eccb64f3a0d8fdfbcc82
5
5
  SHA512:
6
- metadata.gz: 2067d92a8d52c91b76f3f42a470f852567f858ccc34b6ba0d50b0aa2c275ef8ba7b32287bf8592efbea39e18d1a7356ef375e51ffbddd6153d1809173ff5385c
7
- data.tar.gz: 8d18894011ce90246f1cdaf5a8c42043df0c6dcd7a92bf9af6407c300c9c01cdd1d2c4ebe941ed26b4eb09500b0abb45f4d59d1286ee1eb622f3a75b2003dff0
6
+ metadata.gz: cfb9ebd2aa93032848cbf4e27ab8bb3e69c82f2047d788833c76fce9b2f811d184e2d72a6d1186b711de9ae0e3546e6dc0f40870890ccc908ba151ede6c50e9f
7
+ data.tar.gz: '0294e0f6655280cd462aae9da70e79d830be6879a9b2b5a529d352fba13b9c0d1221ead2c44045bd4a33dfbdc1cea108c3ee5b85d96b314349617249660d8018'
data/README.md CHANGED
@@ -33,7 +33,7 @@ All cops are located under [`lib/rubocop/cop/betterment`](lib/rubocop/cop/better
33
33
 
34
34
  ### Betterment/AuthorizationInController
35
35
 
36
- This cop looks for unsafe handling of id-like parameters in controllers that may lead to mass assignment style vulnerabilities. It does this by tracking methods that retrieve input from the client and variables that hold onto these values. Any models initialized or updated using these values will then be flagged by the cop. Take this example controller:
36
+ This cop looks for unsafe handling of id-like parameters in controllers that may lead to [insecure direct object reference vulnerabilities](https://portswigger.net/web-security/access-control/idor). It does this by tracking methods that retrieve input from the client and variables that hold onto these values. Any models initialized or updated using these values will then be flagged by the cop. Take this example controller:
37
37
 
38
38
  ```ruby
39
39
  class Controller
@@ -50,7 +50,7 @@ class Controller
50
50
  end
51
51
  ```
52
52
 
53
- All three `Model.new` calls may be susceptible to a mass assignment vulnerability. To address these vulnerabilities, some form of authorization will be needed to ensure that the user issuing this request is allowed to create a `Model` that references the specific `user_id`. To get a better understanding of what this cop flags and doesn't flag, take a look at its [spec](spec/rubocop/cop/betterment/authorization_in_controller_spec.rb).
53
+ All three `Model.new` calls may be susceptible to an insecure direct object reference vulnerability. This may end up letting attackers read and write content belonging to other users. To address these vulnerabilities, some form of authorization will be needed to ensure that the user issuing this request is allowed to create a `Model` that references the specific `user_id`. To get a better understanding of what this cop flags and doesn't flag, take a look at its [spec](spec/rubocop/cop/betterment/authorization_in_controller_spec.rb).
54
54
 
55
55
  In cases where more fine-grained control over what parameters are considered sensitive is desired, two configuration options can be used: `unsafe_parameters` and `unsafe_regex`. By default this cop will flag unsafe uses of any parameters whose names end in `_id`, but additional parameters can be specified by configuring `unsafe_parameters`. In cases where the default pattern of `.*_id` is insufficient or incorrect, this regex can be swapped out by specifying the `unsafe_regex` configuration option. In total, this cop will flag any parameters whose names are on the `unsafe_parameters` list or matches the `unsafe_regex` pattern.
56
56
 
@@ -69,7 +69,7 @@ Betterment/AuthorizationInController:
69
69
 
70
70
  ### Betterment/UnscopedFind
71
71
 
72
- This cop flags code that passes user input directly into a `find`-like call that may lead to authorization issues. For example, a controller that uses user input to find a document will need to ensure that the user is authorized to access that document. Take the following sample:
72
+ This cop flags code that passes user input directly into a `find`-like call that may lead to authorization issues (such as [indirect object reference vulnerabilities](https://portswigger.net/web-security/access-control/idor)). For example, a controller that uses user input to find a document will need to ensure that the user is authorized to access that document. Take the following sample:
73
73
 
74
74
  ```ruby
75
75
  class Controller
@@ -121,3 +121,38 @@ end
121
121
  ```
122
122
 
123
123
  All three `params.permit` calls will be flagged.
124
+
125
+ ### Betterment/UnsafeJob
126
+
127
+ This cop flags delayed jobs (e.g. ActiveJob, delayed_job) whose classes accept sensitive data via a `perform` or `initialize` method. Jobs are serialized in plaintext, so any sensitive data they accept will be accessible in plaintext to everyone with database access. Instead, consider passing ActiveRecord instances that appropriately handle sensitive data (e.g. encrypted at rest and decrypted when the data is needed) or avoid passing in this data entirely.
128
+
129
+ ```ruby
130
+ class RegistrationJob < ApplicationJob
131
+ def perform(user:, password:, authorization_token:)
132
+ # do something to the user with the password and authorization_token
133
+ end
134
+ end
135
+ ```
136
+
137
+ When a `RegistrationJob` gets queued, this job will get serialized, leaving both `password` and `authorization_token` accessible in plaintext. `Betterment/UnsafeJob` can be configured to flag parameters like these to discourage their use. Some ways to remediate this might be to stop passing in `password`, and to encrypt `authorization_token` and storing it alongside the user object. For example:
138
+
139
+ ```ruby
140
+ class RegistrationJob < ApplicationJob
141
+ def perform(user:)
142
+ authorization_token = user.authorization_token.decrypt
143
+ # do something with the authorization_token
144
+ end
145
+ end
146
+ ```
147
+
148
+ By default, this job will look at classes whose name ends with `Job` but this can be replaced with any regex. This cop can also be configured to take an arbitrary list of parameter names so that any Job found accepting these parameters will be flagged.
149
+
150
+ ```yaml
151
+ Betterment/UnsafeJob:
152
+ class_regex: .*Job$
153
+ sensitive_params:
154
+ - password
155
+ - authorization_token
156
+ ```
157
+
158
+ It may make sense to consult your application's values for `Rails.application.config.filter_parameters`; if the application is filtering specific parameters from being logged, it might be a good idea to prevent these values from being stored in plaintext in a database as well.
data/STYLEGUIDE.md CHANGED
@@ -87,4 +87,25 @@ user1 = User.first
87
87
  user2 = User.second
88
88
  ```
89
89
 
90
- The snake case style is more readable.
90
+ The snake case style is more readable.
91
+
92
+ ## Betterment/ServerErrorAssertion
93
+
94
+ In RSpec tests, we prevent HTTP response status assertions against server error codes (e.g., 500). While it’s acceptable to
95
+ “under-build” APIs under assumption of controlled and well-behaving clients, these exceptions should be treated as undefined behavior and
96
+ thus do not need request spec coverage. In cases where the server must communicate an expected failure to the client, an appropriate
97
+ semantic status code must be used (e.g., 403, 422, etc.).
98
+
99
+ ### GOOD:
100
+
101
+ ```ruby
102
+ expect(response).to have_http_status :forbidden
103
+ expect(response).to have_http_status 422
104
+ ```
105
+
106
+ ### BAD:
107
+
108
+ ```ruby
109
+ expect(response).to have_http_status :internal_server_error
110
+ expect(response).to have_http_status 500
111
+ ```
data/config/default.yml CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  require:
4
4
  - rubocop/cop/betterment.rb
5
+ - rubocop-performance
6
+ - rubocop-rails
7
+ - rubocop-rake
5
8
  - rubocop-rspec
6
9
 
7
10
  AllCops:
@@ -17,10 +20,22 @@ AllCops:
17
20
  DisplayStyleGuide: true
18
21
  DisplayCopNames: true
19
22
 
23
+ Betterment/ServerErrorAssertion:
24
+ Description: 'Detects assertions on 5XX HTTP statuses.'
25
+ Include:
26
+ - 'spec/requests/**/*_spec.rb'
27
+
20
28
  Betterment/AuthorizationInController:
21
29
  Description: 'Detects unsafe handling of id-like parameters in controllers.'
22
30
  Enabled: false
23
31
 
32
+ Betterment/UnsafeJob:
33
+ Enabled: false
34
+ sensitive_params:
35
+ - password
36
+ - social_security_number
37
+ - ssn
38
+
24
39
  Betterment/SitePrismLoaded:
25
40
  Include:
26
41
  - 'spec/features/**/*_spec.rb'
@@ -29,7 +44,7 @@ Betterment/SitePrismLoaded:
29
44
  Capybara/FeatureMethods:
30
45
  Enabled: false
31
46
 
32
- Layout/AlignParameters:
47
+ Layout/ParameterAlignment:
33
48
  Enabled: false
34
49
 
35
50
  Layout/CaseIndentation:
@@ -38,7 +53,7 @@ Layout/CaseIndentation:
38
53
  Layout/ClosingParenthesisIndentation:
39
54
  Enabled: false
40
55
 
41
- Layout/IndentArray:
56
+ Layout/FirstArrayElementIndentation:
42
57
  EnforcedStyle: consistent
43
58
 
44
59
  Layout/MultilineMethodCallIndentation:
@@ -112,7 +127,7 @@ Naming/HeredocDelimiterNaming:
112
127
  Naming/PredicateName:
113
128
  NamePrefix:
114
129
  - is_
115
- NamePrefixBlacklist:
130
+ ForbiddenPrefixes:
116
131
  - is_
117
132
 
118
133
  Naming/VariableNumber:
@@ -145,9 +160,6 @@ RSpec/ContextWording:
145
160
  RSpec/DescribeClass:
146
161
  Enabled: false
147
162
 
148
- RSpec/DescribedClass:
149
- Enabled: false
150
-
151
163
  RSpec/DescribedClass:
152
164
  EnforcedStyle: 'described_class'
153
165
 
@@ -196,6 +208,9 @@ RSpec/MessageSpies:
196
208
  RSpec/MultipleExpectations:
197
209
  Enabled: false
198
210
 
211
+ RSpec/MultipleMemoizedHelpers:
212
+ Enabled: false
213
+
199
214
  RSpec/NamedSubject:
200
215
  Enabled: false
201
216
 
@@ -1,10 +1,15 @@
1
1
  require 'rubocop'
2
+ require 'rubocop/cop/betterment/utils/parser'
3
+ require 'rubocop/cop/betterment/utils/method_return_table'
2
4
  require 'rubocop/cop/betterment/authorization_in_controller'
3
5
  require 'rubocop/cop/betterment/dynamic_params'
4
6
  require 'rubocop/cop/betterment/unscoped_find'
7
+ require 'rubocop/cop/betterment/unsafe_job'
5
8
  require 'rubocop/cop/betterment/timeout'
6
9
  require 'rubocop/cop/betterment/memoization_with_arguments'
7
10
  require 'rubocop/cop/betterment/site_prism_loaded'
8
11
  require 'rubocop/cop/betterment/spec_helper_required_outside_spec_dir'
9
12
  require 'rubocop/cop/betterment/implicit_redirect_type'
10
13
  require 'rubocop/cop/betterment/active_job_performable'
14
+ require 'rubocop/cop/betterment/allowlist_blocklist'
15
+ require 'rubocop/cop/betterment/server_error_assertion'
@@ -0,0 +1,38 @@
1
+ # rubocop:disable Betterment/AllowlistBlocklist
2
+ module RuboCop
3
+ module Cop
4
+ module Betterment
5
+ class AllowlistBlocklist < Cop
6
+ MSG = <<-DOC.freeze
7
+ Betterment has moved away from usages of whitelist & blacklist in favor of more inclusive terms.
8
+ Replace any instances of these terms with more inclusive terms like allowlist, blocklist, denylist,
9
+ ignorelist, warnlist, safelist, etc. We generally use allowlist and blocklist and prefer consistency where
10
+ possible, but other terms may be more appropriate depending on your use case.
11
+
12
+ This is part of a larger initiative to replace exclusionary / harmful language and anti-bias tools and products.
13
+ DOC
14
+
15
+ def on_class(node)
16
+ evaluate_node(node)
17
+ end
18
+
19
+ private
20
+
21
+ def evaluate_node(node)
22
+ return unless should_use_allowlist?(node) || should_use_blocklist?(node)
23
+
24
+ add_offense(node)
25
+ end
26
+
27
+ def should_use_allowlist?(node)
28
+ node.to_s.downcase.include?('whitelist')
29
+ end
30
+
31
+ def should_use_blocklist?(node)
32
+ node.to_s.downcase.include?('blacklist')
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ # rubocop:enable Betterment/AllowlistBlocklist
@@ -36,33 +36,31 @@ module RuboCop
36
36
  config = @config.for_cop(self)
37
37
  @unsafe_parameters = config.fetch("unsafe_parameters", []).map(&:to_sym)
38
38
  @unsafe_regex = Regexp.new config.fetch("unsafe_regex", ".*_id$")
39
- @wrapper_methods = {}
40
- @wrapper_names = []
39
+ @param_wrappers = []
41
40
  end
42
41
 
43
42
  def on_class(node)
44
- track_methods(node)
45
- track_assignments(node)
43
+ Utils::MethodReturnTable.populate_index node
44
+ Utils::MethodReturnTable.indexed_methods.each do |method_name, method_returns|
45
+ method_returns.each do |x|
46
+ name = Utils::Parser.get_root_token(x)
47
+ @param_wrappers << method_name if name == :params || @param_wrappers.include?(name)
48
+ end
49
+ end
46
50
  end
47
51
 
48
- def on_send(node) # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity
49
- _receiver_node, _method_name, *arg_nodes = *node
50
-
52
+ def on_send(node) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
51
53
  return if !model_new?(node) && !model_update?(node)
52
54
 
53
- arg_nodes.each do |argument|
54
- if argument.type == :send
55
- tag_unsafe_param_hash(argument)
56
- tag_unsafe_param_permit_wrapper(argument)
57
- elsif argument.variable?
58
- tag_unsafe_param_permit_wrapper(argument)
59
- elsif argument.type == :hash
60
- argument.children.each do |pair|
61
- next if pair.type != :pair
62
-
55
+ node.arguments.each do |argument|
56
+ if argument.send_type? || argument.variable?
57
+ flag_literal_param_use(argument)
58
+ flag_indirect_param_use(argument)
59
+ elsif argument.hash_type?
60
+ argument.children.select(&:pair_type?).each do |pair|
63
61
  _key, value = *pair.children
64
- tag_unsafe_param_hash(value)
65
- tag_unsafe_param_permit_wrapper(value)
62
+ flag_literal_param_use(value)
63
+ flag_indirect_param_use(value)
66
64
  end
67
65
  end
68
66
  end
@@ -70,190 +68,73 @@ module RuboCop
70
68
 
71
69
  private
72
70
 
73
- def tag_unsafe_param_hash(node)
74
- unsafe_param = extract_parameter(node)
75
- add_offense(unsafe_param, message: MSG_UNSAFE_CREATE) if unsafe_param
76
- end
77
-
78
- def tag_unsafe_param_permit_wrapper(node)
79
- return if !node.send_type? && !node.variable?
80
- return if node.send_type? && (node.method_name == :[])
81
-
82
- name = get_root_token(node)
83
- add_offense(node, message: MSG_UNSAFE_CREATE) if @wrapper_names.include?(name)
84
- end
85
-
86
- # if a method returns params[...] or params.permit(...) then it will
87
- # be tracked; if the method does not explicitly return a params
88
- # sourced argument, then it will not be tracked
89
- def track_methods(node)
90
- methods = get_all_methods(node)
91
-
92
- methods.map do |method|
93
- method_returns = get_return_values(method)
94
-
95
- unless get_param_wrappers(method_returns).empty?
96
- @wrapper_methods[method.method_name] = method
97
- @wrapper_names << method.method_name
98
- end
99
- end
100
- end
101
-
102
- # keep track of all assignments that hold parameters
103
- def track_assignments(node)
104
- get_all_assignments(node).each do |assignment|
105
- variable_name, value = *assignment
106
-
107
- # if rhs calls params.permit, eg
108
- # - @var = params.permit(...)
109
- rhs_wrapper = get_param_wrappers([value])
110
- unless rhs_wrapper.empty?
111
- @wrapper_methods[variable_name] = rhs_wrapper[0]
112
- @wrapper_names << variable_name
113
- end
114
-
115
- # if rhs is a call to a parameter wrapper, eg
116
- # @var = parameter_wrapper
117
- root_token = get_root_token(value)
118
- if root_token && @wrapper_names.include?(root_token)
119
- @wrapper_methods[variable_name] = assignment
120
- @wrapper_names << variable_name
121
- end
122
-
123
- # if rhs extracts a parameter, eg
124
- # @var = params[:user_id]
125
- if extract_parameter(value)
126
- @wrapper_methods[variable_name] = assignment
127
- @wrapper_names << variable_name
128
- end
129
- end
130
- end
131
-
132
- def explicit_returns(node)
133
- node.descendants.select(&:return_type?).map { |x|
134
- x&.children&.first
135
- }.compact
136
- end
137
-
138
- def get_return_values(node) # rubocop:disable Metrics/AbcSize
139
- return [] unless node
140
- return explicit_returns(node) + get_return_values(node.body) if node.def_type?
141
- return [node] if node.literal? || node.variable?
142
-
143
- case node.type
144
- when :begin
145
- get_return_values(node.children.last)
146
- when :block
147
- get_return_values(node.body)
148
- when :if
149
- if_rets = get_return_values(node.if_branch)
150
- else_rets = get_return_values(node.else_branch)
151
- if_rets + else_rets
152
- when :case
153
- cases = []
154
- node.each_when do |block|
155
- cases += get_return_values(block.body)
156
- end
157
-
158
- cases + get_return_values(node.else_branch)
159
- when :send
160
- [node]
71
+ # Flags objects being created/updated with unsafe
72
+ # params directly from params or through params.permit
73
+ #
74
+ # class MyController < ApplicationController
75
+ # def create
76
+ # Object.create params.permit(:user_id)
77
+ # Object.create(user_id: params[:user_id])
78
+ # end
79
+ # end
80
+ #
81
+ def flag_literal_param_use(node)
82
+ name = Utils::Parser.get_root_token(node)
83
+ extracted_params = Utils::Parser.get_extracted_parameters(node)
84
+ add_offense(node, message: MSG_UNSAFE_CREATE) if name == :params && contains_id_parameter?(extracted_params)
85
+ end
86
+
87
+ # Flags objects being created/updated with unsafe
88
+ # params indirectly from params or through params.permit
89
+ def flag_indirect_param_use(node) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
90
+ name = Utils::Parser.get_root_token(node)
91
+ # extracted_params contains parameters used like:
92
+ # def create
93
+ # Object.new(user_id: indirect_params[:user_id])
94
+ # end
95
+ # def indirect_params
96
+ # params.permit(:user_id)
97
+ # end
98
+ extracted_params = Utils::Parser.get_extracted_parameters(node, param_aliases: @param_wrappers)
99
+
100
+ returns = Utils::MethodReturnTable.get_method(name) || []
101
+ returns.each do |ret|
102
+ # # propagated_params contains parameters used like:
103
+ # def create
104
+ # Object.new indirect_params
105
+ # end
106
+ # def indirect_params
107
+ # params.permit(:user_id)
108
+ # end
109
+ propagated_params = Utils::Parser.get_extracted_parameters(ret, param_aliases: @param_wrappers)
110
+
111
+ # # internal_params contains parameters used like:
112
+ # def create
113
+ # Object.new(user_id: indirect_params)
114
+ # end
115
+ # def indirect_params
116
+ # params[:user_id]
117
+ # end
118
+ if ret.send_type? && ret.method?(:[])
119
+ internal_params = ret.arguments.select { |x| x.sym_type? || x.str_type? }.map(&:value)
161
120
  else
162
- []
163
- end
164
- end
165
-
166
- def get_all_assignments(node)
167
- return [] unless node.children && node.type == :class
168
-
169
- node.descendants.select do |descendant|
170
- _lhs, rhs = *descendant
171
- descendant.equals_asgn? && (descendant.type != :casgn) && rhs&.send_type?
172
- end
173
- end
174
-
175
- def param_symbol?(name)
176
- name == :params
177
- end
178
-
179
- def get_all_methods(node)
180
- return [] unless node.children && node.type == :class
181
-
182
- node.descendants.select do |descendant|
183
- descendant.type == :def
184
- end
185
- end
186
-
187
- # fetches the name of the leftmost ("root") token
188
- # @vars.merge(this: that).merge(etc: etc) => @vars
189
- # params.permit(:this) => params
190
- # params[:field] => params
191
- def get_root_token(node) # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
192
- return nil unless node
193
-
194
- return get_root_token(node.receiver) if node.receiver
195
-
196
- if node.send_type?
197
- name = node.method_name
198
- elsif node.variable?
199
- name, = *node
200
- elsif node.literal?
201
- _, name = *node
202
- elsif node.const_type?
203
- name = node.const_name
204
- elsif node.sym_type?
205
- name = node.value
206
- elsif node.variable?
207
- name = node.children[0]
208
- elsif node.parenthesized_call?
209
- name = nil
210
- else
211
- raise "Unknown node type: #{node.type.inspect}"
212
- end
213
-
214
- name
215
- end
216
-
217
- # this finds all calls to any method on a params like object
218
- # then walks up to find calls to permit
219
- # if the arguments to permit are "suspicious", then we add
220
- # the whole method to a list of methods that wrap params.permit
221
- def get_param_wrappers(methods) # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity
222
- wrappers = []
223
-
224
- methods.each do |method|
225
- return [] unless method.type == :def || method.type == :send
226
-
227
- method.descendants.each do |child|
228
- next unless child.type == :send
229
- next unless param_symbol?(child.method_name)
230
-
231
- child.ancestors.each do |ancestor|
232
- _receiver_node, method_name, *arg_nodes = *ancestor
233
- next unless ancestor.send_type?
234
- next unless method_name == :permit
235
-
236
- # we're only interested in calls to params.....permit(...)
237
- wrappers << method if contains_id_param?(arg_nodes)
238
- end
121
+ internal_returns = Utils::MethodReturnTable.get_method(Utils::Parser.get_root_token(ret)) || []
122
+ internal_params = internal_returns.flat_map { |x| Utils::Parser.get_extracted_parameters(x, param_aliases: @param_wrappers) }
239
123
  end
240
- end
241
124
 
242
- wrappers
125
+ add_offense(node, message: MSG_UNSAFE_CREATE) if flag_indirect_param_use?(extracted_params, internal_params, propagated_params)
126
+ end
243
127
  end
244
128
 
245
- def sym_or_str?(arg)
246
- %i(sym str).include?(arg.type)
247
- end
129
+ def flag_indirect_param_use?(extracted_params, internal_params, propagated_params)
130
+ return contains_id_parameter?(extracted_params) if extracted_params.any?
248
131
 
249
- def array_or_hash?(arg)
250
- %i(array hash).include?(arg.type)
132
+ contains_id_parameter?(extracted_params) || contains_id_parameter?(internal_params) || contains_id_parameter?(propagated_params)
251
133
  end
252
134
 
253
- def contains_id_param?(arg_nodes)
254
- arg_nodes.any? do |arg|
255
- sym_or_str?(arg) && suspicious_id?(arg.value) ||
256
- array_or_hash?(arg) && contains_id_param?(arg.values)
135
+ def contains_id_parameter?(params)
136
+ params.any? do |arg|
137
+ suspicious_id?(arg)
257
138
  end
258
139
  end
259
140
 
@@ -261,19 +142,6 @@ module RuboCop
261
142
  def suspicious_id?(symbol_name)
262
143
  @unsafe_parameters.include?(symbol_name.to_sym) || @unsafe_regex.match(symbol_name) # symbol_name.to_s.end_with?("_id")
263
144
  end
264
-
265
- def extract_parameter(argument)
266
- _receiver_node, method_name, *arg_nodes = *argument
267
- return unless argument.send_type? && method_name == :[]
268
-
269
- argument_name = get_root_token(argument)
270
-
271
- if param_symbol?(argument_name) || @wrapper_names.include?(argument_name)
272
- arg_nodes.find do |arg|
273
- sym_or_str?(arg) && suspicious_id?(arg.value)
274
- end
275
- end
276
- end
277
145
  end
278
146
  end
279
147
  end
@@ -14,8 +14,8 @@ module RuboCop
14
14
  PATTERN
15
15
 
16
16
  def on_send(node)
17
- _, _, *arg_nodes = *node
18
- return unless permit_or_hash?(node) && get_root_token(node) == :params
17
+ _, _, *arg_nodes = *node # rubocop:disable InternalAffairs/NodeDestructuring
18
+ return unless permit_or_hash?(node) && Utils::Parser.get_root_token(node) == :params
19
19
 
20
20
  dynamic_param = find_dynamic_param(arg_nodes)
21
21
  add_offense(dynamic_param, message: MSG_DYNAMIC_PARAMS) if dynamic_param
@@ -27,35 +27,9 @@ module RuboCop
27
27
  return unless arg_nodes
28
28
 
29
29
  arg_nodes.find do |arg|
30
- arg.type == :array && find_dynamic_param(arg.values) || !arg.literal? && !arg.const_type?
30
+ arg.array_type? && find_dynamic_param(arg.values) || !arg.literal? && !arg.const_type?
31
31
  end
32
32
  end
33
-
34
- def get_root_token(node) # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
35
- return nil unless node
36
-
37
- return get_root_token(node.receiver) if node.receiver
38
-
39
- if node.send_type?
40
- name = node.method_name
41
- elsif node.variable?
42
- name, = *node
43
- elsif node.literal?
44
- _, name = *node
45
- elsif node.const_type?
46
- name = node.const_name
47
- elsif node.sym_type?
48
- name = node.value
49
- elsif node.variable?
50
- name = node.children[0]
51
- elsif node.parenthesized_call?
52
- name = nil
53
- else
54
- raise "Unknown node type: #{node.type.inspect}"
55
- end
56
-
57
- name
58
- end
59
33
  end
60
34
  end
61
35
  end
@@ -47,7 +47,7 @@ module RuboCop
47
47
  return unless routes_file?
48
48
 
49
49
  if block_form_with_options(node) { |options| options.none?(&method(:valid_status_option?)) } || block_form_without_options?(node)
50
- add_offense(node, message: MSG)
50
+ add_offense(node)
51
51
  end
52
52
  end
53
53
 
@@ -55,7 +55,7 @@ module RuboCop
55
55
  return unless routes_file?
56
56
 
57
57
  if arg_form_with_options(node) { |options| options.none?(&method(:valid_status_option?)) } || arg_form_without_options?(node)
58
- add_offense(node, message: MSG)
58
+ add_offense(node)
59
59
  end
60
60
  end
61
61
 
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Betterment
6
+ # Checks the status passed to have_http_status
7
+ #
8
+ # If a number, enforces that it doesn't start with 5. If a symbol or a string, enforces that it's not one of:
9
+ #
10
+ # * internal_server_error
11
+ # * not_implemented
12
+ # * bad_gateway
13
+ # * service_unavailable
14
+ # * gateway_timeout
15
+ # * http_version_not_supported
16
+ # * insufficient_storage
17
+ # * not_extended
18
+ #
19
+ # @example
20
+ #
21
+ # # bad
22
+ # expect(response).to have_http_status :internal_server_error
23
+ #
24
+ # # bad
25
+ # expect(response).to have_http_status 500
26
+ #
27
+ # # good
28
+ # expect(response).to have_http_status :forbidden
29
+ #
30
+ # # good
31
+ # expect(response).to have_http_status 422
32
+ class ServerErrorAssertion < Cop
33
+ MSG = 'Do not assert on 5XX statuses. Use a semantic status (e.g., 403, 422, etc.) or treat them as bugs (omit tests).'
34
+ BAD_STATUSES = %i(
35
+ internal_server_error
36
+ not_implemented
37
+ bad_gateway
38
+ service_unavailable
39
+ gateway_timeout
40
+ http_version_not_supported
41
+ insufficient_storage
42
+ not_extended
43
+ ).freeze
44
+
45
+ def_node_matcher :offensive_node?, <<-PATTERN
46
+ (send nil? :have_http_status
47
+ {
48
+ (int {#{(500..599).map(&:to_s).join(' ')}})
49
+ (str {#{BAD_STATUSES.map(&:to_s).map(&:inspect).join(' ')}})
50
+ (sym {#{BAD_STATUSES.map(&:inspect).join(' ')}})
51
+ }
52
+ )
53
+ PATTERN
54
+
55
+ def on_send(node)
56
+ return unless offensive_node?(node)
57
+
58
+ add_offense(node)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -21,7 +21,7 @@ module RuboCop
21
21
  PATTERN
22
22
 
23
23
  def on_send(node)
24
- add_offense(node, message: MSG) if requires_spec_helper?(node) && !spec_directory?
24
+ add_offense(node) if requires_spec_helper?(node) && !spec_directory?
25
25
  end
26
26
 
27
27
  private
@@ -0,0 +1,33 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Betterment
4
+ class UnsafeJob < Cop
5
+ attr_accessor :sensitive_params, :class_regex
6
+
7
+ MSG = <<~MSG.freeze
8
+ This job takes a parameter that will end up serialized in plaintext. Do not pass sensitive data as bare arguments into jobs.
9
+
10
+ See here for more information on this error:
11
+ https://github.com/Betterment/rubocop-betterment#bettermentunsafejob
12
+ MSG
13
+
14
+ def initialize(config = nil, options = nil)
15
+ super(config, options)
16
+ config = @config.for_cop(self)
17
+ @sensitive_params = config.fetch("sensitive_params", []).map(&:to_sym)
18
+ @class_regex = Regexp.new config.fetch("class_regex", ".*Job$")
19
+ end
20
+
21
+ def on_def(node)
22
+ return unless %i(perform initialize).include?(node.method_name)
23
+ return unless @class_regex.match(node.parent_module_name)
24
+
25
+ node.arguments.any? do |argument|
26
+ name, = *argument
27
+ add_offense(argument) if @sensitive_params.include?(name)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -31,19 +31,23 @@ module RuboCop
31
31
  def initialize(config = nil, options = nil)
32
32
  super(config, options)
33
33
  config = @config.for_cop(self)
34
- @unauthenticated_models = config.fetch("unauthenticated_models", [])
34
+ @unauthenticated_models = config.fetch("unauthenticated_models", []).map(&:to_sym)
35
+ end
36
+
37
+ def on_class(node)
38
+ Utils::MethodReturnTable.populate_index(node)
35
39
  end
36
40
 
37
41
  def on_send(node)
38
- _, _, *arg_nodes = *node
42
+ _, _, *arg_nodes = *node # rubocop:disable InternalAffairs/NodeDestructuring
39
43
  return unless
40
44
  (
41
45
  find?(node) ||
42
46
  custom_scope_find?(node) ||
43
47
  static_method_name(node.method_name)
44
- ) && !@unauthenticated_models.include?(get_root_token(node))
48
+ ) && !@unauthenticated_models.include?(Utils::Parser.get_root_token(node))
45
49
 
46
- add_offense(node, message: MSG) if find_param_arg(arg_nodes)
50
+ add_offense(node) if find_param_arg(arg_nodes)
47
51
  end
48
52
 
49
53
  private
@@ -52,13 +56,21 @@ module RuboCop
52
56
  return unless arg_nodes
53
57
 
54
58
  arg_nodes.find do |arg|
55
- if arg.type == :hash
59
+ if arg.hash_type?
56
60
  arg.children.each do |pair|
57
61
  _key, value = *pair.children
58
- return arg if get_root_token(value) == :params
62
+ return arg if uses_params?(value)
59
63
  end
60
64
  end
61
- get_root_token(arg) == :params
65
+
66
+ uses_params?(arg)
67
+ end
68
+ end
69
+
70
+ def uses_params?(node)
71
+ root = Utils::Parser.get_root_token(node)
72
+ root == :params || Array(Utils::MethodReturnTable.get_method(root)).find do |x|
73
+ Utils::Parser.get_root_token(x) == :params
62
74
  end
63
75
  end
64
76
 
@@ -69,32 +81,6 @@ module RuboCop
69
81
 
70
82
  match[2] ? 'find_by!' : 'find_by'
71
83
  end
72
-
73
- def get_root_token(node) # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
74
- return nil unless node
75
-
76
- return get_root_token(node.receiver) if node.receiver
77
-
78
- if node.send_type?
79
- name = node.method_name
80
- elsif node.variable?
81
- name, = *node
82
- elsif node.literal?
83
- _, name = *node
84
- elsif node.const_type?
85
- name = node.const_name
86
- elsif node.sym_type?
87
- name = node.value
88
- elsif node.variable?
89
- name = node.children[0]
90
- elsif node.parenthesized_call?
91
- name = nil
92
- else
93
- raise "Unknown node type: #{node.type.inspect}"
94
- end
95
-
96
- name
97
- end
98
84
  end
99
85
  end
100
86
  end
@@ -0,0 +1,48 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Utils
4
+ module MethodReturnTable
5
+ class << self
6
+ def populate_index(node)
7
+ raise "not a class" unless node.class_type?
8
+
9
+ get_methods_for_class(node).each do |method|
10
+ track_method(method.method_name, Utils::Parser.get_return_values(method))
11
+ end
12
+
13
+ node.descendants.each do |descendant|
14
+ lhs, rhs = *descendant
15
+ next unless descendant.equals_asgn? && (descendant.type != :casgn) && rhs&.send_type?
16
+
17
+ track_method(lhs, [rhs])
18
+ end
19
+ end
20
+
21
+ def indexed_methods
22
+ @indexed_methods ||= {}
23
+ end
24
+
25
+ def get_method(method_name)
26
+ indexed_methods[method_name]
27
+ end
28
+
29
+ def has_method?(method_name)
30
+ indexed_methods.include?(method_name)
31
+ end
32
+
33
+ private
34
+
35
+ def track_method(method_name, returns)
36
+ indexed_methods[method_name] = returns
37
+ end
38
+
39
+ def get_methods_for_class(node)
40
+ return [] unless node.children && node.class_type?
41
+
42
+ node.descendants.select(&:def_type?)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,115 @@
1
+ module RuboCop
2
+ module Cop
3
+ module Utils
4
+ module Parser
5
+ def self.get_root_token(node) # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
6
+ return nil unless node
7
+
8
+ return get_root_token(node.receiver) if node.receiver
9
+
10
+ # rubocop:disable InternalAffairs/NodeDestructuring
11
+ if node.send_type?
12
+ name = node.method_name
13
+ elsif node.variable?
14
+ name, = *node
15
+ elsif node.literal?
16
+ _, name = *node
17
+ elsif node.const_type?
18
+ name = node.const_name.to_sym
19
+ elsif node.sym_type?
20
+ name = node.value
21
+ elsif node.self_type?
22
+ name = :self
23
+ elsif node.block_pass_type?
24
+ name, = *node.children
25
+ else
26
+ name = nil
27
+ end
28
+ # rubocop:enable InternalAffairs/NodeDestructuring
29
+
30
+ name
31
+ end
32
+
33
+ def self.get_return_values(node) # rubocop:disable Metrics/AbcSize
34
+ return [] unless node
35
+ return explicit_returns(node) + get_return_values(node.body) if node.def_type?
36
+ return [node] if node.literal? || node.variable?
37
+
38
+ case node.type
39
+ when :begin
40
+ get_return_values(node.children.last)
41
+ when :block
42
+ get_return_values(node.body)
43
+ when :if
44
+ if_rets = get_return_values(node.if_branch)
45
+ else_rets = get_return_values(node.else_branch)
46
+ if_rets + else_rets
47
+ when :case
48
+ cases = []
49
+ node.each_when do |block|
50
+ cases += get_return_values(block.body)
51
+ end
52
+
53
+ cases + get_return_values(node.else_branch)
54
+ when :send
55
+ [node]
56
+ else
57
+ []
58
+ end
59
+ end
60
+
61
+ def self.explicit_returns(node)
62
+ node.descendants.select(&:return_type?).map { |x|
63
+ x&.children&.first
64
+ }.compact
65
+ end
66
+
67
+ def self.params_from_arguments(arguments) # rubocop:disable Metrics/PerceivedComplexity
68
+ parameter_names = []
69
+
70
+ arguments.each do |arg|
71
+ if arg.hash_type?
72
+ arg.children.each do |pair|
73
+ value = pair.value
74
+ parameter_names << value.value if value.sym_type? || value.str_type?
75
+ end
76
+ elsif arg.sym_type? || arg.str_type?
77
+ parameter_names << arg.value
78
+ end
79
+ end
80
+
81
+ parameter_names
82
+ end
83
+
84
+ def self.get_extracted_parameters(node, param_aliases: []) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
85
+ return [] unless node.send_type?
86
+
87
+ parameter_names = []
88
+ param_aliases << :params
89
+
90
+ if node.method?(:[]) && param_aliases.include?(get_root_token(node))
91
+ return node.arguments.select { |x|
92
+ x.sym_type? || x.str_type?
93
+ }.map(&:value)
94
+ end
95
+
96
+ children = node.descendants.select do |child|
97
+ child.send_type? && param_aliases.include?(child.method_name)
98
+ end
99
+
100
+ children.each do |child|
101
+ ancestors = child.ancestors.select do |ancestor|
102
+ ancestor.send_type? && ancestor.method?(:permit)
103
+ end
104
+
105
+ ancestors.each do |ancestor|
106
+ parameter_names += params_from_arguments(ancestor.arguments)
107
+ end
108
+ end
109
+
110
+ parameter_names.map(&:to_sym)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
metadata CHANGED
@@ -1,43 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-betterment
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.15.0
4
+ version: 2.0.0.pre2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Development
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-06 00:00:00.000000000 Z
11
+ date: 2021-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.61.1
19
+ version: '1.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop-performance
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop-rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
25
53
  - !ruby/object:Gem::Version
26
- version: 0.61.1
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
27
69
  - !ruby/object:Gem::Dependency
28
70
  name: rubocop-rspec
29
71
  requirement: !ruby/object:Gem::Requirement
30
72
  requirements:
31
- - - '='
73
+ - - ">="
32
74
  - !ruby/object:Gem::Version
33
- version: 1.28.0
75
+ version: '0'
34
76
  type: :runtime
35
77
  prerelease: false
36
78
  version_requirements: !ruby/object:Gem::Requirement
37
79
  requirements:
38
- - - '='
80
+ - - ">="
39
81
  - !ruby/object:Gem::Version
40
- version: 1.28.0
82
+ version: '0'
41
83
  - !ruby/object:Gem::Dependency
42
84
  name: bundler
43
85
  requirement: !ruby/object:Gem::Requirement
@@ -92,19 +134,24 @@ files:
92
134
  - config/default.yml
93
135
  - lib/rubocop/cop/betterment.rb
94
136
  - lib/rubocop/cop/betterment/active_job_performable.rb
137
+ - lib/rubocop/cop/betterment/allowlist_blocklist.rb
95
138
  - lib/rubocop/cop/betterment/authorization_in_controller.rb
96
139
  - lib/rubocop/cop/betterment/dynamic_params.rb
97
140
  - lib/rubocop/cop/betterment/implicit_redirect_type.rb
98
141
  - lib/rubocop/cop/betterment/memoization_with_arguments.rb
142
+ - lib/rubocop/cop/betterment/server_error_assertion.rb
99
143
  - lib/rubocop/cop/betterment/site_prism_loaded.rb
100
144
  - lib/rubocop/cop/betterment/spec_helper_required_outside_spec_dir.rb
101
145
  - lib/rubocop/cop/betterment/timeout.rb
146
+ - lib/rubocop/cop/betterment/unsafe_job.rb
102
147
  - lib/rubocop/cop/betterment/unscoped_find.rb
103
- homepage:
148
+ - lib/rubocop/cop/betterment/utils/method_return_table.rb
149
+ - lib/rubocop/cop/betterment/utils/parser.rb
150
+ homepage:
104
151
  licenses:
105
152
  - MIT
106
153
  metadata: {}
107
- post_install_message:
154
+ post_install_message:
108
155
  rdoc_options: []
109
156
  require_paths:
110
157
  - lib
@@ -112,15 +159,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
112
159
  requirements:
113
160
  - - ">="
114
161
  - !ruby/object:Gem::Version
115
- version: '0'
162
+ version: '2.4'
116
163
  required_rubygems_version: !ruby/object:Gem::Requirement
117
164
  requirements:
118
- - - ">="
165
+ - - ">"
119
166
  - !ruby/object:Gem::Version
120
- version: '0'
167
+ version: 1.3.1
121
168
  requirements: []
122
- rubygems_version: 3.1.3
123
- signing_key:
169
+ rubygems_version: 3.2.3
170
+ signing_key:
124
171
  specification_version: 4
125
172
  summary: Betterment rubocop configuration
126
173
  test_files: []