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.
- checksums.yaml +4 -4
- data/ext/cs__assess_module/cs__assess_module.c +48 -0
- data/ext/cs__assess_module/cs__assess_module.h +7 -0
- data/ext/cs__common/cs__common.c +5 -0
- data/ext/cs__common/cs__common.h +8 -0
- data/ext/cs__contrast_patch/cs__contrast_patch.c +16 -1
- data/ext/cs__os_information/cs__os_information.c +31 -0
- data/ext/cs__os_information/cs__os_information.h +7 -0
- data/ext/cs__os_information/extconf.rb +5 -0
- data/lib/contrast/agent/assess/policy/propagation_method.rb +2 -116
- data/lib/contrast/agent/assess/policy/propagation_node.rb +4 -4
- data/lib/contrast/agent/assess/policy/source_method.rb +2 -71
- data/lib/contrast/agent/assess/policy/trigger_method.rb +4 -106
- data/lib/contrast/agent/assess/property/tagged.rb +2 -128
- data/lib/contrast/agent/deadzone/policy/policy.rb +1 -1
- data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +1 -0
- data/lib/contrast/agent/metric_telemetry_event.rb +26 -0
- data/lib/contrast/agent/middleware.rb +22 -0
- data/lib/contrast/agent/patching/policy/patch.rb +28 -235
- data/lib/contrast/agent/patching/policy/patcher.rb +2 -41
- data/lib/contrast/agent/request_handler.rb +7 -3
- data/lib/contrast/agent/startup_metrics_telemetry_event.rb +71 -0
- data/lib/contrast/agent/static_analysis.rb +4 -2
- data/lib/contrast/agent/telemetry.rb +129 -0
- data/lib/contrast/agent/telemetry_event.rb +34 -0
- data/lib/contrast/agent/thread_watcher.rb +43 -14
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/agent.rb +6 -0
- data/lib/contrast/components/api.rb +34 -0
- data/lib/contrast/components/app_context.rb +24 -0
- data/lib/contrast/components/config.rb +90 -11
- data/lib/contrast/components/contrast_service.rb +6 -0
- data/lib/contrast/config/api_configuration.rb +22 -0
- data/lib/contrast/config/env_variables.rb +25 -0
- data/lib/contrast/config/root_configuration.rb +1 -0
- data/lib/contrast/config/service_configuration.rb +2 -1
- data/lib/contrast/config.rb +1 -0
- data/lib/contrast/configuration.rb +3 -0
- data/lib/contrast/framework/manager.rb +14 -12
- data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +9 -6
- data/lib/contrast/framework/rails/patch/support.rb +31 -29
- data/lib/contrast/logger/application.rb +4 -0
- data/lib/contrast/utils/assess/propagation_method_utils.rb +129 -0
- data/lib/contrast/utils/assess/property/tagged_utils.rb +142 -0
- data/lib/contrast/utils/assess/source_method_utils.rb +83 -0
- data/lib/contrast/utils/assess/trigger_method_utils.rb +138 -0
- data/lib/contrast/utils/exclude_key.rb +20 -0
- data/lib/contrast/utils/metrics_hash.rb +59 -0
- data/lib/contrast/utils/os.rb +23 -0
- data/lib/contrast/utils/patching/policy/patch_utils.rb +232 -0
- data/lib/contrast/utils/patching/policy/patcher_utils.rb +54 -0
- data/lib/contrast/utils/requests_client.rb +150 -0
- data/lib/contrast/utils/telemetry.rb +78 -0
- data/lib/contrast/utils/telemetry_identifier.rb +137 -0
- data/lib/contrast.rb +18 -0
- data/ruby-agent.gemspec +2 -1
- data/service_executables/VERSION +1 -1
- data/service_executables/linux/contrast-service +0 -0
- data/service_executables/mac/contrast-service +0 -0
- 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
|
data/lib/contrast/utils/os.rb
CHANGED
@@ -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
|