contrast-agent 3.15.0 → 3.16.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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent.rb +2 -9
  3. data/lib/contrast/agent/assess/contrast_event.rb +142 -70
  4. data/lib/contrast/agent/assess/events/source_event.rb +1 -1
  5. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +10 -3
  6. data/lib/contrast/agent/assess/policy/policy_node.rb +15 -10
  7. data/lib/contrast/agent/assess/policy/policy_scanner.rb +7 -1
  8. data/lib/contrast/agent/assess/policy/propagator/insert.rb +1 -1
  9. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +0 -3
  10. data/lib/contrast/agent/assess/policy/propagator/select.rb +1 -3
  11. data/lib/contrast/agent/assess/policy/propagator/splat.rb +0 -5
  12. data/lib/contrast/agent/assess/policy/propagator/split.rb +12 -13
  13. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +21 -14
  14. data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +4 -5
  15. data/lib/contrast/agent/assess/policy/trigger_method.rb +39 -14
  16. data/lib/contrast/agent/assess/policy/trigger_node.rb +31 -37
  17. data/lib/contrast/agent/assess/property/evented.rb +5 -18
  18. data/lib/contrast/agent/assess/property/tagged.rb +9 -3
  19. data/lib/contrast/agent/assess/property/updated.rb +0 -5
  20. data/lib/contrast/agent/assess/rule/provider/hardcoded_key.rb +58 -5
  21. data/lib/contrast/agent/assess/rule/provider/hardcoded_password.rb +23 -8
  22. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +82 -14
  23. data/lib/contrast/agent/assess/tag.rb +1 -1
  24. data/lib/contrast/agent/at_exit_hook.rb +5 -5
  25. data/lib/contrast/agent/patching/policy/after_load_patch.rb +5 -5
  26. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +20 -20
  27. data/lib/contrast/agent/patching/policy/module_policy.rb +10 -10
  28. data/lib/contrast/agent/patching/policy/policy.rb +16 -2
  29. data/lib/contrast/agent/protect/policy/applies_command_injection_rule.rb +3 -5
  30. data/lib/contrast/agent/protect/policy/applies_xxe_rule.rb +1 -1
  31. data/lib/contrast/agent/protect/rule/no_sqli/mongo_no_sql_scanner.rb +1 -0
  32. data/lib/contrast/agent/request.rb +34 -34
  33. data/lib/contrast/agent/static_analysis.rb +6 -6
  34. data/lib/contrast/agent/version.rb +1 -1
  35. data/lib/contrast/api/communication/socket_client.rb +36 -1
  36. data/lib/contrast/api/decorators/address.rb +13 -13
  37. data/lib/contrast/api/decorators/message.rb +1 -0
  38. data/lib/contrast/api/decorators/trace_event.rb +20 -18
  39. data/lib/contrast/components/app_context.rb +39 -30
  40. data/lib/contrast/components/contrast_service.rb +9 -9
  41. data/lib/contrast/components/settings.rb +20 -23
  42. data/lib/contrast/config/service_configuration.rb +4 -2
  43. data/lib/contrast/configuration.rb +1 -1
  44. data/lib/contrast/extension/assess/array.rb +7 -3
  45. data/lib/contrast/extension/assess/erb.rb +5 -0
  46. data/lib/contrast/extension/assess/eval_trigger.rb +6 -6
  47. data/lib/contrast/extension/assess/exec_trigger.rb +1 -1
  48. data/lib/contrast/extension/assess/fiber.rb +3 -3
  49. data/lib/contrast/extension/assess/hash.rb +3 -3
  50. data/lib/contrast/extension/assess/kernel.rb +18 -20
  51. data/lib/contrast/extension/assess/marshal.rb +8 -4
  52. data/lib/contrast/extension/assess/regexp.rb +3 -3
  53. data/lib/contrast/extension/assess/string.rb +13 -11
  54. data/lib/contrast/extension/protect/kernel.rb +3 -3
  55. data/lib/contrast/framework/base_support.rb +1 -1
  56. data/lib/contrast/framework/manager.rb +3 -3
  57. data/lib/contrast/framework/rack/patch/session_cookie.rb +9 -9
  58. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +13 -13
  59. data/lib/contrast/framework/rails/patch/rails_application_configuration.rb +10 -10
  60. data/lib/contrast/framework/rails/patch/support.rb +1 -1
  61. data/lib/contrast/framework/rails/rewrite/action_controller_railties_helper_inherited.rb +11 -11
  62. data/lib/contrast/framework/rails/rewrite/active_record_attribute_methods_read.rb +12 -12
  63. data/lib/contrast/framework/rails/rewrite/active_record_named.rb +3 -3
  64. data/lib/contrast/framework/rails/rewrite/active_record_time_zone_inherited.rb +12 -12
  65. data/lib/contrast/framework/sinatra/patch/base.rb +11 -11
  66. data/lib/contrast/framework/sinatra/support.rb +4 -4
  67. data/lib/contrast/logger/log.rb +7 -2
  68. data/lib/contrast/utils/invalid_configuration_util.rb +2 -5
  69. data/resources/assess/policy.json +31 -12
  70. data/ruby-agent.gemspec +4 -3
  71. data/service_executables/VERSION +1 -1
  72. data/service_executables/linux/contrast-service +0 -0
  73. data/service_executables/mac/contrast-service +0 -0
  74. metadata +31 -17
