contrast-agent 4.12.0 → 4.13.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/ext/cs__assess_module/cs__assess_module.c +48 -0
  3. data/ext/cs__assess_module/cs__assess_module.h +7 -0
  4. data/ext/cs__common/cs__common.c +5 -0
  5. data/ext/cs__common/cs__common.h +8 -0
  6. data/ext/cs__contrast_patch/cs__contrast_patch.c +16 -1
  7. data/ext/cs__os_information/cs__os_information.c +31 -0
  8. data/ext/cs__os_information/cs__os_information.h +7 -0
  9. data/ext/cs__os_information/extconf.rb +5 -0
  10. data/lib/contrast/agent/assess/policy/propagation_method.rb +2 -116
  11. data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
  12. data/lib/contrast/agent/assess/policy/source_method.rb +2 -71
  13. data/lib/contrast/agent/assess/policy/trigger_method.rb +4 -106
  14. data/lib/contrast/agent/assess/property/tagged.rb +2 -128
  15. data/lib/contrast/agent/deadzone/policy/policy.rb +1 -1
  16. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +1 -0
  17. data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
  18. data/lib/contrast/agent/middleware.rb +22 -0
  19. data/lib/contrast/agent/patching/policy/patch.rb +28 -235
  20. data/lib/contrast/agent/patching/policy/patcher.rb +2 -41
  21. data/lib/contrast/agent/request_handler.rb +7 -3
  22. data/lib/contrast/agent/startup_metrics_telemetry_event.rb +71 -0
  23. data/lib/contrast/agent/static_analysis.rb +4 -2
  24. data/lib/contrast/agent/telemetry.rb +129 -0
  25. data/lib/contrast/agent/telemetry_event.rb +34 -0
  26. data/lib/contrast/agent/thread_watcher.rb +43 -14
  27. data/lib/contrast/agent/version.rb +1 -1
  28. data/lib/contrast/agent.rb +6 -0
  29. data/lib/contrast/components/api.rb +34 -0
  30. data/lib/contrast/components/app_context.rb +24 -0
  31. data/lib/contrast/components/config.rb +90 -11
  32. data/lib/contrast/components/contrast_service.rb +6 -0
  33. data/lib/contrast/config/api_configuration.rb +22 -0
  34. data/lib/contrast/config/env_variables.rb +25 -0
  35. data/lib/contrast/config/root_configuration.rb +1 -0
  36. data/lib/contrast/config/service_configuration.rb +2 -1
  37. data/lib/contrast/config.rb +1 -0
  38. data/lib/contrast/configuration.rb +3 -0
  39. data/lib/contrast/framework/manager.rb +14 -12
  40. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
  41. data/lib/contrast/framework/rails/patch/support.rb +31 -29
  42. data/lib/contrast/logger/application.rb +4 -0
  43. data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
  44. data/lib/contrast/utils/assess/property/tagged_utils.rb +142 -0
  45. data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
  46. data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
  47. data/lib/contrast/utils/exclude_key.rb +20 -0
  48. data/lib/contrast/utils/metrics_hash.rb +59 -0
  49. data/lib/contrast/utils/os.rb +23 -0
  50. data/lib/contrast/utils/patching/policy/patch_utils.rb +232 -0
  51. data/lib/contrast/utils/patching/policy/patcher_utils.rb +54 -0
  52. data/lib/contrast/utils/requests_client.rb +150 -0
  53. data/lib/contrast/utils/telemetry.rb +78 -0
  54. data/lib/contrast/utils/telemetry_identifier.rb +137 -0
  55. data/lib/contrast.rb +18 -0
  56. data/ruby-agent.gemspec +2 -1
  57. data/service_executables/VERSION +1 -1
  58. data/service_executables/linux/contrast-service +0 -0
  59. data/service_executables/mac/contrast-service +0 -0
  60. metadata +32 -10
