contrast-agent 4.2.0 → 4.3.0

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