contrast-agent 4.2.0 → 4.3.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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -0
  3. data/ext/cs__assess_marshal_module/cs__assess_marshal_module.c +22 -10
  4. data/ext/cs__assess_marshal_module/cs__assess_marshal_module.h +4 -3
  5. data/lib/contrast/agent/assess/contrast_event.rb +49 -130
  6. data/lib/contrast/agent/assess/contrast_object.rb +51 -0
  7. data/lib/contrast/agent/assess/events/source_event.rb +4 -9
  8. data/lib/contrast/agent/assess/policy/patcher.rb +4 -3
  9. data/lib/contrast/agent/assess/policy/policy_node.rb +31 -59
  10. data/lib/contrast/agent/assess/policy/preshift.rb +3 -3
  11. data/lib/contrast/agent/assess/policy/propagation_method.rb +13 -19
  12. data/lib/contrast/agent/assess/policy/propagation_node.rb +12 -24
  13. data/lib/contrast/agent/assess/policy/propagator/append.rb +1 -2
  14. data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -2
  15. data/lib/contrast/agent/assess/policy/propagator/custom.rb +1 -1
  16. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +1 -3
  17. data/lib/contrast/agent/assess/policy/propagator/insert.rb +1 -2
  18. data/lib/contrast/agent/assess/policy/propagator/keep.rb +1 -2
  19. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +3 -2
  20. data/lib/contrast/agent/assess/policy/propagator/next.rb +1 -2
  21. data/lib/contrast/agent/assess/policy/propagator/prepend.rb +1 -2
  22. data/lib/contrast/agent/assess/policy/propagator/remove.rb +2 -4
  23. data/lib/contrast/agent/assess/policy/propagator/replace.rb +1 -2
  24. data/lib/contrast/agent/assess/policy/propagator/reverse.rb +1 -2
  25. data/lib/contrast/agent/assess/policy/propagator/select.rb +3 -4
  26. data/lib/contrast/agent/assess/policy/propagator/splat.rb +2 -4
  27. data/lib/contrast/agent/assess/policy/propagator/split.rb +73 -117
  28. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +11 -11
  29. data/lib/contrast/agent/assess/policy/propagator/trim.rb +3 -7
  30. data/lib/contrast/agent/assess/policy/source_method.rb +2 -14
  31. data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +5 -8
  32. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +1 -1
  33. data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +1 -1
  34. data/lib/contrast/agent/assess/property/tagged.rb +21 -15
  35. data/lib/contrast/agent/assess/rule/redos.rb +1 -1
  36. data/lib/contrast/agent/assess/tracker.rb +16 -18
  37. data/lib/contrast/agent/deadzone/policy/deadzone_node.rb +7 -0
  38. data/lib/contrast/agent/middleware.rb +50 -1
  39. data/lib/contrast/agent/patching/policy/method_policy.rb +1 -1
  40. data/lib/contrast/agent/patching/policy/patch.rb +4 -4
  41. data/lib/contrast/agent/protect/policy/applies_deserialization_rule.rb +47 -1
  42. data/lib/contrast/agent/protect/policy/rule_applicator.rb +53 -0
  43. data/lib/contrast/agent/protect/rule/base.rb +63 -14
  44. data/lib/contrast/agent/protect/rule/cmd_injection.rb +3 -3
  45. data/lib/contrast/agent/protect/rule/default_scanner.rb +1 -4
  46. data/lib/contrast/agent/protect/rule/deserialization.rb +4 -1
  47. data/lib/contrast/agent/protect/rule/no_sqli.rb +3 -3
  48. data/lib/contrast/agent/protect/rule/sqli.rb +3 -3
  49. data/lib/contrast/agent/protect/rule/xxe.rb +32 -11
  50. data/lib/contrast/agent/protect/rule/xxe/entity_wrapper.rb +10 -6
  51. data/lib/contrast/agent/reaction_processor.rb +1 -1
  52. data/lib/contrast/agent/response.rb +5 -5
  53. data/lib/contrast/agent/rewriter.rb +3 -3
  54. data/lib/contrast/agent/scope.rb +33 -13
  55. data/lib/contrast/agent/static_analysis.rb +13 -7
  56. data/lib/contrast/agent/version.rb +1 -1
  57. data/lib/contrast/api/decorators/library.rb +1 -0
  58. data/lib/contrast/api/decorators/library_usage_update.rb +1 -0
  59. data/lib/contrast/api/decorators/trace_event.rb +19 -31
  60. data/lib/contrast/api/decorators/trace_event_object.rb +11 -3
  61. data/lib/contrast/api/decorators/trace_event_signature.rb +27 -5
  62. data/lib/contrast/api/decorators/user_input.rb +2 -1
  63. data/lib/contrast/common_agent_configuration.rb +1 -1
  64. data/lib/contrast/components/assess.rb +36 -0
  65. data/lib/contrast/components/interface.rb +5 -3
  66. data/lib/contrast/components/scope.rb +23 -0
  67. data/lib/contrast/components/settings.rb +3 -3
  68. data/lib/contrast/config/assess_configuration.rb +2 -1
  69. data/lib/contrast/extension/assess/array.rb +1 -2
  70. data/lib/contrast/extension/assess/erb.rb +1 -3
  71. data/lib/contrast/extension/assess/exec_trigger.rb +1 -1
  72. data/lib/contrast/extension/assess/fiber.rb +2 -3
  73. data/lib/contrast/extension/assess/hash.rb +4 -2
  74. data/lib/contrast/extension/assess/kernel.rb +1 -2
  75. data/lib/contrast/extension/assess/marshal.rb +34 -26
  76. data/lib/contrast/extension/assess/regexp.rb +3 -8
  77. data/lib/contrast/extension/assess/string.rb +1 -2
  78. data/lib/contrast/framework/base_support.rb +51 -53
  79. data/lib/contrast/framework/manager.rb +3 -2
  80. data/lib/contrast/framework/rack/patch/session_cookie.rb +1 -1
  81. data/lib/contrast/framework/rack/support.rb +2 -1
  82. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +1 -1
  83. data/lib/contrast/framework/rails/patch/rails_application_configuration.rb +1 -1
  84. data/lib/contrast/framework/rails/rewrite/action_controller_railties_helper_inherited.rb +1 -1
  85. data/lib/contrast/framework/rails/rewrite/active_record_attribute_methods_read.rb +1 -1
  86. data/lib/contrast/framework/rails/rewrite/active_record_time_zone_inherited.rb +1 -1
  87. data/lib/contrast/framework/rails/support.rb +2 -1
  88. data/lib/contrast/framework/sinatra/support.rb +3 -2
  89. data/lib/contrast/logger/application.rb +0 -3
  90. data/lib/contrast/utils/duck_utils.rb +1 -1
  91. data/lib/contrast/utils/heap_dump_util.rb +1 -1
  92. data/lib/contrast/utils/object_share.rb +3 -3
  93. data/lib/contrast/utils/preflight_util.rb +1 -1
  94. data/lib/contrast/utils/prevent_serialization.rb +1 -1
  95. data/lib/contrast/utils/resource_loader.rb +1 -1
  96. data/lib/contrast/utils/sha256_builder.rb +2 -2
  97. data/lib/contrast/utils/string_utils.rb +1 -1
  98. data/lib/contrast/utils/tag_util.rb +9 -13
  99. data/resources/assess/policy.json +9 -9
  100. data/resources/deadzone/policy.json +156 -0
  101. data/resources/protect/policy.json +12 -0
  102. data/ruby-agent.gemspec +9 -6
  103. data/service_executables/VERSION +1 -1
  104. data/service_executables/linux/contrast-service +0 -0
  105. data/service_executables/mac/contrast-service +0 -0
  106. metadata +68 -25