@@ -0,0 +1,138 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Utils
6
+ module Assess
7
+ # This module will include all methods for some internal validations/appliers in the TriggerMethod module
8
+ # and some other module methods from the same place, so we can ease the main module
9
+ module TriggerMethodUtils
10
+ # A request is reportable if it is not from ActionController::Live
11
+ #
12
+ # @param env [Hash] the env of the Request
13
+ # @return [Boolean]
14
+ def reportable? env
15
+ !(defined?(ActionController::Live) &&
16
+ env &&
17
+ env['action_controller.instance'].cs__class.included_modules.include?(ActionController::Live))
18
+ end
19
+
20
+ # Find the request for this finding. This assumes, for now, that if there is an active request, then that
21
+ # is the request to report. Otherwise, we'll use the first request found in the events of the
22
+ # source_object.
23
+ #
24
+ # @param source [Object,nil] some Object used as the source of a trigger event
25
+ # @return [Contrast::Agent::Request,nil] the request from which the dataflow on the request originated.
26
+ def find_request source
27
+ return Contrast::Agent::REQUEST_TRACKER.current.request if Contrast::Agent::REQUEST_TRACKER.current
28
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties(source))
29
+
30
+ find_event_request(properties.event)
31
+ end
32
+
33
+ # Finds the first request along the left most tree of parent events
34
+ #
35
+ # @param event [Contrast::Agent::Assess::ContrastEvent|Contrast::Agent::Assess::Events::SourceEvent]
36
+ # @return [Contrast::Agent::Request, nil]
37
+ def find_event_request event
38
+ return event.request if event.cs__is_a?(Contrast::Agent::Assess::Events::SourceEvent)
39
+
40
+ idx = 0
41
+ while idx <= event.parent_events.length
42
+ found = find_event_request(event.parent_events[idx])
43
+ return found if found
44
+
45
+ idx += 1
46
+ return event.request if event.request
47
+ end
48
+ return unless event.cs__is_a?(Contrast::Agent::Assess::Events::SourceEvent)
49
+
50
+ event.request
51
+ end
52
+
53
+ # ===== APPLIERS =====
54
+ # This is our method that actually checks the taint on the object our trigger_node targets.
55
+ #
56
+ # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
57
+ # trigger event
58
+ # @param source [Object] the source of the Trigger Event
59
+ # @param object [Object] the Object on which the method was invoked
60
+ # @param ret [Object] the Return of the invoked method
61
+ # @param args [Array<Object>] the Arguments with which the method was invoked
62
+ def apply_trigger trigger_node, source, object, ret, *args
63
+ return unless trigger_node
64
+ return if trigger_node.rule_disabled?
65
+ return if trigger_node.dataflow? && source.nil?
66
+
67
+ if trigger_node.regexp_rule?
68
+ apply_regexp_rule(trigger_node, source, object, ret, *args)
69
+ elsif trigger_node.custom_trigger?
70
+ trigger_node.apply_custom_trigger(trigger_node, source, object, ret, *args)
71
+ elsif trigger_node.dataflow?
72
+ apply_dataflow_rule(trigger_node, source, object, ret, *args)
73
+ else # trigger rule - just calling the method is dangerous
74
+ build_finding(trigger_node, source, object, ret, *args)
75
+ end
76
+ rescue StandardError => e
77
+ logger.warn('Unable to apply trigger', e, node_id: trigger_node.id)
78
+ end
79
+
80
+ # This is our method that actually checks the taint on the object our trigger_node targets for our Regexp
81
+ # based rules.
82
+ #
83
+ # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
84
+ # trigger event
85
+ # @param source [Object] the source of the Trigger Event
86
+ # @param object [Object] the Object on which the method was invoked
87
+ # @param ret [Object] the Return of the invoked method
88
+ # @param args [Array<Object>] the Arguments with which the method was invoked
89
+ def apply_regexp_rule trigger_node, source, object, ret, *args
90
+ return unless source.is_a?(String)
91
+ return if trigger_node.good_value && source.match?(trigger_node.good_value)
92
+ return if trigger_node.bad_value && source !~ trigger_node.bad_value
93
+
94
+ build_finding(trigger_node, source, object, ret, *args)
95
+ end
96
+
97
+ # This is our method that actually checks the taint on the object our trigger_node targets for our Dataflow
98
+ # based rules.
99
+ #
100
+ # @param trigger_node [Contrast::Agent::Assess::Policy::TriggerNode] the node to direct applying this
101
+ # trigger event
102
+ # @param source [Object] the source of the Trigger Event
103
+ # @param object [Object] the Object on which the method was invoked
104
+ # @param ret [Object] the Return of the invoked method
105
+ # @param args [Array<Object>] the Arguments with which the method was invoked
106
+ def apply_dataflow_rule trigger_node, source, object, ret, *args
107
+ return unless source
108
+
109
+ if Contrast::Agent::Assess::Tracker.trackable?(source)
110
+ return unless Contrast::Agent::Assess::Tracker.tracked?(source)
111
+ return unless trigger_node.violated?(source)
112
+
113
+ build_finding(trigger_node, source, object, ret, *args)
114
+ elsif Contrast::Utils::DuckUtils.iterable_hash?(source)
115
+ return unless Contrast::Agent::Assess::Tracker.tracked?(source)
116
+
117
+ source.each_pair do |key, value|
118
+ apply_dataflow_rule(trigger_node, key, object, ret, *args)
119
+ apply_dataflow_rule(trigger_node, value, object, ret, *args)
120
+ end
121
+ elsif Contrast::Utils::DuckUtils.iterable_enumerable?(source)
122
+ return unless Contrast::Agent::Assess::Tracker.tracked?(source)
123
+
124
+ source.each do |value|
125
+ apply_dataflow_rule(trigger_node, value, object, ret, *args)
126
+ end
127
+ else
128
+ logger.debug('Trigger source is untrackable. Unable to inspect.',
129
+ node_id: trigger_node.id,
130
+ source_id: source.__id__,
131
+ source_type: source.cs__class.cs__name,
132
+ frozen: source.cs__frozen?)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,20 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Utils
6
+ # Determine if configuration keys is excluded from logging
7
+ module ExcludeKey
8
+ EXCLUDE_FROM_LOG = %w[api api_key url service_key user_name].cs__freeze
9
+ class << self
10
+ # Check if a config key can be logged or not
11
+ #
12
+ # @param key [String] key to check
13
+ # @return[Boolean] true | false
14
+ def excludable? key
15
+ EXCLUDE_FROM_LOG.any? { |exclude_key| key.include? exclude_key }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,59 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/components/logger'
5
+
6
+ module Contrast
7
+ module Utils
8
+ # This is the MetricsHash, which will take data type, so we now what is introduced/included
9
+ # in the TelemetryEvent
10
+ class MetricsHash < Hash
11
+ include Contrast::Components::Logger::InstanceMethods
12
+
13
+ attr_reader :data_type
14
+
15
+ ERROR_MESSAGES = [
16
+ 'The key is not string or does not meet the requirements.',
17
+ 'The key extends the allowed length.',
18
+ 'VThe provided value is not the right data type'
19
+ ].cs__freeze
20
+ KEY_REGEXP = /[a-zA-Z0-9_-]{1,63}/.cs__freeze
21
+
22
+ def initialize data_type, *several_variants
23
+ super
24
+ @data_type = data_type
25
+ end
26
+
27
+ def []= key, value
28
+ key_val = key.dup
29
+ value_val = value.dup
30
+ key_val.strip! if key_val.cs__is_a?(String)
31
+ value_val.strip! if value_val.cs__is_a?(String)
32
+ return unless valid_pair? key_val, value_val
33
+
34
+ key_val.downcase!
35
+ key_val.strip!
36
+ super(key_val, value_val)
37
+ end
38
+
39
+ def valid_pair? key, value
40
+ if !key.cs__is_a?(String) || (KEY_REGEXP =~ key).nil?
41
+ logger.warn('The following key will be omitted', key: key, error: ERROR_MESSAGES[0])
42
+ return false
43
+ end
44
+ unless key.length <= 28
45
+ logger.warn('The following key will be omitted', key: key, error: ERROR_MESSAGES[1])
46
+ return false
47
+ end
48
+ unless value.cs__is_a?(data_type)
49
+ logger.warn('The following key will be omitted', value: value, error: ERROR_MESSAGES[2])
50
+ return false
51
+ end
52
+ return false if value.cs__is_a?(String) && value.empty?
53
+ return false if value.cs__is_a?(String) && value.length > 200
54
+
55
+ true
56
+ end
57
+ end
58
+ end
59
+ end
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'contrast/components/scope'
5
+ require 'cs__os_information/cs__os_information'
5
6
 
