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.
- checksums.yaml +4 -4
- data/lib/contrast/agent.rb +5 -1
- data/lib/contrast/agent/assess.rb +0 -9
- data/lib/contrast/agent/assess/contrast_event.rb +0 -2
- data/lib/contrast/agent/assess/contrast_object.rb +5 -2
- data/lib/contrast/agent/assess/finalizers/hash.rb +7 -0
- data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +17 -3
- data/lib/contrast/agent/assess/policy/propagation_method.rb +28 -13
- data/lib/contrast/agent/assess/policy/propagator/append.rb +28 -13
- data/lib/contrast/agent/assess/policy/propagator/database_write.rb +21 -16
- data/lib/contrast/agent/assess/policy/propagator/splat.rb +23 -13
- data/lib/contrast/agent/assess/policy/propagator/split.rb +14 -7
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +30 -14
- data/lib/contrast/agent/assess/policy/trigger_method.rb +13 -8
- data/lib/contrast/agent/assess/policy/trigger_node.rb +28 -7
- data/lib/contrast/agent/assess/policy/trigger_validation/redos_validator.rb +59 -0
- data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +1 -2
- data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +6 -4
- data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +2 -4
- data/lib/contrast/agent/assess/properties.rb +0 -2
- data/lib/contrast/agent/assess/property/tagged.rb +37 -19
- data/lib/contrast/agent/assess/tracker.rb +1 -1
- data/lib/contrast/agent/middleware.rb +85 -55
- data/lib/contrast/agent/patching/policy/patch_status.rb +1 -1
- data/lib/contrast/agent/patching/policy/patcher.rb +51 -44
- data/lib/contrast/agent/patching/policy/trigger_node.rb +5 -2
- data/lib/contrast/agent/protect/rule/sqli.rb +17 -11
- data/lib/contrast/agent/request_context.rb +12 -0
- data/lib/contrast/agent/thread.rb +1 -1
- data/lib/contrast/agent/thread_watcher.rb +20 -5
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/api/communication/messaging_queue.rb +18 -21
- data/lib/contrast/api/communication/response_processor.rb +8 -1
- data/lib/contrast/api/communication/socket_client.rb +22 -14
- data/lib/contrast/api/decorators.rb +2 -0
- data/lib/contrast/api/decorators/agent_startup.rb +58 -0
- data/lib/contrast/api/decorators/application_startup.rb +51 -0
- data/lib/contrast/api/decorators/route_coverage.rb +15 -5
- data/lib/contrast/api/decorators/trace_event.rb +42 -14
- data/lib/contrast/components/agent.rb +2 -0
- data/lib/contrast/components/app_context.rb +4 -22
- data/lib/contrast/components/sampling.rb +48 -6
- data/lib/contrast/components/settings.rb +5 -4
- data/lib/contrast/framework/manager.rb +13 -12
- data/lib/contrast/framework/rails/support.rb +42 -43
- data/lib/contrast/framework/sinatra/support.rb +100 -41
- data/lib/contrast/logger/log.rb +31 -15
- data/lib/contrast/utils/class_util.rb +3 -1
- data/lib/contrast/utils/heap_dump_util.rb +103 -87
- data/lib/contrast/utils/invalid_configuration_util.rb +21 -12
- data/resources/assess/policy.json +3 -9
- data/resources/deadzone/policy.json +6 -0
- data/ruby-agent.gemspec +54 -16
- metadata +105 -136
- data/lib/contrast/agent/assess/rule.rb +0 -18
- data/lib/contrast/agent/assess/rule/base.rb +0 -52
- data/lib/contrast/agent/assess/rule/redos.rb +0 -67
- data/lib/contrast/framework/sinatra/patch/base.rb +0 -83
- data/lib/contrast/framework/sinatra/patch/support.rb +0 -27
- data/lib/contrast/utils/prevent_serialization.rb +0 -52
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e9acb88aec7b0f51a076b5358738fa6d0c245bb2d91c2dfcf41d2f9fb3ed204
|
4
|
+
data.tar.gz: 405ba4ca7da645d6f06767961341c587bc6369279f62742ae7b86d5dc0b6101c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4bf982f03e6daa5425c13adcb0b2c332745a9dee27733d5f90d77a4e5b4c9983a71b39f7cc0f836d19fc04b9b2a6e4eaf39ef2732be65921fd7c2f125a53b685
|
7
|
+
data.tar.gz: c18d27bc472db9037ada00561a81cf0eed181ee717ab7c20f40f39d4675fd1a797b00c0109792afb3902b107ae37b69c3c564399a2d2ca1882dd2ed71f0c93fe
|
data/lib/contrast/agent.rb
CHANGED
@@ -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
|
-
|
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
|
-
@
|
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
|
273
|
-
return unless
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
32
|
+
handle_append(propagation_node, source1, source2, target, properties)
|
33
|
+
end
|
34
|
+
properties.cleanup_tags
|
35
|
+
end
|
36
36
|
|
37
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|