contrast-agent 3.8.5 → 3.9.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 (153) hide show
  1. checksums.yaml +4 -4
  2. data/ext/cs__assess_array/cs__assess_array.c +1 -1
  3. data/ext/cs__assess_module/cs__assess_module.c +0 -1
  4. data/ext/cs__assess_yield_track/cs__assess_yield_track.c +34 -0
  5. data/ext/cs__assess_yield_track/cs__assess_yield_track.h +12 -0
  6. data/ext/{cs__scope → cs__assess_yield_track}/extconf.rb +0 -0
  7. data/ext/cs__common/cs__common.c +6 -6
  8. data/ext/cs__common/cs__common.h +3 -1
  9. data/ext/cs__contrast_patch/cs__contrast_patch.c +142 -119
  10. data/ext/cs__contrast_patch/cs__contrast_patch.h +3 -0
  11. data/funchook/autom4te.cache/requests +48 -48
  12. data/funchook/config.log +2 -2
  13. data/lib/contrast/agent.rb +15 -5
  14. data/lib/contrast/agent/assess.rb +0 -1
  15. data/lib/contrast/agent/assess/contrast_event.rb +9 -8
  16. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +68 -18
  17. data/lib/contrast/agent/assess/policy/policy.rb +0 -14
  18. data/lib/contrast/agent/assess/policy/policy_scanner.rb +1 -1
  19. data/lib/contrast/agent/assess/policy/preshift.rb +1 -1
  20. data/lib/contrast/agent/assess/policy/propagation_method.rb +4 -2
  21. data/lib/contrast/agent/assess/policy/propagator/custom.rb +1 -1
  22. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +1 -1
  23. data/lib/contrast/agent/assess/policy/propagator/splat.rb +2 -2
  24. data/lib/contrast/agent/assess/policy/propagator/split.rb +166 -1
  25. data/lib/contrast/agent/assess/policy/rewriter_patch.rb +1 -0
  26. data/lib/contrast/agent/assess/policy/source_method.rb +199 -140
  27. data/lib/contrast/agent/assess/policy/source_validation/cross_site_validator.rb +30 -0
  28. data/lib/contrast/agent/assess/policy/source_validation/source_validation.rb +36 -0
  29. data/lib/contrast/agent/assess/policy/trigger_method.rb +238 -153
  30. data/lib/contrast/agent/assess/policy/trigger_node.rb +54 -9
  31. data/lib/contrast/agent/assess/policy/trigger_validation/trigger_validation.rb +13 -0
  32. data/lib/contrast/agent/assess/properties.rb +29 -0
  33. data/lib/contrast/agent/assess/rule/csrf/csrf_applicator.rb +35 -31
  34. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +1 -1
  35. data/lib/contrast/agent/class_reopener.rb +98 -55
  36. data/lib/contrast/agent/feature_state.rb +1 -1
  37. data/lib/contrast/agent/inventory/policy/policy.rb +1 -1
  38. data/lib/contrast/agent/logger_manager.rb +2 -2
  39. data/lib/contrast/agent/middleware.rb +1 -3
  40. data/lib/contrast/agent/patching/policy/after_load_patch.rb +40 -4
  41. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +33 -8
  42. data/lib/contrast/agent/patching/policy/method_policy.rb +20 -7
  43. data/lib/contrast/agent/patching/policy/patch.rb +54 -23
  44. data/lib/contrast/agent/patching/policy/patch_status.rb +0 -2
  45. data/lib/contrast/agent/patching/policy/patcher.rb +10 -11
  46. data/lib/contrast/agent/patching/policy/policy.rb +4 -0
  47. data/lib/contrast/agent/patching/policy/policy_node.rb +14 -1
  48. data/lib/contrast/agent/patching/policy/trigger_node.rb +2 -1
  49. data/lib/contrast/agent/protect/policy/policy.rb +6 -6
  50. data/lib/contrast/agent/protect/rule/base.rb +1 -1
  51. data/lib/contrast/agent/protect/rule/deserialization.rb +3 -25
  52. data/lib/contrast/agent/protect/rule/sqli.rb +1 -1
  53. data/lib/contrast/agent/railtie.rb +11 -5
  54. data/lib/contrast/agent/request.rb +1 -19
  55. data/lib/contrast/agent/request_context.rb +1 -1
  56. data/lib/contrast/agent/rewriter.rb +4 -3
  57. data/lib/contrast/agent/scope.rb +116 -19
  58. data/lib/contrast/agent/service_heartbeat.rb +5 -2
  59. data/lib/contrast/agent/settings_state.rb +12 -8
  60. data/lib/contrast/agent/version.rb +1 -1
  61. data/lib/contrast/api.rb +1 -0
  62. data/lib/contrast/api/speedracer.rb +2 -2
  63. data/lib/contrast/components/agent.rb +26 -7
  64. data/lib/contrast/components/app_context.rb +8 -45
  65. data/lib/contrast/components/contrast_service.rb +3 -4
  66. data/lib/contrast/components/interface.rb +1 -1
  67. data/lib/contrast/components/scope.rb +56 -26
  68. data/lib/contrast/config/ruby_configuration.rb +8 -3
  69. data/lib/contrast/delegators.rb +9 -0
  70. data/lib/contrast/delegators/application_update.rb +32 -0
  71. data/lib/contrast/extensions/framework/rack/cookie.rb +24 -0
  72. data/lib/contrast/extensions/framework/rack/request.rb +24 -0
  73. data/lib/contrast/extensions/framework/rack/response.rb +23 -0
  74. data/lib/contrast/extensions/framework/rails/action_controller_railties_helper_inherited.rb +20 -0
  75. data/lib/contrast/extensions/framework/rails/active_record.rb +26 -0
  76. data/lib/contrast/extensions/framework/rails/active_record_named.rb +53 -0
  77. data/lib/contrast/extensions/framework/rails/active_record_time_zone_inherited.rb +21 -0
  78. data/lib/contrast/extensions/framework/rails/buffer.rb +28 -0
  79. data/lib/contrast/extensions/framework/rails/configuration.rb +27 -0
  80. data/lib/contrast/extensions/framework/sinatra/base.rb +59 -0
  81. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess.rb +12 -11
  82. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/array.rb +4 -3
  83. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/assess_extension.rb +0 -2
  84. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/basic_object.rb +1 -1
  85. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/erb.rb +0 -0
  86. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/exec_trigger.rb +0 -0
  87. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/fiber.rb +3 -4
  88. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/hash.rb +0 -0
  89. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/kernel.rb +1 -1
  90. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/module.rb +1 -1
  91. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/regexp.rb +0 -0
  92. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/string.rb +0 -0
  93. data/lib/contrast/{core_extensions → extensions/ruby_core}/assess/tilt_template_trigger.rb +0 -0
  94. data/lib/contrast/extensions/ruby_core/assess/xpath_library_trigger.rb +40 -0
  95. data/lib/contrast/{core_extensions → extensions/ruby_core}/delegator.rb +0 -0
  96. data/lib/contrast/{core_extensions → extensions/ruby_core}/eval_trigger.rb +1 -1
  97. data/lib/contrast/{core_extensions → extensions/ruby_core}/inventory.rb +0 -0
  98. data/lib/contrast/{core_extensions → extensions/ruby_core}/inventory/datastores.rb +1 -1
  99. data/lib/contrast/extensions/ruby_core/module.rb +17 -0
  100. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect.rb +0 -0
  101. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_command_injection_rule.rb +8 -6
  102. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_deserialization_rule.rb +7 -5
  103. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_no_sqli_rule.rb +5 -3
  104. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_path_traversal_rule.rb +31 -27
  105. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_sqli_rule.rb +5 -3
  106. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/applies_xxe_rule.rb +9 -7
  107. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/kernel.rb +0 -0
  108. data/lib/contrast/{core_extensions → extensions/ruby_core}/protect/psych.rb +1 -1
  109. data/lib/contrast/{core_extensions → extensions/ruby_core}/thread.rb +0 -0
  110. data/lib/contrast/framework/base_support.rb +63 -0
  111. data/lib/contrast/framework/manager.rb +109 -0
  112. data/lib/contrast/framework/platform_version.rb +21 -0
  113. data/lib/contrast/framework/rails_support.rb +88 -0
  114. data/lib/contrast/framework/sinatra_application_helper.rb +49 -0
  115. data/lib/contrast/framework/sinatra_support.rb +94 -0
  116. data/lib/contrast/framework/view_technologies_descriptor.rb +20 -0
  117. data/lib/contrast/utils/assess/tracking_util.rb +2 -4
  118. data/lib/contrast/utils/class_util.rb +92 -37
  119. data/lib/contrast/utils/duck_utils.rb +59 -39
  120. data/lib/contrast/utils/environment_util.rb +5 -75
  121. data/lib/contrast/utils/freeze_util.rb +3 -7
  122. data/lib/contrast/utils/invalid_configuration_util.rb +5 -5
  123. data/lib/contrast/utils/job_servers_running.rb +39 -0
  124. data/lib/contrast/utils/ruby_ast_rewriter.rb +2 -2
  125. data/lib/contrast/utils/service_response_util.rb +0 -6
  126. data/lib/contrast/utils/sinatra_helper.rb +6 -0
  127. data/lib/contrast/utils/stack_trace_utils.rb +1 -1
  128. data/resources/assess/policy.json +74 -23
  129. data/resources/inventory/policy.json +1 -1
  130. data/resources/protect/policy.json +11 -9
  131. data/resources/rubocops/object/frozen_cop.rb +1 -1
  132. data/ruby-agent.gemspec +2 -0
  133. data/service_executables/VERSION +1 -1
  134. data/service_executables/linux/contrast-service +0 -0
  135. data/service_executables/mac/contrast-service +0 -0
  136. metadata +94 -57
  137. data/ext/cs__scope/cs__scope.c +0 -96
  138. data/ext/cs__scope/cs__scope.h +0 -33
  139. data/lib/contrast/agent/assess/class_reverter.rb +0 -82
  140. data/lib/contrast/agent/patching/policy/policy_unpatcher.rb +0 -28
  141. data/lib/contrast/core_extensions/module.rb +0 -42
  142. data/lib/contrast/core_extensions/object.rb +0 -27
  143. data/lib/contrast/rails_extensions/assess/action_controller_inheritance.rb +0 -48
  144. data/lib/contrast/rails_extensions/assess/active_record.rb +0 -32
  145. data/lib/contrast/rails_extensions/assess/active_record_named.rb +0 -61
  146. data/lib/contrast/rails_extensions/assess/configuration.rb +0 -26
  147. data/lib/contrast/rails_extensions/buffer.rb +0 -30
  148. data/lib/contrast/rails_extensions/rack.rb +0 -45
  149. data/lib/contrast/sinatra_extensions/assess/cookie.rb +0 -26
  150. data/lib/contrast/sinatra_extensions/inventory/sinatra_base.rb +0 -59
  151. data/lib/contrast/utils/operating_environment.rb +0 -38
  152. data/lib/contrast/utils/path_util.rb +0 -151
  153. data/lib/contrast/utils/scope_util.rb +0 -99