6
7
  module Contrast
7
8
  module Utils
@@ -31,6 +32,28 @@ module Contrast
31
32
  zombie_pid_list.split("\n")
32
33
  end
33
34
  end
35
+
36
+ # Check current OS type
37
+ # returns true if check is correct or false if not
38
+ def windows?
39
+ (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
40
+ end
41
+
42
+ def mac?
43
+ RUBY_PLATFORM.include? 'darwin'
44
+ end
45
+
46
+ def unix?
47
+ !windows?
48
+ end
49
+
50
+ def linux?
51
+ unix? and !mac?
52
+ end
53
+
54
+ def jruby?
55
+ RUBY_ENGINE == 'jruby'
56
+ end
34
57
  end
35
58
  end
36
59
  end
@@ -0,0 +1,232 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Utils
6
+ module Patching
7
+ # This module will include all methods for different patch applies from Patch module
8
+ # and some other module methods from the same place, so we can ease the main module
9
+ module PatchUtils
10
+ # Method to choose which replaced return from the post_patch to
11
+ # actually return
12
+ #
13
+ # @param propagated_ret [Object, nil] The replaced return from the
14
+ # propagation patch.
15
+ # @param source_ret [Object, nil] The replaced return from the
16
+ # source patch.
17
+ # @param ret [Object, nil] The original return of the patched
18
+ # method.
19
+ # @return [Object, nil] The thing to return from the post patch.
20
+ def handle_return propagated_ret, source_ret, ret
21
+ safe_return = propagated_ret || source_ret || ret
22
+ safe_return.rewind if Contrast::Utils::IOUtil.should_rewind?(safe_return)
23
+ safe_return
24
+ end
25
+
26
+ # Given a module and method, construct an expected name for the
27
+ # alias by which Contrast will reference the original.
28
+ #
29
+ # @param patched_class [Module] the module being patched
30
+ # @param patched_method [Symbol] the method being patched
31
+ # @return [Symbol]
32
+ def build_method_name patched_class, patched_method
33
+ (Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START +
34
+ patched_class.cs__name.gsub('::', '_').downcase +
35
+ Contrast::Utils::ObjectShare::UNDERSCORE +
36
+ patched_method.to_s).to_sym
37
+ end
38
+
39
+ # Given a method, return a symbol in the format
40
+ # <method_start>_unbound_<method_name>
41
+ def build_unbound_method_name patcher_method
42
+ (Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START +
43
+ 'unbound' +
44
+ Contrast::Utils::ObjectShare::UNDERSCORE +
45
+ patcher_method.to_s).to_sym
46
+ end
47
+
48
+ # ===== PATCH APPLIERS =====
49
+ # THIS IS CALLED FROM C. Do not change the signature lightly.
50
+ #
51
+ # This method functions to call the infilter methods from our
52
+ # patches, allowing for analysis and reporting at the point just
53
+ # before the patched code is invoked.
54
+ #
55
+ # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
56
+ # Mapping of the triggers on the given method.
57
+ # @param method [Symbol] The method into which we're patching
58
+ # @param exception [StandardError] Any exception raised during the
59
+ # call of the patched method.
60
+ # @param object [Object] The object on which the method is invoked,
61
+ # typically what would be returned by self.
62
+ # @param args [Array<Object>] The arguments passed to the method
63
+ # being invoked.
64
+ def apply_pre_patch method_policy, method, exception, object, args
65
+ apply_protect(method_policy, method, exception, object, args)
66
+ apply_inventory(method_policy, method, exception, object, args)
67
+ rescue Contrast::SecurityException => e
68
+ # We were told to block something, so we gotta. Don't catch this
69
+ # one, let it get back to our Middleware or even all the way out to
70
+ # the framework
71
+ raise e
72
+ rescue StandardError => e
73
+ # Anything else was our bad and we gotta catch that to allow for
74
+ # normal application flow
75
+ logger.error('Unable to apply pre patch to method.', e)
76
+ rescue Exception => e # rubocop:disable Lint/RescueException
77
+ # This is something like NoMemoryError that we can't
78
+ # hope to handle. Nonetheless, shouldn't leak scope.
79
+ exit_contrast_scope!
80
+ raise e
81
+ end
82
+
83
+ # THIS IS CALLED FROM C. Do not change the signature lightly.
84
+ #
85
+ # This method functions to call the infilter methods from our
86
+ # patches, allowing for analysis and reporting at the point just
87
+ # after the patched code is invoked
88
+ #
89
+ # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
90
+ # Mapping of the triggers on the given method.
91
+ # @param preshift [Contrast::Agent::Assess::PreShift] The capture
92
+ # of the state of the code just prior to the invocation of the
93
+ # patched method.
94
+ # @param object [Object] The object on which the method was
95
+ # invoked, typically what would be returned by self.
96
+ # @param ret [Object] The return of the method that was invoked.
97
+ # @param args [Array<Object>] The arguments passed to the method
98
+ # being invoked.
99
+ # @param block [Proc] The block passed to the method that was
100
+ # invoked.
101
+ def apply_post_patch method_policy, preshift, object, ret, args, block
102
+ apply_assess(method_policy, preshift, object, ret, args, block)
103
+ rescue StandardError => e
104
+ logger.error('Unable to apply post patch to method.', e)
105
+ end
106
+
107
+ # Apply the Protect patch which applies to the given method.
108
+ #
109
+ # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
110
+ # Mapping of the triggers on the given method.
111
+ # @param method [Symbol] The method into which we're patching
112
+ # @param exception [StandardError] Any exception raised during the
113
+ # call of the patched method.
114
+ # @param object [Object] The object on which the method is invoked,
115
+ # typically what would be returned by self.
116
+ # @param args [Array<Object>] The arguments passed to the method
117
+ # being invoked.
118
+ def apply_protect method_policy, method, exception, object, args
119
+ return unless ::Contrast::AGENT.enabled?
120
+ return unless ::Contrast::PROTECT.enabled?
121
+
122
+ apply_trigger_only(method_policy&.protect_node, method, exception, object, args)
123
+ end
124
+
125
+ # Apply the Inventory patch which applies to the given method.
126
+ #
127
+ # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
128
+ # Mapping of the triggers on the given method.
129
+ # @param method [Symbol] The method into which we're patching
130
+ # @param exception [StandardError] Any exception raised during the
131
+ # call of the patched method.
132
+ # @param object [Object] The object on which the method is invoked,
133
+ # typically what would be returned by self.
134
+ # @param args [Array<Object>] The arguments passed to the method
135
+ # being invoked.
136
+ def apply_inventory method_policy, method, exception, object, args
137
+ return unless ::Contrast::INVENTORY.enabled?
138
+
139
+ apply_trigger_only(method_policy&.inventory_node, method, exception, object, args)
140
+ end
141
+
142
+ # Apply the Assess patches which apply to the given method.
143
+ #
144
+ # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
145
+ # Mapping of the triggers on the given method.
146
+ # @param preshift [Contrast::Agent::Assess::PreShift] The capture
147
+ # of the state of the code just prior to the invocation of the
148
+ # patched method.
149
+ # @param object [Object] The object on which the method was
150
+ # invoked, typically what would be returned by self.
151
+ # @param ret [Object] The return of the method that was invoked.
152
+ # @param args [Array<Object>] The arguments passed to the method
153
+ # being invoked.
154
+ # @param block [Proc] The block passed to the method that was
155
+ # invoked.
156
+ def apply_assess method_policy, preshift, object, ret, args, block
157
+ source_ret = nil
158
+ propagated_ret = nil
159
+ return ret unless method_policy && ::Contrast::ASSESS.enabled?
160
+
161
+ current_context = Contrast::Agent::REQUEST_TRACKER.current
162
+ return ret if current_context && !current_context.analyze_request?
163
+
164
+ trigger_node = method_policy.trigger_node
165
+ if trigger_node
166
+ Contrast::Agent::Assess::Policy::TriggerMethod.apply_trigger_rule(trigger_node, object, ret, args)
167
+ end
168
+ if method_policy.source_node
169
+ # If we were given a frozen return, and it was the target of a
170
+ # source, and we have frozen sources enabled, we'll need to
171
+ # replace the return. Note, this is not the default case.
172
+ source_ret = Contrast::Agent::Assess::Policy::SourceMethod.source_patchers(method_policy, object, ret, args)
173
+ end
174
+ if method_policy.propagation_node
175
+ propagated_ret = Contrast::Agent::Assess::Policy::PropagationMethod.apply_propagation(
176
+ method_policy,
177
+ preshift,
178
+ object,
179
+ source_ret || ret,
180
+ args,
181
+ block)
182
+ end
183
+ handle_return(propagated_ret, source_ret, ret)
184
+ rescue StandardError => e
185
+ logger.error('Unable to assess method call.', e)
186
+ handle_return(propagated_ret, source_ret, ret)
187
+ rescue Exception => e # rubocop:disable Lint/RescueException
188
+ logger.error('Unable to assess method call.', e)
189
+ handle_return(propagated_ret, source_ret, ret)
190
+ raise e
191
+ end
192
+
193
+ # Generic invocation of the Inventory or Protect patch which apply
194
+ # to the given method.
195
+ #
196
+ # @param trigger_node [Contrast::Agent::Inventory::Policy::TriggerNode]
197
+ # Mapping of the specific trigger on the given method.
198
+ # @param method [Symbol] The method into which we're patching
199
+ # @param exception [StandardError] Any exception raised during the
200
+ # call of the patched method.
201
+ # @param object [Object] The object on which the method is invoked,
202
+ # typically what would be returned by self.
203
+ # @param args [Array<Object>] The arguments passed to the method
204
+ # being invoked.
205
+ def apply_trigger_only trigger_node, method, exception, object, args
206
+ return unless trigger_node
207
+
208
+ # If that rule only applies in the case of an exception being
209
+ # thrown and there's no exception here, move along, or vice versa
210
+ return if trigger_node.on_exception && !exception
211
+ return if !trigger_node.on_exception && exception
212
+
213
+ # Each patch has an applicator that handles logic for it. Think
214
+ # of this as being similar to propagator actions, most closely
215
+ # resembling CUSTOM - they all have a common interface but their
216
+ # own logic based on what's in the method(s) they've been patched
217
+ # into.
218
+ # Each patch also knows the method of its applicator. Some
219
+ # things, like AppliesXxeRule, have different methods depending
220
+ # on the library patched. This lets us handle the boilerplate of
221
+ # patching while still allowing for custom handling of the
222
+ # methods.
223
+ applicator_method = trigger_node.applicator_method
224
+ # By calling send like this, we can reuse all the patching.
225
+ # We `send` to the given method of the given class
226
+ # (applicator) since they all accept the same inputs
227
+ trigger_node.applicator.send(applicator_method, method, exception, trigger_node.properties, object, args)
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,54 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Utils
6
+ module Patching
7
+ # This module will include patch methods from Patcher module
8
+ # in case of need to add new logic to the Patcher
9
+ # please do it here
10
+ module PatcherUtils
11
+ # This method is called by TracePointHook to instrument a specific class during a require
12
+ # or eval of dynamic class definition
13
+ def patch_specific_module mod
14
+ with_contrast_scope do
15
+ mod_name = mod.cs__name
16
+ return unless Contrast::Utils::ClassUtil.truly_defined?(mod_name)
17
+ return if ::Contrast::AGENT.skip_instrumentation?(mod_name)
18
+
19
+ load_patches_for_module(mod_name)
20
+
21
+ return if all_module_names.none?(mod_name)
22
+
23
+ module_data = Contrast::Agent::ModuleData.new(mod, mod_name)
24
+ patch_into_module(module_data)
25
+ all_module_names.delete(mod_name) if status_type.get_status(mod).patched?
26
+ rescue StandardError => e
27
+ logger.error('Unable to patch module', e, module: mod_name)
28
+ end
29
+ end
30
+
31
+ # We did it, team. We found a patcher(s) that applies to the given
32
+ # class (or module) and the given method. Time to do some tracking.
33
+ #
34
+ # @param mod [Module] the module in which the patch should be
35
+ # placed.
36
+ # @param methods [Array(Symbol)] all the instance or singleton
37
+ # methods in this mod.
38
+ # @param method_policy [Contrast::Agent::Patching::Policy::MethodPolicy]
39
+ # the policy that applies to the given method_name.
40
+ # @return [Boolean] if patched, either by this invocation or a
41
+ # previous, or not
42
+ def patch_method mod, methods, method_policy
43
+ return false unless methods&.any? # don't even build the name if there are no methods
44
+
45
+ if Contrast::Utils::ClassUtil.prepended_method?(mod, method_policy)
46
+ Contrast::Agent::Patching::Policy::Patch.instrument_with_prepend(mod, method_policy)
47
+ else
48
+ Contrast::Agent::Patching::Policy::Patch.instrument_with_alias(mod, methods, method_policy)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end