contrast-agent 4.1.0 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (139) 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.rb +5 -1
  6. data/lib/contrast/agent/assess.rb +0 -9
  7. data/lib/contrast/agent/assess/contrast_event.rb +49 -132
  8. data/lib/contrast/agent/assess/contrast_object.rb +54 -0
  9. data/lib/contrast/agent/assess/events/source_event.rb +4 -9
  10. data/lib/contrast/agent/assess/finalizers/hash.rb +7 -0
  11. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +17 -3
  12. data/lib/contrast/agent/assess/policy/patcher.rb +4 -3
  13. data/lib/contrast/agent/assess/policy/policy_node.rb +31 -59
  14. data/lib/contrast/agent/assess/policy/preshift.rb +3 -3
  15. data/lib/contrast/agent/assess/policy/propagation_method.rb +41 -32
  16. data/lib/contrast/agent/assess/policy/propagation_node.rb +12 -24
  17. data/lib/contrast/agent/assess/policy/propagator/append.rb +29 -15
  18. data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -2
  19. data/lib/contrast/agent/assess/policy/propagator/custom.rb +1 -1
  20. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +21 -18
  21. data/lib/contrast/agent/assess/policy/propagator/insert.rb +1 -2
  22. data/lib/contrast/agent/assess/policy/propagator/keep.rb +1 -2
  23. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +3 -2
  24. data/lib/contrast/agent/assess/policy/propagator/next.rb +1 -2
  25. data/lib/contrast/agent/assess/policy/propagator/prepend.rb +1 -2
  26. data/lib/contrast/agent/assess/policy/propagator/remove.rb +2 -4
  27. data/lib/contrast/agent/assess/policy/propagator/replace.rb +1 -2
  28. data/lib/contrast/agent/assess/policy/propagator/reverse.rb +1 -2
  29. data/lib/contrast/agent/assess/policy/propagator/select.rb +3 -4
  30. data/lib/contrast/agent/assess/policy/propagator/splat.rb +25 -17
  31. data/lib/contrast/agent/assess/policy/propagator/split.rb +83 -120
  32. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +41 -25
  33. data/lib/contrast/agent/assess/policy/propagator/trim.rb +3 -7
  34. data/lib/contrast/agent/assess/policy/source_method.rb +2 -14
  35. data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +5 -8
  36. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +1 -1
  37. data/lib/contrast/agent/assess/policy/trigger_method.rb +13 -8
  38. data/lib/contrast/agent/assess/policy/trigger_node.rb +28 -7
  39. data/lib/contrast/agent/assess/policy/trigger_validation/redos_validator.rb +59 -0
  40. data/lib/contrast/agent/assess/policy/trigger_validation/ssrf_validator.rb +2 -3
  41. data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +6 -4
  42. data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +2 -4
  43. data/lib/contrast/agent/assess/properties.rb +0 -2
  44. data/lib/contrast/agent/assess/property/tagged.rb +56 -32
  45. data/lib/contrast/agent/assess/tracker.rb +16 -18
  46. data/lib/contrast/agent/deadzone/policy/deadzone_node.rb +7 -0
  47. data/lib/contrast/agent/middleware.rb +134 -55
  48. data/lib/contrast/agent/patching/policy/method_policy.rb +1 -1
  49. data/lib/contrast/agent/patching/policy/patch.rb +6 -0
  50. data/lib/contrast/agent/patching/policy/patch_status.rb +1 -1
  51. data/lib/contrast/agent/patching/policy/patcher.rb +51 -44
  52. data/lib/contrast/agent/patching/policy/trigger_node.rb +5 -2
  53. data/lib/contrast/agent/protect/policy/applies_deserialization_rule.rb +47 -1
  54. data/lib/contrast/agent/protect/policy/rule_applicator.rb +53 -0
  55. data/lib/contrast/agent/protect/rule/base.rb +63 -14
  56. data/lib/contrast/agent/protect/rule/cmd_injection.rb +12 -28
  57. data/lib/contrast/agent/protect/rule/default_scanner.rb +1 -4
  58. data/lib/contrast/agent/protect/rule/deserialization.rb +4 -1
  59. data/lib/contrast/agent/protect/rule/no_sqli.rb +3 -3
  60. data/lib/contrast/agent/protect/rule/sqli.rb +20 -14
  61. data/lib/contrast/agent/protect/rule/xxe.rb +32 -11
  62. data/lib/contrast/agent/protect/rule/xxe/entity_wrapper.rb +10 -6
  63. data/lib/contrast/agent/reaction_processor.rb +1 -1
  64. data/lib/contrast/agent/request_context.rb +12 -0
  65. data/lib/contrast/agent/response.rb +5 -5
  66. data/lib/contrast/agent/rewriter.rb +3 -3
  67. data/lib/contrast/agent/scope.rb +81 -55
  68. data/lib/contrast/agent/static_analysis.rb +13 -7
  69. data/lib/contrast/agent/thread.rb +1 -1
  70. data/lib/contrast/agent/thread_watcher.rb +20 -5
  71. data/lib/contrast/agent/version.rb +1 -1
  72. data/lib/contrast/api/communication/messaging_queue.rb +18 -21
  73. data/lib/contrast/api/communication/response_processor.rb +8 -1
  74. data/lib/contrast/api/communication/socket_client.rb +22 -14
  75. data/lib/contrast/api/decorators.rb +2 -0
  76. data/lib/contrast/api/decorators/agent_startup.rb +58 -0
  77. data/lib/contrast/api/decorators/application_startup.rb +51 -0
  78. data/lib/contrast/api/decorators/library.rb +1 -0
  79. data/lib/contrast/api/decorators/library_usage_update.rb +1 -0
  80. data/lib/contrast/api/decorators/route_coverage.rb +15 -5
  81. data/lib/contrast/api/decorators/trace_event.rb +58 -42
  82. data/lib/contrast/api/decorators/trace_event_object.rb +11 -3
  83. data/lib/contrast/api/decorators/trace_event_signature.rb +27 -5
  84. data/lib/contrast/api/decorators/user_input.rb +2 -1
  85. data/lib/contrast/common_agent_configuration.rb +2 -1
  86. data/lib/contrast/components/agent.rb +2 -0
  87. data/lib/contrast/components/app_context.rb +4 -22
  88. data/lib/contrast/components/assess.rb +36 -0
  89. data/lib/contrast/components/interface.rb +5 -3
  90. data/lib/contrast/components/sampling.rb +48 -6
  91. data/lib/contrast/components/scope.rb +72 -6
  92. data/lib/contrast/components/settings.rb +11 -7
  93. data/lib/contrast/config/assess_configuration.rb +2 -1
  94. data/lib/contrast/extension/assess/array.rb +2 -3
  95. data/lib/contrast/extension/assess/erb.rb +1 -3
  96. data/lib/contrast/extension/assess/exec_trigger.rb +1 -4
  97. data/lib/contrast/extension/assess/fiber.rb +2 -3
  98. data/lib/contrast/extension/assess/hash.rb +4 -2
  99. data/lib/contrast/extension/assess/kernel.rb +1 -2
  100. data/lib/contrast/extension/assess/marshal.rb +34 -26
  101. data/lib/contrast/extension/assess/regexp.rb +3 -8
  102. data/lib/contrast/extension/assess/string.rb +1 -2
  103. data/lib/contrast/framework/base_support.rb +51 -53
  104. data/lib/contrast/framework/manager.rb +16 -14
  105. data/lib/contrast/framework/rack/patch/session_cookie.rb +1 -1
  106. data/lib/contrast/framework/rack/support.rb +2 -1
  107. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +1 -1
  108. data/lib/contrast/framework/rails/patch/rails_application_configuration.rb +1 -1
  109. data/lib/contrast/framework/rails/rewrite/action_controller_railties_helper_inherited.rb +1 -1
  110. data/lib/contrast/framework/rails/rewrite/active_record_attribute_methods_read.rb +1 -1
  111. data/lib/contrast/framework/rails/rewrite/active_record_time_zone_inherited.rb +1 -1
  112. data/lib/contrast/framework/rails/support.rb +44 -44
  113. data/lib/contrast/framework/sinatra/support.rb +102 -42
  114. data/lib/contrast/logger/application.rb +0 -3
  115. data/lib/contrast/logger/log.rb +31 -15
  116. data/lib/contrast/utils/class_util.rb +3 -1
  117. data/lib/contrast/utils/duck_utils.rb +1 -1
  118. data/lib/contrast/utils/heap_dump_util.rb +103 -87
  119. data/lib/contrast/utils/invalid_configuration_util.rb +21 -12
  120. data/lib/contrast/utils/object_share.rb +3 -3
  121. data/lib/contrast/utils/preflight_util.rb +1 -1
  122. data/lib/contrast/utils/resource_loader.rb +1 -1
  123. data/lib/contrast/utils/sha256_builder.rb +2 -2
  124. data/lib/contrast/utils/string_utils.rb +1 -1
  125. data/lib/contrast/utils/tag_util.rb +9 -13
  126. data/resources/assess/policy.json +12 -18
  127. data/resources/deadzone/policy.json +156 -0
  128. data/resources/protect/policy.json +12 -0
  129. data/ruby-agent.gemspec +61 -19
  130. data/service_executables/VERSION +1 -1
  131. data/service_executables/linux/contrast-service +0 -0
  132. data/service_executables/mac/contrast-service +0 -0
  133. metadata +126 -113
  134. data/lib/contrast/agent/assess/rule.rb +0 -18
  135. data/lib/contrast/agent/assess/rule/base.rb +0 -52
  136. data/lib/contrast/agent/assess/rule/redos.rb +0 -67
  137. data/lib/contrast/framework/sinatra/patch/base.rb +0 -83
  138. data/lib/contrast/framework/sinatra/patch/support.rb +0 -27
  139. data/lib/contrast/utils/prevent_serialization.rb +0 -52