@@ -101,13 +101,13 @@ module Contrast
101
101
  return false unless source.cs__tracked?
102
102
 
103
103
  # find the ranges that violate the rule (untrusted, etc)
104
- vulnerable_ranges = find_ranges_by_tag(source.cs__properties, required_tags)
104
+ vulnerable_ranges = find_ranges_by_all_tags(Contrast::Utils::StringUtils.ret_length(source), source.cs__properties, required_tags)
105
105
  # if there aren't any vulnerable ranges, nope out
106
106
  return false if vulnerable_ranges.empty?
107
107
 
108
108
  # find the ranges that are exempt from the rule
109
109
  # (validated, sanitized, etc)
110
- secure_ranges = find_ranges_by_tag(source.cs__properties, disallowed_tags)
110
+ secure_ranges = find_ranges_by_any_tag(source.cs__properties, disallowed_tags)
111
111
  # if there are vulnerable ranges and no secure, report
112
112
  return true if secure_ranges.empty?
113
113
 
@@ -135,7 +135,7 @@ module Contrast
135
135
  return unless dataflow?
136
136
 
137
137
  validate_rule_tags(required_tags)
138
- @required_tags = required_tags || []
138
+ @required_tags = Set.new(required_tags)
139
139
  @required_tags << UNTRUSTED
