rubocop-gusto 11.9.1 → 11.11.0
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 +22 -0
- data/README.md +19 -2
- data/config/default.yml +0 -29
- data/config/gusto_cops.yml +85 -0
- data/config/obsoletion.yml +10 -0
- data/config/sidekiq.yml +1 -1
- data/config/sorbet.yml +3 -0
- data/lib/rubocop/cop/gusto/all.rb +1 -1
- data/lib/rubocop/cop/gusto/logging/log_call_concerns.rb +77 -0
- data/lib/rubocop/cop/gusto/logging/pii_accessor.rb +56 -0
- data/lib/rubocop/cop/gusto/logging/raw_params.rb +76 -0
- data/lib/rubocop/cop/{rack/lowercase_header_keys.rb → gusto/lowercase_headers.rb} +2 -2
- data/lib/rubocop/cop/gusto/sidekiq/perform_async_stub.rb +108 -0
- data/lib/rubocop/cop/gusto/sorbet/struct_default_evaluated_once.rb +88 -0
- data/lib/rubocop/cop/gusto/timeout_timeout.rb +49 -0
- data/lib/rubocop/cop/gusto/unreferenced_let.rb +657 -54
- data/lib/rubocop/gusto/version.rb +1 -1
- metadata +9 -4
- data/lib/rubocop/cop/sidekiq/perform_async_stub.rb +0 -106
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7f748fe3b65ef76b93bb947a6058c589a813ab9cb5b1e941e4728c566b595e3d
|
|
4
|
+
data.tar.gz: 96f689862a97b10381440f8a06ce03dd5c52cc440908249bee2500b74571b16c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a15c149f1360e7bd8a1fa2c49e73f52410a5992c1f7caaf235331fa28127de6011658a1abb36414bb762647f8ddb6afbd4563c7b146e2e31011a340686132e3e
|
|
7
|
+
data.tar.gz: 35e67c0e1cee739e8cb07ad6be077813b0e296c942ca593a1109153894e6bba933a789455b97fdbf94b826135bd91c05f1e618856230a885c0504f2cd2e92e52
|
data/CHANGELOG.md
CHANGED
|
@@ -3,9 +3,31 @@
|
|
|
3
3
|
- Remove redundant `Rails: Enabled: true` from `config/rails.yml` (already set by rubocop-rails' own defaults)
|
|
4
4
|
- Enable `Rails/DefaultScope` cop (disabled by default in rubocop-rails)
|
|
5
5
|
|
|
6
|
+
## [11.11.0](https://github.com/Gusto/rubocop-gusto/compare/v11.10.0...v11.11.0) (2026-10-27)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add Gusto/Sorbet/StructDefaultEvaluatedOnce cop ([#194](https://github.com/Gusto/rubocop-gusto/issues/194)) ([e58c907](https://github.com/Gusto/rubocop-gusto/commit/e58c907195fd589cf94c1ab67fbd89fbc603102b))
|
|
12
|
+
* Flag PII accessors and raw params in logs ([#119](https://github.com/Gusto/rubocop-gusto/issues/119)) ([c9f5342](https://github.com/Gusto/rubocop-gusto/commit/c9f53428b2d99bfb31091f066395d3c896155f4e))
|
|
13
|
+
* move the Rack and Sidekiq cops under the Gusto department ([#192](https://github.com/Gusto/rubocop-gusto/issues/192)) ([acd6a2c](https://github.com/Gusto/rubocop-gusto/commit/acd6a2c65a51abf81f74ab1072dbb8de03af2238))
|
|
14
|
+
|
|
15
|
+
## [11.10.0](https://github.com/Gusto/rubocop-gusto/compare/v11.9.1...v11.10.0) (2026-10-22)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* add Gusto/TimeoutTimeout cop ([#195](https://github.com/Gusto/rubocop-gusto/issues/195)) ([ef28938](https://github.com/Gusto/rubocop-gusto/commit/ef28938194582f3e7f804694a6a9fa7f6b005b99))
|
|
21
|
+
* per-group scoping for Gusto/UnreferencedLet ([de8504c](https://github.com/Gusto/rubocop-gusto/commit/de8504c66110dea8d2ab1a57ea66870a181ce633))
|
|
22
|
+
* per-group UnreferencedLet scoping ([#187](https://github.com/Gusto/rubocop-gusto/issues/187)) ([de8504c](https://github.com/Gusto/rubocop-gusto/commit/de8504c66110dea8d2ab1a57ea66870a181ce633))
|
|
23
|
+
|
|
6
24
|
## [11.9.1](https://github.com/Gusto/rubocop-gusto/compare/v11.9.0...v11.9.1) (2026-09-28)
|
|
7
25
|
|
|
8
26
|
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
* flag transitive discouraged gems via the lockfile and discourage ostruct ([#188](https://github.com/Gusto/rubocop-gusto/issues/188)) ([3fb0a2c](https://github.com/Gusto/rubocop-gusto/commit/3fb0a2c825777d768c4d2d92b8504eae44723d5a))
|
|
30
|
+
|
|
9
31
|
### Bug Fixes
|
|
10
32
|
|
|
11
33
|
* port upstream review of Gusto/HardcodedAbsentRecordId ([#180](https://github.com/Gusto/rubocop-gusto/issues/180)) ([6198928](https://github.com/Gusto/rubocop-gusto/commit/6198928187db6920c18c882c3167a1cf8edeccf1))
|
data/README.md
CHANGED
|
@@ -88,14 +88,31 @@ inherit_gem:
|
|
|
88
88
|
|
|
89
89
|
If your project also uses Rails, include `config/rails.yml` as well (order does not matter). Re-run `bundle exec rubocop-gusto init` to merge this in automatically.
|
|
90
90
|
|
|
91
|
+
#### Logging configuration
|
|
92
|
+
|
|
93
|
+
The `Gusto/Logging` cops report likely sensitive data in `Rails.logger`, `Sidekiq.logger`,
|
|
94
|
+
and bare `logger` calls, including chained `.tagged(...)` loggers and message blocks.
|
|
95
|
+
Each cop has its own severity, exclusions, todo entries, and inline disables:
|
|
96
|
+
|
|
97
|
+
- `PiiAccessor` checks configured PII methods such as `user.email`.
|
|
98
|
+
- `RawParams` checks unfiltered `params`, including serialization and `require` without narrowing.
|
|
99
|
+
|
|
100
|
+
All ship at `Severity: info`, which does not fail RuboCop at its default failure threshold.
|
|
101
|
+
Spec and test files are excluded. Promote each cop independently after reviewing its hits;
|
|
102
|
+
for example, set `Gusto/Logging/PiiAccessor: Severity: warning` in your project configuration.
|
|
103
|
+
Configure `PiiMethods` on `Gusto/Logging` to customize the shared sensitive-method list.
|
|
104
|
+
|
|
105
|
+
These are syntax-based heuristics: selecting a params key is allowed, not proof that it is safe;
|
|
106
|
+
plain local variables and values assembled outside the log call may escape detection.
|
|
107
|
+
|
|
91
108
|
### Available cops
|
|
92
109
|
|
|
93
110
|
Custom cops live under the following namespaces:
|
|
94
111
|
|
|
95
112
|
- `Gusto/` — general Gusto-specific cops (see [`lib/rubocop/cop/gusto/`](lib/rubocop/cop/gusto/))
|
|
96
113
|
- `Gusto/Graphql/` — cops scoped to the graphql-ruby schema DSL (see [`lib/rubocop/cop/gusto/graphql/`](lib/rubocop/cop/gusto/graphql/)); configured in [`config/graphql.yml`](config/graphql.yml)
|
|
97
|
-
- `
|
|
98
|
-
- `
|
|
114
|
+
- `Gusto/Logging/` — independently configurable checks for sensitive data in logs (see [`lib/rubocop/cop/gusto/logging/`](lib/rubocop/cop/gusto/logging/))
|
|
115
|
+
- `Gusto/Sidekiq/` — cops scoped to Sidekiq patterns (see [`lib/rubocop/cop/gusto/sidekiq/`](lib/rubocop/cop/gusto/sidekiq/)); enabled by [`config/sidekiq.yml`](config/sidekiq.yml)
|
|
99
116
|
|
|
100
117
|
## Publishing New Versions
|
|
101
118
|
|
data/config/default.yml
CHANGED
|
@@ -242,38 +242,9 @@ RSpec/StubbedMock:
|
|
|
242
242
|
RSpec/SubjectStub:
|
|
243
243
|
Enabled: false
|
|
244
244
|
|
|
245
|
-
Rack/LowercaseHeaderKeys:
|
|
246
|
-
Description: 'HTTP response header keys should be lowercase for HTTP/2 and modern web standards compatibility.'
|
|
247
|
-
Enabled: true
|
|
248
|
-
Include:
|
|
249
|
-
- 'lib/middleware/**/*.rb'
|
|
250
|
-
- 'app/middleware/**/*.rb'
|
|
251
|
-
- 'app/controllers/**/*.rb'
|
|
252
|
-
- 'app/public/**/*controller*.rb'
|
|
253
|
-
- 'app/helpers/**/*.rb'
|
|
254
|
-
- 'packs/**/middleware/**/*.rb'
|
|
255
|
-
- 'packs/**/controllers/**/*.rb'
|
|
256
|
-
- 'packs/**/app/public/**/*controller*.rb'
|
|
257
|
-
- 'packs/**/app/helpers/**/*.rb'
|
|
258
|
-
- 'packs/**/lib/**/*_app.rb'
|
|
259
|
-
- 'components/**/middleware/**/*.rb'
|
|
260
|
-
- 'components/**/controllers/**/*.rb'
|
|
261
|
-
- 'config/initializers/**/*.rb'
|
|
262
|
-
Exclude:
|
|
263
|
-
- '**/spec/**/*'
|
|
264
|
-
|
|
265
245
|
Rake/ClassDefinitionInTask:
|
|
266
246
|
Enabled: false
|
|
267
247
|
|
|
268
|
-
Sidekiq/PerformAsyncStub:
|
|
269
|
-
Description: 'Prefer checking enqueued jobs over stubbing `perform_async`.'
|
|
270
|
-
Enabled: false
|
|
271
|
-
SafeAutoCorrect: false # Autocorrect appends `.and_call_original`, which runs real `perform_async` during the example
|
|
272
|
-
Include:
|
|
273
|
-
# Not just `*_spec.rb`: shared contexts, support files and system-spec helpers install these
|
|
274
|
-
# stubs on behalf of the specs that include them, which is where they are hardest to spot.
|
|
275
|
-
- '**/spec/**/*'
|
|
276
|
-
|
|
277
248
|
Sorbet/ForbidTUnsafe:
|
|
278
249
|
# T.unsafe completely disables typechecking. Prefer T.cast, shims, or
|
|
279
250
|
# reorganizing the code over silently turning off the type checker.
|
data/config/gusto_cops.yml
CHANGED
|
@@ -112,6 +112,66 @@ Gusto/HardcodedId:
|
|
|
112
112
|
AllowedFactories: []
|
|
113
113
|
AllowedReceivers: []
|
|
114
114
|
|
|
115
|
+
Gusto/Logging:
|
|
116
|
+
# Exclusions live on each cop because RuboCop 1.76 replaces department Exclude arrays.
|
|
117
|
+
PiiMethods:
|
|
118
|
+
- email
|
|
119
|
+
- ssn
|
|
120
|
+
- social_security_number
|
|
121
|
+
- first_name
|
|
122
|
+
- last_name
|
|
123
|
+
- full_name
|
|
124
|
+
- phone
|
|
125
|
+
- phone_number
|
|
126
|
+
- ein
|
|
127
|
+
- tin
|
|
128
|
+
- tax_id
|
|
129
|
+
- account_number
|
|
130
|
+
- routing_number
|
|
131
|
+
- date_of_birth
|
|
132
|
+
- address
|
|
133
|
+
- bank_account_number
|
|
134
|
+
|
|
135
|
+
Gusto/Logging/PiiAccessor:
|
|
136
|
+
Description: 'Flags PII accessor methods such as `.email` in log calls.'
|
|
137
|
+
Severity: info
|
|
138
|
+
inherit_mode:
|
|
139
|
+
merge:
|
|
140
|
+
- Exclude
|
|
141
|
+
Exclude:
|
|
142
|
+
- '**/spec/**/*'
|
|
143
|
+
- '**/test/**/*'
|
|
144
|
+
|
|
145
|
+
Gusto/Logging/RawParams:
|
|
146
|
+
Description: 'Flags logging raw `params`, which may contain PII.'
|
|
147
|
+
Severity: info
|
|
148
|
+
inherit_mode:
|
|
149
|
+
merge:
|
|
150
|
+
- Exclude
|
|
151
|
+
Exclude:
|
|
152
|
+
- '**/spec/**/*'
|
|
153
|
+
- '**/test/**/*'
|
|
154
|
+
|
|
155
|
+
Gusto/LowercaseHeaders:
|
|
156
|
+
Description: 'HTTP response header keys should be lowercase for HTTP/2 and modern web standards compatibility.'
|
|
157
|
+
Enabled: true
|
|
158
|
+
Include:
|
|
159
|
+
- 'lib/middleware/**/*.rb'
|
|
160
|
+
- 'app/middleware/**/*.rb'
|
|
161
|
+
- 'app/controllers/**/*.rb'
|
|
162
|
+
- 'app/public/**/*controller*.rb'
|
|
163
|
+
- 'app/helpers/**/*.rb'
|
|
164
|
+
- 'packs/**/middleware/**/*.rb'
|
|
165
|
+
- 'packs/**/controllers/**/*.rb'
|
|
166
|
+
- 'packs/**/app/public/**/*controller*.rb'
|
|
167
|
+
- 'packs/**/app/helpers/**/*.rb'
|
|
168
|
+
- 'packs/**/lib/**/*_app.rb'
|
|
169
|
+
- 'components/**/middleware/**/*.rb'
|
|
170
|
+
- 'components/**/controllers/**/*.rb'
|
|
171
|
+
- 'config/initializers/**/*.rb'
|
|
172
|
+
Exclude:
|
|
173
|
+
- '**/spec/**/*'
|
|
174
|
+
|
|
115
175
|
Gusto/MinByMaxBy:
|
|
116
176
|
Description: 'Checks for the use of `min` or `max` with a proc. Corrects to `min_by` or `max_by`.'
|
|
117
177
|
Safe: false
|
|
@@ -176,6 +236,16 @@ Gusto/RspecDateTimeMock:
|
|
|
176
236
|
- '**/spec/**/*'
|
|
177
237
|
Safe: false
|
|
178
238
|
|
|
239
|
+
Gusto/Sidekiq/PerformAsyncStub:
|
|
240
|
+
Description: 'Prefer checking enqueued jobs over stubbing `perform_async`.'
|
|
241
|
+
# Opted into via config/sidekiq.yml when the project depends on Sidekiq.
|
|
242
|
+
Enabled: false
|
|
243
|
+
SafeAutoCorrect: false # Autocorrect appends `.and_call_original`, which runs real `perform_async` during the example
|
|
244
|
+
Include:
|
|
245
|
+
# Not just `*_spec.rb`: shared contexts, support files and system-spec helpers install these
|
|
246
|
+
# stubs on behalf of the specs that include them, which is where they are hardest to spot.
|
|
247
|
+
- '**/spec/**/*'
|
|
248
|
+
|
|
179
249
|
Gusto/SidekiqParams:
|
|
180
250
|
Description: 'Sidekiq perform methods cannot take keyword arguments.'
|
|
181
251
|
|
|
@@ -191,6 +261,13 @@ Gusto/Sorbet/PredicateBooleanReturn:
|
|
|
191
261
|
# which can change behavior in subtle ways.
|
|
192
262
|
SafeAutoCorrect: false
|
|
193
263
|
|
|
264
|
+
Gusto/Sorbet/StructDefaultEvaluatedOnce:
|
|
265
|
+
Description: 'T::Struct `default:` values are evaluated once at class load; use `factory:` for clock or random values.'
|
|
266
|
+
# Opted into via config/sorbet.yml when the project depends on Sorbet.
|
|
267
|
+
Enabled: false
|
|
268
|
+
# Autocorrect gives each instance a fresh value instead of the one computed at load time.
|
|
269
|
+
SafeAutoCorrect: false
|
|
270
|
+
|
|
194
271
|
Gusto/SuddenAssociations:
|
|
195
272
|
Description: 'Prevents defining Rails associations on another class from outside the class body.'
|
|
196
273
|
# Opted into via config/rails.yml; the association DSL only exists in a Rails project.
|
|
@@ -198,6 +275,11 @@ Gusto/SuddenAssociations:
|
|
|
198
275
|
Exclude:
|
|
199
276
|
- '**/spec/**/*' # allow stubbing, although this is still odd
|
|
200
277
|
|
|
278
|
+
Gusto/TimeoutTimeout:
|
|
279
|
+
Description: 'Flags `Timeout.timeout`, which interrupts its block with `Thread#raise` at an arbitrary point.'
|
|
280
|
+
Exclude:
|
|
281
|
+
- '**/spec/**/*'
|
|
282
|
+
|
|
201
283
|
Gusto/ToplevelConstants:
|
|
202
284
|
Description: 'Prevents top-level constants from being defined outside of initializers.'
|
|
203
285
|
Include:
|
|
@@ -220,6 +302,9 @@ Gusto/UnreferencedLet:
|
|
|
220
302
|
Include:
|
|
221
303
|
- '**/spec/**/*_spec.rb'
|
|
222
304
|
SafeAutoCorrect: false
|
|
305
|
+
PerExampleGroup: false
|
|
306
|
+
Cascade: false
|
|
307
|
+
LocalContextIncludes: []
|
|
223
308
|
|
|
224
309
|
Gusto/UsePaintNotColorize:
|
|
225
310
|
Description: 'Use Paint instead of colorize for terminal colors.'
|
data/config/obsoletion.yml
CHANGED
|
@@ -22,3 +22,13 @@ renamed:
|
|
|
22
22
|
Graphql/PreventScalarsDate: GraphQL/DisallowedTypes
|
|
23
23
|
Graphql/ResolverIgnoresObject: Gusto/Graphql/ResolverIgnoresObject
|
|
24
24
|
Graphql/TypeVariables: Gusto/Graphql/TypeVariables
|
|
25
|
+
|
|
26
|
+
# `Rack` and `Sidekiq` held one Gusto-authored cop each, alongside departments other gems
|
|
27
|
+
# own. They now live under `Gusto/`. These warn rather than error, so a consumer's existing
|
|
28
|
+
# config for the old name keeps applying to the new one until it is renamed.
|
|
29
|
+
Rack/LowercaseHeaderKeys:
|
|
30
|
+
new_name: Gusto/LowercaseHeaders
|
|
31
|
+
severity: warning
|
|
32
|
+
Sidekiq/PerformAsyncStub:
|
|
33
|
+
new_name: Gusto/Sidekiq/PerformAsyncStub
|
|
34
|
+
severity: warning
|
data/config/sidekiq.yml
CHANGED
data/config/sorbet.yml
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Gusto
|
|
6
|
+
module Logging
|
|
7
|
+
# Log-call detection and argument traversal shared by the `Gusto/Logging` cops.
|
|
8
|
+
module LogCallConcerns
|
|
9
|
+
extend NodePattern::Macros
|
|
10
|
+
|
|
11
|
+
LOG_METHODS = %i(debug info warn error fatal log).freeze
|
|
12
|
+
|
|
13
|
+
# @!method rails_logger?(node)
|
|
14
|
+
def_node_matcher :rails_logger?, <<~PATTERN
|
|
15
|
+
(send (const {nil? cbase} :Rails) :logger)
|
|
16
|
+
PATTERN
|
|
17
|
+
|
|
18
|
+
# @!method sidekiq_logger?(node)
|
|
19
|
+
def_node_matcher :sidekiq_logger?, <<~PATTERN
|
|
20
|
+
(send (const {nil? cbase} :Sidekiq) :logger)
|
|
21
|
+
PATTERN
|
|
22
|
+
|
|
23
|
+
# @!method bare_logger?(node)
|
|
24
|
+
def_node_matcher :bare_logger?, <<~PATTERN
|
|
25
|
+
(send nil? :logger)
|
|
26
|
+
PATTERN
|
|
27
|
+
|
|
28
|
+
# @!method raw_params?(node)
|
|
29
|
+
def_node_matcher :raw_params?, <<~PATTERN
|
|
30
|
+
(send nil? :params)
|
|
31
|
+
PATTERN
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def logger_call?(node)
|
|
36
|
+
supported_logger?(node.receiver)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def supported_logger?(node)
|
|
40
|
+
return false unless node
|
|
41
|
+
return true if rails_logger?(node) || sidekiq_logger?(node) || bare_logger?(node)
|
|
42
|
+
|
|
43
|
+
node.call_type? && node.method?(:tagged) && supported_logger?(node.receiver)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def pii_methods
|
|
47
|
+
@pii_methods ||= Array(config.dig("Gusto/Logging", "PiiMethods")).map(&:to_s).to_set
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def pii_method?(method_name)
|
|
51
|
+
name = method_name.to_s
|
|
52
|
+
pii_methods.include?(name) || pii_methods.any? { |pii| name.end_with?("_#{pii}") }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def each_send_in_log(log_node, &callback)
|
|
56
|
+
log_node.arguments.each do |arg|
|
|
57
|
+
yield_sends_from(arg, &callback)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Block-form logging carries the message in the block, not the arguments: `logger.info { "..." }`
|
|
61
|
+
parent_node = log_node.parent
|
|
62
|
+
if parent_node&.block_type? && parent_node.send_node == log_node
|
|
63
|
+
yield_sends_from(parent_node.body, &callback)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def yield_sends_from(node, &callback)
|
|
68
|
+
return unless node
|
|
69
|
+
|
|
70
|
+
yield node if node.call_type?
|
|
71
|
+
node.each_descendant(:call, &callback)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "log_call_concerns"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Gusto
|
|
8
|
+
module Logging
|
|
9
|
+
# Flags PII accessor methods, such as `.email` or `.ssn`, in log calls.
|
|
10
|
+
#
|
|
11
|
+
# Only calls with a receiver count (`user.email`, not a bare `email`). A predicate on the
|
|
12
|
+
# accessor (`user.email.present?`) is allowed because it logs a boolean, not the value.
|
|
13
|
+
# `PiiMethods` comes from the `Gusto/Logging` department. Each entry also
|
|
14
|
+
# matches as an underscore suffix, so `tax_id` covers `client_tax_id`.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad
|
|
18
|
+
# Rails.logger.info("User: #{user.email}")
|
|
19
|
+
# logger.warn("Failed for #{employee.ssn}")
|
|
20
|
+
# Rails.logger.info("Tax ID: #{input.client_tax_id}")
|
|
21
|
+
#
|
|
22
|
+
# # good
|
|
23
|
+
# Rails.logger.info("User: #{user.id}")
|
|
24
|
+
# Rails.logger.info("Has email: #{user.email.present?}")
|
|
25
|
+
#
|
|
26
|
+
class PiiAccessor < Base
|
|
27
|
+
include LogCallConcerns
|
|
28
|
+
|
|
29
|
+
MSG = "Avoid logging PII accessor `.%{method}`. Log an identifier instead."
|
|
30
|
+
RESTRICT_ON_SEND = LOG_METHODS
|
|
31
|
+
|
|
32
|
+
def on_send(node)
|
|
33
|
+
return unless logger_call?(node)
|
|
34
|
+
|
|
35
|
+
each_send_in_log(node) do |send_node|
|
|
36
|
+
next unless send_node.receiver
|
|
37
|
+
next unless pii_method?(send_node.method_name)
|
|
38
|
+
next if predicate_receiver?(send_node)
|
|
39
|
+
|
|
40
|
+
add_offense(send_node, message: format(MSG, method: send_node.method_name))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
alias_method :on_csend, :on_send
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def predicate_receiver?(node)
|
|
49
|
+
parent = node.parent
|
|
50
|
+
parent.call_type? && parent.predicate_method?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "log_call_concerns"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Gusto
|
|
8
|
+
module Logging
|
|
9
|
+
# Flags logging raw `params`, which may contain PII.
|
|
10
|
+
#
|
|
11
|
+
# `slice`, `permit`, `fetch`, `dig`, and `[]` are allowed; single-key access may still return a nested hash.
|
|
12
|
+
# `params.require(:key)` alone is flagged because it returns the whole nested hash.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad
|
|
16
|
+
# Rails.logger.info(params)
|
|
17
|
+
# logger.info(params.inspect)
|
|
18
|
+
# Rails.logger.info(params.require(:user))
|
|
19
|
+
#
|
|
20
|
+
# # good
|
|
21
|
+
# Rails.logger.info(params.slice(:id, :status))
|
|
22
|
+
# Rails.logger.info(params.require(:user).permit(:id))
|
|
23
|
+
#
|
|
24
|
+
class RawParams < Base
|
|
25
|
+
include LogCallConcerns
|
|
26
|
+
|
|
27
|
+
MSG = "Avoid logging raw `params` which may contain PII. " \
|
|
28
|
+
"Select reviewed fields with `params.slice(...)`. " \
|
|
29
|
+
"In Rails, `params.permit(...)` also selects fields, " \
|
|
30
|
+
"or use `request.filtered_parameters` for configured filtering."
|
|
31
|
+
RESTRICT_ON_SEND = LOG_METHODS
|
|
32
|
+
|
|
33
|
+
# @!method params_serialization?(node)
|
|
34
|
+
def_node_matcher :params_serialization?, <<~PATTERN
|
|
35
|
+
(call (send nil? :params) {:to_s :inspect :to_json :to_yaml} ...)
|
|
36
|
+
PATTERN
|
|
37
|
+
|
|
38
|
+
# @!method required_params?(node)
|
|
39
|
+
def_node_matcher :required_params?, <<~PATTERN
|
|
40
|
+
(call (send nil? :params) :require ...)
|
|
41
|
+
PATTERN
|
|
42
|
+
|
|
43
|
+
# @!method safe_params?(node)
|
|
44
|
+
def_node_matcher :safe_params?, <<~PATTERN
|
|
45
|
+
(call {(send nil? :params) #required_params?} {:slice :permit :fetch :dig :[]} ...)
|
|
46
|
+
PATTERN
|
|
47
|
+
|
|
48
|
+
def on_send(node)
|
|
49
|
+
return unless logger_call?(node)
|
|
50
|
+
|
|
51
|
+
each_send_in_log(node) do |send_node|
|
|
52
|
+
add_offense(send_node) if exposed_params?(send_node)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
alias_method :on_csend, :on_send
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def exposed_params?(node)
|
|
61
|
+
raw_params?(node) ? !params_with_method_call?(node) : params_serialization?(node)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def params_with_method_call?(params_node)
|
|
65
|
+
parent = params_node.parent
|
|
66
|
+
return false unless parent.call_type?
|
|
67
|
+
# `params.require(:key)` returns the whole nested hash, so it is only safe once narrowed.
|
|
68
|
+
return safe_params?(parent.parent) if required_params?(parent)
|
|
69
|
+
|
|
70
|
+
safe_params?(parent) || params_serialization?(parent)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module RuboCop
|
|
4
4
|
module Cop
|
|
5
|
-
module
|
|
5
|
+
module Gusto
|
|
6
6
|
# Detects HTTP response headers with uppercase characters.
|
|
7
7
|
# HTTP response header keys should be lowercase for compatibility
|
|
8
8
|
# with HTTP/2 and modern web standards.
|
|
@@ -24,7 +24,7 @@ module RuboCop
|
|
|
24
24
|
# response.set_header('x-custom', 'value')
|
|
25
25
|
# response.headers['content-security-policy'] += policy
|
|
26
26
|
#
|
|
27
|
-
class
|
|
27
|
+
class LowercaseHeaders < Base
|
|
28
28
|
extend AutoCorrector
|
|
29
29
|
|
|
30
30
|
MSG = "HTTP response header keys should be lowercase. Use `%{downcased}` instead of `%{original}`."
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Gusto
|
|
6
|
+
module Sidekiq
|
|
7
|
+
# Checks that `perform_async` calls to enqueue Sidekiq jobs are not stubbed
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# allow(Foo).to receive(:perform_async)
|
|
12
|
+
# expect(Foo).to receive(:perform_async)
|
|
13
|
+
# expect(Foo).not_to receive(:perform_async)
|
|
14
|
+
#
|
|
15
|
+
# # bad - the stub it verifies is itself the offense, and it may have been
|
|
16
|
+
# # set up in a shared context this cop cannot see
|
|
17
|
+
# expect(Foo).to have_received(:perform_async)
|
|
18
|
+
#
|
|
19
|
+
# # good (still invokes the real method)
|
|
20
|
+
# allow(Foo).to receive(:perform_async).and_call_original
|
|
21
|
+
# expect(Foo).to receive(:perform_async).with(arg).and_call_original
|
|
22
|
+
# allow(Foo).to receive(:perform_async).and_wrap_original { |m, *args| m.call(*args) }
|
|
23
|
+
#
|
|
24
|
+
# # good (checking enqueued jobs)
|
|
25
|
+
# expect { subject }.to change(Foo.jobs, :count).by(n)
|
|
26
|
+
# expect { subject }.not_to change(Foo.jobs, :count)
|
|
27
|
+
# expect(Foo.jobs.count).to eq(n)
|
|
28
|
+
#
|
|
29
|
+
# # good - there is no other way to make an enqueue fail. Sidekiq's testing API
|
|
30
|
+
# # pushes onto Foo.jobs and offers no failure injection, so a spec covering the
|
|
31
|
+
# # rescue around an enqueue has to raise from the stub.
|
|
32
|
+
# allow(Foo).to receive(:perform_async).and_raise(StandardError, "redis down")
|
|
33
|
+
#
|
|
34
|
+
# @safety
|
|
35
|
+
# Autocorrect is unsafe: it appends `.and_call_original` on positive `receive` only, which runs
|
|
36
|
+
# the real `perform_async` during the example (may enqueue jobs, hit external code, or
|
|
37
|
+
# change expectations vs a pure stub). There is no autocorrect for `not_to` / `to_not receive`,
|
|
38
|
+
# since `.and_call_original` would not apply to a negative expectation, nor for
|
|
39
|
+
# `have_received`, whose stub lives elsewhere. Autocorrect is also suppressed when the
|
|
40
|
+
# expectation uses a block, since appending `.and_call_original` would rebind the block to
|
|
41
|
+
# the wrong method.
|
|
42
|
+
class PerformAsyncStub < Base
|
|
43
|
+
extend AutoCorrector
|
|
44
|
+
|
|
45
|
+
MSG = "Prefer checking enqueued jobs over stubbing `perform_async`."
|
|
46
|
+
MSG_RECEIVE = "Prefer checking enqueued jobs over stubbing `perform_async` or add `.and_call_original`."
|
|
47
|
+
RESTRICT_ON_SEND = %i(receive have_received).freeze
|
|
48
|
+
|
|
49
|
+
# Chain modifiers that leave the real `perform_async` reachable, so the example is not
|
|
50
|
+
# replacing the enqueue with a canned result. `and_raise` belongs here even though it
|
|
51
|
+
# never calls the original: simulating a failed enqueue is the one thing Sidekiq's
|
|
52
|
+
# testing API cannot express, so a spec covering the rescue has no other route.
|
|
53
|
+
ALLOWED_CHAIN = %i(and_call_original and_wrap_original and_raise).freeze
|
|
54
|
+
|
|
55
|
+
# @!method stub_perform_async?(node)
|
|
56
|
+
def_node_matcher :stub_perform_async?, <<~PATTERN
|
|
57
|
+
(send nil? {:receive :have_received} (sym :perform_async))
|
|
58
|
+
PATTERN
|
|
59
|
+
|
|
60
|
+
def on_send(node)
|
|
61
|
+
return unless stub_perform_async?(node)
|
|
62
|
+
# `have_received` only verifies a stub that was installed somewhere else, possibly in a
|
|
63
|
+
# shared context or support file out of this cop's reach, so it is flagged on sight and
|
|
64
|
+
# there is nothing local to autocorrect.
|
|
65
|
+
return add_offense(node) if node.method?(:have_received)
|
|
66
|
+
|
|
67
|
+
negative_expectation = false
|
|
68
|
+
allowed_chain = false
|
|
69
|
+
|
|
70
|
+
current = node.parent
|
|
71
|
+
while current&.call_type?
|
|
72
|
+
negative_expectation = true if current.method?(:not_to) || current.method?(:to_not)
|
|
73
|
+
allowed_chain = true if ALLOWED_CHAIN.include?(current.method_name)
|
|
74
|
+
|
|
75
|
+
current = current.parent
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
return add_offense(node) if negative_expectation
|
|
79
|
+
return if allowed_chain
|
|
80
|
+
|
|
81
|
+
tail = message_expectation_chain_tail(node)
|
|
82
|
+
return add_offense(node, message: MSG_RECEIVE) if tail.parent&.block_type?
|
|
83
|
+
|
|
84
|
+
add_offense(node, message: MSG_RECEIVE) do |corrector|
|
|
85
|
+
corrector.insert_after(tail, ".and_call_original")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
alias_method :on_csend, :on_send
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def message_expectation_chain_tail(node)
|
|
94
|
+
tail = node
|
|
95
|
+
loop do
|
|
96
|
+
parent = tail.parent
|
|
97
|
+
break unless parent&.call_type?
|
|
98
|
+
break unless parent.receiver.equal?(tail)
|
|
99
|
+
|
|
100
|
+
tail = parent
|
|
101
|
+
end
|
|
102
|
+
tail
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|