contrast-agent 4.3.2 → 4.4.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent.rb +5 -1
  3. data/lib/contrast/agent/assess.rb +0 -9
  4. data/lib/contrast/agent/assess/contrast_event.rb +0 -2
  5. data/lib/contrast/agent/assess/contrast_object.rb +5 -2
  6. data/lib/contrast/agent/assess/finalizers/hash.rb +7 -0
  7. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +17 -3
  8. data/lib/contrast/agent/assess/policy/propagation_method.rb +28 -13
  9. data/lib/contrast/agent/assess/policy/propagator/append.rb +28 -13
  10. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +21 -16
  11. data/lib/contrast/agent/assess/policy/propagator/splat.rb +23 -13
  12. data/lib/contrast/agent/assess/policy/propagator/split.rb +14 -7
  13. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +30 -14
  14. data/lib/contrast/agent/assess/policy/trigger_method.rb +13 -8
  15. data/lib/contrast/agent/assess/policy/trigger_node.rb +28 -7
  16. data/lib/contrast/agent/assess/policy/trigger_validation/redos_validator.rb +59 -0
  17. data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +1 -2
  18. data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +6 -4
  19. data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +2 -4
  20. data/lib/contrast/agent/assess/properties.rb +0 -2
  21. data/lib/contrast/agent/assess/property/tagged.rb +37 -19
  22. data/lib/contrast/agent/assess/tracker.rb +1 -1
  23. data/lib/contrast/agent/middleware.rb +85 -55
  24. data/lib/contrast/agent/patching/policy/patch_status.rb +1 -1
  25. data/lib/contrast/agent/patching/policy/patcher.rb +51 -44
  26. data/lib/contrast/agent/patching/policy/trigger_node.rb +5 -2
  27. data/lib/contrast/agent/protect/rule/sqli.rb +17 -11
  28. data/lib/contrast/agent/request_context.rb +12 -0
  29. data/lib/contrast/agent/thread.rb +1 -1
  30. data/lib/contrast/agent/thread_watcher.rb +20 -5
  31. data/lib/contrast/agent/version.rb +1 -1
  32. data/lib/contrast/api/communication/messaging_queue.rb +18 -21
  33. data/lib/contrast/api/communication/response_processor.rb +8 -1
  34. data/lib/contrast/api/communication/socket_client.rb +22 -14
  35. data/lib/contrast/api/decorators.rb +2 -0
  36. data/lib/contrast/api/decorators/agent_startup.rb +58 -0
  37. data/lib/contrast/api/decorators/application_startup.rb +51 -0
  38. data/lib/contrast/api/decorators/route_coverage.rb +15 -5
  39. data/lib/contrast/api/decorators/trace_event.rb +42 -14
  40. data/lib/contrast/components/agent.rb +2 -0
  41. data/lib/contrast/components/app_context.rb +4 -22
  42. data/lib/contrast/components/sampling.rb +48 -6
  43. data/lib/contrast/components/settings.rb +5 -4
  44. data/lib/contrast/framework/manager.rb +13 -12
  45. data/lib/contrast/framework/rails/support.rb +42 -43
  46. data/lib/contrast/framework/sinatra/support.rb +100 -41
  47. data/lib/contrast/logger/log.rb +31 -15
  48. data/lib/contrast/utils/class_util.rb +3 -1
  49. data/lib/contrast/utils/heap_dump_util.rb +103 -87
  50. data/lib/contrast/utils/invalid_configuration_util.rb +21 -12
  51. data/resources/assess/policy.json +3 -9
  52. data/resources/deadzone/policy.json +6 -0
  53. data/ruby-agent.gemspec +54 -16
  54. metadata +105 -136
  55. data/lib/contrast/agent/assess/rule.rb +0 -18
  56. data/lib/contrast/agent/assess/rule/base.rb +0 -52
  57. data/lib/contrast/agent/assess/rule/redos.rb +0 -67
  58. data/lib/contrast/framework/sinatra/patch/base.rb +0 -83
  59. data/lib/contrast/framework/sinatra/patch/support.rb +0 -27
  60. data/lib/contrast/utils/prevent_serialization.rb +0 -52
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09fcc059f5c03c966c230f4576d93399f6a35bc15a42cfd4a5a9541d5e292f95'
4
- data.tar.gz: 96d58e585f42c42599744e9170d1051263e661671ae88ea9ca972a9c9be2f954
3
+ metadata.gz: 0e9acb88aec7b0f51a076b5358738fa6d0c245bb2d91c2dfcf41d2f9fb3ed204
4
+ data.tar.gz: 405ba4ca7da645d6f06767961341c587bc6369279f62742ae7b86d5dc0b6101c
5
5
  SHA512:
6
- metadata.gz: d789f3aa5577188dabc62847c921a2a00f0dec9016dc10d6d8bb8ebe6c74c69c485ee81ae0a95e99ef3d82030e620167f3d433220120de611bf021947fb1ab67
7
- data.tar.gz: 55e07ffd5112cbdde2e97b7eab565c24c811318927c13c55bde4f93a9ac4e78ba00bdb25e18d275700bb0433eeb50cee6d5dc4fa704a8526e71ba2d0ce4a99d4
6
+ metadata.gz: 4bf982f03e6daa5425c13adcb0b2c332745a9dee27733d5f90d77a4e5b4c9983a71b39f7cc0f836d19fc04b9b2a6e4eaf39ef2732be65921fd7c2f125a53b685
7
+ data.tar.gz: c18d27bc472db9037ada00561a81cf0eed181ee717ab7c20f40f39d4675fd1a797b00c0109792afb3902b107ae37b69c3c564399a2d2ca1882dd2ed71f0c93fe
@@ -56,8 +56,12 @@ module Contrast
56
56
  @_framework_manager ||= Contrast::Framework::Manager.new
57
57
  end
58
58
 
59
+ def self.heapdump_util
60
+ thread_watcher.heapdump_util
61
+ end
62
+
59
63
  def self.messaging_queue
60
- @_messaging_queue ||= Contrast::Api::Communication::MessagingQueue.new
64
+ thread_watcher.messaging_queue
61
65
  end
62
66
 
63
67
  def self.thread_watcher
@@ -13,18 +13,9 @@ module Contrast
13
13
  require 'contrast/agent/rewriter'
14
14
  require 'contrast/agent/assess/policy/preshift'
15
15
 
16
- require 'contrast/utils/prevent_serialization'
17
-
18
- # Rules - generic
19
- require 'contrast/agent/assess/rule'
20
- require 'contrast/agent/assess/rule/base'
21
-
22
16
  # Dynamic Sources
23
17
  require 'contrast/agent/assess/policy/dynamic_source_factory'
24
18
 
25
- # Rule: REDOS
26
- require 'contrast/agent/assess/rule/redos'
27
-
28
19
  # reporting / tracking
29
20
  require 'contrast/agent/assess/properties'
30
21
  require 'contrast/agent/assess/tag'
@@ -5,7 +5,6 @@ require 'contrast/utils/assess/tracking_util'
5
5
  require 'contrast/utils/class_util'
6
6
  require 'contrast/utils/duck_utils'
7
7
  require 'contrast/utils/object_share'
8
- require 'contrast/utils/prevent_serialization'
9
8
  require 'contrast/utils/stack_trace_utils'
10
9
  require 'contrast/utils/string_utils'
11
10
  require 'contrast/utils/timer'
@@ -35,7 +34,6 @@ module Contrast
35
34
  # @attr_reader args [Array<Contrast::Agent::Assess::ContrastObject>] the
36
35
  # safe representation of the Arguments with which the method was invoked
37
36
  class ContrastEvent
38
- include Contrast::Utils::PreventSerialization
39
37
  include Contrast::Components::Interface
40
38
  access_component :analysis
41
39
 
@@ -2,6 +2,8 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'contrast/utils/class_util'
5
+ require 'contrast/utils/object_share'
6
+ require 'contrast/agent/assess/tracker'
5
7
 
6
8
  module Contrast
7
9
  module Agent
@@ -28,7 +30,7 @@ module Contrast
28
30
  # Capture the details about the object which we need to render it in
29
31
  # TeamServer.
30
32
  #
31
- # @param object [Object] the thing to keep a Contrast String of
33
+ # @param object [Object, nil] the thing to keep a Contrast String of
32
34
  def initialize object
33
35
  if object
34
36
  @object = Contrast::Utils::ClassUtil.to_contrast_string(object)
@@ -38,7 +40,8 @@ module Contrast
38
40
  # String that was then #reverse!'d, would our tags be wrong?