140
140
  end
141
141
 
@@ -153,7 +153,7 @@ module Contrast
153
153
  return unless dataflow?
154
154
 
155
155
  validate_rule_tags(disallowed_tags)
156
- @disallowed_tags = disallowed_tags || []
156
+ @disallowed_tags = Set.new(disallowed_tags)
157
157
  @disallowed_tags << LIMITED_CHARS
158
158
  @disallowed_tags << CUSTOM_ENCODED
159
159
  @disallowed_tags << CUSTOM_VALIDATED
@@ -172,14 +172,60 @@ module Contrast
172
172
  end
173
173
  end
174
174
 
175
- def find_ranges_by_tag cs__properties, tags
175
+ # Find the ranges that satisfy all of the given tags.
176
+ #
177
+ # @param length [Integer] the length of the object which may have the
178
+ # given tags -- used as the maximum index to search for all of the
179
+ # tags.
180
+ # @param cs__properties [Contrast::Agent::Assess::Properties] the
181
+ # properties to check for the tags
182
+ # @param tags [Set<String>] the list of tags on which to match
183
+ # @return [Array<Contrast::Agent::Assess::Tag>] the ranges satisfied
184
+ # by the given conditions
185
+ def find_ranges_by_all_tags length, cs__properties, tags
186
+ # if there aren't any all_tags or tags, break early
187
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless cs__properties.tracked?
188
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags&.any?
189
+
190
+ # :zap: faster to treat all as any if there's only one tag
191
+ return find_ranges_by_any_tag(cs__properties, tags) if tags.length == 1
192
+
176
193
  ranges = []
