contrast-agent 3.8.5 → 3.9.0

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