contrast-agent 3.15.0 → 3.16.0

Sign up to get free protection for your applications and to get access to all the features.
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