194
+ # find all the indicies on the source that have all the given tags
195
+ (0..length).each do |idx|
196
+ tags_at = cs__properties.tag_names_at(idx)
197
+ ranges << idx if tags.all? { |tag| tags_at.include?(tag) }
198
+ end
199
+ # break early if no indicies satisfy all the tags
200
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY if ranges.empty?
201
+
202
+ # chunk all the adjacent ranges
203
+ chunked = ranges.chunk_while { |i, j| i + 1 == j }
204
+ tag_ranges = []
205
+ # and convert them into Tags
206
+ chunked.each do |join|
207
+ start = join[0]
208
+ stop = join[-1]
209
+ # add the 1 to account for end index being exclusive
210
+ tag_length = stop - start + 1
211
+ tag_ranges = Contrast::Utils::TagUtil.ordered_merge(tag_ranges, Tag.new(tag_length, start))
212
+ end
213
+ tag_ranges
214
+ end
177
215
 
216
+ # Find the ranges that satisfy any of the given tags.
217
+ #
218
+ # @param cs__properties [Contrast::Agent::Assess::Properties] the
219
+ # properties to check for the tags
220
+ # @param tags [Set<String>] the list of tags on which to match
221
+ # @return [Array<Contrast::Agent::Assess::Tag>] the ranges satisfied
222
+ # by the given conditions
223
+ def find_ranges_by_any_tag cs__properties, tags
178
224
  # if there aren't any all_tags or tags, break early
179
- return ranges unless cs__properties.tracked?
180
- return ranges unless tags&.any?
225
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless cs__properties.tracked?
226
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags&.any?
181
227
 