@@ -36,11 +36,11 @@ module Contrast
36
36
  # the state of the object and arguments just prior to the method
37
37
  # being called or nil if one is not required.
38
38
  def build_preshift propagation_node, object, args
39
- return nil unless propagation_node
40
- return nil unless ASSESS.enabled?
39
+ return unless propagation_node
40
+ return unless ASSESS.enabled?
41
41
 
42
42
  initializing = propagation_node.method_name == :initialize
43
- return nil if unsafe_io_object?(object, initializing)
43
+ return if unsafe_io_object?(object, initializing)
44
44
 
45
45
  needs_object = propagation_node.needs_object?
46
46
  needs_args = propagation_node.needs_args?
@@ -37,20 +37,15 @@ module Contrast
37
37
 
38
38
  class << self
39
39
  def determine_target propagation_node, ret, object, args
40
- target_key = propagation_node.targets[0]
41
- return ret if target_key == Contrast::Utils::ObjectShare::RETURN_KEY
42
- return object if target_key == Contrast::Utils::ObjectShare::OBJECT_KEY
43
-
44
- return args[target_key] if target_key.is_a?(Integer)
45
-
46
- arg = nil
47
- args.each do |search|
48
- next unless search.is_a?(Hash)
49
-
50
- arg = search[target_key]
51
- break if arg
40
+ target = propagation_node.targets[0]
41
+ case target
42
+ when Contrast::Utils::ObjectShare::OBJECT_KEY
43
+ object
44
+ when Contrast::Utils::ObjectShare::RETURN_KEY
45
+ ret
46
+ else
47
+ args[target]
52
48
  end