@@ -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
@@ -24,25 +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
-
34
- properties = Contrast::Agent::Assess::Tracker.properties(value)
35
- next unless properties
36
-
37
- # TODO: RUBY-540 handle sanitization, handle nested objects
38
- Contrast::Agent::Assess::Policy::PropagationMethod.apply_tags(propagation_node, value)
39
- properties.build_event(propagation_node, value, preshift.object, target, preshift.args)
40
- next unless tracked_value?(value)
41
-
42
- tainted_columns[key] = properties
43
- end
27
+ handle_write(propagation_node, source, preshift, target, known_tainted, tainted_columns)
44
28
  end
45
-
46
29
  return if tainted_columns.empty?
47
30
 
48
31
  if known_tainted
@@ -53,6 +36,26 @@ module Contrast
53
36
 
54
37
  Contrast::Agent::Assess::Policy::DynamicSourceFactory.create_sources class_type, tainted_columns
55
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
56
59
  end
57
60
  end
58
61
  end
@@ -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,
@@ -19,31 +19,17 @@ 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
 
38
26
  splat_tags(tracked_inputs, target)
39
- Contrast::Agent::Assess::Tracker.properties(target).cleanup_tags
27
+ Contrast::Agent::Assess::Tracker.properties(target)&.cleanup_tags
40
28
  end