39
41
  @tags = Contrast::Agent::Assess::Tracker.properties(object)&.tags
40
42
  else
41
- @object_type = Contrast::Utils::ObjectShare::NIL_STRING
43
+ @object = Contrast::Utils::ObjectShare::NIL_STRING
44
+ @object_type = nil.cs__class.name
42
45
  end
43
46
  end
44
47
 
@@ -12,9 +12,14 @@ module Contrast
12
12
  # finalizer on the object to remove its entry from the Hash immediately
13
13
  # after it's GC'd.
14
14
  class Hash < Hash
15
+ include Contrast::Components::Interface
16
+ access_component :agent, :analysis
17
+
15
18
  FROZEN_FINALIZED_IDS = Set.new
16
19
 
17
20
  def []= key, obj
21
+ return unless AGENT.enabled? && ASSESS.enabled?
22
+
18
23
  # We can't finalize frozen things, so only act on those that went
19
24
  # through .pre_freeze
20
25
  if key.cs__frozen?
@@ -35,6 +40,7 @@ module Contrast
35
40
  # @param key [Object] the thing to determine if trackable
36
41
  # @return [Boolean]
37
42
  def trackable? key
43
+ return false unless key
38
44
  # Track things in these, not them themselves.
39
45
  return false if Contrast::Utils::DuckUtils.iterable_hash?(key)
40
46
  return false if Contrast::Utils::DuckUtils.iterable_enumerable?(key)
@@ -81,6 +87,7 @@ module Contrast
81
87
  # @param key [Object] the Object on which we need to pre-define
82
88
  # finalizers
83
89
  def pre_freeze key
90
+ return unless AGENT.enabled? && ASSESS.enabled?
84
91
  return if key.cs__frozen?
85
92
  return if FROZEN_FINALIZED_IDS.include?(key.__id__)
86
93
 
@@ -110,16 +110,30 @@ module Contrast
110
110
  dynamic_source.method_name = Contrast::Utils::StringUtils.force_utf8(field)
111
111
  dynamic_source.instance_method = source_node.instance_method?
112
112
  dynamic_source.target = Contrast::Utils::StringUtils.force_utf8(source_node.target_string)
113
+ append_properties!(dynamic_source, current_context, source_node, field)
114
+ append_events!(dynamic_source, properties.event)
115
+ dynamic_source
116
+ end
117
+
118
+ # Append the properties needed to reconstruct the given DynamicSource in other dataflows and for rendering
119
+ # in TeamServer
120
+ #
121
+ # @param dynamic_source [Contrast::Api::Dtm::DynamicSource] the message to send to the Service to allow it
122
+ # to report the events leading up to the creation of the Dynamic Source
123
+ # @param current_context [Contrast::Agent::RequestContext] the context of the request in which this source
124
+ # is to be created.
125
+ # @param source_node [Contrast::Agent::Assess::Policy::SourceNode] the SourceNode that applies to this
126
+ # method
127
+ # @param field [String] the name of the method to which this source applies
128
+ def append_properties! dynamic_source, current_context, source_node, field
113
129
  dynamic_source.properties[READ_TABLE] = Contrast::Utils::StringUtils.force_utf8(source_node.class_name)
114
130
  dynamic_source.properties[READ_COLUMN] = Contrast::Utils::StringUtils.force_utf8(field)
115
131
  dynamic_source.properties[WRITE_QUERY_TIME] = Contrast::Utils::StringUtils.force_utf8(Contrast::Utils::Timer.now_ms)
116
132
  url = current_context.request.normalized_uri
117
133
  dynamic_source.properties[WRITE_QUERY_URL] = Contrast::Utils::StringUtils.force_utf8(url)
118
- append_events(dynamic_source, properties.event)
119
- dynamic_source
120
134
  end
121
135
 
122
- def append_events dynamic_source, event
136
+ def append_events! dynamic_source, event
123
137
  return unless event
124
138
 
125
139
  event.parent_events&.each do |parent_event|
@@ -258,22 +258,12 @@ module Contrast
258
258
  def handle_cs_properties_propagation propagation_node, preshift, target, object, ret, args, _block
259
259
  return if propagation_node.action == NOOP_ACTION
260
260
  return unless can_propagate?(propagation_node, preshift, target)