182
- # find all tags that match the desired ones
228
+ ranges = []
183
229
  tags.each do |desired|
184
230
  found = cs__properties.fetch_tag(desired)
185
231
  next unless found
@@ -188,7 +234,6 @@ module Contrast
188
234
  # used in another trace
189
235
  ranges = Contrast::Utils::TagUtil.ordered_merge(ranges, found.dup)
190
236
  end
191
-
192
237
  ranges
193
238
  end
194
239
  end
@@ -18,6 +18,19 @@ module Contrast
18
18
  Contrast::Agent::Assess::Policy::TriggerValidation::XSSValidator
19
19
  ].cs__freeze
20
20
 
21
+ # Determines if the conditions in which this trigger was called are
22
+ # valid and should result in the generation of a
23
+ # Contrast::Api::Dtm::Finding.
24
+ #
25
+ # @param patcher [Contrast::Agent::Assess::Policy::TriggerNode] the
26
+ # Node which applies to the Trigger Method
27
+ # @param object [Object] the Object on which the Trigger Method was
28
+ # invoked
29
+ # @param ret [Object] the return of the Trigger Method
30
+ # @param args [Array<Object>] the Arguments with which the Trigger
31
+ # Method was invoked
32
+ # @return [Boolean] if the conditions are valid for the generation of
33
+ # a Contrast::Api::Dtm::Finding
21
34
  def self.valid? patcher, object, ret, args
22
35
  VALIDATORS.each do |validator|
23
36
  return false unless validator.valid?(patcher, object, ret, args)
@@ -66,6 +66,10 @@ module Contrast
66
66
  # to be expanded out to the size of the new String.
67
67
  #
68
68
  # Note: Tags do not know their key, so this is only the range covered
69
+ #
70
+ # @param idx [Integer] the index to check for tags
71
+ # @return [Array<Contrast::Agent::Assess::Tag>] a set of all the Tags
72
+ # covering the given index. This is only the ranges, not the names.
69
73
  def tags_at idx
70
74
  return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags?
71
75
 
@@ -82,6 +86,27 @@ module Contrast
82
86
  at
83
87
  end
84
88
 
89
+ # Find all of the tag names that span a given index.
90
+ #
91
+ # @param idx [Integer] the index to check for tags
92
+ # @return [Set<String>] a set of all the tags covering the given index.
93
+ # This is only the names of the tags, not their ranges.
94
+ def tag_names_at idx
95
+ return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless tags?
96
+
97
+ at = Set.new
98
+ tags.each_pair do |tag_name, tag_array|
99
+ tag_array.each do |tag|
100
+ if tag.covers?(idx)
101
+ at << tag_name
102
+ elsif tag.above?(idx)
103
+ break
104
+ end
105
+ end
106
+ end
107
+ at
108
+ end
109
+
85
110
  # given a range, select all tags in that range the selected tags are
86
111
  # shifted such that the start index of the new tag (0) aligns with the
87
112
  # given start index in the range
@@ -127,6 +152,10 @@ module Contrast
127
152
  # collection. If the given range touches an existing tag,
128
153
  # we'll combine the two, adjusting the existing one and
129
154
  # dropping this new one.
155
+ #
156
+ # @param label [String] the name of the tag
157
+ # @param range [Contrast::Agent::Assess::AdjustedSpan] the span that
158
+ # the tag covers
130
159
  def add_tag label, range
131
160
  length = range.stop - range.start
132
161
  tag = Contrast::Agent::Assess::Tag.new(length, range.start)
@@ -20,46 +20,50 @@ module Contrast
20
20
 
21
21
  CS__CSRF_LOG_MSG = 'applying CSRF assess rule'
22
22
 
23
- def self.rule
24
- @_rule ||= Contrast::Agent::FeatureState.instance.assess_rule(
25
- Contrast::Agent::Assess::Rule::Csrf::NAME)
26
- end
27
-
28
- def self.csrf_tagger patcher, preshift, _ret, _block
29
- return unless rule&.enabled?
23
+ class << self
24
+ def csrf_tagger patcher, preshift, _ret, _block
25
+ return unless rule&.enabled?
30
26
 