@@ -51,16 +51,23 @@ module Contrast
51
51
  incoming_tracked = args && determine_tracked(args)
52
52
  return ret unless self_tracked || incoming_tracked
53
53
 
54
+ parent_events = []
54
55
  if block
55
56
  block_sub(self_tracked, source, ret)
56
57
  elsif args.is_a?(String)
57
- string_sub(self_tracked, preshift, ret, args, incoming_tracked, global)
58
+ string_sub(parent_events, self_tracked, preshift, ret, args, incoming_tracked, global)
58
59
  elsif args.is_a?(Hash)
59
60
  hash_sub(self_tracked, source, ret)
60
61
  else # Enumerator, only for gsub
61
- pattern_gsub(preshift, ret)
62
+ pattern_gsub(parent_events, preshift, ret)
62
63
  end
63
- string_build_event(patcher, preshift, ret)
64
+
65
+ if self_tracked
66
+ source_properties = Contrast::Agent::Assess::Tracker.properties(source)
67
+ parent_event = source_properties&.event
68
+ parent_events.prepend(parent_event) if parent_event
69
+ end
70
+ string_build_event(parent_events, patcher, preshift, ret)
64
71
  rescue StandardError => e
65
72
  logger.error('Unable to apply gsub propagator', e)
66
73
  end
@@ -84,10 +91,14 @@ module Contrast
84
91
  end
85
92
  end
86
93
 
87
- def string_sub self_tracked, preshift, ret, incoming, incoming_tracked, global
94
+ def string_sub parent_events, self_tracked, preshift, ret, incoming, incoming_tracked, global
88
95
  properties = Contrast::Agent::Assess::Tracker.properties(ret)
89
96
  return unless properties
90
97
 
98
+ incoming_properties = Contrast::Agent::Assess::Tracker.properties(incoming)
99
+ parent_event = incoming_properties&.event
100
+ parent_events << parent_event if parent_event
101
+
91
102
  pattern = preshift.args[0]
92
103
  source = preshift.object
93
104
 
@@ -119,8 +130,8 @@ module Contrast
119
130
  properties.delete_tags_at_ranges(ranges)
120
131
  properties.shift_tags(ranges)
121
132
  return unless incoming_tracked
133
+ return unless incoming_properties
122
134
 
123
- incoming_properties = Contrast::Agent::Assess::Tracker.properties(incoming)
124
135
  tags = incoming_properties.tag_keys
125
136
  ranges.each do |range|
126
137
  tags.each do |tag|
@@ -139,7 +150,7 @@ module Contrast
139
150
  properties&.splat_from(source, ret) if self_tracked
140
151
  end
141
152
 
142
- def pattern_gsub preshift, ret
153
+ def pattern_gsub parent_events, preshift, ret
143
154
  properties = Contrast::Agent::Assess::Tracker.properties(ret)
144
155
  return unless properties
145
156
 
@@ -150,20 +161,15 @@ module Contrast
150
161
  source_properties.tag_keys.each do |key|
151
162
  properties.add_tag(key, 0...1)
152
163
  end
164
+ parent_event = source_properties.event
165
+ parent_events << parent_event if parent_event
153
166
  end
154
167
 
155
- def string_build_event patcher, preshift, ret
168
+ def string_build_event parent_events, patcher, preshift, ret
156
169
  return unless Contrast::Agent::Assess::Tracker.tracked?(ret)
157
170
 
158
171
  properties = Contrast::Agent::Assess::Tracker.properties(ret)