261
+ return unless (propagation_class = find_propagation_class(propagation_node))
261
262
 
262
- propagation_class = PROPAGATION_ACTIONS.fetch(propagation_node.action, nil)
263
- unless propagation_class
264
- logger.warn(
265
- 'Unknown propagation action received. Unable to propagate.',
266
- node_id: propagation_node.id,
267
- action: propagation_node.action)
268
- return
269
- end
270
263
  restore_frozen_state = false
271
264
  if target.cs__frozen? && !Contrast::Agent::Assess::Tracker.trackable?(target)
272
- return unless ASSESS.track_frozen_sources?
273
- return unless propagation_node.targets[0] == Contrast::Utils::ObjectShare::RETURN_KEY
274
-
275
- dup = safe_dup(ret)
276
- return unless dup
265
+ return unless can_handle_frozen?(propagation_node)
266
+ return unless (dup = safe_dup(ret))
277
267
 
278
268
  restore_frozen_state = true
279
269
  ret = dup
@@ -302,6 +292,31 @@ module Contrast
302
292
  target_id: target.__id__)
303
293
  restore_frozen_state ? ret : nil
304
294
  end
295
+
296
+ # Find the propagation class from the given node, if one exists.
297
+ #
298
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs a
299
+ # propagation event.
300
+ # @return [Contrast::Agent::Assess::Policy::Propagator, nil]
301
+ def find_propagation_class propagation_node
302
+ unless (propagation_class = PROPAGATION_ACTIONS.fetch(propagation_node.action, nil))
303
+ logger.warn(
304
+ 'Unknown propagation action received. Unable to propagate.',
305
+ node_id: propagation_node.id,
306
+ action: propagation_node.action)
307
+ end
308
+ propagation_class
309
+ end
310
+
311
+ # We can handle frozen propagation iff we're allowed to, as determined by configuration, and the target of
312
+ # the propagation is a return, as that's a replaceable value.
313
+ #
314
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs a
315
+ # propagation event.
316
+ # @return [Boolean]
317
+ def can_handle_frozen? propagation_node
318
+ ASSESS.track_frozen_sources? && propagation_node.targets[0] == Contrast::Utils::ObjectShare::RETURN_KEY
319
+ end
305
320
  end
306
321
  end
307
322
  end
@@ -29,23 +29,38 @@ module Contrast
29
29
  if source1.length == target.length
30
30
  properties.copy_from(source1, target, 0, propagation_node.untags)
31
31
  else
32
- # find original in the target, copy tags to the new position in
33
- # target
34
- original_start_index = target.index(source1)
35
- properties.copy_from(source1, target, original_start_index, propagation_node.untags)
32
+ handle_append(propagation_node, source1, source2, target, properties)
33
+ end
34
+ properties.cleanup_tags
35
+ end
36
36
 
37
- start = original_start_index + source1.length
38
- while start < target.length
39
- properties.copy_from(source2, target, start, propagation_node.untags)
40
- start += source2.length
41
- next unless start > target.length
37
+ private
42
38
 
43
- properties.tags_at(start - source2.length).each do |tag|
44
- tag.update_end(target.length)
45
- end
39
+ # Given the append operation on source 1 added source 2 to it, changing the target output, modify the
40
+ # tags on the target to account for the change.
41
+ #
42
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node responsible for the
43
+ # propagation action required by this method
44
+ # @param source1 [Object] the thing being appended to
45
+ # @param source2 [Object] the thing being appended
46
+ # @param target [Object] the result of the append operation
47
+ # @param properties [Contrast::Agent::Assess::Properties] the properties of the target
48
+ def handle_append propagation_node, source1, source2, target, properties
49
+ # find original in the target, copy tags to the new position in
50
+ # target
51
+ original_start_index = target.index(source1)
52
+ properties.copy_from(source1, target, original_start_index, propagation_node.untags)
53
+
54
+ start = original_start_index + source1.length
55
+ while start < target.length
56
+ properties.copy_from(source2, target, start, propagation_node.untags)
57
+ start += source2.length
58
+ next unless start > target.length
59
+
60
+ properties.tags_at(start - source2.length).each do |tag|
61
+ tag.update_end(target.length)
46
62
  end
47
63
  end
48
- properties.cleanup_tags
49
64
  end
50
65
  end
51
66
  end