31
- idx = patcher.sources[0].to_i
32
- args = preshift.args
33
- return unless args&.length.to_i > idx
27
+ idx = patcher.sources[0].to_i
28
+ args = preshift.args
29
+ return unless args&.length.to_i > idx
34
30
 
35
- sql = args[idx]
36
- return unless sql
31
+ sql = args[idx]
32
+ return unless sql
37
33
 
38
- with_contrast_scope do
39
- logger.debug(nil, CS__CSRF_LOG_MSG)
40
- rule.record_db_state_change(
41
- Contrast::Agent::REQUEST_TRACKER.current,
42
- sql)
34
+ with_contrast_scope do
35
+ logger.debug(nil, CS__CSRF_LOG_MSG)
36
+ rule.record_db_state_change(
37
+ Contrast::Agent::REQUEST_TRACKER.current,
38
+ sql)
39
+ end
43
40
  end
44
- end
45
41
 
46
- def self.cs__assess_apply_csrf_rule sql
47
- context = Contrast::Agent::REQUEST_TRACKER.current
42
+ def apply_csrf_rule sql
43
+ context = Contrast::Agent::REQUEST_TRACKER.current
48
44
 
49
- return unless context&.app_loaded?
50
- return unless ASSESS.enabled?
51
- return unless sql
45
+ return unless context&.app_loaded?
46
+ return unless ASSESS.enabled?
47
+ return unless sql
48
+
49
+ rule = Contrast::Agent::FeatureState.instance.assess_rule(
50
+ Contrast::Agent::Assess::Rule::Csrf::NAME)
51
+ return unless rule&.enabled?
52
+
53
+ with_contrast_scope do
54
+ logger.debug(CS__CSRF_LOG_MSG)
55
+ rule.record_db_state_change(context, sql)
56
+ end
57
+ rescue StandardError => e
58
+ logger.warn(e, 'Error running CSRF assess rule')
59
+ end
52
60
 
53
- rule = Contrast::Agent::FeatureState.instance.assess_rule(
54
- Contrast::Agent::Assess::Rule::Csrf::NAME)
55
- return unless rule&.enabled?
61
+ private
56
62
 
57
- with_contrast_scope do
58
- logger.debug(CS__CSRF_LOG_MSG)
59
- rule.record_db_state_change(context, sql)
63
+ def rule
64
+ @_rule ||= Contrast::Agent::FeatureState.instance.assess_rule(
65
+ Contrast::Agent::Assess::Rule::Csrf::NAME)
60
66
  end
61
- rescue StandardError => e
62
- logger.warn(e, 'Error running CSRF assess rule')
63
67
  end
64
68
  end
65
69
  end
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  cs__scoped_require 'contrast/components/interface'
5
- cs__scoped_require 'contrast/core_extensions/module'
5
+ cs__scoped_require 'contrast/extensions/ruby_core/module'
6
6
 
7
7
  module Contrast
8
8
  module Agent
@@ -1,8 +1,8 @@
1
1
  # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
- # frozen_string_literal: false
2
+ # frozen_string_literal: true
3
3
 
4
4
  cs__scoped_require 'ripper'
5
- cs__scoped_require 'contrast/core_extensions/module'
5
+ cs__scoped_require 'contrast/extensions/ruby_core/module'
6
6
  cs__scoped_require 'contrast/components/interface'
7
7
 
8
8
  # This method is left purposefully at the top level namespace. Moving it
@@ -17,12 +17,12 @@ cs__scoped_require 'contrast/components/interface'
17
17
  def unbound_eval class_name, content
18
18
  # Yuck, this is a top-level method that has to break encapsulation
19
19
  # in order to access scoping!
20
- Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.enter_contrast_scope
20
+ Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.enter_contrast_scope!
21
21
  eval(content) # rubocop:disable Security/Eval