159
172
  args = preshift.args
160
- if args.length > 1
161
- arg = args[1]
162
- arg_properties = Contrast::Agent::Assess::Tracker.properties(arg)
163
- arg_properties&.events&.each do |event|
164
- properties.events << event
165
- end
166
- end
167
173
  properties.build_event(
168
174
  patcher,
169
175
  ret,
@@ -171,6 +177,7 @@ module Contrast
171
177
  ret,
172
178
  args,
173
179
  2)
180
+ properties.event.instance_variable_set(:@_parent_events, parent_events)
174
181
  end
175
182
  end
176
183
  end
@@ -33,16 +33,15 @@ module Contrast
33
33
  interpolated_inputs = []
34
34
  handle_binding_variables(scope, erb_template_prerender, ret, interpolated_inputs)
35
35
  handle_local_variables(args, erb_template_prerender, ret, interpolated_inputs)
36
+ properties.build_event(TEMPLATE_PROPAGATION_NODE, ret, erb_template_prerender, ret, interpolated_inputs)
36
37
  unless interpolated_inputs.empty?
38
+ current_event = properties.event
37
39
  interpolated_inputs.each do |input|
38
40
  input_properties = Contrast::Agent::Assess::Tracker.properties(input)
39
- next unless input_properties
41
+ next unless input_properties&.event
40
42
 
41
- input_properties.events.each do |event|
42
- properties.events << event
43
- end
43
+ current_event.parent_events << input_properties.event
44
44
  end
45
- properties.build_event(TEMPLATE_PROPAGATION_NODE, ret, erb_template_prerender, ret, interpolated_inputs)
46
45
  end
47
46
 
48
47
  if Contrast::Agent::Assess::Tracker.tracked?(ret)
@@ -27,6 +27,24 @@ module Contrast
27
27
  CURRENT_FINDING_VERSION = 4
28
28
 
29
29
  class << self
30
+ # Append the given finding to the given context to be reported when
31
+ # the Context's activity is sent to the Service or, in the absence
32
+ # of that Context, generate an Activity and queue it manually
33
+ # @param finding [Contrast::Api::Dtm::Finding]
34
+ def report_finding finding
35
+ context = Contrast::Agent::REQUEST_TRACKER.current
36
+ if context
37
+ context.activity.findings << finding
38
+ else
39
+ activity = Contrast::Api::Dtm::Activity.new
40
+ activity.findings << finding
41
+
42
+ Contrast::Agent.messaging_queue.send_event_eventually(activity)
43
+ end
44
+ logger.debug('Finding reported',
45
+ rule: finding.rule_id)
46
+ end
47
+
30
48
  # This is called from within our woven proc. It will be called as if it
31
49
  # were inline in the Rack application.
32
50
  #
@@ -69,8 +87,8 @@ module Contrast
69
87
  # This converts the source of the finding, and the events leading
70
88
  # up to it into a Finding
71
89
  #
72
- # @param context [Contrast::Utils::ThreadTracker] the current request
73
- # context
90
+ # @param context [Contrast::Agent::RequestContext] the current
91
+ # request context
74
92
  # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode]
75
93
  # the node to direct applying this trigger event
76
94
  # @param source [Object] the source of the Trigger Event
@@ -98,11 +116,11 @@ module Contrast
98
116
  build_hash(finding, source)
99
117
  finding.routes << context.route if context.route
100
118
  finding.version = determine_compliance_version(finding)
101
- context.activity.findings << finding
102
119
  logger.trace('Finding created',
103
120
  node_id: trigger_node.id,
104
121
  source_id: source.__id__,
105
122
  rule: trigger_node.rule_id)
123
+ report_finding(finding)
106
124
  rescue StandardError => e
107
125
  logger.error('Unable to build a finding', e, rule: trigger_node.rule_id, node_id: trigger_node.id)
108
126
  end
@@ -112,8 +130,8 @@ module Contrast
112
130
  # This is our method that actually checks the taint on the object
113
131
  # our trigger_node targets.
114
132
  #
115
- # @param context [Contrast::Utils::ThreadTracker] the current request
116
- # context
133
+ # @param context [Contrast::Agent::RequestContext] the current
134
+ # request context
117
135
  # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode]
118
136
  # the node to direct applying this trigger event
119
137
  # @param source [Object] the source of the Trigger Event
@@ -181,8 +199,8 @@ module Contrast
181
199
  # This is our method that actually checks the taint on the object