@@ -24,23 +24,8 @@ module Contrast
24
24
 
25
25
  known_tainted = ASSESS.tainted_columns[class_name]
26
26
  propagation_node.sources.each do |source|
27
- arg = preshift.args[source]
28
- next unless arg.cs__respond_to?(:each_pair)
29
-
30
- arg.each_pair do |key, value|
31
- next unless value
32
- next if known_tainted&.include?(key)
33
- next unless (properties = Contrast::Agent::Assess::Tracker.properties!(value))
34
-
35
- # TODO: RUBY-540 handle sanitization, handle nested objects
36
- Contrast::Agent::Assess::Policy::PropagationMethod.apply_tags(propagation_node, value)
37
- properties.build_event(propagation_node, value, preshift.object, target, preshift.args)
38
- next unless tracked_value?(value)
39
-
40
- tainted_columns[key] = properties
41
- end
27
+ handle_write(propagation_node, source, preshift, target, known_tainted, tainted_columns)
42
28
  end
43
-
44
29
  return if tainted_columns.empty?
45
30
 
46
31
  if known_tainted
@@ -51,6 +36,26 @@ module Contrast
51
36
 
52
37
  Contrast::Agent::Assess::Policy::DynamicSourceFactory.create_sources class_type, tainted_columns
53
38
  end
39
+
40
+ private
41
+
42
+ def handle_write propagation_node, source, preshift, target, known_tainted, tainted_columns
43
+ arg = preshift.args[source]
44
+ return unless arg.cs__respond_to?(:each_pair)
45
+
46
+ arg.each_pair do |key, value|
47
+ next unless value
48
+ next if known_tainted&.include?(key)
49
+ next unless (properties = Contrast::Agent::Assess::Tracker.properties!(value))
50
+
51
+ # TODO: RUBY-540 handle sanitization, handle nested objects
52
+ Contrast::Agent::Assess::Policy::PropagationMethod.apply_tags(propagation_node, value)
53
+ properties.build_event(propagation_node, value, preshift.object, target, preshift.args)
54
+ next unless tracked_value?(value)
55
+
56
+ tainted_columns[key] = properties
57
+ end
58
+ end
54
59
  end
55
60
  end
56
61
  end
@@ -19,19 +19,7 @@ module Contrast
19
19
  when Contrast::Utils::ObjectShare::OBJECT_KEY
20
20
  tracked_inputs << preshift.object if Contrast::Agent::Assess::Tracker.tracked?(preshift.object)
21
21
  else
22
- arg = preshift.args[source]
23
- if arg.is_a?(String)
24
- tracked_inputs << arg if Contrast::Agent::Assess::Tracker.tracked?(arg)
25
- elsif Contrast::Utils::DuckUtils.iterable_hash?(arg)
26
- arg.each_pair do |key, value|
27
- tracked_inputs << key if tracked_value?(key)
28
- tracked_inputs << value if tracked_value?(value)
29
- end
30
- elsif Contrast::Utils::DuckUtils.iterable_enumerable?(arg)
31
- arg.each do |value|
32
- tracked_inputs << value if tracked_value?(value)
33
- end
34
- end
22
+ find_argument_inputs(tracked_inputs, preshift.args[source])
35
23
  end
36
24
  end
37
25
 
@@ -50,6 +38,28 @@ module Contrast
50
38
  properties.splat_from(input, target)
51
39
  end
52
40
  end
41
+
42
+ private
43
+
44
+ # The arguments to the splat method are complex and of multiple types. As such, we need to handle
45
+ # Strings and iterables to determine the tracked inputs on which to act.
46
+ #
47
+ # @param tracked_inputs [Array] storage for the inputs to act on later
48
+ # @param arg [Object] an input to the method which act as sources for this propagation.
49
+ def find_argument_inputs tracked_inputs, arg
50
+ if arg.is_a?(String)
51
+ tracked_inputs << arg if Contrast::Agent::Assess::Tracker.tracked?(arg)
52
+ elsif Contrast::Utils::DuckUtils.iterable_hash?(arg)
53
+ arg.each_pair do |key, value|
54
+ tracked_inputs << key if tracked_value?(key)
55
+ tracked_inputs << value if tracked_value?(value)
56
+ end
57
+ elsif Contrast::Utils::DuckUtils.iterable_enumerable?(arg)
58
+ arg.each do |value|
59
+ tracked_inputs << value if tracked_value?(value)
60
+ end
61
+ end
62
+ end
53
63
  end