53
- arg
54
49
  end
55
50
 
56
51
  # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
@@ -208,8 +203,8 @@ module Contrast
208
203
  # If this patcher has tags, apply them to the entire target
209
204
  def apply_tags propagation_node, target
210
205
  return unless propagation_node.tags
206
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties(target))
211
207
 
212
- properties = Contrast::Agent::Assess::Tracker.properties(target)
213
208
  length = Contrast::Utils::StringUtils.ret_length(target)
214
209
  propagation_node.tags.each do |tag|
215
210
  properties.add_tag(tag, 0...length)
@@ -219,9 +214,7 @@ module Contrast
219
214
  # If this patcher has tags, remove them from the entire target
220
215
  def apply_untags propagation_node, target
221
216
  return unless propagation_node.untags
222
-
223
- properties = Contrast::Agent::Assess::Tracker.properties(target)
224
- return unless properties
217
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties(target))
225
218
 
226
219
  propagation_node.untags.each do |tag|
227
220
  properties.delete_tags(tag)
@@ -256,7 +249,7 @@ module Contrast
256
249
 
257
250
  def handle_enumerable_propagation propagation_node, preshift, target, object, ret, args, block
258
251
  target.each do |value|
259
- next if target == value # Some Enumerable#each are overriden to return self the first time which leads to infinite propagation
252
+ next if target == value # Some Enumerable#each are overridden to return self the first time which leads to infinite propagation
260
253
 
261
254
  apply_propagator(propagation_node, preshift, value, object, ret, args, block)
262
255
  end
@@ -300,7 +293,8 @@ module Contrast
300
293
  # both and there should never be a propagator that has a tag in
301
294
  # its untag.
302
295
  apply_untags(propagation_node, target)
303
- properties = Contrast::Agent::Assess::Tracker.properties(target)
296
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
297
+
304
298
  properties.add_properties(propagation_node.properties)
305
299
  properties.build_event(propagation_node, target, object, ret, args)