182
200
  # our trigger_node targets for our Regexp based rules.
183
201
  #
184
- # @param context [Contrast::Utils::ThreadTracker] the current request
185
- # context
202
+ # @param context [Contrast::Agent::RequestContext] the current
203
+ # request context
186
204
  # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode]
187
205
  # the node to direct applying this trigger event
188
206
  # @param source [Object] the source of the Trigger Event
@@ -201,8 +219,8 @@ module Contrast
201
219
  # This is our method that actually checks the taint on the object
202
220
  # our trigger_node targets for our Dataflow based rules.
203
221
  #
204
- # @param context [Contrast::Utils::ThreadTracker] the current request
205
- # context
222
+ # @param context [Contrast::Agent::RequestContext] the current
223
+ # request context
206
224
  # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode]
207
225
  # the node to direct applying this trigger event
208
226
  # @param source [Object] the source of the Trigger Event
@@ -244,11 +262,7 @@ module Contrast
244
262
  properties = Contrast::Agent::Assess::Tracker.properties(source)
245
263
  return unless properties
246
264
 
247
- # events could technically be nil, but we would have failed
248
- # the rule check before getting here. not worth the nil check
249
- properties.events.each do |event|
250
- finding.events << event.to_dtm_event
251
- end
265
+ build_events finding, properties.event if properties.event
252
266
 
253
267
  # Google::Protobuf::Map doesn't support merge!, so we have to do this
254
268
  # long form
@@ -261,6 +275,17 @@ module Contrast
261
275
  end
262
276
  end
263
277
 
278
+ def build_events finding, event
279
+ return unless event
280
+
281
+ event.parent_events&.each do |parent_event|
282
+ build_events(finding, parent_event)
283
+ end
284
+ # events could technically be nil, but we would have failed
285
+ # the rule check before getting here. not worth the nil check
286
+ finding.events << event.to_dtm_event
287
+ end
288
+
264
289
  def build_hash finding, source
265
290
  hash_code = Contrast::Utils::HashDigest.generate_event_hash(finding, source)
266
291
  finding.hash_code = Contrast::Utils::StringUtils.force_utf8(hash_code)
@@ -104,13 +104,13 @@ module Contrast
104
104
 
105
105
  properties = Contrast::Agent::Assess::Tracker.properties(source)
106
106
  # find the ranges that violate the rule (untrusted, etc)
107
- vulnerable_ranges = find_ranges_by_all_tags(Contrast::Utils::StringUtils.ret_length(source), properties, required_tags)
107
+ vulnerable_ranges = ranges_with_all_tags(Contrast::Utils::StringUtils.ret_length(source), properties, required_tags)
108
108
  # if there aren't any vulnerable ranges, nope out
109
109
  return false if vulnerable_ranges.empty?
110
110
 
111
111
  # find the ranges that are exempt from the rule
112
112
  # (validated, sanitized, etc)
113
- secure_ranges = find_ranges_by_any_tag(properties, disallowed_tags)
113
+ secure_ranges = ranges_with_any_tag(properties, disallowed_tags)
114
114
  # if there are vulnerable ranges and no secure, report
115
115
  return true if secure_ranges.empty?
116
116
 
@@ -181,49 +181,43 @@ module Contrast
181
181
  # tags.
182
182
  # @param properties [Contrast::Agent::Assess::Properties] the
183
183
  # properties to check for the tags
184
- # @param tags [Set<String>] the list of tags on which to match
184
+ # @param required_tags [Set<String>] the list of tags on which to match
185
185
  # @return [Array<Contrast::Agent::Assess::Tag>] the ranges satisfied
186
186
  # by the given conditions
187
- def find_ranges_by_all_tags length, properties, tags
188
- # if there aren't any all_tags or tags, break early
187
+ def ranges_with_all_tags length, properties, required_tags
188
+ # if there are no tags, not required tags, or the tags don't have
189
+ # all the required tags, we can just return here.
189
190
  return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless properties.tracked?
190
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags&.any?
191
-
192
- # :zap: faster to treat all as any if there's only one tag
193
- return find_ranges_by_any_tag(properties, tags) if tags.length == 1
191
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless required_tags&.any?
192
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless required_tags.all? { |tag| properties.tag_keys.include?(tag) }
194
193
 
195
194
  ranges = []
196
- # TODO: RUBY-946 clean this up, perhaps with
197
- # tags.each { |tag| applicable << properties.fetch_tag(tag) }
198
- # return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless applicable.length == tags.length
199
- # ...
195
+ chunking = false
200
196
  # find all the indicies on the source that have all the given tags