54
64
  end
55
65
  end
@@ -38,13 +38,7 @@ module Contrast
38
38
  source = find_source(propagation_node.sources[0], preshift)
39
39
  return unless (source_properties = Contrast::Agent::Assess::Tracker.properties(source))
40
40
 
41
- # grapheme_clusters break the string apart based on each "user-perceived" character.
42
- # Otherwise, the default for String#split is to use a single whitespace.
43
- separator_length = if propagation_node.method_name == :grapheme_clusters
44
- 0
45
- else
46
- preshift&.args&.first&.to_s&.length || $FIELD_SEPARATOR&.to_s&.length || 1
47
- end
41
+ separator_length = find_separator_length(propagation_node, preshift)
48
42
 
49
43
  current_index = 0
50
44
  target.each do |target_elem|
@@ -126,6 +120,19 @@ module Contrast
126
120
 
127
121
  private
128
122
 
123
+ # grapheme_clusters break the string apart based on each "user-perceived" character. Otherwise, the
124
+ # default for String#split is to use a single whitespace.
125
+ #
126
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
127
+ # propagation event.
128
+ # @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to
129
+ # the invocation of the patched method.
130
+ def find_separator_length propagation_node, preshift
131
+ return 0 if propagation_node.method_name == :grapheme_clusters
132
+
133
+ preshift&.args&.first&.to_s&.length || $FIELD_SEPARATOR&.to_s&.length || 1
134
+ end
135
+
129
136
  # Save index of the current split object.
130
137
  # Create index tracking array as needed.
131
138
  def save_split_index!
@@ -95,10 +95,8 @@ module Contrast
95
95
  return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
96
96
 
97
97
  incoming_properties = Contrast::Agent::Assess::Tracker.properties(incoming)
98
- parent_event = incoming_properties&.event
99
- parent_events << parent_event if parent_event
98
+ parent_events << incoming_properties&.event if incoming_properties&.event
100
99
 
101
- pattern = preshift.args[0]
102
100
  source = preshift.object
103
101
 
104
102
  # We can't efficiently find the places that things were
@@ -111,6 +109,34 @@ module Contrast
111
109
 
112
110
  # if it's just a straight insert, that we can do
113
111
  # Copy the tags from us to the return
112
+ ranges = find_string_sub_insert(properties, preshift, incoming, ret, global)
113
+
114
+ properties.delete_tags_at_ranges(ranges)
115
+ properties.shift_tags(ranges)
116
+ return unless incoming_tracked
117
+ return unless incoming_properties
118
+
119
+ tags = incoming_properties.tag_keys
120
+ ranges.each do |range|
121
+ tags.each do |tag|
122
+ properties.add_tag(tag, range)
123
+ end
124
+ end
125
+ end
126
+
127
+ # Find the points at which the new String was placed into the original
128
+ #
129
+ # @param properties [Contrast::Agent::Assess::Properties] the Properties of the ret
130
+ # @param preshift [Contrast::Agent::Assess::PreShift] the capture of the state of the code just prior to
131
+ # the invocation of the patched method
132
+ # @param incoming [String] the new String going into the substitution
133
+ # @param ret [String] the result of the substitution
134
+ # @param global [Boolean] if this was a global or single substitution
135
+ # @return [Array<Range>] the Ranges where substitution occurred
136
+ def find_string_sub_insert properties, preshift, incoming, ret, global
137
+ pattern = preshift.args[0]
138
+ source = preshift.object
139
+
114
140
  properties.copy_from(source, ret)
115
141
  # Figure out where inserts occurred
116
142
  last_idx = 0
@@ -126,17 +152,7 @@ module Contrast
126
152
  ranges << (start_index...end_index)
127
153
  break unless global
128
154
  end
129
- properties.delete_tags_at_ranges(ranges)
130
- properties.shift_tags(ranges)
131
- return unless incoming_tracked
132
- return unless incoming_properties
133
-
134
- tags = incoming_properties.tag_keys
135
- ranges.each do |range|
136
- tags.each do |tag|
137
- properties.add_tag(tag, range)
138
- end
139
- end
155
+ ranges
140
156
  end
141
157
 
142
158
  def block_sub self_tracked, source, ret