22
22
  rescue Exception # rubocop:disable Lint/RescueException
23
23
  Contrast::Agent::SettingsState.log_error("Unable to perform unbound eval of new content for #{ class_name }.")
24
24
  ensure
25
- Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.exit_contrast_scope
25
+ Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.exit_contrast_scope!
26
26
  end
27
27
 
28
28
  module Contrast
@@ -32,10 +32,13 @@ module Contrast
32
32
  # @deprecated Changes to this class are discouraged as this approach is
33
33
  # being phased out with support for those language versions.
34
34
  class ClassReopener
35
- END_NEW_LINE = "end\n".cs__freeze
36
- PROTECTED_WITH_NEW_LINE = "protected\n".cs__freeze
37
- PRIVATE_WITH_NEW_LINE = "private\n".cs__freeze
38
- CLASS_SELF_LINE = "class << self\n".cs__freeze
35
+ include Contrast::Components::Interface
36
+ access_component :logging
37
+
38
+ END_NEW_LINE = "end\n"
39
+ PROTECTED_WITH_NEW_LINE = "protected\n"
40
+ PRIVATE_WITH_NEW_LINE = "private\n"
41
+ CLASS_SELF_LINE = "class << self\n"
39
42
 
40
43
  attr_reader :public_singleton_methods, :class_module_path, :class_name, :files, :is_class, :name_space,
41
44
  :public_instance_methods, :protected_instance_methods, :private_instance_methods, :locations
@@ -55,25 +58,9 @@ module Contrast
55
58
  gather_modules
56
59
  end
57
60
 
58
- def gather_modules
59
- return if class_module_path.nil?
60
-
61
- segments = class_module_path.split(Contrast::Utils::ObjectShare::DOUBLE_COLON)
62
- @class_name = segments.last
63
- current = nil
64
- segments[0..-2].each do |chunk|
65
- defined = current ? current.cs__const_defined?(chunk) : Module.cs__const_defined?(chunk)
66
- next unless defined
67
-
68
- current = current ? current.cs__const_get(chunk) : Module.cs__const_get(chunk)
69
- if current.is_a?(Class)
70
- name_space << [chunk, Class]
71
- elsif current.is_a?(Module)
72
- name_space << [chunk, Module]
73
- end
74
- end
75
- end
76
-
61
+ # Indicates if any methods were rewritten for this class.
62
+ #
63
+ # @return [Boolean]
77
64
  def staged_changes?
78
65
  public_singleton_methods.any? ||
79
66
  public_instance_methods.any? ||
@@ -81,18 +68,34 @@ module Contrast
81
68
  private_instance_methods.any?
82
69
  end
83
70
 
71
+ # Indicates if this class was written from the given location.
72
+ #
73
+ # @param source_location [Array<String, Integer>] the result of a
74
+ # Method#source_location call
75
+ # @return [Boolean]
84
76
  def written_from_location? source_location
85
77
  return false unless source_location
86
78
 
87
79
  file = source_location[0]
88
80
  location = source_location[1]
89
- locs = locations[file]
90
- return true if locs.include?(location)
81
+ locations[file].include?(location)
82
+ end
91
83
 
92
- locs << location
93
- false
84
+ # Marks that this class was written from the given location.
85
+ #
86
+ # @param source_location [Array<String, Integer>] the result of a
87
+ # Method#source_location call
88
+ # @return [Boolean]
89
+ def written_from_location! source_location
90
+ return false unless source_location
91
+
92
+ file = source_location[0]
93
+ location = source_location[1]
94
+ locations[file] << location
94
95
  end
95
96
 
97
+ # Evaluate the patches that have been staged for this class, replacing
98
+ # the method definitions with those our rewrite.
96
99
  def commit_patches
97
100
  return unless staged_changes?
98
101
 
@@ -101,46 +104,86 @@ module Contrast
101
104
  unbound_eval(class_name, content) if !!valid && !class_name.empty?
102
105
  end