201
197
  (0..length).each do |idx|
202
- tags_at = properties.tags_at(idx)
203
- ranges << idx if tags.all? do |tag|
204
- found = false
205
- tags_at.each do |tag_at|
206
- found = tag_at.label == tag
207
- break if found
208
- end
209
- found
198
+ tags_at = properties.tags_at(idx).to_a
199
+ # only those that have all the required tags in the tags_at
200
+ # satisfy the requirement
201
+ satisfied = tags_at.any? && required_tags.all? { |tag| tags_at.any? { |found| found.label == tag } }
202
+ # if this range matches all the required tags and we're already
203
+ # chunking, meaning the previous range matched, do nothing
204
+ next if satisfied && chunking
205
+
206
+ # if we are satisfied and we were not chunking, this represents
207
+ # the start of the next range, so create a new entry.
208
+ if satisfied
209
+ ranges << Contrast::Agent::Assess::Tag.new('required', 0, idx)
210
+ chunking = true
211
+ # if we are chunking and not satisfied, this represents the end
212
+ # of the range, meaning the last index is what satisfied the
213
+ # range. Because the range is exclusive end, we can just use this
214
+ # index.
215
+ elsif chunking
216
+ ranges[-1]&.update_end(idx)
217
+ chunking = false
210
218
  end
211
219
  end
212
- # break early if no indicies satisfy all the tags
213
- return Contrast::Utils::ObjectShare::EMPTY_ARRAY if ranges.empty?
214
-
215
- # chunk all the adjacent ranges
216
- chunked = ranges.chunk_while { |i, j| i + 1 == j }
217
- tag_ranges = []
218
- # and convert them into Tags
219
- chunked.each do |join|
220
- start = join[0]
221
- stop = join[-1]
222
- # add the 1 to account for end index being exclusive
223
- tag_length = stop - start + 1
224
- tag_ranges = Contrast::Utils::TagUtil.ordered_merge(tag_ranges, Tag.new(tag_length, start))
225
- end
226
- tag_ranges
220
+ ranges
227
221
  end
228
222
 
229
223
  # Find the ranges that satisfy any of the given tags.
@@ -233,7 +227,7 @@ module Contrast
233
227
  # @param tags [Set<String>] the list of tags on which to match
234
228
  # @return [Array<Contrast::Agent::Assess::Tag>] the ranges satisfied
235
229
  # by the given conditions
236
- def find_ranges_by_any_tag properties, tags
230
+ def ranges_with_any_tag properties, tags
237
231
  # if there aren't any all_tags or tags, break early
238
232
  return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless properties.tracked?
239
233
  return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags&.any?
@@ -10,23 +10,11 @@ module Contrast
10
10
  module Property
11
11
  # This module serves to hold the functionality required for the
12
12
  # management of our dataflow events.
13
+ #
14
+ # @attr_reader event [Contrast::Agent::Assess::ContrastEvent] the
15
+ # latest event to track
13
16
  module Evented
14
- # The events for this object.
15
- #
16
- # @return [Array<Contrast::Agent::Assess::ContrastEvent>]
17
- def events
18
- @_events ||= []
19
- end
20
-
21
- # Add an event to these properties. It will be used to build
22
- # a trace if this object ends up in a trigger.
23
- #
24
- # @param event [Contrast::Agent::Assess::ContrastEvent] the latest
25
- # event to track
26
- def add_event event
27
- events << event
28
- self
29
- end
17
+ attr_accessor :event
30
18
 
31
19
  # Create a new event and add it to the event set.
32
20
  #
@@ -43,8 +31,7 @@ module Contrast
43
31
  # the key used to accessed if from a map or nil if a type like
44
32
  # BODY
45
33
  def build_event policy_node, tagged, object, ret, args, source_type = nil, source_name = nil
46
- event = Contrast::Agent::Assess::Events::EventFactory.build(policy_node, tagged, object, ret, args, source_type, source_name)
47
- add_event(event)
34
+ @event = Contrast::Agent::Assess::Events::EventFactory.build(policy_node, tagged, object, ret, args, source_type, source_name)
48
35
  report_sources(tagged, event)
49
36
  end
50
37
 
@@ -162,15 +162,21 @@ module Contrast
162
162
  end
163
163
 
164
164
  # We'll use this as a helper method to retrieve tags from the hash.