41
29
 
42
30
  def splat_tags tracked_inputs, target
43
31
  return if tracked_inputs.empty?
44
-
45
- properties = Contrast::Agent::Assess::Tracker.properties(target)
46
- return unless properties
32
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
47
33
 
48
34
  tracked_inputs.each do |input|
49
35
  input_properties = Contrast::Agent::Assess::Tracker.properties(input)
@@ -52,6 +38,28 @@ module Contrast
52
38
  properties.splat_from(input, target)
53
39
  end
54
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
55
63
  end
56
64
  end
57
65
  end
@@ -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,139 @@ 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
 
46
- separator_length = if propagation_node.method_name == :grapheme_clusters
47
- # grapheme_clusters break the string apart based on each "user-perceived" character
48
- 0
49
- else
50
- # The default for String#split is to use a single whitespace
51
- preshift&.args&.first&.to_s&.length || $FIELD_SEPARATOR&.to_s&.length || 1
52
- end
41
+ separator_length = find_separator_length(propagation_node, preshift)
53
42
 
54
43
  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
44
+ target.each do |target_elem|
45
+ next unless (elem_properties = Contrast::Agent::Assess::Tracker.properties!(target_elem))
60
46
 
61
- source_properties = Contrast::Agent::Assess::Tracker.properties(source)
47
+ # Get tags for element from source by element range.
48
+ range = current_index...(current_index + target_elem.length)
62
49
  tags = source_properties.tags_at_range(range)
50
+
51
+ # Set element properties accordingly.
63
52
  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)
53
+ tags.each_pair { |key, value| elem_properties.set_tags(key, value) }
54
+ elem_properties.build_event(propagation_node, target_elem, preshift.object, target, preshift.args, 0)
68
55
  elem_properties.add_properties(propagation_node.properties)
69
- current_index = current_index + elem_length + separator_length
56
+
57
+ current_index = range.end + separator_length
70
58
  end
71
59
  nil
72
60
  end
73
61
 
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
62
+ # Context for block split execution.
77
63
  #
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
64
+ # @param string [String] the String on which split is invoked.
65
+ # @param args [Array<Object>] the arguments passed to the original split call.
66
+ def wrap_split string, args
67
+ # String#split start. Build context and yield.
68
+ begin
69
+ enter_split_scope!
70
+ save_split_index!
71
+ save_split_value!(string, args)
72
+ rescue Exception => e # rubocop:disable Lint/RescueException
73
+ logger.warn('Unable to record split context', e)
74
+ end
92
75
 
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)
76
+ yield
77
+ ensure
78
+ # String#split exit. Remove propagation context.
79
+ begin
80
+ exit_split_scope!
81
+ unless in_split_scope?
82
+ SPLIT_TRACKER.delete(:split_index)
83
+ SPLIT_TRACKER.delete(:split_value)
84
+ end
85
+ rescue StandardError => e
86
+ logger.warn('Unable to remove split context', e)
107
87
  end
108
- rescue StandardError => e
109
- logger.warn('Unable to remove split context', e)
110
88
  end
111
89
 
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.
90
+ # This method is called whenever an rb_yield is called.
91
+ # We need to leave it as soon as possible with as little work as possible.
115
92
  #