306
300
  logger.trace('Propagation detected',
@@ -83,35 +83,23 @@ module Contrast
83
83
  end
84
84
 
85
85
  def needs_object?
86
- @_needs_object ||= begin
87
- if action == Contrast::Agent::Assess::Policy::PropagationMethod::CUSTOM_ACTION
88
- true
89
- elsif action == Contrast::Agent::Assess::Policy::PropagationMethod::DB_WRITE_ACTION
90
- true
91
- elsif sources.any? { |source| source == Contrast::Utils::ObjectShare::OBJECT_KEY }
92
- true
93
- elsif targets.any? { |target| target == Contrast::Utils::ObjectShare::OBJECT_KEY }
94
- true
95
- else
96
- false
97
- end
86
+ if @_needs_object.nil?
87
+ @_needs_object = action == Contrast::Agent::Assess::Policy::PropagationMethod::CUSTOM_ACTION ||
88
+ action == Contrast::Agent::Assess::Policy::PropagationMethod::DB_WRITE_ACTION ||
89
+ sources.any? { |source| source == Contrast::Utils::ObjectShare::OBJECT_KEY } ||
90
+ targets.any? { |target| target == Contrast::Utils::ObjectShare::OBJECT_KEY }
98
91
  end
92
+ @_needs_object
99
93
  end
100
94
 
101
95
  def needs_args?
102
- @_needs_args ||= begin
103
- if action == Contrast::Agent::Assess::Policy::PropagationMethod::CUSTOM_ACTION
104
- true
105
- elsif action == Contrast::Agent::Assess::Policy::PropagationMethod::DB_WRITE_ACTION
106
- true
107
- elsif sources.any? { |source| source.is_a?(Integer) || source.is_a?(Symbol) }
108
- true
109
- elsif targets.any? { |target| target.is_a?(Integer) || target.is_a?(Symbol) }
110
- true
111
- else
112
- false
113
- end
96
+ if @_needs_args.nil?
97
+ @_needs_args = action == Contrast::Agent::Assess::Policy::PropagationMethod::CUSTOM_ACTION ||
98
+ action == Contrast::Agent::Assess::Policy::PropagationMethod::DB_WRITE_ACTION ||
99
+ sources.any? { |source| source.is_a?(Integer) || source.is_a?(Symbol) } ||
100
+ targets.any? { |target| target.is_a?(Integer) || target.is_a?(Symbol) }
114
101
  end
102
+ @_needs_args
115
103
  end
116
104
 
117
105
  # This is a tagger if it has a tag or an untag.
@@ -16,8 +16,7 @@ module Contrast
16
16
  # copy tags from the param to the target in chunks of param size or less
17
17
  # if param is appended in space less than param length
18
18
  def propagate propagation_node, preshift, target
19
- properties = Contrast::Agent::Assess::Tracker.properties(target)
20
- return unless properties
19
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
21
20
 
22
21
  sources = propagation_node.sources
23
22
  source1 = find_source(sources[0], preshift)
@@ -12,8 +12,7 @@ module Contrast
12
12
  class Center < Contrast::Agent::Assess::Policy::Propagator::Base
13
13
  class << self
14
14
  def propagate propagation_node, preshift, target
15
- properties = Contrast::Agent::Assess::Tracker.properties(target)
16
- return unless properties
15
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
17
16
 
18
17
  sources = propagation_node.sources
19
18
  source1 = find_source(sources[0], preshift)
@@ -12,7 +12,7 @@ module Contrast
12
12
  # of tags from the source to the target. Each node with the CUSTOM
13
13
  # action knows the class and method it should call to preform this
14
14
  # action.
15
- class Custom
15
+ module Custom
16
16
  class << self
17
17
  def propagate propagation_node, preshift, ret, block
18
18
  clazz = propagation_node.patch_class
@@ -30,9 +30,7 @@ module Contrast
30
30
  arg.each_pair do |key, value|
31
31
  next unless value
32
32
  next if known_tainted&.include?(key)
33
-
34
- properties = Contrast::Agent::Assess::Tracker.properties(value)
35
- next unless properties
33
+ next unless (properties = Contrast::Agent::Assess::Tracker.properties!(value))
36
34
 
37
35
  # TODO: RUBY-540 handle sanitization, handle nested objects
38
36
  Contrast::Agent::Assess::Policy::PropagationMethod.apply_tags(propagation_node, value)
@@ -16,8 +16,7 @@ module Contrast
16
16
  # Unlike additive propagation, this currently only supports one source
17
17
  # We assume that insert changes the preshift target
18
18
  def propagate propagation_node, preshift, target
19
- properties = Contrast::Agent::Assess::Tracker.properties(target)
20
- return unless properties
19
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
21
20
 
22
21
  source = find_source(propagation_node.sources[1], preshift)
23
22
 
@@ -14,8 +14,7 @@ module Contrast
14
14
  # Keep means the tags just pass from the source to the target
15
15
  # as is.
16
16
  def propagate propagation_node, preshift, target
17
- properties = Contrast::Agent::Assess::Tracker.properties(target)
18
- return unless properties
17
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
19
18
 
20
19
  source = find_source(propagation_node.sources[0], preshift)
21
20
  properties.copy_from(source, target, 0, propagation_node.untags)
@@ -67,11 +67,12 @@ module Contrast
67
67
  def square_bracket_single argument_index, preshift, return_value, propagation_node
68
68
  original_start_index = preshift.object.begin(argument_index)
69
69
  original_end_index = preshift.object.end(argument_index)
70
- original_properties = Contrast::Agent::Assess::Tracker.properties(preshift.object)
70
+ return unless (original_properties = Contrast::Agent::Assess::Tracker.properties(preshift.object))
71
+
71
72
  applicable_tags = original_properties.tags_at_range(original_start_index...original_end_index)
72
73
  return if applicable_tags.empty?
74
+ return unless (return_properties = Contrast::Agent::Assess::Tracker.properties!(return_value))
73
75
 
74
- return_properties = Contrast::Agent::Assess::Tracker.properties(return_value)
75
76
  applicable_tags.each do |tag_name, tag_ranges|
76
77
  return_properties.set_tags(tag_name, tag_ranges)
77
78
  end
@@ -15,8 +15,7 @@ module Contrast
15
15
  # String has some silly methods like next. Basically, this flips a
16
16
  # character in a predictable manner
17
17
  def propagate propagation_node, preshift, target
18
- properties = Contrast::Agent::Assess::Tracker.properties(target)
19
- return unless properties
18
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
20
19
 
21
20
  source = find_source(propagation_node.sources[0], preshift)
22
21
  properties.copy_from(source, target, 0, propagation_node.untags)
@@ -14,8 +14,7 @@ module Contrast
14
14
  # For the source, prepend its tags to the target. It's basically the
15
15
  # opposite of append. :-P
16
16
  def propagate propagation_node, preshift, target
17
- properties = Contrast::Agent::Assess::Tracker.properties(target)
18
- return unless properties
17
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
19
18
 
20
19
  sources = propagation_node.sources
21
20
  # source1 is the copy of the thing being prepended to
@@ -17,8 +17,7 @@ module Contrast
17
17
  # Once the tag is applied, remove the section that was removed by the delete.
18
18
  # Unlike additive propagation, this currently only supports one source
19
19
  def propagate propagation_node, preshift, target
20
- properties = Contrast::Agent::Assess::Tracker.properties(target)
21
- return unless properties
20
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
22
21
 
23
22
  source = find_source(propagation_node.sources[0], preshift)
24
23
  properties.copy_from(source, target, 0, propagation_node.untags)
@@ -27,8 +26,7 @@ module Contrast
27
26
  end
28
27
 
29
28
  def handle_removal source_chars, target
30
- properties = Contrast::Agent::Assess::Tracker.properties(target)
31
- return unless properties
29
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
32
30
 
33
31
  source_idx = 0
34
32
 
@@ -14,8 +14,7 @@ module Contrast
14
14
  # Replace means we're replacing the target w/ the source. Anything
15
15
  # on the source should be passed to the target.
16
16
  def propagate propagation_node, preshift, target
17
- properties = Contrast::Agent::Assess::Tracker.properties(target)
18
- return unless properties
17
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
19
18
 
20
19
  source = find_source(propagation_node.sources[0], preshift)
21
20
  properties.clear_tags
@@ -13,8 +13,7 @@ module Contrast
13
13
  class Reverse < Contrast::Agent::Assess::Policy::Propagator::Base
14
14
  class << self
15
15
  def propagate propagation_node, preshift, target
16
- properties = Contrast::Agent::Assess::Tracker.properties(target)
17
- return unless properties
16
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
18
17
 
19
18
  source = find_source(propagation_node.sources[0], preshift)
20
19
  properties.copy_from(source, target, 0, propagation_node.untags)
@@ -14,9 +14,6 @@ module Contrast
14
14
  class Select
15
15
  class << self
16
16
  def select_tagger patcher, preshift, ret, _block
17
- properties = Contrast::Agent::Assess::Tracker.properties(ret)
18
- return unless properties
19
-
20
17
  source = preshift.object
21
18
  args = preshift.args
22
19
 
@@ -31,7 +28,9 @@ module Contrast
31
28
  return
32
29
  end
33
30
 
34
- source_properties = Contrast::Agent::Assess::Tracker.properties(source)
31
+ return unless (source_properties = Contrast::Agent::Assess::Tracker.properties(source))
32
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
33
+
35
34
  properties.build_event(
36
35
  patcher,
37
36
  ret,
@@ -36,14 +36,12 @@ module Contrast
36
36
  end
37
37
 
38
38
  splat_tags(tracked_inputs, target)
39
- Contrast::Agent::Assess::Tracker.properties(target).cleanup_tags
39
+ Contrast::Agent::Assess::Tracker.properties(target)&.cleanup_tags
40
40
  end
41
41
 
42
42
  def splat_tags tracked_inputs, target
43
43
  return if tracked_inputs.empty?
44
-
45
- properties = Contrast::Agent::Assess::Tracker.properties(target)
46
- return unless properties
44
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
47
45
 
48
46
  tracked_inputs.each do |input|
49
47
  input_properties = Contrast::Agent::Assess::Tracker.properties(input)
@@ -15,13 +15,12 @@ module Contrast
15
15
  class Split < Contrast::Agent::Assess::Policy::Propagator::Base
16
16
  include Contrast::Components::Interface
17
17
 
18
- access_component :agent, :logging
18
+ access_component :agent, :logging, :scope
19
19
 
20
20
  SPLIT_TRACKER = Contrast::Utils::ThreadTracker.new
21
+
21
22
  class << self
22
- # Propagate taint from a source as it is split into composite
23
- # sections. This method MUST return nil, otherwise it risks
24
- # changing the result of of the propagation.
23
+ # Propagate taint from a source as it is split into composite sections.
25
24
  #
26
25
  # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode]
27
26
  # the node that governs this propagation event.
@@ -29,171 +28,132 @@ module Contrast
29
28
  # of the state of the code just prior to the invocation of the
30
29
  # patched method.
31
30
  # @param target [Array, String] the target to which to propagate.
32
- # @return [nil]
31
+ # @return [nil] so as not to risk changing the result of the propagation.
32
+
33
33
  def propagate propagation_node, preshift, target
34
34
  logger.trace('Propagation detected',
35
35
  node_id: propagation_node.id,
36
36
  target_id: target.__id__)
37
- unless target.is_a?(Array)
38
- Contrast::Agent::Assess::Policy::Propagator::Keep.propagate(propagation_node, preshift, target)
39
- properties = Contrast::Agent::Assess::Tracker.properties(target)
40
- properties.build_event(propagation_node, target, object, ret, args)
41
- return
42
- end
43
37
 
44
38
  source = find_source(propagation_node.sources[0], preshift)
39
+ return unless (source_properties = Contrast::Agent::Assess::Tracker.properties(source))
45
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.
46
43
  separator_length = if propagation_node.method_name == :grapheme_clusters
47
- # grapheme_clusters break the string apart based on each "user-perceived" character
48
44
  0
49
45
  else
50
- # The default for String#split is to use a single whitespace
51
46
  preshift&.args&.first&.to_s&.length || $FIELD_SEPARATOR&.to_s&.length || 1
52
47
  end
53
48
 
54
49
  current_index = 0
55
- target.each do |elem|
56
- elem_length = elem.length
57
- range = current_index...(current_index + elem_length)
58
- elem_properties = Contrast::Agent::Assess::Tracker.properties(elem)
59
- next unless elem_properties
50
+ target.each do |target_elem|
51
+ next unless (elem_properties = Contrast::Agent::Assess::Tracker.properties!(target_elem))
60
52
 
61
- source_properties = Contrast::Agent::Assess::Tracker.properties(source)
53
+ # Get tags for element from source by element range.
54
+ range = current_index...(current_index + target_elem.length)
62
55
  tags = source_properties.tags_at_range(range)
56
+
57
+ # Set element properties accordingly.
63
58
  elem_properties.clear_tags
64
- tags.each_pair do |key, value|
65
- elem_properties.set_tags(key, value)
66
- end
67
- elem_properties.build_event(propagation_node, elem, preshift.object, target, preshift.args, 0)
59
+ tags.each_pair { |key, value| elem_properties.set_tags(key, value) }
60
+ elem_properties.build_event(propagation_node, target_elem, preshift.object, target, preshift.args, 0)
68
61
  elem_properties.add_properties(propagation_node.properties)
69
- current_index = current_index + elem_length + separator_length
62
+
63
+ current_index = range.end + separator_length
70
64
  end
71
65
  nil
72
66
  end
73
67
 
74
- # Marks the point in which the String#split method is called.
75
- # Responsible for building the context required to propagate when
76
- # the results of #split are yielded directly to a block
68
+ # Context for block split execution.
77
69
  #
78
- # @param string [String] the String on which split is invoked
79
- # @param args [Array<Object>] the arguments passed to the
80
- # original split call
81
- def begin_split string, args
82
- save_split_depth!
83
- depth = SPLIT_TRACKER.get(:split_depth)
84
- save_split_index!(depth)
85
- save_split_value!(depth, string, args)
86
- rescue Exception => e # rubocop:disable Lint/RescueException
87
- # don't let our errors propagate and disable String#split for
88
- # this since we're in an error state
89
- logger.warn('Unable to record split context', e)
90
- end_split
91
- end
70
+ # @param string [String] the String on which split is invoked.
71
+ # @param args [Array<Object>] the arguments passed to the original split call.
72
+ def wrap_split string, args
73
+ # String#split start. Build context and yield.
74
+ begin
75
+ enter_split_scope!
76
+ save_split_index!
77
+ save_split_value!(string, args)
78
+ rescue Exception => e # rubocop:disable Lint/RescueException
79
+ logger.warn('Unable to record split context', e)
80
+ end
92
81
 
93
- # Marks the point in which the String#split method is exited.
94
- # Responsible for removing the context required to propagate when
95
- # the results of #split are yielded directly to a block
96
- def end_split
97
- depth = SPLIT_TRACKER.get(:split_depth)
98
- return unless depth
99
-
100
- depth -= 1
101
- if depth.negative?
102
- SPLIT_TRACKER.delete(:split_depth)
103
- SPLIT_TRACKER.delete(:split_index)
104
- SPLIT_TRACKER.delete(:split_value)
105
- else
106
- SPLIT_TRACKER.set(:split_depth, depth)
82
+ yield
83
+ ensure
84
+ # String#split exit. Remove propagation context.
85
+ begin
86
+ exit_split_scope!
87
+ unless in_split_scope?
88
+ SPLIT_TRACKER.delete(:split_index)
89
+ SPLIT_TRACKER.delete(:split_value)
90
+ end
91
+ rescue StandardError => e
92
+ logger.warn('Unable to remove split context', e)
107
93
  end
108
- rescue StandardError => e
109
- logger.warn('Unable to remove split context', e)
110
94
  end
111
95
 
112
- # This method is called whenever an rb_yield is called. We need
113
- # to leave it as soon as possible with as little work as
114
- # possible.
96
+ # This method is called whenever an rb_yield is called.
97
+ # We need to leave it as soon as possible with as little work as possible.
115
98
  #
116
- # @param target [String] the entity being passed to the yield
117
- # block
99
+ # @param target [String] the entity being passed to the yield block
118
100
  def propagate_yield target
119
- depth, index = nil
120
-
121
- depth = SPLIT_TRACKER.get(:split_depth)
122
- return unless depth
123
-
124
- source = SPLIT_TRACKER.get(:split_value)&.fetch(depth)
125
- return unless source
126
-
127
- index = SPLIT_TRACKER.get(:split_index)&.fetch(depth)
128
- return unless index
129
-
130
- properties = Contrast::Agent::Assess::Tracker.properties(target)
131
- return unless properties
101
+ return unless (source = SPLIT_TRACKER.get(:split_value)&.fetch(split_scope_depth))
102
+ return unless (index = SPLIT_TRACKER.get(:split_index)&.fetch(split_scope_depth))
103
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
132
104
 
133
105
  true_source = source[index]
134
106
  properties.copy_from(true_source, target)
135
107
  rescue StandardError => e
136
108
  logger.warn('Unable to track within split context', e)
137
109
  ensure
138
- if depth && index
110
+ if in_split_scope? && index
139
111
  idx = SPLIT_TRACKER.get(:split_index)
140
- idx[depth] = index + 1 if defined?(idx) && idx.is_a?(Array)
112
+ idx[split_scope_depth] = index + 1 if defined?(idx) && idx.is_a?(Array)
141
113
  end
142
114
  end
143
115
 
116
+ # Load patch.
144
117
  def instrument_string_split
145
- if @_instrument_string_split.nil?
146
- @_instrument_string_split = begin
147
- require 'cs__assess_yield_track/cs__assess_yield_track' if AGENT.patch_yield? && Funchook.available?
148
- true
149
- rescue StandardError => e
150
- logger.error('Error loading split rb_yield patch', e)
151
- false
152
- end
118
+ @_instrument_string_split ||= begin
119
+ require 'cs__assess_yield_track/cs__assess_yield_track' if AGENT.patch_yield? && Funchook.available?
120
+ true
121
+ rescue StandardError => e
122
+ logger.error('Error loading split rb_yield patch', e)
123
+ false
153
124
  end
154
- @_instrument_string_split
155
125
  end
156
126
 
157
127
  private
158
128
 
159
- def save_split_depth!
160
- depth = SPLIT_TRACKER.get(:split_depth)
161
- if depth
162
- depth += 1
163
- SPLIT_TRACKER.set(:split_depth, depth)
164
- else
165
- SPLIT_TRACKER.set(:split_depth, 0)
166
- end
167
- end
168
-
169
- def save_split_index! depth
170
- split_index = SPLIT_TRACKER.get(:split_index)
171
- unless split_index
129
+ # Save index of the current split object.
130
+ # Create index tracking array as needed.
131
+ def save_split_index!
132
+ unless (split_index = SPLIT_TRACKER.get(:split_index))
172
133
  split_index = []
173
134
  SPLIT_TRACKER.set(:split_index, split_index)
174
135
  end
175
- # save the index to the ThreadLocal; not useless
176
- split_index[depth] = 0 # rubocop:disable Lint/UselessSetterCall
136
+ # save the index to the ThreadLocal; not useless.
137
+ split_index[split_scope_depth] = 0 # rubocop:disable Lint/UselessSetterCall
177
138
  end
178
139
 
179
- def save_split_value! depth, string, args
140
+ # Save value of the current split object.
141
+ # Create value tracking array as needed.
142
+ def save_split_value! string, args
180
143
  preshift = Contrast::Agent::Assess::PreShift.build_preshift(split_node, string, args)
181
144
  target = string.split
182
145
  propagate(split_node, preshift, target)
183
- split_value = SPLIT_TRACKER.get(:split_value)
184
- unless split_value
146
+ unless (split_value = SPLIT_TRACKER.get(:split_value))
185
147
  split_value = []
186
148
  SPLIT_TRACKER.set(:split_value, split_value)
187
149
  end
188
- # save the target to the ThreadLocal; not useless
189
- split_value[depth] = target # rubocop:disable Lint/UselessSetterCall
150
+ # Save the target to the ThreadLocal; not useless.
151
+ split_value[split_scope_depth] = target # rubocop:disable Lint/UselessSetterCall
190
152
  end
191
153
 
192
- # Quick hook to the String#split propagation node in our Assess
193
- # policy
154
+ # Quick hook to the String#split propagation node in our Assess policy
194
155
  #
195
- # @return [Contrast::Agent::Assess::Policy::PropagationNode]
196
- # String#split node
156
+ # @return [Contrast::Agent::Assess::Policy::PropagationNode] String#split node
197
157
  def split_node
198
158
  @_split_node ||= begin
199
159
  Contrast::Agent::Assess::Policy::Policy.instance.propagators.find do |node|
@@ -215,19 +175,15 @@ if RUBY_VERSION >= '2.6.0'
215
175
  class String
216
176
  alias_method :cs__patched_string_split_special, :split
217
177
 
218
- # override of the the standard split method to handle the 2.6 direct
219
- # yield case.
178
+ # Override of the the standard split method to handle the 2.6 direct yield case.
220
179
  #
221
180
  # Note: because this patch is applied before our standard propagation, this
222
- # call wrapped in it. As such, any call here happens in scope, so there is
181
+ # call is wrapped in it. As such, any call here happens in scope, so there is
223
182
  # no need to manage it on our own.
224
183
  def split *args, &block
225
184
  if block
226
- Contrast::Agent::Assess::Policy::Propagator::Split.begin_split(self, args)
227
- begin
185
+ Contrast::Agent::Assess::Policy::Propagator::Split.wrap_split(self, args) do
228
186
  cs__patched_string_split_special(*args, &block)
229
- ensure
230
- Contrast::Agent::Assess::Policy::Propagator::Split.end_split
231
187
  end
232
188
  else
233
189
  cs__patched_string_split_special(*args, &block)