103
106
 
107
+ # Find the sourcecode of the method at the given location and return it
108
+ # if it is complete and compilable.
109
+ #
110
+ # @param location [Array<String, Integer>] the result of a
111
+ # Method#source_location call
112
+ # @param method_name [Symbol] the name of the method defined at the given
113
+ # location
114
+ # @return [String, nil] the code defining the method or nil if no valid
115
+ # code could be found.
104
116
  def source_code location, method_name
105
117
  file_name = location[0]
106
118
  line_number = location[1]
107
119
  return unless file_name && line_number
120
+ return unless File.exist?(file_name) && File.readable?(file_name)
121
+ return if File.empty?(file_name)
108
122
 
109
- unless files.key?(file_name)
110
- return unless File.exist?(file_name)
111
-
112
- files[file_name] = File.readlines(file_name)
113
- end
114
-
123
+ files[file_name] = File.readlines(file_name) unless files.key?(file_name)
115
124
  lines = files[file_name]
116
-
117
- old_verbose = $VERBOSE
118
- $VERBOSE = nil
119
- code = ''
120
- complete = false
125
+ code = +''
121
126
  # location#line_number is 1 based, arrays are 0 based
122
127
  line_number -= 1
123
128
  lines[line_number..-1].each do |line|
124
- begin
125
- code << line
126
- RubyVM::InstructionSequence.compile(code) # this will raise SyntaxError for malformed code
127
- # Assert that a line which ends with a , or \ is incomplete.
128
- complete = code !~ /[,\\]\s*\z/
129
- break if complete
130
- rescue SyntaxError
131
- code.gsub(/\#\{.*?\}/, 'temp')
132
- end
129
+ code << line
130
+ next unless compiles?(code)
131
+ break if complete?(code)
132
+ end
133
+ unless complete?(code) && compiles?(code)
134
+ logger.warn("Failed to capture #{ method_name } in #{ file_name } at #{ line_number } for rewriting.")
135
+ return
133
136
  end
134
- $VERBOSE = old_verbose
135
- raise SyntaxError, "Failure: method #{ method_name } in #{ file_name } at #{ line_number }" unless complete
136
-
137
137
  code
138
138
  end
139
139
 
140
140
  private
141
141
 
142
+ def gather_modules
143
+ return if class_module_path.nil?
144
+
145
+ segments = class_module_path.split(Contrast::Utils::ObjectShare::DOUBLE_COLON)
146
+ @class_name = segments.last
147
+ current = nil
148
+ segments[0..-2].each do |chunk|
149
+ defined = current ? current.cs__const_defined?(chunk) : Module.cs__const_defined?(chunk)
150
+ next unless defined
151
+
152
+ current = current ? current.cs__const_get(chunk) : Module.cs__const_get(chunk)
153
+ if current.is_a?(Class)
154
+ name_space << [chunk, Class]
155
+ elsif current.is_a?(Module)
156
+ name_space << [chunk, Module]
157
+ end
158
+ end
159
+ end
160
+
161
+ # code which ends with a , or \ is incomplete.
162
+ #
163
+ # @param code [String] the text to determine if complete
164
+ # @return [Boolean]
165
+ def complete? code
166
+ code !~ /[,\\]\s*\z/
167
+ end
168
+
169
+ # code which does not resolve to RubyVM::InstructionSequence does not
170
+ # compile
171
+ #
172
+ # @param code [String] the text to determine if compiles
173
+ # @return [Boolean]
174
+ def compiles? code
175
+ old_verbose = $VERBOSE
176
+ $VERBOSE = nil
177
+ RubyVM::InstructionSequence.compile(code)
178
+ true
179
+ rescue SyntaxError => _e
180
+ false
181
+ ensure
182
+ $VERBOSE = old_verbose
183
+ end
184
+
142
185
  def build_content
143
- content = ''
186
+ content = +''
144
187
  name_space.each do |arr|
145
188
  name = arr[0]
146
189
  type = arr[1]