116
- # @param target [String] the entity being passed to the yield
117
- # block
93
+ # @param target [String] the entity being passed to the yield block
118
94
  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
95
+ return unless (source = SPLIT_TRACKER.get(:split_value)&.fetch(split_scope_depth))
96
+ return unless (index = SPLIT_TRACKER.get(:split_index)&.fetch(split_scope_depth))
97
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
132
98
 
133
99
  true_source = source[index]
134
100
  properties.copy_from(true_source, target)
135
101
  rescue StandardError => e
136
102
  logger.warn('Unable to track within split context', e)
137
103
  ensure
138
- if depth && index
104
+ if in_split_scope? && index
139
105
  idx = SPLIT_TRACKER.get(:split_index)
140
- idx[depth] = index + 1 if defined?(idx) && idx.is_a?(Array)
106
+ idx[split_scope_depth] = index + 1 if defined?(idx) && idx.is_a?(Array)
141
107
  end
142
108
  end
143
109
 
110
+ # Load patch.
144
111
  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
112
+ @_instrument_string_split ||= begin
113
+ require 'cs__assess_yield_track/cs__assess_yield_track' if AGENT.patch_yield? && Funchook.available?
114
+ true
115
+ rescue StandardError => e
116
+ logger.error('Error loading split rb_yield patch', e)
117
+ false
153
118
  end
154
- @_instrument_string_split
155
119
  end
156
120
 
157
121
  private
158
122
 
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
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
167
134
  end
168
135
 
169
- def save_split_index! depth
170
- split_index = SPLIT_TRACKER.get(:split_index)
171
- unless split_index
136
+ # Save index of the current split object.
137
+ # Create index tracking array as needed.
138
+ def save_split_index!
139
+ unless (split_index = SPLIT_TRACKER.get(:split_index))
172
140
  split_index = []
173
141
  SPLIT_TRACKER.set(:split_index, split_index)
174
142
  end
175
- # save the index to the ThreadLocal; not useless
176
- split_index[depth] = 0 # rubocop:disable Lint/UselessSetterCall
143
+ # save the index to the ThreadLocal; not useless.
144
+ split_index[split_scope_depth] = 0 # rubocop:disable Lint/UselessSetterCall
177
145
  end
178
146
 
179
- def save_split_value! depth, string, args
147
+ # Save value of the current split object.
148
+ # Create value tracking array as needed.
149
+ def save_split_value! string, args
180
150
  preshift = Contrast::Agent::Assess::PreShift.build_preshift(split_node, string, args)
181
151
  target = string.split
182
152
  propagate(split_node, preshift, target)
183
- split_value = SPLIT_TRACKER.get(:split_value)
184
- unless split_value
153
+ unless (split_value = SPLIT_TRACKER.get(:split_value))
185
154
  split_value = []
186
155
  SPLIT_TRACKER.set(:split_value, split_value)
187
156
  end
188
- # save the target to the ThreadLocal; not useless
189
- split_value[depth] = target # rubocop:disable Lint/UselessSetterCall
157
+ # Save the target to the ThreadLocal; not useless.
158
+ split_value[split_scope_depth] = target # rubocop:disable Lint/UselessSetterCall
190
159
  end
191
160
 
192
- # Quick hook to the String#split propagation node in our Assess
193
- # policy
161
+ # Quick hook to the String#split propagation node in our Assess policy
194
162
  #
195
- # @return [Contrast::Agent::Assess::Policy::PropagationNode]
196
- # String#split node
163
+ # @return [Contrast::Agent::Assess::Policy::PropagationNode] String#split node
197
164
  def split_node
198
165
  @_split_node ||= begin
199
166
  Contrast::Agent::Assess::Policy::Policy.instance.propagators.find do |node|
@@ -215,19 +182,15 @@ if RUBY_VERSION >= '2.6.0'
215
182
  class String
216
183
  alias_method :cs__patched_string_split_special, :split
217
184
 
218
- # override of the the standard split method to handle the 2.6 direct
219
- # yield case.
185
+ # Override of the the standard split method to handle the 2.6 direct yield case.
220
186
  #
221
187
  # 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
188
+ # call is wrapped in it. As such, any call here happens in scope, so there is
223
189
  # no need to manage it on our own.
224
190
  def split *args, &block
225
191
  if block
226
- Contrast::Agent::Assess::Policy::Propagator::Split.begin_split(self, args)
227
- begin
192
+ Contrast::Agent::Assess::Policy::Propagator::Split.wrap_split(self, args) do
228
193
  cs__patched_string_split_special(*args, &block)
229
- ensure
230
- Contrast::Agent::Assess::Policy::Propagator::Split.end_split
231
194
  end
232
195
  else
233
196
  cs__patched_string_split_special(*args, &block)