165
- # Because the hash auto-populates an empty array when we try to access
166
- # a tag in it, we cannot use the [] method without side effect. To get
167
- # around this, we'll use a fetch work around.
165
+ # Because the hash auto-populates an empty array when we try to
166
+ # access a tag in it, we cannot use the [] method without side
167
+ # effect. To get around this, we'll use a fetch work around.
168
+ #
169
+ # @param label [Symbol] the label to look up
170
+ # @return [Array<Contrast::Agent::Assess::Tag>] all the tags with
171
+ # that label
168
172
  def fetch_tag label
169
173
  tags.fetch(label, nil) if tracked?
170
174
  end
171
175
 
172
176
  # Convert the tags of this object into the TraceTaintRange required
173
177
  # to be sent to the service
178
+ #
179
+ # @return [Array<Contrast::Api::Dtm::TraceTaintRange>]
174
180
  def tags_to_dtm
175
181
  Contrast::Api::Dtm::TraceTaintRange.build_for_event(tags)
176
182
  end
@@ -26,11 +26,6 @@ module Contrast
26
26
  return unless original
27
27
 
28
28
  adjust_duplicate(original)
29
-
30
- original.events.each do |event|
31
- events << event
32
- end
33
-
34
29
  original.tag_keys.each do |key|
35
30
  next if skip_tags&.include?(key)
36
31
 
@@ -33,6 +33,64 @@ module Contrast
33
33
  NON_KEY_PARTIAL_NAMES.none? { |name| constant_string.index(name) }
34
34
  end
35
35
 
36
+ BYTE_HOLDERS = %i[ARRAY LIST].cs__freeze
37
+ # Determine if the given value node violates the hardcode key rule
38
+ # @param value_node [RubyVM::AbstractSyntaxTree::Node] the node to
39
+ # evaluate
40
+ # @return [Boolean]
41
+ def value_node_passes? value_node
42
+ # If it's a freeze call, then evaluate the entity being frozen
43
+ value_node = value_node.children[0] if freeze_call?(value_node)
44
+ # If it's a String being turned into bytes, then it matches key
45
+ # expectations
46
+ return true if bytes_call?(value_node)
47
+
48
+ type = value_node.type
49
+ return false unless BYTE_HOLDERS.include?(type)
50
+ return false unless value_node.children.any?
51
+
52
+ # Unless this is an array of literal numerics, we don't match.
53
+ # That array seems to always end in a nil value, so we allow
54
+ # those as well.
55
+ value_node.children.each do |child|
56
+ next unless child
57
+
58
+ return false unless child.cs__is_a?(RubyVM::AbstractSyntaxTree::Node) &&
59
+ child.type == :LIT &&
60
+ child.children[0]&.cs__is_a?(Integer)
61
+ end
62
+
63
+ true
64
+ end
65
+
66
+ REDACTED_MARKER = ' = [**REDACTED**]'
67
+ def redacted_marker
68
+ REDACTED_MARKER
69
+ end
70
+
71
+ # A node is a bytes_call if it's the Node for String#bytes. We care
72
+ # about this specifically as it's likely to be a common way to
73
+ # generate a key constant, rather than directly declaring an
74
+ # integer array.
75
+ #
76
+ # @param value_node [RubyVM::AbstractSyntaxTree::Node] the node to
77
+ # evaluate
78
+ # @return [Boolean] is this a node for String#bytes or not
79
+ def bytes_call? value_node
80
+ return false unless value_node.type == :CALL
81
+
82
+ children = value_node.children
83
+ return false unless children
84
+ return false unless children.length >= 2
85
+
86
+ potential_string_node = children[0]
87
+ return false unless potential_string_node.cs__is_a?(RubyVM::AbstractSyntaxTree::Node) &&
88
+ potential_string_node.type == :STR
89
+
90
+ children[1] == :bytes
91
+ end
92
+
93
+ # TODO: RUBY-1014 remove `#value_type_passes?` and `#value_passes?`
36
94
  # If the value is a byte array, or at least an array of numbers, it
37
95
  # passes for this rule
38
96
  def value_type_passes? value
@@ -49,11 +107,6 @@ module Contrast
49
107
  def value_passes? _value
50
108
  true
51
109
  end
52
-
53
- REDACTED_MARKER = ' = [**REDACTED**]'
54
- def redacted_marker
55
- REDACTED_MARKER
56
- end
57
110
  end
58
111
  end
59
112
  end